1/**
2 * Copyright (c) 2019-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: Giacomo Alberini <giacomoalbe@gmail.com>
20 * Authored by: Alessandro "Alecaddd" Castellani <castellani.ale@gmail.com>
21 */
22
23public class Akira.Lib.Managers.ItemsManager : Object {
24    public weak Akira.Window window { get; construct; }
25
26    public Akira.Models.ListModel<Lib.Items.CanvasItem> free_items;
27    public Akira.Models.ListModel<Lib.Items.CanvasArtboard> artboards;
28    public Akira.Models.ListModel<Lib.Items.CanvasImage> images;
29    private GLib.Type item_type { get; set; }
30    private Goo.CanvasItem root;
31    private int border_size;
32    private Gdk.RGBA border_color;
33    private Gdk.RGBA fill_color;
34
35    // Keep track of the expensive Artboard change method.
36    private bool is_changing = false;
37
38    // Keep track of newly imported images before creation.
39    public Lib.Managers.ImageManager? image_manager;
40
41    public ItemsManager (Akira.Window window) {
42        Object (
43            window: window
44        );
45    }
46
47    construct {
48        free_items = new Akira.Models.ListModel<Lib.Items.CanvasItem> ();
49        artboards = new Akira.Models.ListModel<Lib.Items.CanvasArtboard> ();
50        images = new Akira.Models.ListModel<Lib.Items.CanvasImage> ();
51
52        border_color = Gdk.RGBA ();
53        fill_color = Gdk.RGBA ();
54
55        window.event_bus.insert_item.connect (set_item_to_insert);
56        window.event_bus.request_delete_item.connect (on_request_delete_item);
57        window.event_bus.detect_artboard_change.connect (on_detect_artboard_change);
58    }
59
60    public void insert_image (Lib.Managers.ImageManager manager) {
61        image_manager = manager;
62        window.event_bus.insert_item ("image");
63    }
64
65    public Items.CanvasItem? insert_item (
66        double x,
67        double y,
68        Lib.Managers.ImageManager? manager = null,
69        Items.CanvasArtboard? artboard = null
70    ) {
71        update_default_values ();
72
73        Items.CanvasItem? new_item = null;
74
75        // Populate root item here and not in the construct @since
76        // there the canvas is not yet defined, so we need to wait for
77        // the first item to be created to fill this variable
78        if (root == null) {
79            root = window.main_window.main_canvas.canvas.get_root_item ();
80        }
81
82        if (artboard == null) {
83            foreach (Items.CanvasArtboard _artboard in artboards) {
84                if (_artboard.is_inside (x, y)) {
85                    artboard = _artboard;
86                    break;
87                }
88            }
89        }
90
91        // We can't use a switch () method here because the typeof () method is not supported.
92        if (item_type == typeof (Items.CanvasArtboard)) {
93            new_item = add_artboard (x, y);
94        }
95
96        if (item_type == typeof (Items.CanvasRect)) {
97            new_item = add_rect (x, y, root, artboard);
98        }
99
100        if (item_type == typeof (Items.CanvasEllipse)) {
101            new_item = add_ellipse (x, y, root, artboard);
102        }
103
104        if (item_type == typeof (Items.CanvasText)) {
105            new_item = add_text (x, y, root, artboard);
106        }
107
108        if (item_type == typeof (Items.CanvasImage)) {
109            // If we don't have a manager passed to this method but a general image manager
110            // is available in the class, it means the user is importing a new image.
111            if (manager == null && image_manager != null) {
112                manager = image_manager;
113            }
114            new_item = add_image (x, y, manager, root, artboard);
115
116            // Empty the image manager since we used it.
117            image_manager = null;
118        }
119
120        if (new_item == null) {
121            return null;
122        }
123
124        if (new_item is Items.CanvasArtboard) {
125            artboards.add_item.begin ((Items.CanvasArtboard) new_item);
126        } else {
127            // Add it to "free items" if it doesn't belong to an artboard.
128            if (new_item.artboard == null) {
129                free_items.add_item.begin ((Items.CanvasItem) new_item);
130            }
131
132            // We need to additionally store images in a dedicated list in order
133            // to easily access them when saving the .akira/Pictures folder.
134            // If we don't curate this dedicated list, it would be a nightamer to
135            // loop through all the free items and artboard items to check for images.
136            if (new_item is Items.CanvasImage) {
137                images.add_item.begin ((new_item as Akira.Lib.Items.CanvasImage));
138            }
139        }
140
141        window.event_bus.item_inserted ();
142        window.event_bus.file_edited ();
143
144        return new_item;
145    }
146
147    /**
148     * Helper method to add an item to the canvas, used when dragging an item
149     * outside an artboard where a reset of the parent root is necessary.
150     */
151    public void add_item_to_canvas (Lib.Items.CanvasItem item) {
152        item.set_parent (root);
153        item.parent.add_child (item, -1);
154        free_items.add_item.begin (item);
155        window.event_bus.file_edited ();
156        ((Lib.Canvas) item.canvas).update_canvas ();
157    }
158
159    /**
160     * Helper method to add an item to an artboard, used when dragging an item
161     * from the canvas or another artboard where a reset of the parent root is necessary.
162     */
163    public void add_item_to_artboard (Lib.Items.CanvasItem item, Lib.Items.CanvasArtboard artboard) {
164        item.set_parent (artboard);
165        item.artboard = artboard;
166        item.parent.add_child (item, -1);
167        item.check_add_to_artboard (item);
168        window.event_bus.file_edited ();
169    }
170
171    public void on_request_delete_item (Lib.Items.CanvasItem item) {
172        // Remove the layer from the Artboards list if it's an artboard.
173        if (item is Items.CanvasArtboard) {
174            artboards.remove_item.begin (item as Items.CanvasArtboard);
175        }
176
177        // Remove the image from the list so we don't keep it in the saved file.
178        if (item is Items.CanvasImage) {
179            images.remove_item.begin ((item as Akira.Lib.Items.CanvasImage));
180
181            // Mark it for removal if we have a saved file.
182            if (window.akira_file != null) {
183                window.akira_file.remove_image.begin (
184                    ((Akira.Lib.Items.CanvasImage) item).manager.filename
185                );
186            }
187        }
188
189        // Remove the layer from the Free Items list only if the item doesn't
190        // belong to an artboard, and it's not an artboard itself.
191        if (item.artboard == null && !(item is Items.CanvasArtboard)) {
192            free_items.remove_item.begin (item);
193        }
194
195        // Let the app know we're deleting an item.
196        window.event_bus.item_deleted (item);
197        item.delete ();
198        window.event_bus.file_edited ();
199    }
200
201    public Items.CanvasItem add_artboard (double x, double y) {
202        var artboard = new Items.CanvasArtboard (
203            Utils.AffineTransform.fix_size (x),
204            Utils.AffineTransform.fix_size (y),
205            root
206        );
207
208        return artboard as Items.CanvasItem;
209    }
210
211    public Items.CanvasItem add_rect (
212        double x,
213        double y,
214        Goo.CanvasItem parent,
215        Items.CanvasArtboard? artboard
216    ) {
217        return new Items.CanvasRect (
218            Utils.AffineTransform.fix_size (x),
219            Utils.AffineTransform.fix_size (y),
220            border_size,
221            border_color,
222            fill_color,
223            parent,
224            artboard
225        );
226    }
227
228    public Items.CanvasEllipse add_ellipse (
229        double x,
230        double y,
231        Goo.CanvasItem parent,
232        Items.CanvasArtboard? artboard
233    ) {
234        return new Items.CanvasEllipse (
235            Utils.AffineTransform.fix_size (x),
236            Utils.AffineTransform.fix_size (y),
237            border_size,
238            border_color,
239            fill_color,
240            parent,
241            artboard
242        );
243    }
244
245    public Items.CanvasText add_text (
246        double x,
247        double y,
248        Goo.CanvasItem parent,
249        Items.CanvasArtboard? artboard
250    ) {
251        return new Items.CanvasText (
252            "Akira is awesome :)",
253            Utils.AffineTransform.fix_size (x),
254            Utils.AffineTransform.fix_size (y),
255            200,
256            25f,
257            Goo.CanvasAnchorType.NW,
258            "Open Sans 18",
259            parent,
260            artboard
261        );
262    }
263
264    public Items.CanvasImage add_image (
265        double x,
266        double y,
267        Lib.Managers.ImageManager manager,
268        Goo.CanvasItem parent,
269        Items.CanvasArtboard? artboard
270    ) {
271        return new Items.CanvasImage (
272            Utils.AffineTransform.fix_size (x),
273            Utils.AffineTransform.fix_size (y),
274            manager,
275            parent,
276            artboard
277        );
278    }
279
280    public int get_item_z_index (Items.CanvasItem item) {
281        if (item.artboard != null) {
282            var items_count = (int) item.artboard.items.get_n_items ();
283            return items_count - 1 - item.artboard.items.index (item);
284        }
285
286        return (int) free_items.get_n_items () - 1 - free_items.index (item);
287    }
288
289    public int get_item_top_position (Items.CanvasItem item) {
290        if (item.artboard != null) {
291            return (int) item.artboard.items.get_n_items () - 1;
292        }
293
294        return (int) free_items.get_n_items () - 1;
295    }
296
297    public void set_item_to_insert (string insert_type) {
298        switch (insert_type) {
299            case "rectangle":
300                item_type = typeof (Items.CanvasRect);
301                break;
302
303            case "ellipse":
304                item_type = typeof (Items.CanvasEllipse);
305                break;
306
307            case "text":
308                item_type = typeof (Items.CanvasText);
309                break;
310
311            case "artboard":
312                item_type = typeof (Items.CanvasArtboard);
313                break;
314
315            case "image":
316                item_type = typeof (Items.CanvasImage);
317                break;
318        }
319    }
320
321    public void swap_items (int source_z_index, int target_z_index) {
322        // z-index is the exact opposite of items placement
323        // inside the free_items list
324        // last in is the topmost element
325        var free_items_length = (int) free_items.get_n_items ();
326
327        var source = free_items_length - 1 - source_z_index;
328        var target = free_items_length - 1 - target_z_index;
329
330        // Remove item at source position
331        var item_to_swap = free_items.remove_at (source);
332
333        // Insert item at target position
334        free_items.insert_at (target, item_to_swap);
335    }
336
337    private void update_default_values () {
338        fill_color.parse (settings.fill_color);
339
340        // Do not set the border if the user disabled it.
341        if (settings.set_border) {
342            border_size = (int) settings.border_size;
343            border_color.parse (settings.border_color);
344        }
345    }
346
347    /*
348     * Create an item loaded from an opened file.
349     *
350     * @param Json.Object obj - The json object containing the item to load.
351     */
352    public void load_item (Json.Object obj) {
353        Items.CanvasItem? item = null;
354        Items.CanvasArtboard? artboard = null;
355
356        var components = obj.get_member ("Components").get_object ();
357        var coordinates = components.get_member ("Coordinates").get_object ();
358        var pos_x = coordinates.get_double_member ("x");
359        var pos_y = coordinates.get_double_member ("y");
360
361        // If item is inside an artboard update the coordinates accordingly.
362        if (obj.has_member ("artboard")) {
363            foreach (var _artboard in artboards) {
364                if (_artboard.name.id == obj.get_string_member ("artboard")) {
365                    window.main_window.main_canvas.canvas.convert_from_item_space (
366                        _artboard, ref pos_x, ref pos_y
367                    );
368                    artboard = _artboard;
369                    break;
370                }
371            }
372        }
373
374        switch (obj.get_string_member ("type")) {
375            case "rectangle":
376                item_type = typeof (Items.CanvasRect);
377                item = insert_item (pos_x, pos_y, null, artboard);
378                break;
379
380            case "ellipse":
381                item_type = typeof (Items.CanvasEllipse);
382                item = insert_item (pos_x, pos_y, null, artboard);
383                break;
384
385            case "text":
386                item_type = typeof (Items.CanvasText);
387                item = insert_item (pos_x, pos_y, null, artboard);
388                break;
389
390            case "artboard":
391                item_type = typeof (Items.CanvasArtboard);
392                item = insert_item (pos_x, pos_y, null, artboard);
393                break;
394
395            case "image":
396                item_type = typeof (Items.CanvasImage);
397                var filename = obj.get_string_member ("image_id");
398                var file = File.new_for_path (
399                    Path.build_filename (
400                        window.akira_file.pictures_folder.get_path (),
401                        filename
402                    )
403                );
404                var manager = new Lib.Managers.ImageManager.from_archive (file, filename);
405                item = insert_item (pos_x, pos_y, manager, artboard);
406                break;
407        }
408
409        var selected_bound_manager = window.main_window.main_canvas.canvas.selected_bound_manager;
410        selected_bound_manager.add_item_to_selection (item);
411
412        restore_attributes (item, artboard, components);
413
414        // Restore the matrix transform to properly reset position and rotation.
415        var matrix = obj.get_member ("matrix").get_object ();
416        var new_matrix = Cairo.Matrix (
417            matrix.get_double_member ("xx"),
418            matrix.get_double_member ("yx"),
419            matrix.get_double_member ("xy"),
420            matrix.get_double_member ("yy"),
421            matrix.get_double_member ("x0"),
422            matrix.get_double_member ("y0")
423        );
424        item.set_transform (new_matrix);
425
426        selected_bound_manager.reset_selection ();
427    }
428
429    /*
430     * Restore the saved attributes of a loaded object.
431     *
432     * @param Items.CanvasItem item - The newly created item.
433     * @param Json.Object obj - The json object containing the item's attributes.
434     */
435    private void restore_attributes (Items.CanvasItem item, Items.CanvasArtboard? artboard, Json.Object components) {
436        // Restore identifiers.
437        if (components.has_member ("Name")) {
438            var name = components.get_member ("Name").get_object ();
439            item.name.id = name.get_string_member ("id");
440            item.name.name = name.get_string_member ("name");
441            item.name.icon = name.get_string_member ("icon");
442        }
443
444        // Restore opacity.
445        if (components.has_member ("Opacity")) {
446            var opacity = components.get_member ("Opacity").get_object ();
447            item.opacity.opacity = opacity.get_double_member ("opacity");
448        }
449
450        // Restore rotation.
451        if (components.has_member ("Rotation")) {
452            var rotation = components.get_member ("Rotation").get_object ();
453            item.rotation.rotation = rotation.get_double_member ("rotation");
454        }
455
456        // Restore size.
457        if (components.has_member ("Size")) {
458            var size = components.get_member ("Size").get_object ();
459            item.size.locked = size.get_boolean_member ("locked");
460            item.size.ratio = size.get_double_member ("ratio");
461            item.size.width = size.get_double_member ("width");
462            item.size.height = size.get_double_member ("height");
463        }
464
465        // Restore flipped.
466        if (components.has_member ("Flipped")) {
467            var flipped = components.get_member ("Flipped").get_object ();
468            item.flipped.horizontal = flipped.get_boolean_member ("horizontal");
469            item.flipped.vertical = flipped.get_boolean_member ("vertical");
470        }
471
472        // Restore border radius.
473        if (components.has_member ("BorderRadius")) {
474            var border_radius = components.get_member ("BorderRadius").get_object ();
475            item.border_radius.x = border_radius.get_double_member ("x");
476            item.border_radius.y = border_radius.get_double_member ("y");
477            item.border_radius.uniform = border_radius.get_boolean_member ("uniform");
478            item.border_radius.autoscale = border_radius.get_boolean_member ("autoscale");
479        }
480
481        // Restore layer.
482        if (components.has_member ("Layer")) {
483            var layer = components.get_member ("Layer").get_object ();
484            item.layer.locked = layer.get_boolean_member ("locked");
485        }
486
487        // Restore fills.
488        if (components.has_member ("Fills")) {
489            // Delete all pre-existing fills to be sure we're starting with a clean slate.
490            foreach (Lib.Components.Fill fill in item.fills.fills) {
491                item.fills.fills.remove (fill);
492            }
493
494            var fills = components.get_member ("Fills").get_object ();
495            fills.foreach_member ((i, name, node) => {
496                var obj = node.get_object ();
497                var color = Gdk.RGBA ();
498                color.parse (obj.get_string_member ("color"));
499                var fill = item.fills.add_fill_color (color);
500                fill.alpha = (int) obj.get_int_member ("alpha");
501                fill.hidden = obj.get_boolean_member ("hidden");
502            });
503
504            item.fills.reload ();
505        }
506
507        // Restore borders.
508        if (components.has_member ("Borders")) {
509            // Delete all pre-existing borders to be sure we're starting with a clean slate.
510            foreach (Lib.Components.Border border in item.borders.borders) {
511                item.borders.borders.remove (border);
512            }
513
514            var borders = components.get_member ("Borders").get_object ();
515            borders.foreach_member ((i, name, node) => {
516                var obj = node.get_object ();
517                var color = Gdk.RGBA ();
518                color.parse (obj.get_string_member ("color"));
519                var border = item.borders.add_border_color (color, (int) obj.get_int_member ("size"));
520                border.alpha = (int) obj.get_int_member ("alpha");
521                border.hidden = obj.get_boolean_member ("hidden");
522            });
523
524            item.borders.reload ();
525        }
526
527        // Restore image size.
528        if (item is Items.CanvasImage) {
529            ((Items.CanvasImage) item).resize_pixbuf (
530                (int) item.size.width,
531                (int) item.size.height,
532                true
533            );
534        }
535    }
536
537    /**
538     * Handle the aftermath of an item transformation, like size changes or movement
539     * to see if we need to add or remove an item to an Artboard.
540     */
541    private async void on_detect_artboard_change () {
542        // Interrupt if no artboard is currently present.
543        if (artboards.get_n_items () == 0) {
544            return;
545        }
546
547        // Interrupt if this is already running.
548        if (is_changing) {
549            return;
550        }
551
552        // Interrupt if no item is selected.
553        if (window.main_window.main_canvas.canvas.selected_bound_manager.selected_items.length () == 0) {
554            return;
555        }
556
557        is_changing = true;
558
559        // We need to copy the array of selected items as we need to remove and add items once
560        // moved to force the natural redraw of the canvas.
561        var items = window.main_window.main_canvas.canvas.selected_bound_manager.selected_items.copy ();
562
563        // Update the size ratio to always be faithful to the updated size.
564        foreach (var item in items) {
565            if (item is Items.CanvasArtboard) {
566                continue;
567            }
568            item.size.update_ratio ();
569        }
570
571        // Check if any of the currently moved items was dropped inside or outside any artboard.
572        foreach (var item in items) {
573            if (item is Items.CanvasArtboard) {
574                continue;
575            }
576
577            // Interrupt if the item is already inside an artboard and was only moved within it.
578            if (item.artboard != null && !item.artboard.is_outside (item)) {
579                continue;
580            }
581
582            Items.CanvasArtboard? new_artboard = null;
583
584            foreach (Items.CanvasArtboard artboard in artboards) {
585                // Interrupt the loop if we find an artboard that matches the dropped coordinate.
586                if (!artboard.is_outside (item)) {
587                    new_artboard = artboard;
588                    break;
589                }
590            }
591
592            yield change_artboard (item, new_artboard);
593        }
594
595        is_changing = false;
596    }
597
598    /**
599     * Add or remove an item from an artboard.
600     */
601    public async void change_artboard (Items.CanvasItem item, Items.CanvasArtboard? new_artboard) {
602        // Interrupt if the item was moved within its original artboard.
603        if (item.artboard == new_artboard) {
604            debug ("Same parent");
605            return;
606        }
607
608        // Save the coordinates before removing the item.
609        Cairo.Matrix matrix;
610        item.get_transform (out matrix);
611
612        // If the item was moved from inside an Artboard to the empty Canvas.
613        if (item.artboard != null && new_artboard == null) {
614            debug ("Artboard => Free Item");
615
616            // Convert the matrix transform before removing the item from the artboard.
617            item.canvas.convert_from_item_space (item.artboard, ref matrix.x0, ref matrix.y0);
618
619            // Remove the item from the Artboard.
620            item.artboard.remove_item (item);
621
622            // Remove the item from the selection and redraw the layers panel.
623            window.event_bus.item_deleted (item);
624
625            // Attach the item to the Canvas.
626            add_item_to_canvas (item);
627
628            // Apply the updated coordinates.
629            item.set_transform (matrix);
630
631            window.event_bus.item_inserted ();
632            window.event_bus.request_add_item_to_selection (item);
633
634            return;
635        }
636
637        // If the item was moved from the empty Canvas to an Artboard.
638        if (item.artboard == null && new_artboard != null) {
639            debug ("Free Item => Artboard");
640
641            // Convert the matrix transform to the new artboard.
642            item.canvas.convert_to_item_space (new_artboard, ref matrix.x0, ref matrix.y0);
643
644            // Remove the child from the GooCanvasItem parent.
645            item.parent.remove_child (item.parent.find_child (item));
646
647            // Remove the item from the free items list.
648            free_items.remove_item.begin (item);
649
650            // Remove the item from the selection and redraw the layers panel.
651            window.event_bus.item_deleted (item);
652
653            // Attach the item to the Artboard.
654            add_item_to_artboard (item, new_artboard);
655
656            // Apply the updated coordinates.
657            item.set_transform (matrix);
658
659            window.event_bus.item_inserted ();
660            window.event_bus.request_add_item_to_selection (item);
661
662            return;
663        }
664
665        // If the item was moved from inside an Artboard to another Artboard.
666        if (item.artboard != null && new_artboard != null) {
667            debug ("Artboard => Artboard");
668
669            // Passing from an artboard to another we need to first convert the coordinates
670            // from the old artboard to the global canvas, and then convert them again
671            // to the new artboard.
672            item.canvas.convert_from_item_space (item.artboard, ref matrix.x0, ref matrix.y0);
673            item.canvas.convert_to_item_space (new_artboard, ref matrix.x0, ref matrix.y0);
674
675            // Remove the item from the Artboard.
676            item.artboard.remove_item (item);
677
678            // Remove the item from the selection and redraw the layers panel.
679            window.event_bus.item_deleted (item);
680
681            // Attach the item to the Artboard.
682            add_item_to_artboard (item, new_artboard);
683
684            // Apply the updated coordinates.
685            item.set_transform (matrix);
686
687            // Trigger the canvas repaint after the item was added back.
688            window.event_bus.item_inserted ();
689            window.event_bus.request_add_item_to_selection (item);
690
691            return;
692        }
693    }
694}
695