1/*
2 * Copyright 2012–2021 elementary, Inc. (https://elementary.io)
3 * SPDX-License-Identifier: LGPL-3.0-or-later
4 */
5
6[Version (deprecated = true, deprecated_since = "0.4.2", replacement = "")]
7public enum Granite.TextStyle {
8    /**
9     * Highest level header
10     */
11    TITLE,
12
13    /**
14     * Second highest header
15     */
16    H1,
17
18    /**
19     * Third highest header
20     */
21    H2,
22
23    /**
24     * Fourth Highest Header
25     */
26    H3;
27
28    /**
29     * Converts this to a CSS style string that could be used with e.g: {@link Granite.Widgets.Utils.set_theming}.
30     *
31     * @param style_class the style class used for this
32     *
33     * @return CSS of text style
34     */
35    public string get_stylesheet (out string style_class = null) {
36        switch (this) {
37            case TITLE:
38                style_class = StyleClass.TITLE_TEXT;
39                return @".$style_class { font: raleway 36; }";
40            case H1:
41                style_class = StyleClass.H1_TEXT;
42                return @".$style_class { font: open sans bold 24; }";
43            case H2:
44                style_class = StyleClass.H2_TEXT;
45                return @".$style_class { font: open sans light 18; }";
46            case H3:
47                style_class = StyleClass.H3_TEXT;
48                return @".$style_class { font: open sans bold 12; }";
49            default:
50                assert_not_reached ();
51        }
52    }
53}
54
55/**
56 * An enum used to derermine where the window manager currently displays its close button on windows.
57 * Used with {@link Granite.Widgets.Utils.get_default_close_button_position}.
58 */
59public enum Granite.CloseButtonPosition {
60    LEFT,
61    RIGHT
62}
63
64namespace Granite {
65
66/**
67 * Converts a {@link Gtk.accelerator_parse} style accel string to a human-readable string.
68 *
69 * @param accel an accelerator label like “<Control>a” or “<Super>Right”
70 *
71 * @return a human-readable string like "Ctrl + A" or "⌘ + →"
72 */
73public static string accel_to_string (string? accel) {
74    if (accel == null) {
75        return "";
76    }
77
78    // We need to make sure that the translation domain is correctly setup
79    Granite.init ();
80
81    uint accel_key;
82    Gdk.ModifierType accel_mods;
83    Gtk.accelerator_parse (accel, out accel_key, out accel_mods);
84
85    string[] arr = {};
86    if (Gdk.ModifierType.SUPER_MASK in accel_mods) {
87        arr += "⌘";
88    }
89
90    if (Gdk.ModifierType.SHIFT_MASK in accel_mods) {
91        arr += _("Shift");
92    }
93
94    if (Gdk.ModifierType.CONTROL_MASK in accel_mods) {
95        arr += _("Ctrl");
96    }
97
98    if (Gdk.ModifierType.MOD1_MASK in accel_mods) {
99        arr += _("Alt");
100    }
101
102    switch (accel_key) {
103        case Gdk.Key.Up:
104            arr += "↑";
105            break;
106        case Gdk.Key.Down:
107            arr += "↓";
108            break;
109        case Gdk.Key.Left:
110            arr += "←";
111            break;
112        case Gdk.Key.Right:
113            arr += "→";
114            break;
115        case Gdk.Key.Alt_L:
116            ///TRANSLATORS: The Alt key on the left side of the keyboard
117            arr += _("Left Alt");
118            break;
119        case Gdk.Key.Alt_R:
120            ///TRANSLATORS: The Alt key on the right side of the keyboard
121            arr += _("Right Alt");
122            break;
123        case Gdk.Key.backslash:
124            arr += "\\";
125            break;
126        case Gdk.Key.Control_R:
127            ///TRANSLATORS: The Ctrl key on the right side of the keyboard
128            arr += _("Right Ctrl");
129            break;
130        case Gdk.Key.Control_L:
131            ///TRANSLATORS: The Ctrl key on the left side of the keyboard
132            arr += _("Left Ctrl");
133            break;
134        case Gdk.Key.minus:
135        case Gdk.Key.KP_Subtract:
136            ///TRANSLATORS: This is a non-symbol representation of the "-" key
137            arr += _("Minus");
138            break;
139        case Gdk.Key.KP_Add:
140        case Gdk.Key.plus:
141            ///TRANSLATORS: This is a non-symbol representation of the "+" key
142            arr += _("Plus");
143            break;
144        case Gdk.Key.KP_Equal:
145        case Gdk.Key.equal:
146            ///TRANSLATORS: This is a non-symbol representation of the "=" key
147            arr += _("Equals");
148            break;
149        case Gdk.Key.Return:
150            arr += _("Enter");
151            break;
152        case Gdk.Key.Shift_L:
153            ///TRANSLATORS: The Shift key on the left side of the keyboard
154            arr += _("Left Shift");
155            break;
156        case Gdk.Key.Shift_R:
157            ///TRANSLATORS: The Shift key on the right side of the keyboard
158            arr += _("Right Shift");
159            break;
160        default:
161            // If a specified accelarator contains only modifiers e.g. "<Control><Shift>",
162            // we don't get anything from accelerator_get_label method, so skip that case
163            string accel_label = Gtk.accelerator_get_label (accel_key, 0);
164            if (accel_label != "") {
165                arr += accel_label;
166            }
167            break;
168    }
169
170    if (accel_mods != 0) {
171        return string.joinv (" + ", arr);
172    }
173
174    return arr[0];
175}
176
177/**
178 * Pango markup to use for secondary text in a {@link Gtk.Tooltip}, such as for accelerators, extended descriptions, etc.
179 */
180public const string TOOLTIP_SECONDARY_TEXT_MARKUP = """<span weight="600" size="smaller" alpha="75%">%s</span>""";
181
182/**
183 * Takes a description and an array of accels and returns {@link Pango} markup for use in a {@link Gtk.Tooltip}. This method uses {@link Granite.accel_to_string}.
184 *
185 * Example:
186 *
187 * Description
188 * Shortcut 1, Shortcut 2
189 *
190 * @param a string array of accelerator labels like {"<Control>a", "<Super>Right"}
191 *
192 * @param description a standard tooltip text string
193 *
194 * @return {@link Pango} markup with the description label on one line and a list of human-readable accels on a new line
195 */
196public static string markup_accel_tooltip (string[]? accels, string? description = null) {
197    string[] parts = {};
198    if (description != null && description != "") {
199        parts += description;
200    }
201
202    if (accels != null && accels.length > 0) {
203        string[] unique_accels = {};
204
205        // We need to make sure that the translation domain is correctly setup
206        Granite.init ();
207
208        for (int i = 0; i < accels.length; i++) {
209            if (accels[i] == "") {
210                continue;
211            }
212
213            var accel_string = accel_to_string (accels[i]);
214            if (!(accel_string in unique_accels)) {
215                unique_accels += accel_string;
216            }
217        }
218
219        if (unique_accels.length > 0) {
220            ///TRANSLATORS: This is a delimiter that separates two keyboard shortcut labels like "⌘ + →, Control + A"
221            var accel_label = string.joinv (_(", "), unique_accels);
222
223            var accel_markup = TOOLTIP_SECONDARY_TEXT_MARKUP.printf (accel_label);
224
225            parts += accel_markup;
226        }
227    }
228
229    return string.joinv ("\n", parts);
230}
231
232private static double contrast_ratio (Gdk.RGBA bg_color, Gdk.RGBA fg_color) {
233    // From WCAG 2.0 https://www.w3.org/TR/WCAG20/#contrast-ratiodef
234    var bg_luminance = get_luminance (bg_color);
235    var fg_luminance = get_luminance (fg_color);
236
237    if (bg_luminance > fg_luminance) {
238        return (bg_luminance + 0.05) / (fg_luminance + 0.05);
239    }
240
241    return (fg_luminance + 0.05) / (bg_luminance + 0.05);
242}
243
244private static double get_luminance (Gdk.RGBA color) {
245    // Values from WCAG 2.0 https://www.w3.org/TR/WCAG20/#relativeluminancedef
246    var red = sanitize_color (color.red) * 0.2126;
247    var green = sanitize_color (color.green) * 0.7152;
248    var blue = sanitize_color (color.blue) * 0.0722;
249
250    return red + green + blue;
251}
252
253private static double sanitize_color (double color) {
254    // From WCAG 2.0 https://www.w3.org/TR/WCAG20/#relativeluminancedef
255    if (color <= 0.03928) {
256        return color / 12.92;
257    }
258
259    return Math.pow ((color + 0.055) / 1.055, 2.4);
260}
261
262/**
263 * Takes a {@link Gdk.RGBA} background color and returns a suitably-contrasting foreground color, i.e. for determining text color on a colored background. There is a slight bias toward returning white, as white generally looks better on a wider range of colored backgrounds than black.
264 *
265 * @param bg_color any {@link Gdk.RGBA} background color
266 *
267 * @return a contrasting {@link Gdk.RGBA} foreground color, i.e. white ({ 1.0, 1.0, 1.0, 1.0}) or black ({ 0.0, 0.0, 0.0, 1.0}).
268 */
269public static Gdk.RGBA contrasting_foreground_color (Gdk.RGBA bg_color) {
270    Gdk.RGBA gdk_white = { 1.0, 1.0, 1.0, 1.0 };
271    Gdk.RGBA gdk_black = { 0.0, 0.0, 0.0, 1.0 };
272
273    var contrast_with_white = contrast_ratio (
274        bg_color,
275        gdk_white
276    );
277    var contrast_with_black = contrast_ratio (
278        bg_color,
279        gdk_black
280    );
281
282    // Default to white
283    var fg_color = gdk_white;
284
285    // NOTE: We cheat and add 3 to contrast when checking against black,
286    // because white generally looks better on a colored background
287    if ( contrast_with_black > (contrast_with_white + 3) ) {
288        fg_color = gdk_black;
289    }
290
291    return fg_color;
292}
293
294}
295
296/**
297 * This namespace contains functions to apply CSS stylesheets to widgets.
298 */
299namespace Granite.Widgets.Utils {
300    /**
301     * Applies colorPrimary property to the window. The colorPrimary property currently changes
302     * the color of the {@link Gtk.HeaderBar} and it's children so that the application window
303     * can have a so-called "brand color".
304     *
305     * Note that this currently only works with the default stylesheet that elementary OS uses.
306     *
307     * @param window the widget to apply the color, for most cases the widget will be actually the {@link Gtk.Window} itself
308     * @param color the color to apply
309     * @param priority priorty of change, by default {@link Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION}
310     *
311     * @return the added {@link Gtk.CssProvider}, or null in case the parsing of
312     *         stylesheet failed.
313     */
314    public Gtk.CssProvider? set_color_primary (
315        Gtk.Widget window,
316        Gdk.RGBA color,
317        int priority = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
318    ) {
319        assert (window != null);
320
321        string hex = color.to_string ();
322        return set_theming_for_screen (window.get_screen (), @"@define-color color_primary $hex;@define-color colorPrimary $hex;", priority);
323    }
324
325    /**
326     * Applies the //stylesheet// to the widget.
327     *
328     * @param widget widget to apply style to
329     * @param stylesheet CSS style to apply to the widget
330     * @param class_name class name to add style to, pass null if no class should be applied to the //widget//
331     * @param priority priorty of change, for most cases this will be {@link Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION}
332     *
333     * @return the {@link Gtk.CssProvider} that was applied to the //widget//.
334     */
335    [Version (deprecated = true, deprecated_since = "5.5.0", replacement = "")]
336    public Gtk.CssProvider? set_theming (Gtk.Widget widget, string stylesheet,
337                              string? class_name, int priority) {
338        var css_provider = get_css_provider (stylesheet);
339
340        var context = widget.get_style_context ();
341
342        if (css_provider != null)
343            context.add_provider (css_provider, priority);
344
345        if (class_name != null && class_name.strip () != "")
346            context.add_class (class_name);
347
348        return css_provider;
349    }
350
351    /**
352     * Applies a stylesheet to the given //screen//. This will affect all the
353     * widgets which are part of that screen.
354     *
355     * @param screen screen to apply style to, use {@link Gtk.Widget.get_screen} in order to get the screen that the widget is on
356     * @param stylesheet CSS style to apply to screen
357     * @param priority priorty of change, for most cases this will be {@link Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION}
358     *
359     * @return the {@link Gtk.CssProvider} that was applied to the //screen//.
360     */
361    [Version (deprecated = true, deprecated_since = "5.5.0", replacement = "Gtk.StyleContext.add_provider_for_screen")]
362    public Gtk.CssProvider? set_theming_for_screen (Gdk.Screen screen, string stylesheet, int priority) {
363        var css_provider = get_css_provider (stylesheet);
364
365        if (css_provider != null)
366            Gtk.StyleContext.add_provider_for_screen (screen, css_provider, priority);
367
368        return css_provider;
369    }
370
371    /**
372     * Constructs a new {@link Gtk.CssProvider} that will store the //stylesheet// data.
373     * This function uses {@link Gtk.CssProvider.load_from_data} internally so if this method fails
374     * then a warning will be thrown and null returned as a result.
375     *
376     * @param stylesheet CSS style to apply to the returned provider
377     *
378     * @return a new {@link Gtk.CssProvider}, or null in case the parsing of
379     *         //stylesheet// failed.
380     */
381    [Version (deprecated = true, deprecated_since = "5.5.0", replacement = "Gtk.CssProvider.load_from_data")]
382    public Gtk.CssProvider? get_css_provider (string stylesheet) {
383        Gtk.CssProvider provider = new Gtk.CssProvider ();
384
385        try {
386            provider.load_from_data (stylesheet, -1);
387        }
388        catch (Error e) {
389            warning ("Could not create CSS Provider: %s\nStylesheet:\n%s",
390                     e.message, stylesheet);
391            return null;
392        }
393
394        return provider;
395    }
396
397    /**
398     * This method applies given text style to given label
399     *
400     * @param text_style text style to apply
401     * @param label label to apply style to
402     */
403    [Version (deprecated = true, deprecated_since = "0.4.2", replacement = "")]
404    public void apply_text_style_to_label (TextStyle text_style, Gtk.Label label) {
405        var style_provider = new Gtk.CssProvider ();
406        var style_context = label.get_style_context ();
407
408        string style_class, stylesheet;
409        stylesheet = text_style.get_stylesheet (out style_class);
410        style_context.add_class (style_class);
411
412        try {
413            style_provider.load_from_data (stylesheet, -1);
414        } catch (Error err) {
415            warning ("Couldn't apply style to label: %s", err.message);
416            return;
417        }
418
419        style_context.add_provider (style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
420    }
421
422    const string WM_SETTINGS_PATH = "org.gnome.desktop.wm.preferences";
423    const string PANTHEON_SETTINGS_PATH = "org.pantheon.desktop.gala.appearance";
424    const string WM_BUTTON_LAYOUT_KEY = "button-layout";
425
426    /**
427     * This method detects the close button position as configured for the window manager. If you
428     * need to know when this key changed, it's best to listen on the schema returned by
429     * {@link Granite.Widgets.Utils.get_button_layout_schema} for changes and then call this method again.
430     *
431     * @param position a {@link Granite.CloseButtonPosition} indicating where to best put the close button
432     * @return if no schema was detected by {@link Granite.Widgets.Utils.get_button_layout_schema}
433     *         or there was no close value in the button-layout string, false will be returned. The position
434     *         will be LEFT in that case.
435     */
436    [Version (deprecated = true, deprecated_since = "5.5.0", replacement = "")]
437    public bool get_default_close_button_position (out CloseButtonPosition position) {
438        // default value
439        position = CloseButtonPosition.LEFT;
440
441        var schema = get_button_layout_schema ();
442        if (schema == null) {
443            return false;
444        }
445
446        var layout = new GLib.Settings (schema).get_string (WM_BUTTON_LAYOUT_KEY);
447        var parts = layout.split (":");
448
449        if (parts.length < 2) {
450            return false;
451        }
452
453        if ("close" in parts[0]) {
454            position = CloseButtonPosition.LEFT;
455            return true;
456        } else if ("close" in parts[1]) {
457            position = CloseButtonPosition.RIGHT;
458            return true;
459        }
460
461        return false;
462    }
463
464    /**
465     * This methods returns the schema used by {@link Granite.Widgets.Utils.get_default_close_button_position}
466     * to determine the close button placement. It will first check for the pantheon/gala schema and then fallback
467     * to the default gnome one. If neither is available, null is returned. Make sure to check for this case,
468     * as otherwise your program may crash on startup.
469     *
470     * @return the schema name. If the layout could not be determined, a warning will be thrown and null will be returned
471     */
472    [Version (deprecated = true, deprecated_since = "5.5.0", replacement = "")]
473    public string? get_button_layout_schema () {
474        var sss = SettingsSchemaSource.get_default ();
475
476        if (sss != null) {
477            if (sss.lookup (PANTHEON_SETTINGS_PATH, true) != null) {
478                return PANTHEON_SETTINGS_PATH;
479            } else if (sss.lookup (WM_SETTINGS_PATH, true) != null) {
480                return WM_SETTINGS_PATH;
481            }
482        }
483
484        warning ("No schema indicating the button-layout is installed.");
485        return null;
486    }
487}
488