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