diff --git a/docs/api/app.md b/docs/api/app.md index ed50216261..1fe1773331 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -836,6 +836,34 @@ identity — packaged apps should always set it explicitly. This API must be called before the `ready` event. The value can also be set using `desktopName` in `package.json`. +### `app.setActivationToken(token)` _Linux_ + + + +* `token` string - An [XDG activation](https://wayland.app/protocols/xdg-activation-v1) token, or a + [startup notification](https://specifications.freedesktop.org/startup-notification-spec/latest/) ID on X11. + +Sets the token that the next window activation uses, for example +[`win.focus()`](base-window.md#winfocus) or [`win.show()`](base-window.md#winshow). + +A Wayland compositor only lets an app raise and focus one of its windows when +the request carries a token that proves the user asked for it. Electron passes +such tokens along by itself when the app is launched or when one of its +[`Notification`](notification.md)s is clicked. Use this method when the app gets +a token some other way, for example from a notification it sent over D-Bus +itself or from another process, and wants the window it activates next to come +to the front. The token is used once. Set it before the window is created or +shown, or once the window is on screen: a token that is set while a new window +is still being shown may not be used. + +On X11 the token is set as the `_NET_STARTUP_ID` of the next window that is +created or activated, which is where window managers read it. + ### `app.getLocale()` Returns `string` - The current application locale, fetched using Chromium's `l10n_util` library. diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index 5995e3831a..91df03c537 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -1038,6 +1038,12 @@ void App::SetDesktopName(const std::string& desktop_name) { #endif } +void App::SetActivationToken(const std::string& token) { +#if BUILDFLAG(IS_LINUX) + base::nix::SetActivationToken(token); +#endif +} + std::string App::GetLocale() { return g_browser_process->GetApplicationLocale(); } @@ -2175,6 +2181,7 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { .SetMethod("getPath", &App::GetPath) .SetMethod("setAppLogsPath", &App::SetAppLogsPath) .SetMethod("setDesktopName", &App::SetDesktopName) + .SetMethod("setActivationToken", &App::SetActivationToken) .SetMethod("getLocale", &App::GetLocale) .SetMethod("getPreferredSystemLanguages", &GetPreferredLanguages) .SetMethod("getSystemLocale", &App::GetSystemLocale) diff --git a/shell/browser/api/electron_api_app.h b/shell/browser/api/electron_api_app.h index ef17674a38..506789d787 100644 --- a/shell/browser/api/electron_api_app.h +++ b/shell/browser/api/electron_api_app.h @@ -211,6 +211,7 @@ class App final : public gin::Wrappable, const base::FilePath& path); void SetDesktopName(const std::string& desktop_name); + void SetActivationToken(const std::string& token); std::string GetLocale(); std::string GetLocaleCountryCode(); std::string GetSystemLocale(gin_helper::ErrorThrower thrower) const; diff --git a/spec/api-app.spec.ts b/spec/api-app.spec.ts index d409618024..04a35dbc30 100644 --- a/spec/api-app.spec.ts +++ b/spec/api-app.spec.ts @@ -2063,6 +2063,38 @@ describe('app module', () => { }); }); + ifdescribe(process.platform === 'linux')('app.setActivationToken(token)', () => { + it('throws when the token is not a string', () => { + expect(() => app.setActivationToken(1 as any)).toThrow(/Error processing argument at index 0/); + }); + + ifit(isWayland)('activates the next window with the token', async (ctx) => { + const child = cp.spawn( + process.execPath, + [path.join(fixturesPath, 'api', 'activation-token'), '--ozone-platform=wayland'], + { env: { ...process.env, WAYLAND_DEBUG: '1' }, stdio: ['ignore', 'ignore', 'pipe'] } + ); + defer(() => { + if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); + }); + let protocolLog = ''; + child.stderr.on('data', (chunk) => { + protocolLog += chunk; + }); + const hasActivationProtocol = () => protocolLog.includes('"xdg_activation_v1"'); + await waitUntil(() => { + expect(child.exitCode ?? child.signalCode, protocolLog).toBeNull(); + return ( + /xdg_activation_v1[#@]\d+\.activate\(/.test(protocolLog) || + (/xdg_surface[#@]\d+\.ack_configure\(/.test(protocolLog) && !hasActivationProtocol()) + ); + }); + + if (!hasActivationProtocol()) ctx.skip(); + expect(protocolLog).toMatch(/xdg_activation_v1[#@]\d+\.activate\("electron-test-token"/); + }); + }); + ifdescribe(process.platform === 'linux')('display server selection', () => { let runtimeDir: string; beforeEach(() => { diff --git a/spec/fixtures/api/activation-token/main.js b/spec/fixtures/api/activation-token/main.js new file mode 100644 index 0000000000..7594a05f9c --- /dev/null +++ b/spec/fixtures/api/activation-token/main.js @@ -0,0 +1,9 @@ +const { app, BrowserWindow } = require('electron'); + +// Stays open. The spec reads the window's activation request from the Wayland +// protocol log and then ends this process. +app.whenReady().then(() => { + app.setActivationToken('electron-test-token'); + const w = new BrowserWindow({ width: 300, height: 200 }); + w.once('closed', () => app.quit()); +}); diff --git a/spec/fixtures/api/activation-token/package.json b/spec/fixtures/api/activation-token/package.json new file mode 100644 index 0000000000..8ee3f16e8a --- /dev/null +++ b/spec/fixtures/api/activation-token/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-ozone-platform", + "main": "main.js" +}