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