From 137bbc1355ae6aa10f2372ec05a0fc4dd5821dcd Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 6 Jul 2022 18:39:34 +0100 Subject: [PATCH] Add a custom theme dialog (#662) --- data/Application.css | 7 + meson.build | 1 + src/Dialogs/ColorPreferencesDialog.vala | 353 ++++++++++++++++++++++++ src/MainWindow.vala | 34 +++ src/Themes.vala | 76 +++-- 5 files changed, 442 insertions(+), 29 deletions(-) create mode 100644 src/Dialogs/ColorPreferencesDialog.vala diff --git a/data/Application.css b/data/Application.css index 145d814513..d4c3f996ba 100644 --- a/data/Application.css +++ b/data/Application.css @@ -50,3 +50,10 @@ vte-terminal { color: #000; } +.color-custom radio { + -gtk-icon-source: -gtk-icontheme("list-add-symbolic"); +} + +.color-custom radio:checked { + -gtk-icon-source: -gtk-icontheme("check-active-symbolic"); +} diff --git a/meson.build b/meson.build index f8c3af6913..b9ef8dca22 100644 --- a/meson.build +++ b/meson.build @@ -56,6 +56,7 @@ executable( 'src/Themes.vala', 'src/Dialogs/ForegroundProcessDialog.vala', 'src/Dialogs/UnsafePasteDialog.vala', + 'src/Dialogs/ColorPreferencesDialog.vala', 'src/Widgets/SearchToolbar.vala', 'src/Widgets/TerminalWidget.vala', 'src/Utils.vala', diff --git a/src/Dialogs/ColorPreferencesDialog.vala b/src/Dialogs/ColorPreferencesDialog.vala new file mode 100644 index 0000000000..cf42a3eb30 --- /dev/null +++ b/src/Dialogs/ColorPreferencesDialog.vala @@ -0,0 +1,353 @@ +/* +* Copyright 2022 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License version 3, as published by the Free Software Foundation. +* +* 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 Lesser 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 +*/ + +public class Terminal.Dialogs.ColorPreferences : Gtk.Dialog { + private Gtk.ColorButton black_button; + private Gtk.ColorButton red_button; + private Gtk.ColorButton green_button; + private Gtk.ColorButton yellow_button; + private Gtk.ColorButton blue_button; + private Gtk.ColorButton magenta_button; + private Gtk.ColorButton cyan_button; + private Gtk.ColorButton light_gray_button; + private Gtk.ColorButton dark_gray_button; + private Gtk.ColorButton light_red_button; + private Gtk.ColorButton light_green_button; + private Gtk.ColorButton light_yellow_button; + private Gtk.ColorButton light_blue_button; + private Gtk.ColorButton light_magenta_button; + private Gtk.ColorButton light_cyan_button; + private Gtk.ColorButton white_button; + private Gtk.ColorButton background_button; + private Gtk.ColorButton foreground_button; + private Gtk.ColorButton cursor_button; + + public ColorPreferences (Gtk.Window? parent) { + Object ( + deletable: false, + resizable: false, + title: _("Color Preferences"), + transient_for: parent + ); + } + + construct { + var window_theme_label = new SettingsLabel (_("Window style:")) { + margin_bottom = 12 + }; + + var window_theme_switch = new Granite.ModeSwitch.from_icon_name ( + "display-brightness-symbolic", + "weather-clear-night-symbolic" + ) { + primary_icon_tooltip_text = _("Light"), + secondary_icon_tooltip_text = _("Dark") + }; + Application.settings.bind ("prefer-dark-style", window_theme_switch, "active", SettingsBindFlags.DEFAULT); + + var palette_header = new Gtk.Label (_("Color Palette")) { + margin_top = 12, + margin_bottom = 12 + }; + palette_header.get_style_context ().add_class (Granite.STYLE_CLASS_PRIMARY_LABEL); + + var default_button = new Gtk.Button.from_icon_name ("edit-clear-all-symbolic") { + halign = Gtk.Align.END, + margin_top = 12, + margin_bottom = 6, + tooltip_text = _("Reset to elementaryos default color palette") + }; + default_button.clicked.connect (() => { + Terminal.Themes.set_default_palette_for_style (); + update_buttons_from_settings (); + }); + + var black_color_label = new SettingsLabel (_("Black:")); + var red_color_label = new SettingsLabel (_("Red:")); + var green_color_label = new SettingsLabel (_("Green:")); + var yellow_color_label = new SettingsLabel (_("Yellow:")); + var blue_color_label = new SettingsLabel (_("Blue:")); + var magenta_color_label = new SettingsLabel (_("Magenta:")); + var cyan_color_label = new SettingsLabel (_("Cyan:")); + var dark_gray_color_label = new SettingsLabel (_("Gray:")); + var white_color_label = new SettingsLabel (_("White:")); + var light_red_color_label = new SettingsLabel (_("Light Red:")); + var light_green_color_label = new SettingsLabel (_("Light Green:")); + var light_yellow_color_label = new SettingsLabel (_("Light Yellow:")); + var light_blue_color_label = new SettingsLabel (_("Light Blue:")); + var light_magenta_color_label = new SettingsLabel (_("Light Magenta:")); + var light_cyan_color_label = new SettingsLabel (_("Light Cyan:")); + var light_gray_color_label = new SettingsLabel (_("Light Gray:")); + var background_label = new SettingsLabel (_("Background:")); + var foreground_label = new SettingsLabel (_("Foreground:")); + var cursor_label = new SettingsLabel (_("Cursor:")); + + black_button = new Gtk.ColorButton (); + red_button = new Gtk.ColorButton (); + green_button = new Gtk.ColorButton (); + yellow_button = new Gtk.ColorButton (); + blue_button = new Gtk.ColorButton (); + magenta_button = new Gtk.ColorButton (); + cyan_button = new Gtk.ColorButton (); + light_gray_button = new Gtk.ColorButton (); + dark_gray_button = new Gtk.ColorButton (); + light_red_button = new Gtk.ColorButton (); + light_green_button = new Gtk.ColorButton (); + light_yellow_button = new Gtk.ColorButton (); + light_blue_button = new Gtk.ColorButton (); + light_magenta_button = new Gtk.ColorButton (); + light_cyan_button = new Gtk.ColorButton (); + white_button = new Gtk.ColorButton (); + background_button = new Gtk.ColorButton () { + use_alpha = true + }; + foreground_button = new Gtk.ColorButton (); + cursor_button = new Gtk.ColorButton () { + use_alpha = true + }; + + var contrast_top_label = new Gtk.Label (""); // Text will be set on showing + var contrast_bottom_label = new Gtk.Label (""); // Text will be set on showing + var contrast_image = new Gtk.Image.from_icon_name ("process-completed", Gtk.IconSize.LARGE_TOOLBAR); + + var contrast_grid = new Gtk.Grid () { + row_spacing = 3 + }; + contrast_grid.attach (contrast_top_label, 0, 0); + contrast_grid.attach (contrast_image, 0, 1); + contrast_grid.attach (contrast_bottom_label, 0, 2); + + var colors_grid = new Gtk.Grid () { + column_spacing = 12, + row_spacing = 6, + margin_top = 12, + margin_bottom = 12, + margin_start = 12, + margin_end = 12, + halign = Gtk.Align.CENTER + }; + + colors_grid.attach (window_theme_label, 0, 1); + colors_grid.attach (window_theme_switch, 1, 1, 2); + colors_grid.attach (palette_header, 0, 2, 1); + colors_grid.attach (default_button, 3, 2, 1); + colors_grid.attach (background_label, 0, 4, 1); + colors_grid.attach (background_button, 1, 4, 1); + colors_grid.attach (foreground_label, 0, 5, 1); + colors_grid.attach (foreground_button, 1, 5, 1); + colors_grid.attach (contrast_grid, 2, 4, 1, 2); + colors_grid.attach (cursor_label, 0, 6, 1); + colors_grid.attach (cursor_button, 1, 6, 1); + + colors_grid.attach (black_color_label, 0, 8, 1); + colors_grid.attach (black_button, 1, 8, 1); + colors_grid.attach (dark_gray_color_label, 2, 8, 1); + colors_grid.attach (dark_gray_button, 3, 8, 1); + colors_grid.attach (red_color_label, 0, 9, 1); + colors_grid.attach (red_button, 1, 9, 1); + colors_grid.attach (light_red_color_label, 2, 9, 1); + colors_grid.attach (light_red_button, 3, 9, 1); + colors_grid.attach (green_color_label, 0, 10, 1); + colors_grid.attach (green_button, 1, 10, 1); + colors_grid.attach (light_green_color_label, 2, 10, 1); + colors_grid.attach (light_green_button, 3, 10, 1); + colors_grid.attach (yellow_color_label, 0, 11, 1); + colors_grid.attach (yellow_button, 1, 11, 1); + colors_grid.attach (light_yellow_color_label, 2, 11, 1); + colors_grid.attach (light_yellow_button, 3, 11, 1); + colors_grid.attach (blue_color_label, 0, 12, 1); + colors_grid.attach (blue_button, 1, 12, 1); + colors_grid.attach (light_blue_color_label, 2, 12, 1); + colors_grid.attach (light_blue_button, 3, 12, 1); + colors_grid.attach (magenta_color_label, 0, 13, 1); + colors_grid.attach (magenta_button, 1, 13, 1); + colors_grid.attach (light_magenta_color_label, 2, 13, 1); + colors_grid.attach (light_magenta_button, 3, 13, 1); + colors_grid.attach (cyan_color_label, 0, 14, 1); + colors_grid.attach (cyan_button, 1, 14, 1); + colors_grid.attach (light_cyan_color_label, 2, 14, 1); + colors_grid.attach (light_cyan_button, 3, 14, 1); + colors_grid.attach (light_gray_color_label, 0, 15, 1); + colors_grid.attach (light_gray_button, 1, 15, 1); + colors_grid.attach (white_color_label, 2, 15, 1); + colors_grid.attach (white_button, 3, 15, 1); + + update_buttons_from_settings (); + update_contrast (contrast_image); + + get_content_area ().add (colors_grid); + + var close_button = (Gtk.Button) add_button (_("Close"), Gtk.ResponseType.CLOSE); + close_button.clicked.connect (destroy); + + Application.settings.set_string ("theme", Themes.CUSTOM); + + black_button.color_set.connect (update_palette_settings); + red_button.color_set.connect (update_palette_settings); + green_button.color_set.connect (update_palette_settings); + yellow_button.color_set.connect (update_palette_settings); + blue_button.color_set.connect (update_palette_settings); + magenta_button.color_set.connect (update_palette_settings); + cyan_button.color_set.connect (update_palette_settings); + light_gray_button.color_set.connect (update_palette_settings); + dark_gray_button.color_set.connect (update_palette_settings); + light_red_button.color_set.connect (update_palette_settings); + light_green_button.color_set.connect (update_palette_settings); + light_yellow_button.color_set.connect (update_palette_settings); + light_blue_button.color_set.connect (update_palette_settings); + light_magenta_button.color_set.connect (update_palette_settings); + light_cyan_button.color_set.connect (update_palette_settings); + white_button.color_set.connect (update_palette_settings); + + background_button.color_set.connect (() => { + Application.settings.set_string ("background", background_button.rgba.to_string ()); + update_contrast (contrast_image); + }); + + foreground_button.color_set.connect (() => { + Application.settings.set_string ("foreground", foreground_button.rgba.to_string ()); + update_contrast (contrast_image); + }); + + cursor_button.color_set.connect (() => { + Application.settings.set_string ("cursor-color", cursor_button.rgba.to_string ()); + }); + + contrast_top_label.state_flags_changed.connect ((previous_flags) => { + var state_flags = get_state_flags (); + contrast_top_label.label = Gtk.StateFlags.DIR_LTR in state_flags ? "┐" : "┌"; + contrast_bottom_label.label = Gtk.StateFlags.DIR_LTR in state_flags ? "┘" : "└"; + }); + } + + private void update_palette_settings () { + string[] colors = { + black_button.rgba.to_string (), + red_button.rgba.to_string (), + green_button.rgba.to_string (), + yellow_button.rgba.to_string (), + blue_button.rgba.to_string (), + magenta_button.rgba.to_string (), + cyan_button.rgba.to_string (), + light_gray_button.rgba.to_string (), + dark_gray_button.rgba.to_string (), + light_red_button.rgba.to_string (), + light_green_button.rgba.to_string (), + light_yellow_button.rgba.to_string (), + light_blue_button.rgba.to_string (), + light_magenta_button.rgba.to_string (), + light_cyan_button.rgba.to_string (), + white_button.rgba.to_string () + }; + + Application.settings.set_string ("palette", string.joinv (":", colors)); + } + + private void update_buttons_from_settings () { + var color_palette = new Gdk.RGBA[Terminal.Themes.PALETTE_SIZE]; + + var palette = Application.settings.get_string ("palette"); + var background = Application.settings.get_string ("background"); + var foreground = Application.settings.get_string ("foreground"); + var cursor = Application.settings.get_string ("cursor-color"); + + var input_palette = @"$palette:$background:$foreground:$cursor".split (":"); + + for (int i = 0; i < input_palette.length; i++) { + if (!color_palette[i].parse (input_palette[i])) { + warning ("Found invalid color in one of the color palette settings"); + return; + } + } + + black_button.rgba = color_palette[0]; + red_button.rgba = color_palette[1]; + green_button.rgba = color_palette[2]; + yellow_button.rgba = color_palette[3]; + blue_button.rgba = color_palette[4]; + magenta_button.rgba = color_palette[5]; + cyan_button.rgba = color_palette[6]; + light_gray_button.rgba = color_palette[7]; + dark_gray_button.rgba = color_palette[8]; + light_red_button.rgba = color_palette[9]; + light_green_button.rgba = color_palette[10]; + light_yellow_button.rgba = color_palette[11]; + light_blue_button.rgba = color_palette[12]; + light_magenta_button.rgba = color_palette[13]; + light_cyan_button.rgba = color_palette[14]; + white_button.rgba = color_palette[15]; + + background_button.rgba = color_palette[16]; + foreground_button.rgba = color_palette[17]; + cursor_button.rgba = color_palette[18]; + } + + private void update_contrast (Gtk.Image contrast_image) { + var contrast_ratio = get_contrast_ratio (foreground_button.rgba, background_button.rgba); + if (contrast_ratio < 3) { + contrast_image.icon_name = "dialog-warning"; + contrast_image.tooltip_text = _("Contrast is very low"); + } else if (contrast_ratio < 4.5) { + contrast_image.icon_name = "dialog-warning"; + contrast_image.tooltip_text = _("Contrast is low"); + } else if (contrast_ratio < 7) { + contrast_image.icon_name = "process-completed"; + contrast_image.tooltip_text = _("Contrast is good"); + } else { + contrast_image.icon_name = "process-completed"; + contrast_image.tooltip_text = _("Contrast is high"); + } + } + + // contrast ratio code is taken from https://github.com/danrabbit/harvey/ + private double get_contrast_ratio (Gdk.RGBA foreground, Gdk.RGBA background) { + var foreground_luminance = get_luminance (foreground); + var background_luminance = get_luminance (background); + + if (background_luminance > foreground_luminance) { + return (background_luminance + 0.05) / (foreground_luminance + 0.05); + } else { + return (foreground_luminance + 0.05) / (background_luminance + 0.05); + } + } + + private double get_luminance (Gdk.RGBA color) { + var red = sanitize_color (color.red) * 0.2126; + var green = sanitize_color (color.green) * 0.7152; + var blue = sanitize_color (color.blue) * 0.0722; + + return (red + green + blue); + } + + private double sanitize_color (double color) { + if (color <= 0.03928) { + color = color / 12.92; + } else { + color = Math.pow ((color + 0.055) / 1.055, 2.4); + } + return color; + } + + private class SettingsLabel : Gtk.Label { + public SettingsLabel (string text) { + label = text; + halign = Gtk.Align.END; + margin_start = 12; + } + } +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index d8cabb5e78..4c9c5ab9f4 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -26,6 +26,7 @@ namespace Terminal { private Gtk.Revealer search_revealer; private Gtk.ToggleButton search_button; private Gtk.Button zoom_default_button; + private Dialogs.ColorPreferences? color_preferences_dialog; private Granite.AccelLabel open_in_browser_menuitem_label; private HashTable restorable_terminals; @@ -421,6 +422,15 @@ namespace Terminal { color_button_dark_context.add_class (Granite.STYLE_CLASS_COLOR_BUTTON); color_button_dark_context.add_class ("color-dark"); + var color_button_custom = new Gtk.RadioButton.from_widget (color_button_white) { + halign = Gtk.Align.CENTER, + tooltip_text = _("Custom") + }; + + unowned Gtk.StyleContext color_button_custom_context = color_button_custom.get_style_context (); + color_button_custom_context.add_class (Granite.STYLE_CLASS_COLOR_BUTTON); + color_button_custom_context.add_class ("color-custom"); + var color_grid = new Gtk.Grid () { column_homogeneous = true, margin_start = 12, @@ -431,6 +441,7 @@ namespace Terminal { color_grid.add (color_button_white); color_grid.add (color_button_light); color_grid.add (color_button_dark); + color_grid.add (color_button_custom); var natural_copy_paste_button = new Granite.SwitchModelButton (_("Natural Copy/Paste")) { description = _("Shortcuts don’t require Shift; may interfere with CLI apps") @@ -549,6 +560,9 @@ namespace Terminal { case Themes.DARK: color_button_dark.active = true; break; + case Themes.CUSTOM: + color_button_custom.active = true; + break; } color_button_dark.button_release_event.connect (() => { @@ -566,6 +580,13 @@ namespace Terminal { return Gdk.EVENT_PROPAGATE; }); + color_button_custom.button_release_event.connect (() => { + color_button_custom.active = true; + open_color_preferences (); + menu_popover.popdown (); + return Gdk.EVENT_STOP; + }); + Application.settings.bind ( "natural-copy-paste", natural_copy_paste_button, @@ -1481,6 +1502,19 @@ namespace Terminal { } } + private void open_color_preferences () { + if (color_preferences_dialog == null) { + color_preferences_dialog = new Dialogs.ColorPreferences (this); + color_preferences_dialog.show_all (); + + color_preferences_dialog.destroy.connect (() => { + color_preferences_dialog = null; + }); + } + + color_preferences_dialog.present (); + } + private TerminalWidget get_term_widget (Granite.Widgets.Tab tab) { return (TerminalWidget)((Gtk.Bin)tab.page).get_child (); } diff --git a/src/Themes.vala b/src/Themes.vala index c1379ae13f..0be3b927aa 100644 --- a/src/Themes.vala +++ b/src/Themes.vala @@ -17,10 +17,11 @@ */ public class Terminal.Themes { - public const int PALETTE_SIZE = 19; public const string DARK = "dark"; public const string HIGH_CONTRAST = "high-contrast"; public const string LIGHT = "solarized-light"; + public const string CUSTOM = "custom"; + public const int PALETTE_SIZE = 19; static construct { Application.settings.changed["theme"].connect (() => { @@ -40,12 +41,36 @@ public class Terminal.Themes { // format is color01:color02:...:color16:background:foreground:cursor public static Gdk.RGBA[] get_rgba_palette (string theme) { - const string[] DARK_PALETTE = { - "#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#ec0048", "#2aa198", "#94a3a5", - "#586e75", "#cb4b16", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#6c71c4", - "rgba(46, 46, 46, 0.95)", "#a5a5a5", "#839496" - }; + var string_palette = get_string_palette (theme); + bool settings_valid = string_palette.length == PALETTE_SIZE; + + var rgba_palette = new Gdk.RGBA[PALETTE_SIZE]; + for (int i = 0; i < PALETTE_SIZE; i++) { + var new_color = Gdk.RGBA (); + // If custom palette invalid use a fallback one + if (!new_color.parse (string_palette[i])) { + critical ("Color %i '%s' is not valid - replacing with default", i, string_palette[i]); + settings_valid = false; + + var fallback_palette = get_string_palette ( + Application.settings.get_boolean ("prefer-dark-style") ? DARK : LIGHT + ); + string_palette[i] = fallback_palette[i]; + new_color.parse (fallback_palette[i]); + } + + rgba_palette[i] = new_color; + } + + if (!settings_valid) { + /* Remove invalid colors from setting */ + Application.settings.set_string ("palette", string.joinv (":", string_palette)); + } + + return rgba_palette; + } + private static string[] get_string_palette (string theme) { var string_palette = new string[PALETTE_SIZE]; switch (theme) { case (HIGH_CONTRAST): @@ -63,9 +88,13 @@ public class Terminal.Themes { }; break; case (DARK): - string_palette = DARK_PALETTE; + string_palette = { + "#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#ec0048", "#2aa198", "#94a3a5", + "#586e75", "#cb4b16", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#6c71c4", + "rgba(46, 46, 46, 0.95)", "#a5a5a5", "#839496" + }; break; - default: + case (CUSTOM): string_palette = Application.settings.get_string ("palette").split (":"); string_palette += Application.settings.get_string ("background"); string_palette += Application.settings.get_string ("foreground"); @@ -73,27 +102,16 @@ public class Terminal.Themes { break; } - bool settings_valid = string_palette.length == PALETTE_SIZE; - - var rgba_palette = new Gdk.RGBA[PALETTE_SIZE]; - for (int i = 0; i < PALETTE_SIZE; i++) { - var new_color = Gdk.RGBA (); - // Replace invalid color with corresponding one from default palette - if (!new_color.parse (string_palette[i])) { - critical ("Color %i '%s' is not valid - replacing with default", i, string_palette[i]); - string_palette[i] = DARK_PALETTE[i]; - new_color.parse (DARK_PALETTE[i]); - settings_valid = false; - } - - rgba_palette[i] = new_color; - } - - if (!settings_valid) { - /* Remove invalid colors from setting */ - Application.settings.set_string ("palette", string.joinv (":", string_palette)); - } + return string_palette; + } - return rgba_palette; + public static void set_default_palette_for_style () { + var string_palette = get_string_palette ( + Application.settings.get_boolean ("prefer-dark-style") ? DARK : LIGHT + ); + Application.settings.set_string ("palette", string.joinv (":", string_palette[0:PALETTE_SIZE - 3])); + Application.settings.set_string ("background", string_palette[PALETTE_SIZE - 3]); + Application.settings.set_string ("foreground", string_palette[PALETTE_SIZE - 2]); + Application.settings.set_string ("cursor-color", string_palette[PALETTE_SIZE - 1]); } }