Skip to content

Commit

Permalink
Set accent color based on wallpaper (#1104)
Browse files Browse the repository at this point in the history
* Add libgexiv2-dev as dependency

* Add classes

* Add demo

* Fix signal

* Cleanup

* Use Granite.Settings

* Revert "Use Granite.Settings"

This reverts commit a2da4bb.

* Sort dependencies alphabetically

* Cleanup class NamedColor

* Read color from Gtk theme

* Cleanup
  • Loading branch information
meisenzahl committed Apr 29, 2021
1 parent 3d3a69a commit 5cf9308
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Install Dependencies
run: |
apt update
apt install -y gettext gnome-settings-daemon-dev gsettings-desktop-schemas-dev libbamf3-dev libcanberra-dev libcanberra-gtk3-dev libclutter-1.0-dev libgee-0.8-dev libglib2.0-dev libgnome-desktop-3-dev libgranite-dev libgtk-3-dev libmutter-6-dev libplank-dev libxml2-utils meson valac valadoc
apt install -y gettext gnome-settings-daemon-dev gsettings-desktop-schemas-dev libbamf3-dev libcanberra-dev libcanberra-gtk3-dev libclutter-1.0-dev libgee-0.8-dev libglib2.0-dev libgnome-desktop-3-dev libgranite-dev libgtk-3-dev libmutter-6-dev libplank-dev libxml2-utils libgexiv2-dev meson valac valadoc
- name: Build
env:
DESTDIR: out
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ You'll need the following dependencies:
* libcanberra-gtk3-dev
* libclutter-1.0-dev (>= 1.12.0)
* libgee-0.8-dev
* libgexiv2-dev
* libglib2.0-dev (>= 2.44)
* libgnome-desktop-3-dev
* libgranite-dev (>= 5.4.0)
Expand Down
3 changes: 2 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ granite_dep = dependency('granite', version: '>= 5.4.0')
gnome_desktop_dep = dependency('gnome-desktop-3.0')
gsd_dep = dependency('gnome-settings-daemon', version: '>= @0@'.format(gsd_version_required))
m_dep = cc.find_library('m', required: false)
gexiv2_dep = dependency('gexiv2')

mutter_dep = []
libmutter_dep = []
Expand Down Expand Up @@ -133,7 +134,7 @@ mutter_typelib_dir = libmutter_dep.get_pkgconfig_variable('typelibdir')
add_project_arguments(vala_flags, language: 'vala')
add_project_link_arguments(['-Wl,-rpath,@0@'.format(mutter_typelib_dir)], language: 'c')

gala_base_dep = [glib_dep, gobject_dep, gio_dep, gmodule_dep, gee_dep, gtk_dep, plank_dep, mutter_dep, granite_dep, gnome_desktop_dep, m_dep, config_dep]
gala_base_dep = [glib_dep, gobject_dep, gio_dep, gmodule_dep, gee_dep, gtk_dep, plank_dep, mutter_dep, granite_dep, gnome_desktop_dep, m_dep, gexiv2_dep, config_dep]

subdir('data')
subdir('lib')
Expand Down
166 changes: 166 additions & 0 deletions src/AccentColor/AccentColorManager.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2021 elementary, Inc. (https://elementary.io)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Marius Meisenzahl <mariusmeisenzahl@gmail.com>
*/

public class Gala.AccentColorManager : Object {
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";

private Gala.AccountsService? gala_accounts_service = null;

private Settings background_settings;
private Settings interface_settings;

private NamedColor[] theme_colors = {
new NamedColor ("Blue", THEME_BLUE),
new NamedColor ("Mint", THEME_MINT),
new NamedColor ("Green", THEME_GREEN),
new NamedColor ("Yellow", THEME_YELLOW),
new NamedColor ("Orange", THEME_ORANGE),
new NamedColor ("Red", THEME_RED),
new NamedColor ("Pink", THEME_PINK),
new NamedColor ("Purple", THEME_PURPLE),
new NamedColor ("Brown", THEME_BROWN),
new NamedColor ("Gray", THEME_GRAY)
};

construct {
background_settings = new Settings ("org.gnome.desktop.background");
interface_settings = new Settings (INTERFACE_SCHEMA);

string? user_path = null;
try {
FDO.Accounts? accounts_service = GLib.Bus.get_proxy_sync (
GLib.BusType.SYSTEM,
"org.freedesktop.Accounts",
"/org/freedesktop/Accounts"
);

user_path = accounts_service.find_user_by_name (GLib.Environment.get_user_name ());
} catch (Error e) {
critical (e.message);
}

if (user_path != null) {
try {
gala_accounts_service = GLib.Bus.get_proxy_sync (
GLib.BusType.SYSTEM,
"org.freedesktop.Accounts",
user_path
);

((DBusProxy)gala_accounts_service).g_properties_changed.connect (() => {
update_accent_color ();
});
} catch (Error e) {
warning ("Unable to get AccountsService proxy, accent color preference may be incorrect");
}
}

background_settings.changed["picture-uri"].connect (update_accent_color);

update_accent_color ();
}

private void update_accent_color () {
bool set_accent_color_based_on_wallpaper = gala_accounts_service.prefers_accent_color == 0;

if (set_accent_color_based_on_wallpaper) {
var picture_uri = background_settings.get_string ("picture-uri");

var current_stylesheet = interface_settings.get_string (STYLESHEET_KEY);

debug ("Current wallpaper: %s", picture_uri);
debug ("Current stylesheet: %s", current_stylesheet);

NamedColor? new_color = null;
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);
} catch (Error e) {
warning ("Error parsing exif metadata of \"%s\": %s", path, e.message);
return null;
}

return metadata.get_tag_string (TAG_ACCENT_COLOR);
}

public NamedColor? get_accent_color_of_picture_simple (string picture_uri) {
NamedColor new_color = null;

var file = File.new_for_uri (picture_uri);

try {
var pixbuf = new Gdk.Pixbuf.from_file (file.get_path ());
var color_extractor = new ColorExtractor (pixbuf);

var palette = new Gee.ArrayList<Granite.Drawing.Color> ();
for (int i = 0; i < theme_colors.length; i++) {
palette.add (theme_colors[i].color);
}

var index = color_extractor.get_dominant_color_index (palette);
new_color = theme_colors[index];
} catch (Error e) {
warning (e.message);
}

return new_color;
}
}
87 changes: 87 additions & 0 deletions src/AccentColor/ColorExtractor.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2021 elementary, Inc. (https://elementary.io)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Marius Meisenzahl <mariusmeisenzahl@gmail.com>
*/

public class Gala.ColorExtractor : Object {
private const double PERCENTAGE_SAMPLE_PIXELS = 0.01;

public Gdk.Pixbuf pixbuf { get; construct set; }

private Gee.List<Granite.Drawing.Color> pixels;

public ColorExtractor (Gdk.Pixbuf pixbuf) {
Object (pixbuf: pixbuf);

pixels = convert_pixels_to_rgb (pixbuf.get_pixels_with_length (), pixbuf.has_alpha);
}

public int get_dominant_color_index (Gee.List<Granite.Drawing.Color> 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 distance = Math.sqrt (
Math.pow ((pixel.R - color.R), 2) +
Math.pow ((pixel.G - color.G), 2) +
Math.pow ((pixel.B - color.B), 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<Granite.Drawing.Color> convert_pixels_to_rgb (uint8[] pixels, bool has_alpha) {
var list = new Gee.ArrayList<Granite.Drawing.Color> ();

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] / 255.0;
double green = pixels[offset + 1] / 255.0;
double blue = pixels[offset + 2] / 255.0;

list.add (new Granite.Drawing.Color (red, green, blue, 0.0));
}

return list;
}
}
34 changes: 34 additions & 0 deletions src/AccentColor/NamedColor.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2021 elementary, Inc. (https://elementary.io)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Marius Meisenzahl <mariusmeisenzahl@gmail.com>
*/

public class Gala.NamedColor : Object {
public string name { get; construct set; }
public string theme { get; construct set; }
public Granite.Drawing.Color color { get; construct set; }

public NamedColor (string name, string theme) {
Object (
name: name,
theme: theme,
color: InternalUtils.get_accent_color_by_theme_name (theme)
);
}
}
9 changes: 9 additions & 0 deletions src/GalaAccountsServicePlugin.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[DBus (name = "io.elementary.pantheon.AccountsService")]
interface Gala.AccountsService : Object {
public abstract int prefers_accent_color { get; }
}

[DBus (name = "org.freedesktop.Accounts")]
interface Gala.FDO.Accounts : Object {
public abstract string find_user_by_name (string username) throws GLib.Error;
}
18 changes: 18 additions & 0 deletions src/InternalUtils.vala
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,24 @@ namespace Gala {
);
}

public static Granite.Drawing.Color get_accent_color_by_theme_name (string theme_name) {
var label_widget_path = new Gtk.WidgetPath ();
label_widget_path.append_type (GLib.Type.from_name ("label"));
label_widget_path.iter_set_object_name (-1, "selection");

var selection_style_context = new Gtk.StyleContext ();
unowned Gtk.CssProvider theme_provider = Gtk.CssProvider.get_named (theme_name, null);
selection_style_context.add_provider (theme_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
selection_style_context.set_path (label_widget_path);

var rgba = (Gdk.RGBA) selection_style_context.get_property (
Gtk.STYLE_PROPERTY_BACKGROUND_COLOR,
Gtk.StateFlags.NORMAL
);

return new Granite.Drawing.Color.from_rgba (rgba);
}

/**
* Returns the workspaces geometry following the only_on_primary settings.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/WindowManager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ namespace Gala {
*/
Zoom? zoom = null;

AccentColorManager accent_color_manager;

Clutter.Actor? tile_preview;

private Meta.Window? moving; //place for the window that is being moved over
Expand Down Expand Up @@ -292,6 +294,8 @@ namespace Gala {

zoom = new Zoom (this);

accent_color_manager = new AccentColorManager ();

// initialize plugins and add default components if no plugin overrides them
var plugin_manager = PluginManager.get_default ();
plugin_manager.initialize (this);
Expand Down

0 comments on commit 5cf9308

Please sign in to comment.