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