diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2023-12-29 18:43:43 +0100 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2023-12-29 18:43:43 +0100 |
commit | 14bc7db2e07c5d1ccfb4d723c9dba395e6c93171 (patch) | |
tree | 7b00861348dedd18d3651537128bbe783dab364c /src | |
parent | 1bf62485f701b52ae27433e3570ac24cd7da886f (diff) | |
parent | 67d142325abb0d9345baef77f00a047a7dc1eca3 (diff) |
Merge branch 'release/debian/0.32.4-1'debian/0.32.4-1
Diffstat (limited to 'src')
-rw-r--r-- | src/AppWindow.vala | 11 | ||||
-rw-r--r-- | src/CollectionPage.vala | 46 | ||||
-rw-r--r-- | src/DragAndDropHandler.vala | 10 | ||||
-rw-r--r-- | src/Exporter.vala | 42 | ||||
-rw-r--r-- | src/PhotoPage.vala | 6 | ||||
-rw-r--r-- | src/PixbufCache.vala | 2 | ||||
-rw-r--r-- | src/ProfileBrowser.vala | 2 | ||||
-rw-r--r-- | src/Properties.vala | 4 | ||||
-rw-r--r-- | src/SlideshowPage.vala | 43 | ||||
-rw-r--r-- | src/config/ConfigurationInterfaces.vala | 25 | ||||
-rw-r--r-- | src/config/GSettingsEngine.vala | 2 | ||||
-rw-r--r-- | src/core/ViewCollection.vala | 51 | ||||
-rw-r--r-- | src/dialogs/MultiTextEntryDialog.vala | 22 | ||||
-rw-r--r-- | src/direct/DirectPhotoPage.vala | 6 | ||||
-rw-r--r-- | src/editing_tools/EditingTools.vala | 10 | ||||
-rw-r--r-- | src/library/LibraryWindow.vala | 16 | ||||
-rw-r--r-- | src/main.vala | 63 | ||||
-rw-r--r-- | src/photos/JfifSupport.vala | 2 | ||||
-rw-r--r-- | src/video-support/meson.build | 4 |
19 files changed, 308 insertions, 59 deletions
diff --git a/src/AppWindow.vala b/src/AppWindow.vala index 1fb0515..7398c74 100644 --- a/src/AppWindow.vala +++ b/src/AppWindow.vala @@ -546,14 +546,21 @@ public abstract class AppWindow : PageWindow { } public static int export_overwrite_or_replace_question(string message, - string alt1, string alt2, string alt3, string alt4, string alt5, string alt6, + string alt1, string alt2, string alt4, string alt6, string? title = null, Gtk.Window? parent = null) { Gtk.MessageDialog dialog = new Gtk.MessageDialog((parent != null) ? parent : get_instance(), Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, "%s", message); dialog.title = (title != null) ? title : Resources.APP_TITLE; - dialog.add_buttons(alt1, 1, alt2, 2, alt3, 3, alt4, 4, alt5, 5, alt6, 6); + var content = (Gtk.Box)dialog.get_message_area(); + var c = new Gtk.CheckButton.with_label("Apply conflict resolution to all other conflicts"); + c.show(); + content.pack_end(c); + dialog.add_buttons(alt1, 1, alt2, 2, alt4, 4, alt6, 6); int response = dialog.run(); + if (c.get_active()) { + response |= 0x80; + } dialog.destroy(); diff --git a/src/CollectionPage.vala b/src/CollectionPage.vala index ac05f8b..9a96041 100644 --- a/src/CollectionPage.vala +++ b/src/CollectionPage.vala @@ -240,6 +240,7 @@ public abstract class CollectionPage : MediaPage { primary_is_video = true; bool selection_has_videos = selection_has_video(); + bool selection_has_photos = selection_has_photo(); bool page_has_photos = page_has_photo(); // don't allow duplication of the selection if it contains a video -- videos are huge and @@ -270,11 +271,14 @@ public abstract class CollectionPage : MediaPage { set_action_sensitive("NewEvent", has_selected); set_action_sensitive("AddTags", has_selected); set_action_sensitive("ModifyTags", one_selected); - set_action_sensitive("Slideshow", page_has_photos && (!primary_is_video)); + + // Allow starting slideshow even if first selected item is a video. Otherwise the + // behavior is quite confusing, it will start if you do not select anything and just skipt the video + set_action_sensitive("Slideshow", (page_has_photos && !has_selected) || selection_has_photos); set_action_sensitive("Print", (!selection_has_videos) && has_selected); set_action_sensitive("Publish", has_selected); - set_action_sensitive("SetBackground", (!selection_has_videos) && has_selected ); + set_action_sensitive("SetBackground", has_selected && selection_has_photos); if (has_selected) { debug ("Setting action label for SetBackground..."); var label = one_selected @@ -679,23 +683,27 @@ public abstract class CollectionPage : MediaPage { if (get_view().get_count() == 0) return; - // use first selected photo, else use first photo - Gee.List<DataSource>? sources = (get_view().get_selected_count() > 0) - ? get_view().get_selected_sources_of_type(typeof(LibraryPhoto)) - : get_view().get_sources_of_type(typeof(LibraryPhoto)); - if (sources == null || sources.size == 0) - return; - - Thumbnail? thumbnail = (Thumbnail?) get_view().get_view_for_source(sources[0]); - if (thumbnail == null) - return; - - LibraryPhoto? photo = thumbnail.get_media_source() as LibraryPhoto; - if (photo == null) - return; - - AppWindow.get_instance().go_fullscreen(new SlideshowPage(LibraryPhoto.global, get_view(), - photo)); + // check selection for valid starting photo, otherwise start at beginning of collection + if (get_view().get_selected_count() > 0) { + Gee.List<DataSource>? sources = + get_view().get_selected_sources_of_type(typeof(LibraryPhoto)); + if (sources == null || sources.size == 0) + return; + + Thumbnail? thumbnail = (Thumbnail?) get_view().get_view_for_source(sources[0]); + if (thumbnail == null) + return; + + LibraryPhoto? photo = thumbnail.get_media_source() as LibraryPhoto; + if (photo == null) + return; + + AppWindow.get_instance().go_fullscreen(new SlideshowPage(LibraryPhoto.global, + get_view(), photo)); + } else { + AppWindow.get_instance().go_fullscreen(new SlideshowPage(LibraryPhoto.global, + get_view())); + } } protected override bool on_ctrl_pressed(Gdk.EventKey? event) { diff --git a/src/DragAndDropHandler.vala b/src/DragAndDropHandler.vala index ece6d9d..9ac6e46 100644 --- a/src/DragAndDropHandler.vala +++ b/src/DragAndDropHandler.vala @@ -28,6 +28,7 @@ public class DragAndDropHandler { private Gtk.Widget event_source; private File? drag_destination = null; private ExporterUI exporter = null; + private Gdk.DragAction action = Gdk.DragAction.COPY; public DragAndDropHandler(Page page) { this.page = page; @@ -47,7 +48,7 @@ public class DragAndDropHandler { // register what's available on this DnD Source Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES, - Gdk.DragAction.COPY); + Gdk.DragAction.COPY | Gdk.DragAction.MOVE); // attach to the event source's DnD signals, not the Page's, which is a NO_WINDOW widget // and does not emit them @@ -100,6 +101,8 @@ public class DragAndDropHandler { if (page == null || page.get_view().get_selected_count() == 0) return; + action = context.get_suggested_action(); + switch (target_type) { case TargetType.XDS: // Fetch the XDS property that has been set with the destination path @@ -147,7 +150,7 @@ public class DragAndDropHandler { return; } - debug("Exporting to %s", drag_destination.get_path()); + debug("Exporting to %s, mode %s", drag_destination.get_path(), action == Gdk.DragAction.COPY ? "current" : "unmodified"); // drag-and-drop export doesn't pop up an export dialog, so use what are likely the // most common export settings (the current -- or "working" -- file format, with @@ -155,7 +158,8 @@ public class DragAndDropHandler { if (drag_destination.get_path() != null) { exporter = new ExporterUI(new Exporter( (Gee.Collection<Photo>) page.get_view().get_selected_sources(), - drag_destination, Scaling.for_original(), ExportFormatParameters.current())); + drag_destination, Scaling.for_original(), action == Gdk.DragAction.COPY ? ExportFormatParameters.current() + : ExportFormatParameters.unmodified())); exporter.export(on_export_completed); } else { AppWindow.error_message(_("Photos cannot be exported to this directory.")); diff --git a/src/Exporter.vala b/src/Exporter.vala index a7f7b6b..cf63938 100644 --- a/src/Exporter.vala +++ b/src/Exporter.vala @@ -54,6 +54,7 @@ public class Exporter : Object { public enum Overwrite { YES, NO, + SKIP_ALL, CANCEL, REPLACE_ALL, RENAME, @@ -270,6 +271,15 @@ public class Exporter : Object { return false; + case Overwrite.SKIP_ALL: + completed_count = to_export.size; + if (monitor != null) { + if (!monitor(completed_count, to_export.size)) { + cancellable.cancel(); + + } + } + return false; case Overwrite.NO: default: completed_count++; @@ -346,30 +356,40 @@ public class ExporterUI { progress_dialog.set_modal(false); string question = _("File %s already exists. Replace?").printf(file.get_basename()); int response = AppWindow.export_overwrite_or_replace_question(question, - _("_Skip"), _("Rename"), _("Rename All"),_("_Replace"), _("Replace _All"), _("_Cancel"), _("Export")); + _("_Skip"), _("Rename"), _("_Replace"), _("_Cancel"), _("Export file conflict")); progress_dialog.set_modal(true); + var apply_all = (response & 0x80) != 0; + response &= 0x0f; + switch (response) { case 2: - return Exporter.Overwrite.RENAME; - - case 3: - return Exporter.Overwrite.RENAME_ALL; - + if (apply_all) { + return Exporter.Overwrite.RENAME_ALL; + } + else { + return Exporter.Overwrite.RENAME; + } case 4: - return Exporter.Overwrite.YES; - - case 5: - return Exporter.Overwrite.REPLACE_ALL; + if (apply_all) { + return Exporter.Overwrite.REPLACE_ALL; + } else { + return Exporter.Overwrite.YES; + } case 6: return Exporter.Overwrite.CANCEL; case 1: + if (apply_all) { + return Exporter.Overwrite.SKIP_ALL; + } else { + return Exporter.Overwrite.NO; + } default: return Exporter.Overwrite.NO; - } + } } private bool on_export_failed(Exporter exporter, File file, int remaining, Error err) { diff --git a/src/PhotoPage.vala b/src/PhotoPage.vala index a279d89..5e94c24 100644 --- a/src/PhotoPage.vala +++ b/src/PhotoPage.vala @@ -966,7 +966,7 @@ public abstract class EditingHostPage : SinglePhotoPage { return photo.has_transformations() || photo.has_editable(); } - private void on_pixbuf_fetched(Photo photo, Gdk.Pixbuf? pixbuf, Error? err) { + private void on_pixbuf_fetched(Photo photo, owned Gdk.Pixbuf? pixbuf, Error? err) { // if not of the current photo, nothing more to do if (!photo.equals(get_photo())) return; @@ -986,7 +986,7 @@ public abstract class EditingHostPage : SinglePhotoPage { photo, out tool_pixbuf_dim); if (tool_pixbuf != null) { - pixbuf = tool_pixbuf; + pixbuf = tool_pixbuf; max_dim = tool_pixbuf_dim; } } catch(Error err) { @@ -1410,7 +1410,7 @@ public abstract class EditingHostPage : SinglePhotoPage { Gdk.Pixbuf original; try { original = get_photo().get_original_orientation().rotate_pixbuf( - get_photo().get_prefetched_copy()); + get_photo().get_master_pixbuf(cache.get_scaling())); } catch (Error err) { return; } diff --git a/src/PixbufCache.vala b/src/PixbufCache.vala index 6ff740e..cee33c6 100644 --- a/src/PixbufCache.vala +++ b/src/PixbufCache.vala @@ -80,7 +80,7 @@ public class PixbufCache : Object { private Gee.ArrayList<Photo> lru = new Gee.ArrayList<Photo>(); private Gee.HashMap<Photo, FetchJob> in_progress = new Gee.HashMap<Photo, FetchJob>(); - public signal void fetched(Photo photo, Gdk.Pixbuf? pixbuf, Error? err); + public signal void fetched(Photo photo, owned Gdk.Pixbuf? pixbuf, Error? err); public PixbufCache(SourceCollection sources, PhotoType type, Scaling scaling, int max_count, CacheFilter? filter = null) { diff --git a/src/ProfileBrowser.vala b/src/ProfileBrowser.vala index 1591fce..7331918 100644 --- a/src/ProfileBrowser.vala +++ b/src/ProfileBrowser.vala @@ -133,7 +133,7 @@ namespace Shotwell { pack_end(revealer, true); var label = new Gtk.Label(null); - label.set_markup("<span weight=\"bold\">%s</span>".printf(profile.name)); + label.set_markup("<span weight=\"bold\">%s</span>".printf(Markup.escape_text(profile.name))); label.halign = Gtk.Align.START; content.pack_start(label, true, true, 6); diff --git a/src/Properties.vala b/src/Properties.vala index b8c3e0d..7c6ab89 100644 --- a/src/Properties.vala +++ b/src/Properties.vala @@ -50,7 +50,7 @@ private abstract class Properties : Gtk.Box { } if (href == null) { - info_label.set_text(is_string_empty(info_text) ? "" : info_text); + info_label.set_markup(is_string_empty(info_text) ? "" : info_text); } else { info_label.set_markup("<a href=\"%s\">%s</a>".printf(href, Markup.escape_text(info_text))); } @@ -596,7 +596,7 @@ private class ExtendedProperties : Properties { // nothing special to be done for now for Events } else { add_line(_("Location:"), (file_path != "" && file_path != null) ? - file_path.replace("&", "&") : NO_VALUE); + Markup.escape_text(file_path) : NO_VALUE); add_line(_("File size:"), (filesize > 0) ? format_size((int64) filesize) : NO_VALUE); diff --git a/src/SlideshowPage.vala b/src/SlideshowPage.vala index adfec7f..94dd3ae 100644 --- a/src/SlideshowPage.vala +++ b/src/SlideshowPage.vala @@ -9,6 +9,7 @@ class SlideshowPage : SinglePhotoPage { private const int CHECK_ADVANCE_MSEC = 250; private SourceCollection sources; + private ViewCollection controller_source; private ViewCollection controller; private Photo current; private Gtk.ToolButton play_pause_button; @@ -17,6 +18,7 @@ class SlideshowPage : SinglePhotoPage { private Timer timer = new Timer(); private bool playing = true; private bool exiting = false; + private bool shuffled; private string[] transitions; private Screensaver screensaver; @@ -39,6 +41,8 @@ class SlideshowPage : SinglePhotoPage { unowned Gtk.Adjustment transition_effect_adjustment; [GtkChild] unowned Gtk.CheckButton show_title_button; + [GtkChild] + unowned Gtk.CheckButton shuffle_button; public SettingsDialog() { Object (use_header_bar: Resources.use_header_bar()); @@ -78,6 +82,9 @@ class SlideshowPage : SinglePhotoPage { bool show_title = Config.Facade.get_instance().get_slideshow_show_title(); show_title_button.active = show_title; + bool shuffle = Config.Facade.get_instance().get_slideshow_shuffle(); + shuffle_button.active = shuffle; + on_transition_changed(); } @@ -111,13 +118,19 @@ class SlideshowPage : SinglePhotoPage { public bool get_show_title() { return show_title_button.active; } + + public bool get_shuffle() { + return shuffle_button.active; + } } - public SlideshowPage(SourceCollection sources, ViewCollection controller, Photo start) { + public SlideshowPage(SourceCollection sources, ViewCollection controller, Photo? start = null) { base(_("Slideshow"), true); this.sources = sources; - this.controller = controller; + controller_source = controller; + shuffled = Config.Facade.get_instance().get_slideshow_shuffle(); + this.controller = shuffled ? controller.shuffled_copy(start) : controller; Gee.Collection<string> pluggables = TransitionEffectsManager.get_instance().get_effect_ids(); Gee.ArrayList<string> a = new Gee.ArrayList<string>(); @@ -125,7 +138,9 @@ class SlideshowPage : SinglePhotoPage { a.remove(NullTransitionDescriptor.EFFECT_ID); a.remove(RandomEffectDescriptor.EFFECT_ID); transitions = a.to_array(); - current = start; + + current = (start == null) + ? (Photo) this.controller.get_first_photo().get_source() : start; update_transition_effect(); @@ -284,8 +299,13 @@ class SlideshowPage : SinglePhotoPage { protected override void on_next_photo() { DataView view = controller.get_view_for_source(current); + bool wrapped; Photo? next_photo = null; - DataView? start_view = controller.get_next(view); + DataView? start_view = controller.get_next(view, out wrapped); + if (wrapped && shuffled) { + controller = controller_source.shuffled_copy(); + start_view = controller.get_first(); + } DataView? next_view = start_view; while (next_view != null) { @@ -294,7 +314,11 @@ class SlideshowPage : SinglePhotoPage { break; } - next_view = controller.get_next(next_view); + next_view = controller.get_next(next_view, out wrapped); + if (wrapped && shuffled) { + controller = controller_source.shuffled_copy(); + start_view = controller.get_first(); + } if (next_view == start_view) { warning("on_next( ): can't advance to next photo: collection has only videos"); @@ -369,6 +393,7 @@ class SlideshowPage : SinglePhotoPage { playing = false; hide_toolbar(); suspend_cursor_hiding(); + bool old_shuffled = Config.Facade.get_instance().get_slideshow_shuffle(); if (settings_dialog.run() == Gtk.ResponseType.OK) { // sync with the config setting so it will persist @@ -378,9 +403,17 @@ class SlideshowPage : SinglePhotoPage { Config.Facade.get_instance().set_slideshow_transition_effect_id(settings_dialog.get_transition_effect_id()); Config.Facade.get_instance().set_slideshow_show_title(settings_dialog.get_show_title()); + shuffled = settings_dialog.get_shuffle(); + Config.Facade.get_instance().set_slideshow_shuffle(shuffled); + update_transition_effect(); } + if (old_shuffled && !shuffled) + controller = controller_source; + else if (!old_shuffled && shuffled) + controller = controller_source.shuffled_copy(current); + settings_dialog.destroy(); restore_cursor_hiding(); playing = slideshow_playing; diff --git a/src/config/ConfigurationInterfaces.vala b/src/config/ConfigurationInterfaces.vala index 12c7da1..ffd0d31 100644 --- a/src/config/ConfigurationInterfaces.vala +++ b/src/config/ConfigurationInterfaces.vala @@ -89,6 +89,7 @@ public enum ConfigurableProperty { SLIDESHOW_TRANSITION_DELAY, SLIDESHOW_TRANSITION_EFFECT_ID, SLIDESHOW_SHOW_TITLE, + SLIDESHOW_SHUFFLE, USE_24_HOUR_TIME, USE_LOWERCASE_FILENAMES, @@ -304,6 +305,9 @@ public enum ConfigurableProperty { case SLIDESHOW_SHOW_TITLE: return "SLIDESHOW_SHOW_TITLE"; + case SLIDESHOW_SHUFFLE: + return "SLIDESHOW_SHUFFLE"; + case USE_24_HOUR_TIME: return "USE_24_HOUR_TIME"; @@ -1782,6 +1786,27 @@ public abstract class ConfigurationFacade : Object { } // + // Slideshow shuffle + // + public virtual bool get_slideshow_shuffle() { + try { + return get_engine().get_bool_property(ConfigurableProperty.SLIDESHOW_SHUFFLE); + } catch (ConfigurationError err) { + on_configuration_error(err); + + return false; + } + } + + public virtual void set_slideshow_shuffle(bool shuffle) { + try { + get_engine().set_bool_property(ConfigurableProperty.SLIDESHOW_SHUFFLE, shuffle); + } catch (ConfigurationError err) { + on_configuration_error(err); + } + } + + // // use 24 hour time // public virtual bool get_use_24_hour_time() { diff --git a/src/config/GSettingsEngine.vala b/src/config/GSettingsEngine.vala index d4d95c6..8bf53ec 100644 --- a/src/config/GSettingsEngine.vala +++ b/src/config/GSettingsEngine.vala @@ -101,6 +101,7 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object { schema_names[ConfigurableProperty.SLIDESHOW_TRANSITION_DELAY] = SLIDESHOW_PREFS_SCHEMA_NAME; schema_names[ConfigurableProperty.SLIDESHOW_TRANSITION_EFFECT_ID] = SLIDESHOW_PREFS_SCHEMA_NAME; schema_names[ConfigurableProperty.SLIDESHOW_SHOW_TITLE] = SLIDESHOW_PREFS_SCHEMA_NAME; + schema_names[ConfigurableProperty.SLIDESHOW_SHUFFLE] = SLIDESHOW_PREFS_SCHEMA_NAME; schema_names[ConfigurableProperty.USE_24_HOUR_TIME] = UI_PREFS_SCHEMA_NAME; schema_names[ConfigurableProperty.USE_LOWERCASE_FILENAMES] = FILES_PREFS_SCHEMA_NAME; @@ -175,6 +176,7 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object { key_names[ConfigurableProperty.SLIDESHOW_TRANSITION_DELAY] = "transition-delay"; key_names[ConfigurableProperty.SLIDESHOW_TRANSITION_EFFECT_ID] = "transition-effect-id"; key_names[ConfigurableProperty.SLIDESHOW_SHOW_TITLE] = "show-title"; + key_names[ConfigurableProperty.SLIDESHOW_SHUFFLE] = "shuffle"; key_names[ConfigurableProperty.USE_24_HOUR_TIME] = "use-24-hour-time"; key_names[ConfigurableProperty.USE_LOWERCASE_FILENAMES] = "use-lowercase-filenames"; } diff --git a/src/core/ViewCollection.vala b/src/core/ViewCollection.vala index 7e13fb0..c3848c1 100644 --- a/src/core/ViewCollection.vala +++ b/src/core/ViewCollection.vala @@ -298,6 +298,33 @@ public class ViewCollection : DataCollection { add_many(copy_view); } + // for use in shuffled slideshow + public ViewCollection shuffled_copy(Photo? start = null) { + ViewCollection shuffled = new ViewCollection("shuffled copy of " + to_string()); + + Gee.Collection<DataObject> data = get_all(); + Gee.ArrayList<DataObject> data_copy = new Gee.ArrayList<DataObject>(); + foreach (DataObject object in data) { + DataView view = (DataView) object; + if (view.get_source() is PhotoSource && + (start == null || view.get_source() != start)) { + data_copy.add(new PhotoView((PhotoSource) ((DataView) object).get_source())); + } + } + + for (int ctr = data_copy.size - 1; ctr > 0; ctr--) { + int rand = GLib.Random.int_range(0, ctr + 1); + DataObject temp = data_copy.get(rand); + data_copy.set(rand, data_copy.get(ctr)); + data_copy.set(ctr, temp); + } + + if (start != null) + shuffled.add(new PhotoView(start)); + shuffled.add_many(data_copy); + return shuffled; + } + public bool is_view_filter_installed(ViewFilter f) { return filters.contains(f); } @@ -740,12 +767,30 @@ public class ViewCollection : DataCollection { // _something_... return get_first(); } + + public virtual DataView? get_first_photo() { + if (get_count() < 1) + return null; + + DataView dv = get_first(); + int num_views = get_count(); + + while ((dv != null) && (index_of(dv) < (num_views))) { + if (dv.get_source() is PhotoSource) + return dv; + else + dv = get_next(dv); + } + + return null; + } public virtual DataView? get_last() { return (get_count() > 0) ? (DataView?) get_at(get_count() - 1) : null; } - public virtual DataView? get_next(DataView view) { + public virtual DataView? get_next(DataView view, out bool? wrapped = null) { + wrapped = false; if (get_count() == 0) return null; @@ -754,8 +799,10 @@ public class ViewCollection : DataCollection { return null; index++; - if (index >= get_count()) + if (index >= get_count()) { index = 0; + wrapped = true; + } return (DataView?) get_at(index); } diff --git a/src/dialogs/MultiTextEntryDialog.vala b/src/dialogs/MultiTextEntryDialog.vala index ddbd59b..097dabf 100644 --- a/src/dialogs/MultiTextEntryDialog.vala +++ b/src/dialogs/MultiTextEntryDialog.vala @@ -5,16 +5,36 @@ * See the COPYING file in this distribution. */ + class DismissableTextView : Gtk.TextView { + public signal void edit_done(); + + public override bool key_press_event(Gdk.EventKey ev) { + if (!(Gdk.ModifierType.CONTROL_MASK in ev.state)) { + return base.key_press_event(ev); + } + + if (Gdk.keyval_name(ev.keyval) == "KP_Enter" || + Gdk.keyval_name(ev.keyval) == "Return") { + edit_done(); + return true; + } + + return base.key_press_event(ev); + } + } + [GtkTemplate (ui = "/org/gnome/Shotwell/ui/multitextentrydialog.ui")] public class MultiTextEntryDialog : Gtk.Dialog { public delegate bool OnModifyValidateType(string text); private unowned OnModifyValidateType on_modify_validate; [GtkChild] - private unowned Gtk.TextView entry; + private unowned DismissableTextView entry; public MultiTextEntryDialog() { Object (use_header_bar: Resources.use_header_bar()); + + entry.edit_done.connect(() => {response(Gtk.ResponseType.OK);}); } public void setup(OnModifyValidateType? modify_validate, string title, string label, string? initial_text) { diff --git a/src/direct/DirectPhotoPage.vala b/src/direct/DirectPhotoPage.vala index 50321e9..b79eb0b 100644 --- a/src/direct/DirectPhotoPage.vala +++ b/src/direct/DirectPhotoPage.vala @@ -411,18 +411,24 @@ public class DirectPhotoPage : EditingHostPage { string[] output_format_extensions = effective_export_format.get_properties().get_known_extensions(); Gtk.FileFilter output_format_filter = new Gtk.FileFilter(); + output_format_filter.set_filter_name(_("Supported image formats")); foreach(string extension in output_format_extensions) { string uppercase_extension = extension.up(); output_format_filter.add_pattern("*." + extension); output_format_filter.add_pattern("*." + uppercase_extension); } + Gtk.FileFilter all_files = new Gtk.FileFilter(); + all_files.add_pattern("*"); + all_files.set_filter_name(_("All files")); + var save_as_dialog = new Gtk.FileChooserNative(_("Save As"), AppWindow.get_instance(), Gtk.FileChooserAction.SAVE, Resources.OK_LABEL, Resources.CANCEL_LABEL); save_as_dialog.set_select_multiple(false); save_as_dialog.set_current_name(filename); save_as_dialog.set_current_folder(current_save_dir.get_path()); save_as_dialog.add_filter(output_format_filter); + save_as_dialog.add_filter(all_files); save_as_dialog.set_do_overwrite_confirmation(true); save_as_dialog.set_local_only(false); diff --git a/src/editing_tools/EditingTools.vala b/src/editing_tools/EditingTools.vala index 0042d57..3345a3f 100644 --- a/src/editing_tools/EditingTools.vala +++ b/src/editing_tools/EditingTools.vala @@ -1294,11 +1294,13 @@ public class CropTool : EditingTool { // scaled_crop is not maintained relative to photo's position on canvas Box offset_scaled_crop = scaled_crop.get_offset(scaled_pixbuf_pos.x, scaled_pixbuf_pos.y); + var xmul = (int)Math.lround(x * Application.get_scale()); + var ymul = (int)Math.lround(y * Application.get_scale()); + // determine where the mouse down landed and store for future events - in_manipulation = offset_scaled_crop.approx_location((int)Math.lround(x * Application.get_scale()), - (int)Math.lround(y * Application.get_scale())); - last_grab_x = x -= scaled_pixbuf_pos.x; - last_grab_y = y -= scaled_pixbuf_pos.y; + in_manipulation = offset_scaled_crop.approx_location(xmul, ymul); + last_grab_x = xmul - scaled_pixbuf_pos.x; + last_grab_y = ymul - scaled_pixbuf_pos.y; // repaint because the crop changes on a mouse down canvas.repaint(); diff --git a/src/library/LibraryWindow.vala b/src/library/LibraryWindow.vala index 849ae2e..280a50b 100644 --- a/src/library/LibraryWindow.vala +++ b/src/library/LibraryWindow.vala @@ -765,17 +765,27 @@ public class LibraryWindow : AppWindow { } Gee.ArrayList<FileImportJob> jobs = new Gee.ArrayList<FileImportJob>(); + Gee.ArrayList<string> rejected = new Gee.ArrayList<string>(); foreach (string uri in uris) { File file_or_dir = File.new_for_uri(uri); + if (file_or_dir.get_path() == null) { - // TODO: Specify which directory/file. - AppWindow.error_message(_("Photos cannot be imported from this directory.")); - + rejected.add(uri); continue; } jobs.add(new FileImportJob(file_or_dir, copy_to_library, recurse)); } + + if (rejected.size > 0) { + // TODO: Specify which directory/file. + //var message = ngettext("Photos cannot be imported from this folder", "Photos cannot be imported from these folders", rejected.size) + var message = _("Photos cannot be imported from this directory."); + foreach (var uri in rejected) { + message += uri; + } + AppWindow.error_message(message); + } if (jobs.size > 0) { BatchImport batch_import = new BatchImport(jobs, job_name, import_reporter); diff --git a/src/main.vala b/src/main.vala index 32e3d83..d07b7f5 100644 --- a/src/main.vala +++ b/src/main.vala @@ -205,6 +205,67 @@ void library_exec(string[] mounts) { run_system_pictures_import(); } + bool heif = false; + bool jxl = false; + bool png = false; + bool tiff = false; + bool avif = false; + bool bmp = false; + bool gif = false; + + var formats = Gdk.Pixbuf.get_formats(); + foreach (var format in formats) { + if ("image/heif" in format.get_mime_types()) { + heif = true; + } + if ("image/jxl" in format.get_mime_types()) { + jxl = true; + } + if ("image/png" in format.get_mime_types()) { + png = true; + } + if ("image/tiff" in format.get_mime_types()) { + tiff = true; + } + if ("image/avif" in format.get_mime_types()) { + avif = true; + } + if ("image/bmp" in format.get_mime_types()) { + bmp = true; + } + if ("image/gif" in format.get_mime_types()) { + gif = true; + } + } + + bool can_read_bmff = false; + Bytes b = null; + try { + b = GLib.resources_lookup_data("/org/gnome/Shotwell/misc/canary.avif", GLib.ResourceLookupFlags.NONE); + } catch (Error err) { + error("Failed to look up mandatory resource: %s", err.message); + } + + try { + var m = new GExiv2.Metadata(); + m.open_buf(b.get_data()); + can_read_bmff = true; + } catch (Error err) { + // Do nothing + } + + message("Supported codecs...."); + message(" WEBP : yes, builtin"); + message(" RAW : yes, builtin"); + message(" CR3 : %s", can_read_bmff ? "yes" : "no"); + message(" JPEG : yes, gdk-pixbuf"); + message(" PNG : %s, gdk-pixbuf", png ? "yes" : "no"); + message(" GIF : %s, gdk-pixbuf", gif ? "yes" : "no"); + message(" TIFF : %s, gdk-pixbuf", tiff ? "yes" : "no"); + message(" JPEG XL: %s, gdk-pixbuf, %s meta-data", jxl ? "yes" : "no", can_read_bmff ? "yes" : "no"); + message(" AVIF : %s, gdk-pixbuf, %s meta-data", avif ? "yes" : "no", can_read_bmff ? "yes" : "no"); + message(" HEIF : %s, gdk-pixbuf, %s meta-data", heif ? "yes" : "no", can_read_bmff ? "yes" : "no"); + debug("%lf seconds to Gtk.main()", startup_timer.elapsed()); Application.get_instance().start(); @@ -405,6 +466,8 @@ void main(string[] args) { return; } + typeof(DismissableTextView).ensure(); + if (CommandlineOptions.browse_profiles) { var window = new Gtk.Dialog(); window.set_title (_("Choose Shotwell's profile")); diff --git a/src/photos/JfifSupport.vala b/src/photos/JfifSupport.vala index 27a8b11..fc43663 100644 --- a/src/photos/JfifSupport.vala +++ b/src/photos/JfifSupport.vala @@ -51,7 +51,7 @@ public class JfifFileFormatDriver : PhotoFileFormatDriver { public class JfifFileFormatProperties : PhotoFileFormatProperties { private static string[] KNOWN_EXTENSIONS = { - "jpg", "jpeg", "jpe", "thm" + "jpg", "jpeg", "jpe", "thm", "mpo" }; private static string[] KNOWN_MIME_TYPES = { diff --git a/src/video-support/meson.build b/src/video-support/meson.build index da3f9d7..187d723 100644 --- a/src/video-support/meson.build +++ b/src/video-support/meson.build @@ -8,8 +8,10 @@ executable( gstreamer, gstreamer_pbu ], - c_args : '-DGST_PB_UTILS_IS_DISCOVERER_INFO=GST_IS_DISCOVERER_INFO' # Work-around for wrong type-check macro generated by valac + c_args : '-DGST_PB_UTILS_IS_DISCOVERER_INFO=GST_IS_DISCOVERER_INFO', + install: true, + install_dir : join_paths(get_option('libexecdir'), 'shotwell') ) libvideometadata_handling = static_library( |