1/*
2* Copyright (c) 2019-2020 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* Authored by: Ivan "isneezy" Vilanculo <vilanculoivan@gmail.com>
21*/
22
23public class Akira.Services.ActionManager : Object {
24    public weak Akira.Application app { get; construct; }
25    public weak Akira.Window window { get; construct; }
26
27    private const int PREVIEW_SIZE = 300;
28    private const int PREVIEW_PADDING = 3;
29
30    private Gtk.FileChooserNative dialog;
31    private Gtk.Image preview_image;
32
33    public SimpleActionGroup actions { get; construct; }
34
35    public const string ACTION_PREFIX = "win.";
36    public const string ACTION_NEW_WINDOW = "action_new_window";
37    public const string ACTION_OPEN = "action_open";
38    public const string ACTION_SAVE = "action_save";
39    public const string ACTION_SAVE_AS = "action_save_as";
40    public const string ACTION_LOAD_FIRST = "action_load_first";
41    public const string ACTION_LOAD_SECOND = "action_load_second";
42    public const string ACTION_LOAD_THIRD = "action_load_third";
43    public const string ACTION_TOGGLE_PIXEL_GRID = "action-show-pixel-grid";
44    public const string ACTION_PRESENTATION = "action_presentation";
45    public const string ACTION_PREFERENCES = "action_preferences";
46    public const string ACTION_EXPORT_SELECTION = "action_export_selection";
47    public const string ACTION_EXPORT_ARTBOARDS = "action_export_artboards";
48    public const string ACTION_EXPORT_GRAB = "action_export_grab";
49    public const string ACTION_QUIT = "action_quit";
50    public const string ACTION_ZOOM_IN = "action_zoom_in";
51    public const string ACTION_ZOOM_IN_2 = "action_zoom_in_2";
52    public const string ACTION_ZOOM_OUT = "action_zoom_out";
53    public const string ACTION_ZOOM_RESET = "action_zoom_reset";
54    public const string ACTION_MOVE_UP = "action_move_up";
55    public const string ACTION_MOVE_DOWN = "action_move_down";
56    public const string ACTION_MOVE_TOP = "action_move_top";
57    public const string ACTION_MOVE_BOTTOM = "action_move_bottom";
58    public const string ACTION_ARTBOARD_TOOL = "action_artboard_tool";
59    public const string ACTION_RECT_TOOL = "action_rect_tool";
60    public const string ACTION_ELLIPSE_TOOL = "action_ellipse_tool";
61    public const string ACTION_TEXT_TOOL = "action_text_tool";
62    public const string ACTION_IMAGE_TOOL = "action_image_tool";
63    public const string ACTION_DELETE = "action_delete";
64    public const string ACTION_FLIP_H = "action_flip_h";
65    public const string ACTION_FLIP_V = "action_flip_v";
66    public const string ACTION_ESCAPE = "action_escape";
67    public const string ACTION_SHORTCUTS = "action_shortcuts";
68    public const string ACTION_PICK_COLOR = "action_pick_color";
69
70    public static Gee.MultiMap<string, string> action_accelerators = new Gee.HashMultiMap<string, string> ();
71    public static Gee.MultiMap<string, string> typing_accelerators = new Gee.HashMultiMap<string, string> ();
72
73    private const ActionEntry[] ACTION_ENTRIES = {
74        { ACTION_NEW_WINDOW, action_new_window },
75        { ACTION_OPEN, action_open },
76        { ACTION_SAVE, action_save },
77        { ACTION_SAVE_AS, action_save_as },
78        { ACTION_LOAD_FIRST, action_load_first },
79        { ACTION_LOAD_SECOND, action_load_second },
80        { ACTION_LOAD_THIRD, action_load_third },
81        { ACTION_TOGGLE_PIXEL_GRID, action_toggle_pixel_grid },
82        { ACTION_PRESENTATION, action_presentation },
83        { ACTION_PREFERENCES, action_preferences },
84        { ACTION_EXPORT_SELECTION, action_export_selection },
85        { ACTION_EXPORT_ARTBOARDS, action_export_artboards },
86        { ACTION_EXPORT_GRAB, action_export_grab },
87        { ACTION_QUIT, action_quit },
88        { ACTION_ZOOM_IN, action_zoom_in },
89        { ACTION_ZOOM_IN_2, action_zoom_in_2 },
90        { ACTION_ZOOM_OUT, action_zoom_out },
91        { ACTION_MOVE_UP, action_move_up },
92        { ACTION_MOVE_DOWN, action_move_down },
93        { ACTION_MOVE_TOP, action_move_top },
94        { ACTION_MOVE_BOTTOM, action_move_bottom },
95        { ACTION_ZOOM_RESET, action_zoom_reset },
96        { ACTION_ARTBOARD_TOOL, action_artboard_tool },
97        { ACTION_RECT_TOOL, action_rect_tool },
98        { ACTION_ELLIPSE_TOOL, action_ellipse_tool },
99        { ACTION_TEXT_TOOL, action_text_tool },
100        { ACTION_IMAGE_TOOL, action_image_tool },
101        { ACTION_DELETE, action_delete },
102        { ACTION_FLIP_H, action_flip_h },
103        { ACTION_FLIP_V, action_flip_v },
104        { ACTION_ESCAPE, action_escape },
105        { ACTION_SHORTCUTS, action_shortcuts },
106        { ACTION_PICK_COLOR, action_pick_color },
107    };
108
109    public ActionManager (Akira.Application akira_app, Akira.Window window) {
110        Object (
111            app: akira_app,
112            window: window
113        );
114    }
115
116    static construct {
117        action_accelerators.set (ACTION_NEW_WINDOW, "<Control>n");
118        action_accelerators.set (ACTION_OPEN, "<Control>o");
119        action_accelerators.set (ACTION_SAVE, "<Control>s");
120        action_accelerators.set (ACTION_SAVE_AS, "<Control><Shift>s");
121        action_accelerators.set (ACTION_LOAD_FIRST, "<Control><Alt>1");
122        action_accelerators.set (ACTION_LOAD_SECOND, "<Control><Alt>2");
123        action_accelerators.set (ACTION_LOAD_THIRD, "<Control><Alt>3");
124        action_accelerators.set (ACTION_PRESENTATION, "<Control>period");
125        action_accelerators.set (ACTION_PREFERENCES, "<Control>comma");
126        action_accelerators.set (ACTION_EXPORT_SELECTION, "<Control><Alt>e");
127        action_accelerators.set (ACTION_EXPORT_ARTBOARDS, "<Control><Alt>a");
128        action_accelerators.set (ACTION_EXPORT_GRAB, "<Control><Alt>g");
129        action_accelerators.set (ACTION_QUIT, "<Control>q");
130        action_accelerators.set (ACTION_ZOOM_IN_2, "<Control>equal");
131        action_accelerators.set (ACTION_ZOOM_IN, "<Control>plus");
132        action_accelerators.set (ACTION_ZOOM_OUT, "<Control>minus");
133        action_accelerators.set (ACTION_ZOOM_RESET, "<Control>0");
134        action_accelerators.set (ACTION_MOVE_UP, "<Control>Up");
135        action_accelerators.set (ACTION_MOVE_DOWN, "<Control>Down");
136        action_accelerators.set (ACTION_MOVE_TOP, "<Control><Shift>Up");
137        action_accelerators.set (ACTION_MOVE_BOTTOM, "<Control><Shift>Down");
138        action_accelerators.set (ACTION_FLIP_H, "<Control>bracketleft");
139        action_accelerators.set (ACTION_FLIP_V, "<Control>bracketright");
140        action_accelerators.set (ACTION_SHORTCUTS, "F1");
141        action_accelerators.set (ACTION_PICK_COLOR, "<Alt>c");
142
143        typing_accelerators.set (ACTION_ESCAPE, "Escape");
144        typing_accelerators.set (ACTION_ARTBOARD_TOOL, "a");
145        typing_accelerators.set (ACTION_RECT_TOOL, "r");
146        typing_accelerators.set (ACTION_ELLIPSE_TOOL, "e");
147        typing_accelerators.set (ACTION_TEXT_TOOL, "t");
148        typing_accelerators.set (ACTION_IMAGE_TOOL, "i");
149        typing_accelerators.set (ACTION_DELETE, "Delete");
150        typing_accelerators.set (ACTION_DELETE, "BackSpace");
151        typing_accelerators.set (ACTION_TOGGLE_PIXEL_GRID, "<Shift>Tab");
152    }
153
154    construct {
155        actions = new SimpleActionGroup ();
156        actions.add_action_entries (ACTION_ENTRIES, this);
157        window.insert_action_group ("win", actions);
158
159        var iter = action_accelerators.map_iterator ();
160        while (iter.next ()) {
161            app.set_accels_for_action (ACTION_PREFIX + iter.get_key (), { iter.get_value () });
162        }
163
164        enable_typing_accels ();
165
166        window.event_bus.disconnect_typing_accel.connect (disable_typing_accels);
167        window.event_bus.connect_typing_accel.connect (enable_typing_accels);
168    }
169
170    // Temporarily disable all the accelerators that might interfere with input fields.
171    private void disable_typing_accels () {
172        var iter = typing_accelerators.map_iterator ();
173        while (iter.next ()) {
174            app.set_accels_for_action (ACTION_PREFIX + iter.get_key (), {});
175        }
176    }
177
178    // Enable all the accelerators that might interfere with input fields.
179    private void enable_typing_accels () {
180        var iter = typing_accelerators.map_iterator ();
181        while (iter.next ()) {
182            app.set_accels_for_action (ACTION_PREFIX + iter.get_key (), { iter.get_value () });
183        }
184    }
185
186    private void action_quit () {
187        window.before_destroy ();
188    }
189
190    private void action_presentation () {
191        window.event_bus.toggle_presentation_mode ();
192    }
193
194    private void action_new_window () {
195        app.new_window ();
196    }
197
198    private void action_open () {
199        window.file_manager.open_file ();
200    }
201
202    private void action_save () {
203        window.file_manager.save_file ();
204    }
205
206    private void action_save_as () {
207        window.file_manager.save_file_as ();
208    }
209
210    public void action_load_first () {
211        if (settings.recently_opened.length == 0 || settings.recently_opened[0] == null) {
212            window.event_bus.canvas_notification (_("No recently opened file available!"));
213            return;
214        }
215
216        var file = File.new_for_path (settings.recently_opened[0]);
217        if (!file.query_exists ()) {
218            window.event_bus.canvas_notification (
219                _("Unable to open file at '%s'").printf (settings.recently_opened[0])
220            );
221            return;
222        }
223
224        File[] files = {};
225        files += file;
226        window.app.open (files, "");
227    }
228
229    private void action_load_second () {
230        if (settings.recently_opened.length < 1 || settings.recently_opened[1] == null) {
231            window.event_bus.canvas_notification (_("No second most recently opened file available!"));
232            return;
233        }
234
235        var file = File.new_for_path (settings.recently_opened[1]);
236        if (!file.query_exists ()) {
237            window.event_bus.canvas_notification (
238                _("Unable to open file at '%s'").printf (settings.recently_opened[1])
239            );
240            return;
241        }
242
243        File[] files = {};
244        files += file;
245        window.app.open (files, "");
246    }
247
248    private void action_load_third () {
249        if (settings.recently_opened.length < 2 || settings.recently_opened[2] == null) {
250            window.event_bus.canvas_notification (_("No third most recently opened file available!"));
251            return;
252        }
253
254        var file = File.new_for_path (settings.recently_opened[2]);
255        if (!file.query_exists ()) {
256            window.event_bus.canvas_notification (
257                _("Unable to open file at '%s'").printf (settings.recently_opened[2])
258            );
259            return;
260        }
261
262        File[] files = {};
263        files += file;
264        window.app.open (files, "");
265    }
266
267    private void action_toggle_pixel_grid () {
268        window.event_bus.toggle_pixel_grid ();
269    }
270
271    private void action_preferences () {
272        var settings_dialog = new Akira.Dialogs.SettingsDialog (window);
273        settings_dialog.show_all ();
274        settings_dialog.present ();
275        settings_dialog.close.connect (() => {
276            window.event_bus.set_focus_on_canvas ();
277        });
278    }
279
280    private void action_export_selection () {
281        weak Akira.Lib.Canvas canvas = window.main_window.main_canvas.canvas;
282        if (canvas.selected_bound_manager.selected_items.length () == 0) {
283            // Check if an element is currently selected.
284            window.event_bus.canvas_notification (_("Nothing selected to export!"));
285            return;
286        }
287
288        canvas.export_manager.create_selection_snapshot ();
289    }
290
291    private void action_export_artboards () {
292        // Check if at least an artboard is present.
293        window.event_bus.canvas_notification (_("Export of Artboards currently unavailable…sorry ��️"));
294        // TODO: Trigger artboards pixbuf generation.
295    }
296
297    private void action_export_grab () {
298        weak Akira.Lib.Canvas canvas = window.main_window.main_canvas.canvas;
299        canvas.start_export_area_selection ();
300    }
301
302    private void action_zoom_in () {
303        window.event_bus.update_scale (0.1);
304    }
305
306    private void action_zoom_in_2 () {
307        action_zoom_in ();
308    }
309
310    private void action_zoom_out () {
311        window.event_bus.update_scale (-0.1);
312    }
313
314    private void action_zoom_reset () {
315        window.event_bus.set_scale (1);
316    }
317
318    private void action_move_up () {
319        window.event_bus.change_z_selected (true, false);
320    }
321
322    private void action_move_down () {
323        window.event_bus.change_z_selected (false, false);
324    }
325
326    private void action_move_top () {
327        window.event_bus.change_z_selected (true, true);
328    }
329
330    private void action_move_bottom () {
331        window.event_bus.change_z_selected (false, true);
332    }
333
334    private void action_artboard_tool () {
335        window.event_bus.insert_item ("artboard");
336    }
337
338    private void action_rect_tool () {
339        window.event_bus.insert_item ("rectangle");
340    }
341
342    // Delete the currently selected items.
343    private void action_delete () {
344        window.main_window.main_canvas.canvas.selected_bound_manager.delete_selection ();
345    }
346
347    private void action_flip_h () {
348        window.event_bus.flip_item ();
349    }
350
351    private void action_flip_v () {
352        window.event_bus.flip_item (true);
353    }
354
355    private void action_ellipse_tool () {
356        window.event_bus.insert_item ("ellipse");
357    }
358
359    private void action_text_tool () {
360        window.event_bus.insert_item ("text");
361    }
362
363    private void action_image_tool () {
364        dialog = new Gtk.FileChooserNative (
365            _("Choose image file"), window, Gtk.FileChooserAction.OPEN, _("Select"), _("Close"));
366
367        preview_image = new Gtk.Image ();
368        dialog.preview_widget = preview_image;
369        dialog.update_preview.connect (on_update_preview);
370
371        dialog.select_multiple = true;
372
373        dialog.response.connect ((response_id) => on_choose_image_response (dialog, response_id));
374        dialog.show ();
375    }
376
377    private void on_update_preview () {
378        string? filename = dialog.get_preview_filename ();
379        if (filename == null) {
380            dialog.set_preview_widget_active (false);
381            return;
382        }
383
384        // Read the image format data first.
385        int width = 0;
386        int height = 0;
387        Gdk.PixbufFormat? format = Gdk.Pixbuf.get_file_info (filename, out width, out height);
388
389        if (format == null) {
390            dialog.set_preview_widget_active (false);
391            return;
392        }
393
394        // If the image is too big, resize it.
395        Gdk.Pixbuf pixbuf;
396        try {
397            pixbuf = new Gdk.Pixbuf.from_file_at_scale (filename, PREVIEW_SIZE, PREVIEW_SIZE, true);
398        } catch (Error e) {
399            dialog.set_preview_widget_active (false);
400            return;
401        }
402
403        if (pixbuf == null) {
404            dialog.set_preview_widget_active (false);
405            return;
406        }
407
408        pixbuf = pixbuf.apply_embedded_orientation ();
409
410        // Distribute the extra space around the image.
411        int extra_space = PREVIEW_SIZE - pixbuf.width;
412        int smaller_half = extra_space / 2;
413        int larger_half = extra_space - smaller_half;
414
415        // Pad the image manually and avoids rounding errors.
416        preview_image.set_margin_start (PREVIEW_PADDING + smaller_half);
417        preview_image.set_margin_end (PREVIEW_PADDING + larger_half);
418
419        // Show the preview.
420        preview_image.set_from_pixbuf (pixbuf);
421        dialog.set_preview_widget_active (true);
422    }
423
424    private void on_choose_image_response (Gtk.FileChooserNative dialog, int response_id) {
425        switch (response_id) {
426            case Gtk.ResponseType.ACCEPT:
427            case Gtk.ResponseType.OK:
428                SList<File> files = dialog.get_files ();
429                files.@foreach ((file) => {
430                    if (!Akira.Utils.Image.is_valid_image (file)) {
431                        window.event_bus.canvas_notification (
432                            _("Error! .%s files are not supported!"
433                        ).printf (Akira.Utils.Image.get_extension (file)));
434                        return;
435                    }
436
437                    var manager = new Akira.Lib.Managers.ImageManager (file, files.index (file));
438                    window.items_manager.insert_image (manager);
439                });
440                break;
441        }
442        dialog.destroy ();
443    }
444
445    private void action_escape () {
446        window.event_bus.request_escape ();
447        // If the layout is hidden, allow users to easily get out of presentation mode
448        // when they press Escape.
449        if (!window.headerbar.toggled) {
450            action_presentation ();
451        }
452    }
453
454    private void action_shortcuts () {
455        var dialog = new Akira.Dialogs.ShortcutsDialog (window);
456        dialog.show_all ();
457        dialog.present ();
458    }
459
460    private void action_pick_color () {
461        weak Akira.Lib.Canvas canvas = window.main_window.main_canvas.canvas;
462
463        // Interrupt if no item is selected.
464        if (canvas.selected_bound_manager.selected_items.length () == 0) {
465            return;
466        }
467
468        // Hide the ghost bound manager.
469        canvas.toggle_item_ghost (false);
470
471        bool is_holding_shift = false;
472        var color_picker = new Akira.Utils.ColorPicker ();
473        color_picker.show_all ();
474
475        color_picker.key_pressed.connect (e => {
476            is_holding_shift = e.keyval == Gdk.Key.Shift_L;
477        });
478
479        color_picker.key_released.connect (e => {
480            is_holding_shift = e.keyval == Gdk.Key.Shift_L;
481        });
482
483        color_picker.cancelled.connect (() => {
484            color_picker.close ();
485        });
486
487        color_picker.picked.connect (color => {
488            foreach (var item in canvas.selected_bound_manager.selected_items) {
489                // Ignore the item if it doesn't have a fills or border component
490                // based on the shift key pressed by the user.
491                if ((item.fills == null && !is_holding_shift) || (item.borders == null && is_holding_shift)) {
492                    continue;
493                }
494
495                if (is_holding_shift) {
496                    item.borders.update_color_from_action (color);
497                    continue;
498                }
499
500                item.fills.update_color_from_action (color);
501            }
502
503            color_picker.close ();
504
505            // Force a UI reload of the fills and borders panel since some items
506            // had their properties changed.
507            canvas.window.event_bus.selected_items_list_changed (canvas.selected_bound_manager.selected_items);
508        });
509    }
510
511    public static void action_from_group (string action_name, ActionGroup? action_group) {
512        action_group.activate_action (action_name, null);
513    }
514}
515