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