From 79e0ba41a9bed627ffd5996edcc162023b9eb96a Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 22 Sep 2018 00:56:28 +1200 Subject: [PATCH] feat: add APIs to support mojave dark mode Closes #13387 --- .../api/atom_api_system_preferences.cc | 6 ++ .../browser/api/atom_api_system_preferences.h | 6 ++ .../api/atom_api_system_preferences_mac.mm | 73 +++++++++++++++++++ atom/browser/mac/atom_application.h | 15 ++++ atom/browser/mac/atom_application.mm | 5 ++ default_app/default_app.js | 4 +- docs/api/system-preferences.md | 48 ++++++++++++ lib/browser/api/browser-window.js | 2 +- lib/browser/api/system-preferences.js | 31 ++++++++ 9 files changed, 188 insertions(+), 2 deletions(-) diff --git a/atom/browser/api/atom_api_system_preferences.cc b/atom/browser/api/atom_api_system_preferences.cc index 0ce291e78d1c2..d008405fde98a 100644 --- a/atom/browser/api/atom_api_system_preferences.cc +++ b/atom/browser/api/atom_api_system_preferences.cc @@ -77,6 +77,12 @@ void SystemPreferences::BuildPrototype( .SetMethod("removeUserDefault", &SystemPreferences::RemoveUserDefault) .SetMethod("isSwipeTrackingFromScrollEventsEnabled", &SystemPreferences::IsSwipeTrackingFromScrollEventsEnabled) + .SetMethod("getEffectiveAppearance", + &SystemPreferences::GetEffectiveAppearance) + .SetMethod("getAppLevelAppearance", + &SystemPreferences::GetAppLevelAppearance) + .SetMethod("setAppLevelAppearance", + &SystemPreferences::SetAppLevelAppearance) #endif .SetMethod("isInvertedColorScheme", &SystemPreferences::IsInvertedColorScheme) diff --git a/atom/browser/api/atom_api_system_preferences.h b/atom/browser/api/atom_api_system_preferences.h index 7ea9d4cb29fbb..04fd31f7159d6 100644 --- a/atom/browser/api/atom_api_system_preferences.h +++ b/atom/browser/api/atom_api_system_preferences.h @@ -89,6 +89,12 @@ class SystemPreferences : public mate::EventEmitter mate::Arguments* args); void RemoveUserDefault(const std::string& name); bool IsSwipeTrackingFromScrollEventsEnabled(); + + // TODO(MarshallOfSound): Write tests for these methods once we + // are running tests on a Mojave machine + v8::Local GetEffectiveAppearance(v8::Isolate* isolate); + v8::Local GetAppLevelAppearance(v8::Isolate* isolate); + void SetAppLevelAppearance(mate::Arguments* args); #endif bool IsDarkMode(); bool IsInvertedColorScheme(); diff --git a/atom/browser/api/atom_api_system_preferences_mac.mm b/atom/browser/api/atom_api_system_preferences_mac.mm index 99fb216f5c48e..ecd5523dcd080 100644 --- a/atom/browser/api/atom_api_system_preferences_mac.mm +++ b/atom/browser/api/atom_api_system_preferences_mac.mm @@ -8,13 +8,57 @@ #import +#include "atom/browser/mac/atom_application.h" #include "atom/browser/mac/dict_util.h" #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/value_converter.h" #include "base/strings/sys_string_conversions.h" #include "base/values.h" +#include "native_mate/object_template_builder.h" #include "net/base/mac/url_conversions.h" +namespace mate { +template <> +struct Converter { + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + NSAppearance** out) { + if (val->IsNull()) { + *out = nil; + } else { + std::string name; + if (!mate::ConvertFromV8(isolate, val, &name)) { + return false; + } + if (name == "light") { + *out = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + return true; + } else if (name == "dark") { + *out = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + return true; + } + + return false; + } + return true; + } + + static v8::Local ToV8(v8::Isolate* isolate, NSAppearance* val) { + if (val == nil) { + return v8::Null(isolate); + } + + if (val.name == NSAppearanceNameAqua) { + return mate::ConvertToV8(isolate, "light"); + } else if (val.name == NSAppearanceNameDarkAqua) { + return mate::ConvertToV8(isolate, "dark"); + } + + return mate::ConvertToV8(isolate, "unknown"); + } +}; +} // namespace mate + namespace atom { namespace api { @@ -323,6 +367,35 @@ return [NSEvent isSwipeTrackingFromScrollEventsEnabled]; } +v8::Local SystemPreferences::GetEffectiveAppearance( + v8::Isolate* isolate) { + if (@available(macOS 10.14, *)) { + return mate::ConvertToV8( + isolate, [NSApplication sharedApplication].effectiveAppearance); + } + return v8::Null(isolate); +} + +v8::Local SystemPreferences::GetAppLevelAppearance( + v8::Isolate* isolate) { + if (@available(macOS 10.14, *)) { + return mate::ConvertToV8(isolate, + [NSApplication sharedApplication].appearance); + } + return v8::Null(isolate); +} + +void SystemPreferences::SetAppLevelAppearance(mate::Arguments* args) { + if (@available(macOS 10.14, *)) { + NSAppearance* appearance; + if (args->GetNext(&appearance)) { + [[NSApplication sharedApplication] setAppearance:appearance]; + } else { + args->ThrowError("Invalid app appearance provided as first argument"); + } + } +} + } // namespace api } // namespace atom diff --git a/atom/browser/mac/atom_application.h b/atom/browser/mac/atom_application.h index 81e66bff1d638..7eae3813faf92 100644 --- a/atom/browser/mac/atom_application.h +++ b/atom/browser/mac/atom_application.h @@ -6,6 +6,21 @@ #include "base/mac/scoped_nsobject.h" #include "base/mac/scoped_sending_event.h" +// Forward Declare Appareance APIs +@interface NSApplication (HighSierraSDK) +@property(copy, readonly) + NSAppearance* effectiveAppearance API_AVAILABLE(macosx(10.14)); +@property(copy, readonly) NSAppearance* appearance API_AVAILABLE(macosx(10.14)); +- (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14)); +@end + +extern "C" { +#if !defined(MAC_OS_X_VERSION_10_14) || \ + MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 +BASE_EXPORT extern NSString* const NSAppearanceNameDarkAqua; +#endif // MAC_OS_X_VERSION_10_14 +} // extern "C" + @interface AtomApplication : NSApplication { diff --git a/atom/browser/mac/atom_application.mm b/atom/browser/mac/atom_application.mm index eb00a415bf243..35cab8b0f649b 100644 --- a/atom/browser/mac/atom_application.mm +++ b/atom/browser/mac/atom_application.mm @@ -11,6 +11,11 @@ #include "base/strings/sys_string_conversions.h" #include "content/public/browser/browser_accessibility_state.h" +#if !defined(MAC_OS_X_VERSION_10_14) || \ + MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 +NSString* const NSAppearanceNameDarkAqua = @"NSAppearanceNameDarkAqua"; +#endif // MAC_OS_X_VERSION_10_14 + namespace { inline void dispatch_sync_main(dispatch_block_t block) { diff --git a/default_app/default_app.js b/default_app/default_app.js index c4621c748feba..ae4137861e515 100644 --- a/default_app/default_app.js +++ b/default_app/default_app.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow } = require('electron') +const { app, BrowserWindow, systemPreferences } = require('electron') const path = require('path') let mainWindow = null @@ -11,6 +11,8 @@ app.on('window-all-closed', () => { exports.load = async (appUrl) => { await app.whenReady() + systemPreferences.startAppLevelAppearanceTrackingOS() + const options = { width: 900, height: 600, diff --git a/docs/api/system-preferences.md b/docs/api/system-preferences.md index ab4b91dbaf9ec..b9e8b7f52e802 100644 --- a/docs/api/system-preferences.md +++ b/docs/api/system-preferences.md @@ -35,6 +35,14 @@ Returns: * `invertedColorScheme` Boolean - `true` if an inverted color scheme, such as a high contrast theme, is being used, `false` otherwise. +### Event: 'appearance-changed' _macOS_ + +Returns: + +* `newAppearance` String - Can be `dark` or `light` + +**NOTE:** This event is only emitted after you have called `startAppLevelAppearanceTrackingOS` + ## Methods ### `systemPreferences.isDarkMode()` _macOS_ @@ -274,3 +282,43 @@ Returns `Boolean` - `true` if an inverted color scheme, such as a high contrast theme, is active, `false` otherwise. [windows-colors]:https://msdn.microsoft.com/en-us/library/windows/desktop/ms724371(v=vs.85).aspx + +### `systemPreferences.getEffectiveAppearance()` _macOS_ + +Returns `String` - Can be `dark`, `light` or `unknown`. + +Gets the macOS appearance setting that is currently applied to your application, +maps to [NSApplication.effectiverAppearance](https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance?language=objc) + +Please note that because Electron is not built targetting the 10.14 SDK your applications +`effectiveAppearance` will always default to "light" and never automatically inherit the OS +level setting. We have provided a helper method `startAppLevelAppearanceTrackingOS()` which +will emulate this behavior until we start targetting the 10.14 SDK. + +### `systemPreferences.getAppLevelAppearance()` _macOS_ + +Returns `String` | `null` - Can be `dark`, `light` or `unknown`. + +Gets the macOS appearance setting that is you have declared you want for +your application, maps to [NSApplication.appearance](https://developer.apple.com/documentation/appkit/nsapplication/2967170-appearance?language=objc). +You can use the `setAppLevelAppearance` API to set this value. + +### `systemPreferences.setAppLevelAppearance(appearance)` _macOS_ + +* `appearance` String | null - Can be `dark` or `light` + +Sets the appearance setting for your application, this should override the +system default and override the value of `getEffectiveAppearance`. + +### `systemPreferences.startAppLevelAppearanceTrackingOS()` _macOS_ + +This is a helper method to make your applications "appearance" setting track the +users OS level appearance setting. I.e. Your app will have dark mode enabled if +the users system has dark mode enabled. + +You can track this automatic change with the `appearance-changed` event. + +### `systemPreferences.stopAppLevelAppearanceTrackingOS()` _macOS_ + +This is a helper method to stop your application tracking the OS level appearance +setting. It is a no-op if you have not called `startAppLevelAppearanceTrackingOS()` diff --git a/lib/browser/api/browser-window.js b/lib/browser/api/browser-window.js index 7f083edb127ed..1cc4ba429f8f3 100644 --- a/lib/browser/api/browser-window.js +++ b/lib/browser/api/browser-window.js @@ -141,7 +141,7 @@ BrowserWindow.fromId = (id) => { } BrowserWindow.getAllWindows = () => { - return TopLevelWindow.getAllWindows().filter(isBrowserWindow) + return TopLevelWindow.getAllWindows() } BrowserWindow.getFocusedWindow = () => { diff --git a/lib/browser/api/system-preferences.js b/lib/browser/api/system-preferences.js index 5d963baae84ad..1b0366ebc2e22 100644 --- a/lib/browser/api/system-preferences.js +++ b/lib/browser/api/system-preferences.js @@ -1,3 +1,4 @@ +const { app } = require('electron') const { EventEmitter } = require('events') const { systemPreferences, SystemPreferences } = process.atomBinding('system_preferences') @@ -5,4 +6,34 @@ const { systemPreferences, SystemPreferences } = process.atomBinding('system_pre Object.setPrototypeOf(SystemPreferences.prototype, EventEmitter.prototype) EventEmitter.call(systemPreferences) +let appearanceTrackingSubscriptionID = null + +systemPreferences.startAppLevelAppearanceTrackingOS = () => { + if (appearanceTrackingSubscriptionID !== null) return + + const updateAppearanceBasedOnOS = () => { + const newAppearance = systemPreferences.isDarkMode() + ? 'dark' + : 'light' + + systemPreferences.emit('appearance-changed', newAppearance) + systemPreferences.setAppLevelAppearance(newAppearance) + } + + appearanceTrackingSubscriptionID = systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + updateAppearanceBasedOnOS + ) + + updateAppearanceBasedOnOS() +} + +systemPreferences.stopAppLevelAppearanceTrackingOS = () => { + if (appearanceTrackingSubscriptionID === null) return + + systemPreferences.unsubscribeNotification(appearanceTrackingSubscriptionID) +} + +app.on('quit', systemPreferences.stopAppLevelAppearanceTrackingOS) + module.exports = systemPreferences