diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7c0577d3..70bec4b8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev libfwupd-dev libpackagekit-glib2-dev meson valac + apt install -y libaccountsservice-dev libdbus-1-dev libgranite-dev libgeoclue-2-dev libfwupd-dev libpackagekit-glib2-dev libgexiv2-dev meson valac - name: Build env: DESTDIR: out diff --git a/meson.build b/meson.build index 28b576b4..9c994edc 100644 --- a/meson.build +++ b/meson.build @@ -10,6 +10,7 @@ gio_dep = dependency ('gio-2.0') glib_dep = dependency('glib-2.0') granite_dep = dependency('granite', version: '>= 5.3.0') pk_dep = dependency('packagekit-glib2') +gexiv2_dep = dependency('gexiv2') i18n = import('i18n') gettext_name = meson.project_name() diff --git a/src/AccountsService.vala b/src/AccountsService.vala index f7405b3d..99db8318 100644 --- a/src/AccountsService.vala +++ b/src/AccountsService.vala @@ -80,6 +80,7 @@ public interface SettingsDaemon.AccountsService : Object { [DBus (name = "io.elementary.pantheon.AccountsService")] public interface Pantheon.AccountsService : Object { public abstract int prefers_color_scheme { get; set; } + public abstract int prefers_accent_color { get; set; } } [DBus (name = "org.freedesktop.DisplayManager.AccountsService")] diff --git a/src/Application.vala b/src/Application.vala index 1efb885e..33e71348 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -19,6 +19,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application { private Backends.PrefersColorSchemeSettings prefers_color_scheme_settings; private Backends.Housekeeping housekeeping; + private Backends.AccentColorManager accent_color_manager; private const string FDO_ACCOUNTS_NAME = "org.freedesktop.Accounts"; private const string FDO_ACCOUNTS_PATH = "/org/freedesktop/Accounts"; @@ -117,6 +118,7 @@ public sealed class SettingsDaemon.Application : Gtk.Application { try { pantheon_service = yield connection.get_proxy (FDO_ACCOUNTS_NAME, path, GET_INVALIDATED_PROPERTIES); prefers_color_scheme_settings = new Backends.PrefersColorSchemeSettings (pantheon_service); + accent_color_manager = new Backends.AccentColorManager (pantheon_service); } catch { warning ("Unable to get pantheon's AccountsService proxy, color scheme preference may be incorrect"); } diff --git a/src/Backends/AccentColor/AccentColorManager.vala b/src/Backends/AccentColor/AccentColorManager.vala new file mode 100644 index 00000000..d2154532 --- /dev/null +++ b/src/Backends/AccentColor/AccentColorManager.vala @@ -0,0 +1,172 @@ +/* + * Copyright 2021-2024 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored by: Marius Meisenzahl + */ + +public class SettingsDaemon.Backends.AccentColorManager : GLib.Object { + private class NamedColor : GLib.Object { + public string name { get; construct set; } + public string theme { get; construct set; } + public uint32 color { get; construct set; } + + public NamedColor (string name, string theme, uint32 color) { + Object ( + name: name, + theme: theme, + color: color + ); + } + } + + private const string INTERFACE_SCHEMA = "org.gnome.desktop.interface"; + private const string STYLESHEET_KEY = "gtk-theme"; + private const string TAG_ACCENT_COLOR = "Xmp.xmp.io.elementary.AccentColor"; + + private const string THEME_BLUE = "io.elementary.stylesheet.blueberry"; + private const string THEME_MINT = "io.elementary.stylesheet.mint"; + private const string THEME_GREEN = "io.elementary.stylesheet.lime"; + private const string THEME_YELLOW = "io.elementary.stylesheet.banana"; + private const string THEME_ORANGE = "io.elementary.stylesheet.orange"; + private const string THEME_RED = "io.elementary.stylesheet.strawberry"; + private const string THEME_PINK = "io.elementary.stylesheet.bubblegum"; + private const string THEME_PURPLE = "io.elementary.stylesheet.grape"; + private const string THEME_BROWN = "io.elementary.stylesheet.cocoa"; + private const string THEME_GRAY = "io.elementary.stylesheet.slate"; + + public Pantheon.AccountsService accounts_service { get; construct; } + + private GLib.Settings background_settings; + private GLib.Settings interface_settings; + + private NamedColor[] theme_colors = { + new NamedColor ("Blue", THEME_BLUE, 0x3689e6), + new NamedColor ("Mint", THEME_MINT, 0x28bca3), + new NamedColor ("Green", THEME_GREEN, 0x68b723), + new NamedColor ("Yellow", THEME_YELLOW, 0xf9c440), + new NamedColor ("Orange", THEME_ORANGE, 0xffa154), + new NamedColor ("Red", THEME_RED, 0xed5353), + new NamedColor ("Pink", THEME_PINK, 0xde3e80), + new NamedColor ("Purple", THEME_PURPLE, 0xa56de2), + new NamedColor ("Brown", THEME_BROWN, 0x8a715e), + new NamedColor ("Gray", THEME_GRAY, 0x667885) + }; + + public AccentColorManager (Pantheon.AccountsService accounts_service) { + Object (accounts_service: accounts_service); + } + + construct { + background_settings = new GLib.Settings ("org.gnome.desktop.background"); + interface_settings = new GLib.Settings (INTERFACE_SCHEMA); + + ((DBusProxy) accounts_service).g_properties_changed.connect (() => { + update_accent_color (); + }); + + background_settings.changed["picture-options"].connect (update_accent_color); + background_settings.changed["picture-uri"].connect (update_accent_color); + background_settings.changed["primary-color"].connect (update_accent_color); + + update_accent_color (); + } + + private void update_accent_color () { + bool set_accent_color_auto = accounts_service.prefers_accent_color == 0; + + if (!set_accent_color_auto) { + return; + } + + bool set_accent_color_based_on_primary_color = background_settings.get_enum ("picture-options") == 0; + + var current_stylesheet = interface_settings.get_string (STYLESHEET_KEY); + + debug ("Current stylesheet: %s", current_stylesheet); + + NamedColor? new_color = null; + if (set_accent_color_based_on_primary_color) { + var primary_color = background_settings.get_string ("primary-color"); + debug ("Current primary color: %s", primary_color); + + new_color = get_accent_color_based_on_primary_color (primary_color); + } else { + var picture_uri = background_settings.get_string ("picture-uri"); + debug ("Current wallpaper: %s", picture_uri); + + var accent_color_name = read_accent_color_name_from_exif (picture_uri); + if (accent_color_name != null) { + for (int i = 0; i < theme_colors.length; i++) { + if (theme_colors[i].name == accent_color_name) { + new_color = theme_colors[i]; + break; + } + } + } else { + new_color = get_accent_color_of_picture_simple (picture_uri); + } + } + + if (new_color != null && new_color.theme != current_stylesheet) { + debug ("New stylesheet: %s", new_color.theme); + + interface_settings.set_string ( + STYLESHEET_KEY, + new_color.theme + ); + } + } + + private string? read_accent_color_name_from_exif (string picture_uri) { + string path = ""; + GExiv2.Metadata metadata; + try { + path = Filename.from_uri (picture_uri); + metadata = new GExiv2.Metadata (); + metadata.open_path (path); + + return metadata.try_get_tag_string (TAG_ACCENT_COLOR); + } catch (Error e) { + warning ("Error parsing exif metadata of \"%s\": %s", path, e.message); + return null; + } + } + + private NamedColor? get_accent_color (ColorExtractor color_extractor) { + var palette = new Gee.ArrayList (); + for (int i = 0; i < theme_colors.length; i++) { + palette.add (theme_colors[i].color); + } + + var index = color_extractor.get_dominant_color_index (palette); + return theme_colors[index]; + } + + private NamedColor? get_accent_color_of_picture_simple (string picture_uri) { + var file = File.new_for_uri (picture_uri); + + try { + var pixbuf = new Gdk.Pixbuf.from_file (file.get_path ()); + var color_extractor = new ColorExtractor.from_pixbuf (pixbuf); + + return get_accent_color (color_extractor); + } catch (Error e) { + warning (e.message); + } + + return null; + } + + private NamedColor? get_accent_color_based_on_primary_color (string primary_color) { + Gdk.RGBA color = {}; + color.parse (primary_color); + + var r = (uint32) (color.red * 255); + var g = (uint32) (color.green * 255); + var b = (uint32) (color.blue * 255); + + var color_extractor = new ColorExtractor.from_primary_color (r * 65536 + g * 256 + b); + + return get_accent_color (color_extractor); + } +} diff --git a/src/Backends/AccentColor/ColorExtractor.vala b/src/Backends/AccentColor/ColorExtractor.vala new file mode 100644 index 00000000..e348e46b --- /dev/null +++ b/src/Backends/AccentColor/ColorExtractor.vala @@ -0,0 +1,88 @@ +/* + * Copyright 2021-2024 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + * Authored by: Marius Meisenzahl + */ + +public class SettingsDaemon.ColorExtractor : Object { + private const double PERCENTAGE_SAMPLE_PIXELS = 0.01; + + public Gdk.Pixbuf? pixbuf { get; construct set; } + public uint32 primary_color { get; construct set; } + + private Gee.List pixels; + + public ColorExtractor.from_pixbuf (Gdk.Pixbuf pixbuf) { + Object (pixbuf: pixbuf); + + pixels = convert_pixels_to_rgb (pixbuf.get_pixels_with_length (), pixbuf.has_alpha); + } + + public ColorExtractor.from_primary_color (uint32 primary_color) { + Object (primary_color: primary_color); + + pixels = new Gee.ArrayList (); + pixels.add (primary_color); + } + + public int get_dominant_color_index (Gee.List palette) { + int index = 0; + var matches = new double[palette.size]; + + pixels.foreach ((pixel) => { + for (int i = 0; i < palette.size; i++) { + var color = palette.get (i); + + var pixel_r = (int) (pixel / 65536); + var pixel_g = (int) ((pixel - pixel_r * 65536) / 256); + var pixel_b = (int) (pixel - pixel_r * 65536 - pixel_g * 255); + + var color_r = (int) (color / 65536); + var color_g = (int) ((color - color_r * 65536) / 256); + var color_b = (int) (color - color_r * 65536 - color_g * 255); + + var distance = Math.sqrt ( + Math.pow (((pixel_r - color_r) / 255.0), 2) + + Math.pow (((pixel_g - color_g) / 255.0), 2) + + Math.pow (((pixel_b - color_b) / 255.0), 2) + ); + + if (distance > 0.25) { + continue; + } + + matches[i] += 1.0 - distance; + } + + return true; + }); + + double best_match = double.MIN; + for (int i = 0; i < matches.length; i++) { + if (matches[i] > best_match) { + best_match = matches[i]; + index = i; + } + } + + return index; + } + + private Gee.ArrayList convert_pixels_to_rgb (uint8[] pixels, bool has_alpha) { + var list = new Gee.ArrayList (); + + int factor = 3 + (int) has_alpha; + int step_size = (int) (pixels.length / factor * PERCENTAGE_SAMPLE_PIXELS); + + for (int i = 0; i < pixels.length / factor; i += step_size) { + int offset = i * factor; + double red = pixels[offset]; + double green = pixels[offset + 1]; + double blue = pixels[offset + 2]; + + list.add ((uint32) (red * 65536 + green * 256 + blue)); + } + + return list; + } +} diff --git a/src/meson.build b/src/meson.build index b35eed56..1d10324b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,8 @@ sources = files( 'AccountsService.vala', 'Application.vala', + 'Backends/AccentColor/AccentColorManager.vala', + 'Backends/AccentColor/ColorExtractor.vala', 'Backends/Housekeeping.vala', 'Backends/InterfaceSettings.vala', 'Backends/KeyboardSettings.vala', @@ -22,7 +24,8 @@ executable( granite_dep, libgeoclue_dep, m_dep, - pk_dep + pk_dep, + gexiv2_dep ], install: true, )