1/* 2 * Copyright (c) 2021 Alecaddd (https://alecaddd.com) 3 * 4 * This file is part of Akira. 5 * 6 * Akira is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * Akira is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with Akira. If not, see <https://www.gnu.org/licenses/>. 18 * 19 * Authored by: Alessandro "alecaddd" Castellani <castellani.ale@gmail.com> 20 */ 21 22/* 23 * Helper class to quickly create a container with a color button and a color 24 * picker. The color button opens up the GtkColorChooser. 25 */ 26public class Akira.Widgets.ColorRow : Gtk.Grid { 27 private unowned Akira.Window window; 28 private unowned Models.ColorModel model; 29 30 private Gtk.Button color_button; 31 private Gtk.Popover color_popover; 32 private Gtk.ColorChooserWidget? color_chooser_widget = null; 33 private Gtk.FlowBox global_colors_flowbox; 34 private ColorField field; 35 private InputField opacity_field; 36 37 /* 38 * If the color or alpha are manually set from the ColorPicker. 39 * If true, the ColorChooserWidget doesn't need to be updated. 40 */ 41 private bool color_set_manually = false; 42 43 /* 44 * Keep track of the current color when the user is updating the string 45 * and the format is not valid. 46 */ 47 private string old_color; 48 49 /* 50 * Type of color containers to add new colors to. We can potentially create 51 * an API to allow adding more containers to the color picker popup. 52 */ 53 private enum Container { 54 GLOBAL, 55 DOCUMENT 56 } 57 58 public ColorRow (Akira.Window window, Models.ColorModel model) { 59 this.window = window; 60 this.model = model; 61 62 old_color = model.color; 63 64 margin_top = margin_bottom = 1; 65 66 var container = new Gtk.Grid (); 67 var context = container.get_style_context (); 68 context.add_class ("selected-color-container"); 69 context.add_class ("bg-pattern"); 70 71 color_button = new Gtk.Button (); 72 color_button.vexpand = true; 73 color_button.width_request = 40; 74 color_button.can_focus = false; 75 color_button.get_style_context ().add_class ("selected-color"); 76 color_button.set_tooltip_text (_("Choose color")); 77 78 color_popover = new Gtk.Popover (color_button); 79 color_popover.position = Gtk.PositionType.BOTTOM; 80 81 color_button.clicked.connect (() => { 82 init_color_chooser (); 83 color_popover.popup (); 84 }); 85 86 container.add (color_button); 87 set_button_color (model.color, model.alpha); 88 89 // Define the eye dropper button. 90 var eyedropper_button = new Gtk.Button (); 91 eyedropper_button.get_style_context ().add_class ("color-picker-button"); 92 eyedropper_button.can_focus = false; 93 eyedropper_button.valign = Gtk.Align.CENTER; 94 eyedropper_button.set_tooltip_text (_("Pick color")); 95 eyedropper_button.add ( 96 new Gtk.Image.from_icon_name ("color-select-symbolic", 97 Gtk.IconSize.SMALL_TOOLBAR) 98 ); 99 eyedropper_button.clicked.connect (on_eyedropper_click); 100 101 add (container); 102 add (eyedropper_button); 103 104 field = new ColorField (window); 105 field.text = Utils.Color.rgba_to_hex (model.color); 106 field.changed.connect (() => { 107 // Don't do anything if the color change came from the chooser. 108 if (color_set_manually) { 109 return; 110 } 111 112 var field_hex = field.text; 113 // Interrupt if what's written is not a valid color value. 114 if (!Utils.Color.is_valid_hex (field_hex)) { 115 return; 116 } 117 118 // Since we will update the color picker, prevent an infinite loop. 119 color_set_manually = true; 120 121 var new_rgba = Utils.Color.hex_to_rgba (field_hex); 122 model.color = new_rgba.to_string (); 123 set_button_color (field_hex, model.alpha); 124 125 // Update the chooser widget only if it was already initialized. 126 if (color_chooser_widget != null) { 127 set_chooser_color (new_rgba.to_string (), model.alpha); 128 } 129 130 // Reset the bool to allow edits from the color chooser. 131 color_set_manually = false; 132 }); 133 134 add (field); 135 136 // Show the opacity field if this widget was generated from the Fills list. 137 if (model.type == Models.ColorModel.Type.FILL) { 138 opacity_field = new InputField (InputField.Unit.PERCENTAGE, 7, true, true); 139 opacity_field.entry.sensitive = true; 140 opacity_field.entry.value = Math.round ((double) model.alpha / 255 * 100); 141 142 opacity_field.entry.value_changed.connect (() => { 143 // Don't do anything if the color change came from the chooser. 144 if (color_set_manually) { 145 return; 146 } 147 148 // Since we will update the color picker, prevent an infinite loop. 149 color_set_manually = true; 150 151 var alpha = (int) ((double) opacity_field.entry.value / 100 * 255); 152 model.alpha = alpha; 153 set_button_color (model.color, alpha); 154 155 // Update the chooser widget only if it was already initialized. 156 if (color_chooser_widget != null) { 157 set_chooser_color (model.color, alpha); 158 } 159 160 // Reset the bool to allow edits from the color chooser. 161 color_set_manually = false; 162 }); 163 164 add (opacity_field); 165 } 166 167 // Show the border field if this widget was generated from the Borders list. 168 if (model.type == Models.ColorModel.Type.BORDER) { 169 var border = new InputField (InputField.Unit.PIXEL, 7, true, true); 170 border.set_range (0, Layouts.MainCanvas.CANVAS_SIZE / 2); 171 border.entry.sensitive = true; 172 border.entry.value = model.size; 173 border.entry.bind_property ("value", model, "size", BindingFlags.BIDIRECTIONAL); 174 175 add (border); 176 } 177 } 178 179 private void init_color_chooser () { 180 if (color_chooser_widget != null) { 181 return; 182 } 183 184 color_chooser_widget = new Gtk.ColorChooserWidget (); 185 color_chooser_widget.hexpand = true; 186 color_chooser_widget.show_editor = true; 187 188 var color_grid = new Gtk.Grid (); 189 color_grid.get_style_context ().add_class ("color-picker"); 190 color_grid.row_spacing = 12; 191 192 var global_colors_label = new Gtk.Label (_("Global colors")); 193 global_colors_label.halign = Gtk.Align.START; 194 global_colors_label.margin_start = global_colors_label.margin_end = 6; 195 196 global_colors_flowbox = new Gtk.FlowBox (); 197 global_colors_flowbox.get_style_context ().add_class ("color-grid"); 198 global_colors_flowbox.selection_mode = Gtk.SelectionMode.NONE; 199 global_colors_flowbox.homogeneous = false; 200 global_colors_flowbox.column_spacing = global_colors_flowbox.row_spacing = 6; 201 global_colors_flowbox.margin_start = global_colors_flowbox.margin_end = 6; 202 // Large number to allow children to spread out the available space. 203 global_colors_flowbox.max_children_per_line = 100; 204 global_colors_flowbox.set_sort_func (sort_colors_function); 205 206 var add_global_color_btn = new AddColorButton (); 207 add_global_color_btn.clicked.connect (() => { 208 on_save_color (Container.GLOBAL); 209 }); 210 global_colors_flowbox.add (add_global_color_btn); 211 212 foreach (string color in settings.global_colors) { 213 var btn = create_color_button (color); 214 global_colors_flowbox.add (btn); 215 } 216 217 color_grid.attach (color_chooser_widget, 0, 0, 1, 1); 218 color_grid.attach (global_colors_label, 0, 1, 1, 1); 219 color_grid.attach (global_colors_flowbox, 0, 2, 1, 1); 220 color_grid.show_all (); 221 color_popover.add (color_grid); 222 223 // Set the chooser color before connecting the signal. 224 set_chooser_color (model.color, model.alpha); 225 226 color_chooser_widget.notify["rgba"].connect (on_color_changed); 227 } 228 229 private int sort_colors_function (Gtk.FlowBoxChild a, Gtk.FlowBoxChild b) { 230 return (a is AddColorButton) ? -1 : 1; 231 } 232 233 /* 234 * Add the current color to the parent flowbox. 235 */ 236 private void on_save_color (Container parent) { 237 // Store the currently active color. 238 var color = color_chooser_widget.rgba.to_string (); 239 240 // Create the new color button and connect to its signal. 241 var btn = create_color_button (color); 242 243 // Update the colors list and the schema based on the colors container. 244 switch (parent) { 245 case Container.GLOBAL: 246 global_colors_flowbox.add (btn); 247 var array = settings.global_colors; 248 array += color; 249 settings.global_colors = array; 250 break; 251 252 case Container.DOCUMENT: 253 // TODO... 254 break; 255 } 256 } 257 258 private void set_chooser_color (string color, int alpha) { 259 var new_rgba = Gdk.RGBA (); 260 new_rgba.parse (color); 261 new_rgba.alpha = (double) alpha / 255; 262 color_chooser_widget.set_rgba (new_rgba); 263 } 264 265 private void set_button_color (string color, int alpha) { 266 try { 267 var provider = new Gtk.CssProvider (); 268 var context = color_button.get_style_context (); 269 270 var new_rgba = Gdk.RGBA (); 271 new_rgba.parse (color); 272 new_rgba.alpha = (double) alpha / 255; 273 var new_color = new_rgba.to_string (); 274 275 var css = """.selected-color { 276 background-color: %s; 277 border-color: shade (%s, 0.75); 278 }""".printf (new_color, new_color); 279 280 provider.load_from_data (css, css.length); 281 282 context.add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); 283 } catch (Error e) { 284 warning ("Style error: %s", e.message); 285 } 286 } 287 288 private Gtk.FlowBoxChild create_color_button (string color) { 289 var child = new Gtk.FlowBoxChild (); 290 child.valign = child.halign = Gtk.Align.CENTER; 291 292 var btn = new Widgets.RoundedColorButton (color); 293 btn.set_color.connect ((color) => { 294 var rgba_color = Gdk.RGBA (); 295 rgba_color.parse (color); 296 color_chooser_widget.set_rgba (rgba_color); 297 }); 298 299 child.add (btn); 300 child.show_all (); 301 return child; 302 } 303 304 private void on_color_changed () { 305 // The color change came from the input field, prevent an infinite loop. 306 if (color_set_manually) { 307 return; 308 } 309 310 // Prevent visible updated fields like hex and opacity from triggering 311 // the update of the model values. 312 color_set_manually = true; 313 314 // Update the model values. 315 model.color = color_chooser_widget.rgba.to_string (); 316 model.alpha = (int) (color_chooser_widget.rgba.alpha * 255); 317 318 // Update the UI. 319 set_button_color (model.color, model.alpha); 320 field.text = Utils.Color.rgba_to_hex (model.color); 321 if (model.type == Models.ColorModel.Type.FILL) { 322 opacity_field.entry.value = Math.round ((double) model.alpha / 255 * 100); 323 } 324 325 // Allow manual edit from the input fields. 326 color_set_manually = false; 327 } 328 329 private void on_eyedropper_click () { 330 var eyedropper = new Akira.Utils.ColorPicker (); 331 eyedropper.show_all (); 332 333 eyedropper.picked.connect ((picked_color) => { 334 init_color_chooser (); 335 color_chooser_widget.set_rgba (picked_color); 336 eyedropper.close (); 337 }); 338 339 eyedropper.cancelled.connect (() => { 340 eyedropper.close (); 341 }); 342 } 343} 344