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

~~Marked as draft for now since my [upstream change](https://chromium-review.googlesource.com/c/chromium/src/+/6831464) has not yet been reviewed/approved.~~

The included patch has been [merged upstream](https://chromium-review.googlesource.com/c/chromium/src/+/6831464) in Chromium.

#### Description of Change

* Adds support for `systemPreferences.getAccentColor` and `accent-color-changed` on Linux.
* Resolves #42622

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

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: @@ -10,6 +10,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 {
 
@@ -20,6 +26,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
@@ -44,6 +55,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) {
@@ -54,9 +88,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');
     });