summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff.email>2023-12-17 19:58:25 +0100
committerJörg Frings-Fürst <debian@jff.email>2023-12-17 19:58:25 +0100
commit4cb46f4de4b881e5b1f65af017dca6f3917e55e5 (patch)
treefb359e210d2d9c30f5ad36a447ea29b62ae9bb56 /src
parent841f952294b349b2b8e2afb5305ce34a3b59bb4b (diff)
parentf5a0cee8ccecc7b6c6c2d8e9fb6f6eecd53531fe (diff)
Update upstream source from tag 'upstream/0.32.4'
Update to upstream version '0.32.4' with Debian dir 51798a6af1c9b7e48f979c020d72f3c5ddd964a6
Diffstat (limited to 'src')
-rw-r--r--src/AppWindow.vala11
-rw-r--r--src/CollectionPage.vala46
-rw-r--r--src/DragAndDropHandler.vala10
-rw-r--r--src/Exporter.vala42
-rw-r--r--src/PhotoPage.vala6
-rw-r--r--src/PixbufCache.vala2
-rw-r--r--src/ProfileBrowser.vala2
-rw-r--r--src/Properties.vala4
-rw-r--r--src/SlideshowPage.vala43
-rw-r--r--src/config/ConfigurationInterfaces.vala25
-rw-r--r--src/config/GSettingsEngine.vala2
-rw-r--r--src/core/ViewCollection.vala51
-rw-r--r--src/dialogs/MultiTextEntryDialog.vala22
-rw-r--r--src/direct/DirectPhotoPage.vala6
-rw-r--r--src/editing_tools/EditingTools.vala10
-rw-r--r--src/library/LibraryWindow.vala16
-rw-r--r--src/main.vala63
-rw-r--r--src/photos/JfifSupport.vala2
-rw-r--r--src/video-support/meson.build4
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("&", "&amp;") : 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(