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

Set accent color based on wallpaper #1104

Merged
merged 15 commits into from
Apr 29, 2021
Merged
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 = {
meisenzahl marked this conversation as resolved.
Show resolved Hide resolved
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;
meisenzahl marked this conversation as resolved.
Show resolved Hide resolved

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) {
JoseExposito marked this conversation as resolved.
Show resolved Hide resolved
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