From 3f549120e4af1aafa11361b8e874fd29a70a681f Mon Sep 17 00:00:00 2001 From: Jeremy Rose Date: Thu, 4 Aug 2022 12:11:23 -0700 Subject: [PATCH] feat: add more functions for NSApp activation state --- docs/api/app.md | 49 +++++++++++++++++-- shell/browser/api/electron_api_app.cc | 8 +++ shell/browser/api/electron_api_app.h | 4 ++ shell/browser/api/electron_api_app_mac.mm | 15 ++++++ shell/browser/browser.cc | 5 ++ shell/browser/browser.h | 2 + shell/browser/browser_observer.h | 2 + .../mac/electron_application_delegate.mm | 4 ++ spec-main/api-app-spec.ts | 49 ++++++++++++++++++- 9 files changed, 134 insertions(+), 4 deletions(-) diff --git a/docs/api/app.md b/docs/api/app.md index 960bed3dc8782..c5c5e71832aa2 100755 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -153,9 +153,23 @@ Returns: * `event` Event -Emitted when mac application become active. Difference from `activate` event is -that `did-become-active` is emitted every time the app becomes active, not only -when Dock icon is clicked or application is re-launched. +Emitted when the application becomes active (i.e., the macOS title bar is +showing the name of the app). The difference between this and the `activate` +event is that `did-become-active` is emitted every time the app becomes active, +whereas `activate` is only emitted when the Dock icon is clicked or the +application is re-launched. + +See [`-[NSApplicationDelegate applicationDidBecomeActive:]`](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428577-applicationdidbecomeactive?language=objc) + +### Event: 'did-resign-active' _macOS_ + +Returns: + +* `event` Event + +Emitted when the application loses active state. + +See [`-[NSApplicationDelegate applicationDidResignActive:]`](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428636-applicationdidresignactive?language=objc) ### Event: 'continue-activity' _macOS_ @@ -1426,6 +1440,28 @@ details. **Note:** Enable `Secure Keyboard Entry` only when it is needed and disable it when it is no longer needed. +### `app.isActive()` _macOS_ + +Returns `boolean` - `true` if the app is currently active (i.e., the macOS +title bar is showing the name of the app). + +See [`-[NSApplication active]`](https://developer.apple.com/documentation/appkit/nsapplication/1428493-active?language=objc). + +### `app.activate([opts])` _macOS_ + +* `opts` Object (optional) + * `ignoreOtherApps` boolean - If false, the app is activated only if no other + app is currently active. If true, the app activates regardless. Defaults to + false. + +See [`-[NSApplication activateIgnoringOtherApps:]`](https://developer.apple.com/documentation/appkit/nsapplication/1428468-activateignoringotherapps?language=objc). + +### `app.deactivate()` _macOS_ + +Deactivates the app. + +See [`-[NSApplication deactivate]`](https://developer.apple.com/documentation/appkit/nsapplication/1428428-deactivate?language=objc). + ## Properties ### `app.accessibilitySupportEnabled` _macOS_ _Windows_ @@ -1438,6 +1474,13 @@ This API must be called after the `ready` event is emitted. **Note:** Rendering accessibility tree can significantly affect the performance of your app. It should not be enabled by default. +### `app.active` _macOS_ _Readonly_ + +A `boolean` property, `true` if the app is currently active (i.e., the macOS +title bar is showing the name of the app). + +See [`-[NSApplication active]`](https://developer.apple.com/documentation/appkit/nsapplication/1428493-active?language=objc). + ### `app.applicationMenu` A `Menu | null` property that returns [`Menu`](menu.md) if one has been set and `null` otherwise. diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index c3f9bf1ed827c..ef60e337059bf 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -782,6 +782,10 @@ void App::OnNewWindowForTab() { void App::OnDidBecomeActive() { Emit("did-become-active"); } + +void App::OnDidResignActive() { + Emit("did-resign-active"); +} #endif bool App::CanCreateWindow( @@ -1742,6 +1746,9 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { .SetMethod("moveToApplicationsFolder", &App::MoveToApplicationsFolder) .SetMethod("isInApplicationsFolder", &App::IsInApplicationsFolder) .SetMethod("setActivationPolicy", &App::SetActivationPolicy) + .SetMethod("isActive", &App::IsActive) + .SetMethod("activate", &App::Activate) + .SetMethod("deactivate", &App::Deactivate) #endif .SetMethod("setAboutPanelOptions", base::BindRepeating(&Browser::SetAboutPanelOptions, browser)) @@ -1805,6 +1812,7 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { .SetProperty("dock", &App::GetDockAPI) .SetProperty("runningUnderRosettaTranslation", &App::IsRunningUnderRosettaTranslation) + .SetProperty("active", &App::IsActive) #endif #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) .SetProperty("runningUnderARM64Translation", diff --git a/shell/browser/api/electron_api_app.h b/shell/browser/api/electron_api_app.h index c253088deb64d..68bfd7c735d8d 100644 --- a/shell/browser/api/electron_api_app.h +++ b/shell/browser/api/electron_api_app.h @@ -116,6 +116,7 @@ class App : public ElectronBrowserClient::Delegate, base::Value::Dict user_info) override; void OnNewWindowForTab() override; void OnDidBecomeActive() override; + void OnDidResignActive() override; #endif // content::ContentBrowserClient: @@ -227,6 +228,9 @@ class App : public ElectronBrowserClient::Delegate, bool IsInApplicationsFolder(); v8::Local GetDockAPI(v8::Isolate* isolate); bool IsRunningUnderRosettaTranslation() const; + bool IsActive() const; + void Activate(absl::optional opts) const; + void Deactivate() const; v8::Global dock_; #endif diff --git a/shell/browser/api/electron_api_app_mac.mm b/shell/browser/api/electron_api_app_mac.mm index 3a42f85f6259e..25ad7c51e69ea 100644 --- a/shell/browser/api/electron_api_app_mac.mm +++ b/shell/browser/api/electron_api_app_mac.mm @@ -80,4 +80,19 @@ return proc_translated == 1; } +bool App::IsActive() const { + return NSApp.active; +} + +void App::Activate(absl::optional opts) const { + bool ignore_other_apps = false; + if (opts) + opts->Get("ignoreOtherApps", &ignore_other_apps); + [NSApp activateIgnoringOtherApps:ignore_other_apps]; +} + +void App::Deactivate() const { + [NSApp deactivate]; +} + } // namespace electron::api diff --git a/shell/browser/browser.cc b/shell/browser/browser.cc index 959b2cc3ec447..62c65e3888178 100644 --- a/shell/browser/browser.cc +++ b/shell/browser/browser.cc @@ -285,6 +285,11 @@ void Browser::DidBecomeActive() { for (BrowserObserver& observer : observers_) observer.OnDidBecomeActive(); } + +void Browser::DidResignActive() { + for (BrowserObserver& observer : observers_) + observer.OnDidResignActive(); +} #endif } // namespace electron diff --git a/shell/browser/browser.h b/shell/browser/browser.h index 7fc47ef826dad..a0584189a8308 100644 --- a/shell/browser/browser.h +++ b/shell/browser/browser.h @@ -278,6 +278,8 @@ class Browser : public WindowListObserver { // Tell the application that application did become active void DidBecomeActive(); + + void DidResignActive(); #endif // BUILDFLAG(IS_MAC) // Tell the application that application is activated with visible/invisible diff --git a/shell/browser/browser_observer.h b/shell/browser/browser_observer.h index 16576a1e586a8..18d21c498b082 100644 --- a/shell/browser/browser_observer.h +++ b/shell/browser/browser_observer.h @@ -84,6 +84,8 @@ class BrowserObserver : public base::CheckedObserver { // Browser did become active. virtual void OnDidBecomeActive() {} + + virtual void OnDidResignActive() {} #endif protected: diff --git a/shell/browser/mac/electron_application_delegate.mm b/shell/browser/mac/electron_application_delegate.mm index 7dc351f4cc654..650ec359d52da 100644 --- a/shell/browser/mac/electron_application_delegate.mm +++ b/shell/browser/mac/electron_application_delegate.mm @@ -92,6 +92,10 @@ - (void)applicationDidBecomeActive:(NSNotification*)notification { electron::Browser::Get()->DidBecomeActive(); } +- (void)applicationDidResignActive:(NSNotification*)notification { + electron::Browser::Get()->DidResignActive(); +} + - (NSMenu*)applicationDockMenu:(NSApplication*)sender { if (menu_controller_) return [menu_controller_ menu]; diff --git a/spec-main/api-app-spec.ts b/spec-main/api-app-spec.ts index 347dae24d2b79..b694bed1110ce 100644 --- a/spec-main/api-app-spec.ts +++ b/spec-main/api-app-spec.ts @@ -9,7 +9,7 @@ import { promisify } from 'util'; import { app, BrowserWindow, Menu, session, net as electronNet } from 'electron/main'; import { emittedOnce } from './events-helpers'; import { closeWindow, closeAllWindows } from './window-helpers'; -import { ifdescribe, ifit, waitUntil } from './spec-helpers'; +import { defer, ifdescribe, ifit, waitUntil } from './spec-helpers'; import split = require('split') const fixturesPath = path.resolve(__dirname, '../spec/fixtures'); @@ -1837,6 +1837,53 @@ describe('app module', () => { })).to.eventually.be.rejectedWith(/ERR_NAME_NOT_RESOLVED/); }); }); + + ifdescribe(process.platform === 'darwin')('active state', () => { + afterEach(closeAllWindows); + it('is inactive after deactivating', async () => { + const b = new BrowserWindow(); + // It's not clear what state the app will be in prior to the test, so get + // to a known state first. + if (!app.active) { + app.activate({ ignoreOtherApps: true }); + await emittedOnce(app, 'did-become-active'); + expect(app.active).to.be.true(); + } + + // [NSApplication deactivate] confusingly doesn't actually deactivate the + // app. But it does blur the windows? So let's test that. + b.focus(); + expect(b.isFocused()).to.be.true(); + app.deactivate(); + expect(b.isFocused()).to.be.false(); + }); + + it('is active after activating', async () => { + // If the app is active, force it to be inactivated. + // app.deactivate() doesn't actually do that, for unknown reasons. But + // setActivationPolicy('prohibited') works, so let's do that instead. + if (app.active) { + app.setActivationPolicy('prohibited'); + defer(() => app.setActivationPolicy('regular')); + await emittedOnce(app, 'did-resign-active'); + app.setActivationPolicy('regular'); + expect(app.active).to.be.false(); + } + app.activate({ ignoreOtherApps: true }); + await emittedOnce(app, 'did-become-active'); + expect(app.active).to.be.true(); + }); + + it('emits did-resign-active', async () => { + app.activate({ ignoreOtherApps: true }); + expect(app.active).to.be.true(); + app.setActivationPolicy('prohibited'); + defer(() => app.setActivationPolicy('regular')); + await emittedOnce(app, 'did-resign-active'); + app.setActivationPolicy('regular'); + expect(app.active).to.be.false(); + }); + }); }); describe('default behavior', () => {