diff --git a/src/FileFormat/AkiraFile.vala b/src/FileFormat/AkiraFile.vala index 6a4e19101..8b71ec7e2 100644 --- a/src/FileFormat/AkiraFile.vala +++ b/src/FileFormat/AkiraFile.vala @@ -26,6 +26,8 @@ public class Akira.FileFormat.AkiraFile : Akira.FileFormat.ZipArchiveHandler { public File pictures_folder { get; private set; } public File thumbnails_folder { get; private set; } + public bool overwrite = false; + private File content_file { get; set; } public string path { owned get { @@ -45,6 +47,7 @@ public class Akira.FileFormat.AkiraFile : Akira.FileFormat.ZipArchiveHandler { new FileFormat.JsonLoader (window, content_json); update_recent_list.begin (); + debug ("Version from file: %s", content_json.get_string_member ("version")); } catch (Error e) { error ("Could not load file: %s", e.message); @@ -53,6 +56,7 @@ public class Akira.FileFormat.AkiraFile : Akira.FileFormat.ZipArchiveHandler { public void save_file () { try { + save_images.begin (); var content = new FileFormat.JsonContent (window); content.save_content (); @@ -126,4 +130,50 @@ public class Akira.FileFormat.AkiraFile : Akira.FileFormat.ZipArchiveHandler { window.app.update_recent_files_list (); } + + /** + * Save all the images used in the Canvas and make a copy in the Pictures folder. + */ + public async void save_images () { + // Clear potential leftover images if we're overwriting an existing file. + if (overwrite) { + try { + Dir dir = Dir.open (pictures_folder.get_path (), 0); + string? name = null; + while ((name = dir.read_name ()) != null) { + var file = File.new_for_path (Path.build_filename (pictures_folder.get_path (), name)); + file_collector.mark_for_deletion (file); + } + } catch (FileError err) { + stderr.printf (err.message); + } + overwrite = false; + } + + foreach (var image in window.items_manager.images) { + var image_file = File.new_for_path ( + Path.build_filename (pictures_folder.get_path (), image.manager.filename) + ); + + // Copy the file if it doesn't exist, or increase the reference count. + if (!image_file.query_exists ()) { + copy_image (image.manager.file, image_file); + continue; + } + } + } + + /** + * Decrease the reference count to an existing image, which will cause its + * deletion if the count reaches 0. + */ + public async void remove_image (string filename) { + var image_file = File.new_for_path ( + Path.build_filename (pictures_folder.get_path (), filename) + ); + + if (image_file.query_exists ()) { + file_collector.unref_file (image_file); + } + } } diff --git a/src/FileFormat/FileManager.vala b/src/FileFormat/FileManager.vala index 8b05fbf74..5754d1902 100644 --- a/src/FileFormat/FileManager.vala +++ b/src/FileFormat/FileManager.vala @@ -78,6 +78,8 @@ public class Akira.FileFormat.FileManager : Object { } private void save_file_as_response (Gtk.FileChooserNative dialog, int response_id) { + bool overwrite = false; + switch (response_id) { case Gtk.ResponseType.ACCEPT: File file; @@ -85,10 +87,11 @@ public class Akira.FileFormat.FileManager : Object { var path = save_file.get_path (); if (path.has_suffix (".akira")) { file = save_file; + overwrite = true; } else { file = File.new_for_path (path + ".akira"); } - window.save_new_file (file); + window.save_new_file (file, overwrite); window.event_bus.file_saved (dialog.get_current_name ()); break; } diff --git a/src/FileFormat/JsonObject.vala b/src/FileFormat/JsonObject.vala index 8677896b9..1d40410c1 100644 --- a/src/FileFormat/JsonObject.vala +++ b/src/FileFormat/JsonObject.vala @@ -45,6 +45,7 @@ public class Akira.FileFormat.JsonObject : GLib.Object { } transform = new Json.Object (); + write_transform (); } @@ -91,8 +92,12 @@ public class Akira.FileFormat.JsonObject : GLib.Object { } else if (type == typeof (Goo.CanvasItemVisibility)) { item.get_property (spec.get_name (), ref val); obj.set_int_member (spec.get_name (), val.get_enum ()); + } else if (type == typeof (Akira.Lib.Managers.ImageManager)) { + var canvas_image = item as Akira.Lib.Models.CanvasImage; + obj.set_string_member ("image_id", canvas_image.manager.filename); } else { - // warning ("Property type %s not yet supported: %s\n", type.name (), spec.get_name ()); + // Leave this comment for debug purpose. + // warning ("Property type %s not yet supported: %s\n", type.name (), spec.get_name ()); } } diff --git a/src/FileFormat/ZipArchiveHandler.vala b/src/FileFormat/ZipArchiveHandler.vala index aa22c134a..945acebb7 100644 --- a/src/FileFormat/ZipArchiveHandler.vala +++ b/src/FileFormat/ZipArchiveHandler.vala @@ -367,6 +367,19 @@ public class Akira.FileFormat.ZipArchiveHandler : GLib.Object { } } + public virtual void copy_image (GLib.File old_file, GLib.File new_file) { + try { + old_file.copy (new_file, 0, null, (current_num_bytes, total_num_bytes) => { + // Report copy-status: + print ("%" + int64.FORMAT + " bytes of %" + int64.FORMAT + " bytes copied.\n", + current_num_bytes, total_num_bytes); + }); + file_collector.ref_file (new_file); + } catch (Error e) { + print ("Error: %s\n", e.message); + } + } + /** * Takes care of the reference counting for files inside the archive. diff --git a/src/Layouts/Partials/TransformPanel.vala b/src/Layouts/Partials/TransformPanel.vala index 3dbe20877..4b2e7f7e2 100644 --- a/src/Layouts/Partials/TransformPanel.vala +++ b/src/Layouts/Partials/TransformPanel.vala @@ -341,6 +341,8 @@ public class Akira.Layouts.Partials.TransformPanel : Gtk.Grid { y.value = position["y"]; y.notify["value"].connect (y_notify_value); } + + window.event_bus.file_edited (); } public void x_notify_value () { diff --git a/src/Lib/Managers/ImageManager.vala b/src/Lib/Managers/ImageManager.vala new file mode 100644 index 000000000..6b5d418af --- /dev/null +++ b/src/Lib/Managers/ImageManager.vala @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020 Alecaddd (https://alecaddd.com) + * + * This file is part of Akira. + * + * Akira is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Akira is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Akira. If not, see . + * + * Authored by: Adam Bieńkowski + * Authored by: Alessandro "Alecaddd" Castellani + */ + +public class Akira.Lib.Managers.ImageManager : Object { + public GLib.File file { get; set; } + + // Save the generated Pixbuf for later reference. + public Gdk.Pixbuf pixbuf; + // The unique name of the loaded image, including the current timestamp. + public string filename; + + private const string[] ACCEPTED_TYPES = { + "image/jpeg", + "image/png", + "image/tiff", + "image/svg+xml", + "image/gif" + }; + + public ImageManager (GLib.File _file, int id) { + file = _file; + + var timestamp = new GLib.DateTime.now_utc (); + filename = ("akira-img-%i-%s.%s").printf ( + id, + timestamp.to_unix ().to_string (), + Utils.Image.get_extension (file) + ); + } + + /** + * Initialize a new ImageManager from a previously saved file. + * We use this to avoid changing the filename which should be unique. + * + * @param {GLib.File} _file - The file loaded from the saved archive. + * @param {string} _filename - The original filename of the saved file. + */ + public ImageManager.from_archive (GLib.File _file, string _filename) { + file = _file; + filename = _filename; + } + + /** + * Generate the Pixbuf from the given file. This method is also called to + * resample the quality of the pixbuf when the image is resized. + * + * @param {int} width - The requested width for the resample. + * @param {int} height - The requested height for the resample. + * @return Gdk.Pixbuf + */ + public async Gdk.Pixbuf get_pixbuf (int width = -1, int height = -1) throws Error { + FileInputStream stream; + + try { + stream = yield file.read_async (); + } catch (Error e) { + throw e; + } + + if (width != -1 && height != -1) { + try { + pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async (stream, width, height, false); + return pixbuf; + } catch (Error e) { + throw e; + } + } else { + try { + pixbuf = yield new Gdk.Pixbuf.from_stream_async (stream); + return pixbuf; + } catch (Error e) { + throw e; + } + } + } +} diff --git a/src/Lib/Managers/ItemsManager.vala b/src/Lib/Managers/ItemsManager.vala index 73d32dd4b..d700227a0 100644 --- a/src/Lib/Managers/ItemsManager.vala +++ b/src/Lib/Managers/ItemsManager.vala @@ -25,6 +25,7 @@ public class Akira.Lib.Managers.ItemsManager : Object { public Akira.Models.ListModel free_items; public Akira.Models.ListModel artboards; + public Akira.Models.ListModel images; private Models.CanvasItemType? insert_type { get; set; } private Goo.CanvasItem root; private int border_size; @@ -40,6 +41,7 @@ public class Akira.Lib.Managers.ItemsManager : Object { construct { free_items = new Akira.Models.ListModel (); artboards = new Akira.Models.ListModel (); + images = new Akira.Models.ListModel (); border_color = Gdk.RGBA (); fill_color = Gdk.RGBA (); @@ -50,7 +52,7 @@ public class Akira.Lib.Managers.ItemsManager : Object { window.event_bus.hold_released.connect (on_hold_released); } - public void insert_image (Services.FileImageProvider provider) { + public void insert_image (Lib.Managers.ImageManager manager) { var selected_bound_manager = window.main_window.main_canvas.canvas.selected_bound_manager; double start_x, start_y, scale, rotation; @@ -74,7 +76,8 @@ public class Akira.Lib.Managers.ItemsManager : Object { } set_item_to_insert ("image"); - var new_item = insert_item (start_x, start_y, provider); + var new_item = insert_item (start_x, start_y, manager); + (new_item as Models.CanvasImage).resize_pixbuf (-1, -1, true); selected_bound_manager.add_item_to_selection (new_item); selected_bound_manager.set_initial_coordinates (start_x, start_y); @@ -83,7 +86,8 @@ public class Akira.Lib.Managers.ItemsManager : Object { public Models.CanvasItem? insert_item ( double x, double y, - Services.FileImageProvider? provider = null + Lib.Managers.ImageManager? manager = null, + bool loaded = false ) { udpate_default_values (); @@ -106,15 +110,15 @@ public class Akira.Lib.Managers.ItemsManager : Object { switch (insert_type) { case Models.CanvasItemType.RECT: - new_item = add_rect (x, y, root, artboard); + new_item = add_rect (x, y, root, artboard, loaded); break; case Models.CanvasItemType.ELLIPSE: - new_item = add_ellipse (x, y, root, artboard); + new_item = add_ellipse (x, y, root, artboard, loaded); break; case Models.CanvasItemType.TEXT: - new_item = add_text (x, y, root, artboard); + new_item = add_text (x, y, root, artboard, loaded); break; case Models.CanvasItemType.ARTBOARD: @@ -122,7 +126,7 @@ public class Akira.Lib.Managers.ItemsManager : Object { break; case Models.CanvasItemType.IMAGE: - new_item = add_image (x, y, provider, root, artboard); + new_item = add_image (x, y, manager, root, artboard, loaded); break; } @@ -132,10 +136,17 @@ public class Akira.Lib.Managers.ItemsManager : Object { artboards.add_item.begin ((Models.CanvasArtboard) new_item); break; + case Akira.Lib.Models.CanvasItemType.IMAGE: default: + // We need to store images in a dedicated list since we will need + // to easily access them and save them in the .akira/Pictures folder. + if (new_item.item_type == Akira.Lib.Models.CanvasItemType.IMAGE) { + images.add_item.begin ((new_item as Akira.Lib.Models.CanvasImage), loaded); + } + if (new_item.artboard == null) { // Add it to "free items" - free_items.add_item.begin (new_item, false); + free_items.add_item.begin (new_item, loaded); } break; } @@ -158,7 +169,20 @@ public class Akira.Lib.Managers.ItemsManager : Object { artboards.remove_item.begin (item as Models.CanvasArtboard); break; + case Akira.Lib.Models.CanvasItemType.IMAGE: default: + // Remove the image from the list so we don't keep it in the saved file. + if (item.item_type == Akira.Lib.Models.CanvasItemType.IMAGE) { + images.remove_item.begin ((item as Akira.Lib.Models.CanvasImage)); + + // Mark it for removal if we have a saved file. + if (window.akira_file != null) { + window.akira_file.remove_image.begin ( + (item as Akira.Lib.Models.CanvasImage).manager.filename + ); + } + } + if (item.artboard == null) { free_items.remove_item.begin (item); } @@ -179,7 +203,13 @@ public class Akira.Lib.Managers.ItemsManager : Object { return artboard as Models.CanvasItem; } - public Models.CanvasItem add_rect (double x, double y, Goo.CanvasItem parent, Models.CanvasArtboard? artboard) { + public Models.CanvasItem add_rect ( + double x, + double y, + Goo.CanvasItem parent, + Models.CanvasArtboard? artboard, + bool loaded + ) { return new Models.CanvasRect ( Utils.AffineTransform.fix_size (x), Utils.AffineTransform.fix_size (y), @@ -189,10 +219,18 @@ public class Akira.Lib.Managers.ItemsManager : Object { border_color, fill_color, parent, - artboard); + artboard, + loaded + ); } - public Models.CanvasEllipse add_ellipse (double x, double y, Goo.CanvasItem parent, Models.CanvasArtboard? artboard) { + public Models.CanvasEllipse add_ellipse ( + double x, + double y, + Goo.CanvasItem parent, + Models.CanvasArtboard? artboard, + bool loaded + ) { return new Models.CanvasEllipse ( Utils.AffineTransform.fix_size (x), Utils.AffineTransform.fix_size (y), @@ -202,10 +240,18 @@ public class Akira.Lib.Managers.ItemsManager : Object { border_color, fill_color, parent, - artboard); + artboard, + loaded + ); } - public Models.CanvasText add_text (double x, double y, Goo.CanvasItem parent, Models.CanvasArtboard? artboard) { + public Models.CanvasText add_text ( + double x, + double y, + Goo.CanvasItem parent, + Models.CanvasArtboard? artboard, + bool loaded + ) { return new Models.CanvasText ( "Akira is awesome :)", Utils.AffineTransform.fix_size (x), @@ -215,22 +261,27 @@ public class Akira.Lib.Managers.ItemsManager : Object { Goo.CanvasAnchorType.NW, "Open Sans 18", parent, - artboard); + artboard, + loaded + ); } public Models.CanvasImage add_image ( double x, double y, - Services.FileImageProvider provider, + Lib.Managers.ImageManager manager, Goo.CanvasItem parent, - Models.CanvasArtboard? artboard + Models.CanvasArtboard? artboard, + bool loaded ) { return new Models.CanvasImage ( Utils.AffineTransform.fix_size (x), Utils.AffineTransform.fix_size (y), - provider, + manager, parent, - artboard); + artboard, + loaded + ); } public int get_item_position (Lib.Models.CanvasItem item) { @@ -386,22 +437,35 @@ public class Akira.Lib.Managers.ItemsManager : Object { switch (obj.get_string_member ("type")) { case "AkiraLibModelsCanvasRect": insert_type = Models.CanvasItemType.RECT; - item = insert_item (pos_x, pos_y); + item = insert_item (pos_x, pos_y, null, true); break; case "AkiraLibModelsCanvasEllipse": insert_type = Models.CanvasItemType.ELLIPSE; - item = insert_item (pos_x, pos_y); + item = insert_item (pos_x, pos_y, null, true); break; case "AkiraLibModelsCanvasText": insert_type = Models.CanvasItemType.TEXT; - item = insert_item (pos_x, pos_y); + item = insert_item (pos_x, pos_y, null, true); break; case "AkiraLibModelsCanvasArtboard": insert_type = Models.CanvasItemType.ARTBOARD; - item = insert_item (pos_x, pos_y); + item = insert_item (pos_x, pos_y, null, true); + break; + + case "AkiraLibModelsCanvasImage": + insert_type = Models.CanvasItemType.IMAGE; + var filename = obj.get_string_member ("image_id"); + var file = File.new_for_path ( + Path.build_filename ( + window.akira_file.pictures_folder.get_path (), + filename + ) + ); + var manager = new Akira.Lib.Managers.ImageManager.from_archive (file, filename); + item = insert_item (pos_x, pos_y, manager, true); break; } @@ -441,6 +505,15 @@ public class Akira.Lib.Managers.ItemsManager : Object { item.set ("global-radius", obj.get_double_member ("global-radius")); } + // Restore image size. + if (item is Models.CanvasImage) { + (item as Models.CanvasImage).resize_pixbuf ( + (int) obj.get_double_member ("width"), + (int) obj.get_double_member ("height"), + true + ); + } + // Restore layer options. item.locked = obj.get_boolean_member ("locked"); item.visibility = @@ -480,7 +553,7 @@ public class Akira.Lib.Managers.ItemsManager : Object { } } - /* + /** * Restore the selected status of an object. * * @param bool selected - If the object is selected. @@ -504,6 +577,15 @@ public class Akira.Lib.Managers.ItemsManager : Object { return; } + // If we have images in the canvas, check if they're part of the selection to recalculate the size. + if (images.get_n_items () > 0) { + foreach (var image in images) { + if (items.find (image) != null) { + image.check_resize_pixbuf (); + } + } + } + // Interrupt if no artboard is currently present. if (artboards.get_n_items () == 0) { return; diff --git a/src/Lib/Models/CanvasArtboard.vala b/src/Lib/Models/CanvasArtboard.vala index 4f931aa9f..97abfabc2 100644 --- a/src/Lib/Models/CanvasArtboard.vala +++ b/src/Lib/Models/CanvasArtboard.vala @@ -95,6 +95,9 @@ public class Akira.Lib.Models.CanvasArtboard : Goo.CanvasItemSimple, Goo.CanvasI public double initial_relative_x { get; set; } public double initial_relative_y { get; set; } + // Knows if an item was created or loaded for ordering purpose. + public bool loaded { get; set; default = false; } + public CanvasArtboard (double _x = 0, double _y = 0, Goo.CanvasItem? _parent = null) { parent_item = _parent; @@ -172,7 +175,7 @@ public class Akira.Lib.Models.CanvasArtboard : Goo.CanvasItemSimple, Goo.CanvasI return; } - items.add_item.begin (canvas_item, false); + items.add_item.begin (canvas_item, (item as Models.CanvasItem).loaded); item.set_parent (this); request_update (); diff --git a/src/Lib/Models/CanvasEllipse.vala b/src/Lib/Models/CanvasEllipse.vala index 7add0b44c..359fdc2c3 100644 --- a/src/Lib/Models/CanvasEllipse.vala +++ b/src/Lib/Models/CanvasEllipse.vala @@ -69,6 +69,9 @@ public class Akira.Lib.Models.CanvasEllipse : Goo.CanvasEllipse, Models.CanvasIt public double initial_relative_x { get; set; } public double initial_relative_y { get; set; } + // Knows if an item was created or loaded for ordering purpose. + public bool loaded { get; set; default = false; } + public CanvasEllipse ( double _center_x = 0, double _center_y = 0, @@ -78,8 +81,10 @@ public class Akira.Lib.Models.CanvasEllipse : Goo.CanvasEllipse, Models.CanvasIt Gdk.RGBA _border_color, Gdk.RGBA _fill_color, Goo.CanvasItem? _parent = null, - Models.CanvasArtboard? _artboard = null + Models.CanvasArtboard? _artboard = null, + bool _loaded = false ) { + loaded = _loaded; artboard = _artboard; parent = _artboard != null ? _artboard : _parent; canvas = parent.get_canvas () as Akira.Lib.Canvas; diff --git a/src/Lib/Models/CanvasImage.vala b/src/Lib/Models/CanvasImage.vala index f67dac51a..567fd276d 100644 --- a/src/Lib/Models/CanvasImage.vala +++ b/src/Lib/Models/CanvasImage.vala @@ -76,17 +76,29 @@ public class Akira.Lib.Models.CanvasImage : Goo.CanvasImage, Models.CanvasItem { public double initial_relative_x { get; set; } public double initial_relative_y { get; set; } + // Knows if an item was created or loaded for ordering purpose. + public bool loaded { get; set; default = false; } + + // CanvasImage unique attributes. + public Lib.Managers.ImageManager manager { get; set; } + private Gdk.Pixbuf original_pixbuf; + public CanvasImage ( double _x = 0, double _y = 0, - Services.ImageProvider provider, + Lib.Managers.ImageManager _manager, Goo.CanvasItem? _parent = null, - Models.CanvasArtboard? _artboard = null + Models.CanvasArtboard? _artboard = null, + bool _loaded = false ) { + loaded = _loaded; artboard = _artboard; parent = _artboard != null ? _artboard : _parent; canvas = parent.get_canvas () as Akira.Lib.Canvas; + // Set the ImageManager. + manager = _manager; + item_type = Models.CanvasItemType.IMAGE; id = Models.CanvasItem.create_item_id (this); Models.CanvasItem.init_item (this); @@ -106,25 +118,53 @@ public class Akira.Lib.Models.CanvasImage : Goo.CanvasImage, Models.CanvasItem { position_item (_x, _y); - provider.get_pixbuf.begin (-1, -1, (obj, res) => { + // Save the unedited pixbuf to enable resampling and restoring. + manager.get_pixbuf.begin (-1, -1, (obj, res) => { try { - var _pixbuf = provider.get_pixbuf.end (res); - pixbuf = _pixbuf; - width = _pixbuf.get_width (); - height = _pixbuf.get_height (); - fix_image_size (); + original_pixbuf = manager.get_pixbuf.end (res); + // Imported images should keep their aspect ratio by default. + size_ratio = original_pixbuf.get_width () / original_pixbuf.get_height (); + size_locked = true; } catch (Error e) { warning (e.message); - // TODO: handle error here + canvas.window.event_bus.canvas_notification (e.message); } }); reset_colors (); } - public void fix_image_size () { - // Imported images should keep their aspect ratio by default. - size_ratio = width / height; - size_locked = true; + /** + * Trigger the pixbuf resampling only if the image size changed. + */ + public void check_resize_pixbuf () { + if (width == manager.pixbuf.get_width () && height == manager.pixbuf.get_height ()) { + return; + } + + resize_pixbuf ((int) width, (int) height); + } + + /** + * Resample the pixbuf size. + * + * @param {int} w - The new width. + * @param {int} h - The new height. + * @param {bool} update - If the updated pixbuf size should be applied to the CanvasItem. + */ + public void resize_pixbuf (int w, int h, bool update = false) { + manager.get_pixbuf.begin (w, h, (obj, res) => { + try { + var _pixbuf = manager.get_pixbuf.end (res); + pixbuf = _pixbuf; + if (update) { + width = _pixbuf.get_width (); + height = _pixbuf.get_height (); + } + } catch (Error e) { + warning (e.message); + canvas.window.event_bus.canvas_notification (e.message); + } + }); } } diff --git a/src/Lib/Models/CanvasItem.vala b/src/Lib/Models/CanvasItem.vala index a77ed0102..7f02654e8 100644 --- a/src/Lib/Models/CanvasItem.vala +++ b/src/Lib/Models/CanvasItem.vala @@ -80,6 +80,9 @@ public interface Akira.Lib.Models.CanvasItem : Goo.CanvasItemSimple, Goo.CanvasI public abstract double initial_relative_x { get; set; } public abstract double initial_relative_y { get; set; } + // Knows if an item was created or loaded for ordering purpose. + public abstract bool loaded { get; set; default = false; } + public double get_coords (string coord_id) { double _coord = 0.0; diff --git a/src/Lib/Models/CanvasRect.vala b/src/Lib/Models/CanvasRect.vala index 68bcf6c12..f653840d8 100644 --- a/src/Lib/Models/CanvasRect.vala +++ b/src/Lib/Models/CanvasRect.vala @@ -83,6 +83,9 @@ public class Akira.Lib.Models.CanvasRect : Goo.CanvasRect, Models.CanvasItem { public double initial_relative_x { get; set; } public double initial_relative_y { get; set; } + // Knows if an item was created or loaded for ordering purpose. + public bool loaded { get; set; default = false; } + public CanvasRect ( double _x = 0, double _y = 0, @@ -92,8 +95,10 @@ public class Akira.Lib.Models.CanvasRect : Goo.CanvasRect, Models.CanvasItem { Gdk.RGBA _border_color, Gdk.RGBA _fill_color, Goo.CanvasItem? _parent = null, - Models.CanvasArtboard? _artboard = null + Models.CanvasArtboard? _artboard = null, + bool _loaded = false ) { + loaded = _loaded; artboard = _artboard; parent = _artboard != null ? _artboard : _parent; canvas = parent.get_canvas () as Akira.Lib.Canvas; diff --git a/src/Lib/Models/CanvasText.vala b/src/Lib/Models/CanvasText.vala index 23636e1e5..789ff5ed8 100644 --- a/src/Lib/Models/CanvasText.vala +++ b/src/Lib/Models/CanvasText.vala @@ -69,6 +69,9 @@ public class Akira.Lib.Models.CanvasText : Goo.CanvasText, Models.CanvasItem { public double initial_relative_x { get; set; } public double initial_relative_y { get; set; } + // Knows if an item was created or loaded for ordering purpose. + public bool loaded { get; set; default = false; } + public CanvasText ( string _text = "", double _x = 0, @@ -78,7 +81,8 @@ public class Akira.Lib.Models.CanvasText : Goo.CanvasText, Models.CanvasItem { Goo.CanvasAnchorType _anchor = Goo.CanvasAnchorType.NW, string _font = "Open Sans 16", Goo.CanvasItem? _parent = null, - Models.CanvasArtboard? _artboard = null + Models.CanvasArtboard? _artboard = null, + bool _loaded = false ) { Object ( x: _x, @@ -87,6 +91,7 @@ public class Akira.Lib.Models.CanvasText : Goo.CanvasText, Models.CanvasItem { height: _height ); + loaded = _loaded; artboard = _artboard; parent = _artboard != null ? _artboard : _parent; canvas = parent.get_canvas () as Akira.Lib.Canvas; diff --git a/src/Services/ActionManager.vala b/src/Services/ActionManager.vala index 40a08d6a2..9780b80a2 100644 --- a/src/Services/ActionManager.vala +++ b/src/Services/ActionManager.vala @@ -360,8 +360,16 @@ public class Akira.Services.ActionManager : Object { case Gtk.ResponseType.OK: SList files = dialog.get_files (); files.@foreach ((file) => { - var provider = new Akira.Services.FileImageProvider (file); - window.items_manager.insert_image (provider); + if (Akira.Utils.Image.is_valid_image (file)) { + var manager = new Akira.Lib.Managers.ImageManager (file, files.index (file)); + window.items_manager.insert_image (manager); + return; + } + + var ext = Akira.Utils.Image.get_extension (file); + window.event_bus.canvas_notification ( + _("Error! .%s files are not supported!" + ).printf (ext)); }); break; } diff --git a/src/Services/ImageProvider.vala b/src/Services/ImageProvider.vala deleted file mode 100644 index 062d3c3cc..000000000 --- a/src/Services/ImageProvider.vala +++ /dev/null @@ -1,55 +0,0 @@ -/* -* Copyright (c) 2020 Adam Bieńkowski -* -* This file is part of Akira. -* -* Akira is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. - -* Akira is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. - -* You should have received a copy of the GNU General Public License -* along with Akira. If not, see . -* -* Authored by: Adam Bieńkowski -*/ - -public interface Akira.Services.ImageProvider : Object { - public abstract async Gdk.Pixbuf get_pixbuf (int width = -1, int height = -1) throws Error; -} - -public class Akira.Services.FileImageProvider : ImageProvider, Object { - public File file { get; construct; } - - public FileImageProvider (File file) { - Object (file: file); - } - - public async Gdk.Pixbuf get_pixbuf (int width = -1, int height = -1) throws Error { - FileInputStream stream; - try { - stream = yield file.read_async (); - } catch (Error e) { - throw e; - } - - if (width != -1 && height != -1) { - try { - return yield new Gdk.Pixbuf.from_stream_at_scale_async (stream, width, height, false); - } catch (Error e) { - throw e; - } - } else { - try { - return yield new Gdk.Pixbuf.from_stream_async (stream); - } catch (Error e) { - throw e; - } - } - } -} diff --git a/src/Utils/Image.vala b/src/Utils/Image.vala new file mode 100644 index 000000000..03c7371b5 --- /dev/null +++ b/src/Utils/Image.vala @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Alecaddd (https://alecaddd.com) + * + * This file is part of Akira. + * + * Akira is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * Akira is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with Akira. If not, see . + * + * Authored by: Alessandro "Alecaddd" Castellani + */ + +public class Akira.Utils.Image : Object { + private const string[] ACCEPTED_TYPES = { + "image/jpeg", + "image/png", + "image/tiff", + "image/svg+xml", + "image/gif" + }; + + /** + * Check if the filename has a picture file extension. + */ + public static bool is_valid_image (GLib.File file) { + try { + var file_info = file.query_info ("standard::*", 0); + + // Check for correct file type, don't try to load directories. + if (file_info.get_file_type () != GLib.FileType.REGULAR) { + return false; + } + try { + var pixbuf = new Gdk.Pixbuf.from_file (file.get_path ()); + var width = pixbuf.get_width (); + var height = pixbuf.get_height (); + + if (width < 1 || height < 1) return false; + } catch (Error e) { + warning ("Invalid image loaded: %s", e.message); + return false; + } + + foreach (var type in ACCEPTED_TYPES) { + if (GLib.ContentType.equals (file_info.get_content_type (), type)) { + return true; + } + } + } catch (Error e) { + warning ("Could not get file info: %s", e.message); + } + + return false; + } + + /** + * Return only the file extension, PNG if not extension was found. + */ + public static string get_extension (GLib.File file) { + var parts = file.get_basename ().split ("."); + + return parts.length > 1 ? parts[parts.length - 1] : "png"; + } +} diff --git a/src/Window.vala b/src/Window.vala index e31152505..c42f91b5b 100644 --- a/src/Window.vala +++ b/src/Window.vala @@ -185,8 +185,9 @@ public class Akira.Window : Gtk.ApplicationWindow { akira_file.load_file (); } - public void save_new_file (File file) { + public void save_new_file (File file, bool overwrite = false) { akira_file = new FileFormat.AkiraFile (file, this); + akira_file.overwrite = overwrite; akira_file.prepare (); akira_file.save_file (); diff --git a/src/meson.build b/src/meson.build index a4241d0aa..bcacf3f98 100644 --- a/src/meson.build +++ b/src/meson.build @@ -29,12 +29,12 @@ sources = files( 'Services/Settings.vala', 'Services/ActionManager.vala', 'Services/EventBus.vala', - 'Services/ImageProvider.vala', 'Utils/Dialogs.vala', 'Utils/BlendingMode.vala', 'Utils/AffineTransform.vala', 'Utils/Color.vala', + 'Utils/Image.vala', 'Layouts/HeaderBar.vala', 'Layouts/LeftSideBar.vala', @@ -82,6 +82,7 @@ sources = files( 'Lib/Managers/HoverManager.vala', 'Lib/Managers/SelectedBoundManager.vala', 'Lib/Managers/ItemsManager.vala', + 'Lib/Managers/ImageManager.vala', 'Lib/Managers/NobManager.vala', 'Lib/Models/CanvasItem.vala',