Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add APIs to support mojave dark modes #14755

Merged
merged 2 commits into from Sep 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions atom/browser/api/atom_api_system_preferences.cc
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions atom/browser/api/atom_api_system_preferences.h
Expand Up @@ -89,6 +89,12 @@ class SystemPreferences : public mate::EventEmitter<SystemPreferences>
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<v8::Value> GetEffectiveAppearance(v8::Isolate* isolate);
v8::Local<v8::Value> GetAppLevelAppearance(v8::Isolate* isolate);
void SetAppLevelAppearance(mate::Arguments* args);
#endif
bool IsDarkMode();
bool IsInvertedColorScheme();
Expand Down
74 changes: 74 additions & 0 deletions atom/browser/api/atom_api_system_preferences_mac.mm
Expand Up @@ -8,13 +8,58 @@

#import <Cocoa/Cocoa.h>

#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<NSAppearance*> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
NSAppearance** out) {
if (val->IsNull()) {
*out = nil;
MarshallOfSound marked this conversation as resolved.
Show resolved Hide resolved
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<v8::Value> 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 {
Expand Down Expand Up @@ -323,6 +368,35 @@
return [NSEvent isSwipeTrackingFromScrollEventsEnabled];
}

v8::Local<v8::Value> SystemPreferences::GetEffectiveAppearance(
v8::Isolate* isolate) {
if (@available(macOS 10.14, *)) {
return mate::ConvertToV8(
isolate, [NSApplication sharedApplication].effectiveAppearance);
}
return v8::Null(isolate);
}

v8::Local<v8::Value> 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
15 changes: 15 additions & 0 deletions atom/browser/mac/atom_application.h
Expand Up @@ -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 <CrAppProtocol,
CrAppControlProtocol,
NSUserActivityDelegate> {
Expand Down
5 changes: 5 additions & 0 deletions atom/browser/mac/atom_application.mm
Expand Up @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion 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
Expand All @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions docs/api/system-preferences.md
Expand Up @@ -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_
Expand Down Expand Up @@ -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
37 changes: 37 additions & 0 deletions lib/browser/api/system-preferences.js
@@ -1,10 +1,47 @@
'use strict'

const { app } = require('electron')
const { EventEmitter } = require('events')
const { systemPreferences, SystemPreferences } = process.atomBinding('system_preferences')

// SystemPreferences is an EventEmitter.
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