From 0d2a0c758353667aaf37450612585562a9ef1b55 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 28 Sep 2018 01:33:31 +1000 Subject: [PATCH] feat: add APIs to support mojave dark modes (#14755) * feat: add APIs to support mojave dark mode Closes #13387 * docs: fix system-prefs typo --- .../api/atom_api_system_preferences.cc | 6 ++ .../browser/api/atom_api_system_preferences.h | 6 ++ .../api/atom_api_system_preferences_mac.mm | 74 +++++++++++++++++++ atom/browser/mac/atom_application.h | 15 ++++ atom/browser/mac/atom_application.mm | 5 ++ default_app/default_app.js | 6 +- docs/api/system-preferences.md | 56 ++++++++++++++ lib/browser/api/system-preferences.js | 37 ++++++++++ 8 files changed, 204 insertions(+), 1 deletion(-) diff --git a/atom/browser/api/atom_api_system_preferences.cc b/atom/browser/api/atom_api_system_preferences.cc index 0ce291e78d1c..d008405fde98 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 7ea9d4cb29fb..04fd31f7159d 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 99fb216f5c48..814abe7875be 100644 --- a/atom/browser/api/atom_api_system_preferences_mac.mm +++ b/atom/browser/api/atom_api_system_preferences_mac.mm @@ -8,13 +8,58 @@ #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; + return true; + } + + 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; + } + + 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"); + } + if (val.name == NSAppearanceNameDarkAqua) { + return mate::ConvertToV8(isolate, "dark"); + } + + return mate::ConvertToV8(isolate, "unknown"); + } +}; +} // namespace mate + namespace atom { namespace api { @@ -323,6 +368,35 @@ bool SystemPreferences::IsSwipeTrackingFromScrollEventsEnabled() { 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 81e66bff1d63..7eae3813faf9 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 eb00a415bf24..35cab8b0f649 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 c4621c748feb..60a81a618889 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,10 @@ app.on('window-all-closed', () => { exports.load = async (appUrl) => { await app.whenReady() + if (process.platform === 'darwin') { + systemPreferences.startAppLevelAppearanceTrackingOS() + } + const options = { width: 900, height: 600, diff --git a/docs/api/system-preferences.md b/docs/api/system-preferences.md index ab4b91dbaf9e..58e27e0bba4f 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,51 @@ 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.effectiveAppearance](https://developer.apple.com/documentation/appkit/nsapplication/2967171-effectiveappearance?language=objc) + +Please note that until Electron is built targeting the 10.14 SDK, your application's +`effectiveAppearance` will default to 'light' and won't inherit the OS preference. In +the interim we have provided a helper method `startAppLevelAppearanceTrackingOS()` +which emulates this behavior. + +### `systemPreferences.getAppLevelAppearance()` _macOS_ + +Returns `String` | `null` - Can be `dark`, `light` or `unknown`. + +Gets the macOS appearance setting that 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 application's "appearance" setting track the +user's OS level appearance setting. I.e. your app will have dark mode enabled if +the user's system has dark mode enabled. + +You can track this automatic change with the `appearance-changed` event. + +**Note:** This method is exempt from our standard deprecation cycle and will be removed +without deprecation in an upcoming major release of Electron as soon as we target the 10.14 +SDK + +### `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()` + +**Note:** This method is exempt from our standard deprecation cycle and will be removed +without deprecation in an upcoming major release of Electron as soon as we target the 10.14 +SDK diff --git a/lib/browser/api/system-preferences.js b/lib/browser/api/system-preferences.js index bb663e886fd3..9460e533b2b9 100644 --- a/lib/browser/api/system-preferences.js +++ b/lib/browser/api/system-preferences.js @@ -1,5 +1,6 @@ 'use strict' +const { app } = require('electron') const { EventEmitter } = require('events') const { systemPreferences, SystemPreferences } = process.atomBinding('system_preferences') @@ -7,4 +8,40 @@ const { systemPreferences, SystemPreferences } = process.atomBinding('system_pre Object.setPrototypeOf(SystemPreferences.prototype, EventEmitter.prototype) EventEmitter.call(systemPreferences) +if (process.platform === 'darwin') { + let appearanceTrackingSubscriptionID = null + + systemPreferences.startAppLevelAppearanceTrackingOS = () => { + if (appearanceTrackingSubscriptionID !== null) return + + const updateAppearanceBasedOnOS = () => { + const newAppearance = systemPreferences.isDarkMode() + ? 'dark' + : 'light' + + if (systemPreferences.getAppLevelAppearance() !== newAppearance) { + systemPreferences.setAppLevelAppearance(newAppearance) + // TODO(MarshallOfSound): Once we remove this logic and build against 10.14 + // SDK we should re-implement this event as a monitor of `effectiveAppearance` + systemPreferences.emit('appearance-changed', newAppearance) + } + } + + appearanceTrackingSubscriptionID = systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + updateAppearanceBasedOnOS + ) + + updateAppearanceBasedOnOS() + } + + systemPreferences.stopAppLevelAppearanceTrackingOS = () => { + if (appearanceTrackingSubscriptionID === null) return + + systemPreferences.unsubscribeNotification(appearanceTrackingSubscriptionID) + } + + app.on('quit', systemPreferences.stopAppLevelAppearanceTrackingOS) +} + module.exports = systemPreferences