Repository: electron/electron
Pull request: feat: Add `getAccentColor` on Linux

Backport of #48027

See that PR for details.


Notes: Added the ability to retrieve the system accent color on Linux using `systemPreferences.getAccentColor`.


Changed files:
filename: docs/api/system-preferences.md
patch: @@ -14,7 +14,7 @@ console.log(systemPreferences.getEffectiveAppearance())
 
 The `systemPreferences` object emits the following events:
 
-### Event: 'accent-color-changed' _Windows_
+### Event: 'accent-color-changed' _Windows_ _Linux_
 
 Returns:
 
@@ -182,7 +182,7 @@ Some popular `key` and `type`s are:
 Removes the `key` in `NSUserDefaults`. This can be used to restore the default
 or global value of a `key` previously set with `setUserDefault`.
 
-### `systemPreferences.getAccentColor()` _Windows_ _macOS_
+### `systemPreferences.getAccentColor()`
 
 Returns `string` - The users current system wide accent color preference in RGBA
 hexadecimal form.

filename: shell/browser/api/electron_api_system_preferences.cc
patch: @@ -11,6 +11,12 @@
 #include "shell/common/gin_helper/handle.h"
 #include "shell/common/node_includes.h"
 #include "ui/gfx/animation/animation.h"
+#if BUILDFLAG(IS_LINUX)
+#include "content/public/browser/browser_task_traits.h"
+#include "content/public/browser/browser_thread.h"
+#include "shell/browser/api/electron_api_system_preferences.h"
+#include "shell/common/color_util.h"
+#endif
 
 namespace electron::api {
 
@@ -21,6 +27,11 @@ gin::DeprecatedWrapperInfo SystemPreferences::kWrapperInfo = {
 SystemPreferences::SystemPreferences() {
   InitializeWindow();
 }
+#elif BUILDFLAG(IS_LINUX)
+SystemPreferences::SystemPreferences()
+    : ui_theme_(ui::NativeTheme::GetInstanceForNativeUi()) {
+  ui_theme_->AddObserver(this);
+}
 #else
 SystemPreferences::SystemPreferences() = default;
 #endif
@@ -45,6 +56,29 @@ v8::Local<v8::Value> SystemPreferences::GetAnimationSettings(
   return dict.GetHandle();
 }
 
+#if BUILDFLAG(IS_LINUX)
+std::string SystemPreferences::GetAccentColor() {
+  auto const color = ui_theme_->user_color();
+  if (!color.has_value())
+    return "";
+  return ToRGBAHex(*color);
+}
+
+void SystemPreferences::OnNativeThemeUpdatedOnUI() {
+  auto const new_accent_color = GetAccentColor();
+  if (current_accent_color_ == new_accent_color)
+    return;
+  Emit("accent-color-changed", new_accent_color);
+  current_accent_color_ = new_accent_color;
+}
+
+void SystemPreferences::OnNativeThemeUpdated(ui::NativeTheme* theme) {
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE, base::BindOnce(&SystemPreferences::OnNativeThemeUpdatedOnUI,
+                                base::Unretained(this)));
+}
+#endif
+
 // static
 gin_helper::Handle<SystemPreferences> SystemPreferences::Create(
     v8::Isolate* isolate) {
@@ -55,9 +89,9 @@ gin::ObjectTemplateBuilder SystemPreferences::GetObjectTemplateBuilder(
     v8::Isolate* isolate) {
   return gin_helper::EventEmitterMixin<
              SystemPreferences>::GetObjectTemplateBuilder(isolate)
+      .SetMethod("getAccentColor", &SystemPreferences::GetAccentColor)
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
       .SetMethod("getColor", &SystemPreferences::GetColor)
-      .SetMethod("getAccentColor", &SystemPreferences::GetAccentColor)
       .SetMethod("getMediaAccessStatus",
                  &SystemPreferences::GetMediaAccessStatus)
 #endif

filename: shell/browser/api/electron_api_system_preferences.h
patch: @@ -18,6 +18,11 @@
 #include "shell/browser/browser.h"
 #include "shell/browser/browser_observer.h"
 #endif
+#if BUILDFLAG(IS_LINUX)
+#include "base/memory/raw_ptr.h"
+#include "ui/native_theme/native_theme.h"
+#include "ui/native_theme/native_theme_observer.h"
+#endif
 
 namespace gin_helper {
 template <typename T>
@@ -44,6 +49,9 @@ class SystemPreferences final
 #if BUILDFLAG(IS_WIN)
     ,
       public BrowserObserver
+#elif BUILDFLAG(IS_LINUX)
+    ,
+      public ui::NativeThemeObserver
 #endif
 {
  public:
@@ -55,8 +63,8 @@ class SystemPreferences final
       v8::Isolate* isolate) override;
   const char* GetTypeName() override;
 
+  std::string GetAccentColor();
 #if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
-  static std::string GetAccentColor();
   std::string GetColor(gin_helper::ErrorThrower thrower,
                        const std::string& color);
   std::string GetMediaAccessStatus(gin_helper::ErrorThrower thrower,
@@ -117,6 +125,10 @@ class SystemPreferences final
   // TODO(MarshallOfSound): Write tests for these methods once we
   // are running tests on a Mojave machine
   v8::Local<v8::Value> GetEffectiveAppearance(v8::Isolate* isolate);
+
+#elif BUILDFLAG(IS_LINUX)
+  // ui::NativeThemeObserver:
+  void OnNativeThemeUpdated(ui::NativeTheme* theme) override;
 #endif
   v8::Local<v8::Value> GetAnimationSettings(v8::Isolate* isolate);
 
@@ -162,6 +174,12 @@ class SystemPreferences final
   // Color/high contrast mode change observer.
   base::CallbackListSubscription hwnd_subscription_;
 #endif
+#if BUILDFLAG(IS_LINUX)
+  void OnNativeThemeUpdatedOnUI();
+
+  raw_ptr<ui::NativeTheme> ui_theme_;
+  std::string current_accent_color_;
+#endif
 };
 
 }  // namespace electron::api

filename: spec/api-system-preferences-spec.ts
patch: @@ -2,11 +2,17 @@ import { systemPreferences } from 'electron/main';
 
 import { expect } from 'chai';
 
-import { ifdescribe } from './lib/spec-helpers';
+import { ifdescribe, ifit } from './lib/spec-helpers';
 
 describe('systemPreferences module', () => {
-  ifdescribe(process.platform === 'win32')('systemPreferences.getAccentColor', () => {
-    it('should return a non-empty string', () => {
+  ifdescribe(['win32', 'linux'].includes(process.platform))('systemPreferences.getAccentColor', () => {
+    ifit(process.platform === 'linux')('should return a string', () => {
+      // Testing this properly (i.e. non-empty string) requires
+      // some tricky D-Bus mock setup.
+      const accentColor = systemPreferences.getAccentColor();
+      expect(accentColor).to.be.a('string');
+    });
+    ifit(process.platform === 'win32')('should return a non-empty string', () => {
       const accentColor = systemPreferences.getAccentColor();
       expect(accentColor).to.be.a('string').that.is.not.empty('accent color');
     });