summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff.email>2023-06-14 20:35:58 +0200
committerJörg Frings-Fürst <debian@jff.email>2023-06-14 20:35:58 +0200
commitd443a3c2509889533ca812c163056bace396b586 (patch)
treee94ffc0d9c054ca4efb8fb327e18dfac88e15dc7 /src
parentbb9797c14470641b082ebf635e2ae3cfd5f27a3b (diff)
New upstream version 0.32.1upstream/0.32.1
Diffstat (limited to 'src')
-rw-r--r--src/AppDirs.vala58
-rw-r--r--src/AppWindow.vala56
-rw-r--r--src/Application.vala12
-rw-r--r--src/BatchImport.vala11
-rw-r--r--src/CheckerboardItem.vala734
-rw-r--r--src/CheckerboardItemText.vala98
-rw-r--r--src/CheckerboardLayout.vala922
-rw-r--r--src/CheckerboardPage.vala758
-rw-r--r--src/Commands.vala89
-rw-r--r--src/Debug.vala5
-rw-r--r--src/DesktopIntegration.vala2
-rw-r--r--src/Dialogs.vala63
-rw-r--r--src/Dimensions.vala3
-rw-r--r--src/DirectoryMonitor.vala8
-rw-r--r--src/DragAndDropHandler.vala182
-rw-r--r--src/Event.vala77
-rw-r--r--src/Exporter.vala75
-rw-r--r--src/LibraryFiles.vala10
-rw-r--r--src/LibraryMonitor.vala8
-rw-r--r--src/MapWidget.vala788
-rw-r--r--src/MediaDataRepresentation.vala8
-rw-r--r--src/MediaInterfaces.vala4
-rw-r--r--src/MediaMetadata.vala125
-rw-r--r--src/MediaPage.vala7
-rw-r--r--src/MetadataWriter.vala18
-rw-r--r--src/Page.vala1557
-rw-r--r--src/PageMessagePane.vala19
-rw-r--r--src/Photo.vala161
-rw-r--r--src/PhotoPage.vala19
-rw-r--r--src/Printing.vala28
-rw-r--r--src/ProfileBrowser.vala294
-rw-r--r--src/Profiles.vala303
-rw-r--r--src/Properties.vala101
-rw-r--r--src/Resources.vala45
-rw-r--r--src/SearchFilter.vala14
-rw-r--r--src/SinglePhotoPage.vala537
-rw-r--r--src/SlideshowPage.vala14
-rw-r--r--src/SortedList.vala4
-rw-r--r--src/Tag.vala6
-rw-r--r--src/Thumbnail.vala9
-rw-r--r--src/ThumbnailCache.vala28
-rw-r--r--src/TimedQueue.vala2
-rw-r--r--src/Upgrades.vala2
-rw-r--r--src/VideoMetadata.vala655
-rw-r--r--src/camera/CameraBranch.vala2
-rw-r--r--src/camera/CameraTable.vala83
-rw-r--r--src/camera/DiscoveredCamera.vala119
-rw-r--r--src/camera/GPhoto.vala5
-rw-r--r--src/camera/ImportPage.vala162
-rw-r--r--src/config/Config.vala2
-rw-r--r--src/config/ConfigurationInterfaces.vala30
-rw-r--r--src/config/GSettingsEngine.vala88
-rw-r--r--src/core/DataCollection.vala2
-rw-r--r--src/core/DataSourceTypes.vala4
-rw-r--r--src/core/SourceInterfaces.vala15
-rw-r--r--src/core/util.vala11
-rw-r--r--src/data_imports/DataImportJob.vala6
-rw-r--r--src/data_imports/DataImportSource.vala6
-rw-r--r--src/data_imports/DataImports.vala2
-rw-r--r--src/data_imports/DataImportsPluginHost.vala5
-rw-r--r--src/data_imports/DataImportsUI.vala10
-rw-r--r--src/db/DatabaseTable.vala22
-rw-r--r--src/db/Db.vala65
-rw-r--r--src/db/EventTable.vala14
-rw-r--r--src/db/FaceLocationTable.vala75
-rw-r--r--src/db/FaceTable.vala56
-rw-r--r--src/db/PhotoTable.vala206
-rw-r--r--src/db/TagTable.vala8
-rw-r--r--src/db/TombstoneTable.vala6
-rw-r--r--src/db/VideoTable.vala64
-rw-r--r--src/dialogs/AdjustDateTimeDialog.vala71
-rw-r--r--src/dialogs/MultiTextEntryDialog.vala2
-rw-r--r--src/dialogs/Preferences.vala46
-rw-r--r--src/dialogs/SetBackground.vala4
-rw-r--r--src/dialogs/SetBackgroundSlideshow.vala8
-rw-r--r--src/dialogs/TextEntry.vala4
-rw-r--r--src/dialogs/WelcomeDialog.vala4
-rw-r--r--src/direct/DirectPhotoPage.vala5
-rw-r--r--src/editing_tools/EditingTools.vala81
-rw-r--r--src/editing_tools/StraightenTool.vala14
-rw-r--r--src/events/EventDirectoryItem.vala2
-rw-r--r--src/events/EventsBranch.vala40
-rw-r--r--src/events/EventsDirectoryPage.vala40
-rw-r--r--src/faces/Face.vala30
-rw-r--r--src/faces/FaceDetect.vala146
-rw-r--r--src/faces/FaceLocation.vala45
-rw-r--r--src/faces/FacePage.vala14
-rw-r--r--src/faces/FaceShape.vala120
-rw-r--r--src/faces/FacesTool.vala230
-rw-r--r--src/import-roll/ImportRollBranch.vala3
-rw-r--r--src/library/BackgroundProgressBar.vala109
-rw-r--r--src/library/LibraryWindow.vala175
-rw-r--r--src/library/TrashPage.vala4
-rw-r--r--src/libshotwell.deps20
-rw-r--r--src/main.vala171
-rw-r--r--src/meson.build486
-rw-r--r--src/metadata/MediaMetadata.vala15
-rw-r--r--src/metadata/MetadataDateTime.vala78
-rw-r--r--src/metadata/MetadataRational.vala26
-rw-r--r--src/metadata/meson.build16
-rw-r--r--src/photos/AvifSupport.vala140
-rw-r--r--src/photos/BmpSupport.vala27
-rw-r--r--src/photos/GdkSupport.vala28
-rw-r--r--src/photos/GifSupport.vala27
-rw-r--r--src/photos/HeifSupport.vala150
-rw-r--r--src/photos/JfifSupport.vala108
-rw-r--r--src/photos/JpegXLSupport.vala149
-rw-r--r--src/photos/PhotoFileFormat.vala72
-rw-r--r--src/photos/PhotoFileSniffer.vala28
-rw-r--r--src/photos/PhotoMetadata.vala258
-rw-r--r--src/photos/PngSupport.vala27
-rw-r--r--src/photos/RawSupport.vala19
-rw-r--r--src/photos/TiffSupport.vala6
-rw-r--r--src/photos/WebPSupport.vala240
-rw-r--r--src/plugins/DataImportsInterfaces.vala4
-rw-r--r--src/plugins/ManifestWidget.vala412
-rw-r--r--src/plugins/Plugins.vala45
-rw-r--r--src/plugins/PublishingInterfaces.vala44
-rw-r--r--src/plugins/SpitInterfaces.vala33
-rw-r--r--src/plugins/StandardHostInterface.vala9
-rw-r--r--src/publishing/APIGlue.vala6
-rw-r--r--src/publishing/LoginWelcomePaneWidget.vala45
-rw-r--r--src/publishing/ProgressPaneWidget.vala44
-rw-r--r--src/publishing/Publishing.vala7
-rw-r--r--src/publishing/PublishingPluginHost.vala8
-rw-r--r--src/publishing/PublishingUI.vala202
-rw-r--r--src/publishing/StaticMessagePaneWidget.vala62
-rw-r--r--src/publishing/SuccessPaneWidget.vala39
-rw-r--r--src/publishing/meson.build27
-rw-r--r--src/searches/SavedSearchDialog.vala37
-rw-r--r--src/searches/SearchBoolean.vala6
-rw-r--r--src/sidebar/Tree.vala12
-rw-r--r--src/slideshow/Slideshow.vala20
-rw-r--r--src/slideshow/TransitionEffects.vala12
-rw-r--r--src/unit/rc/Unit.m429
-rw-r--r--src/unit/rc/UnitInternals.m432
-rw-r--r--src/unit/rc/template.vala7
-rw-r--r--src/unit/rc/unitize_entry.m419
-rw-r--r--src/util/Util.vala2
-rw-r--r--src/util/file.vala26
-rw-r--r--src/util/image.vala8
-rw-r--r--src/util/misc.vala30
-rw-r--r--src/util/string.vala14
-rw-r--r--src/util/system.vala5
-rw-r--r--src/util/ui.vala11
-rw-r--r--src/video-support/AVIChunk.vala121
-rw-r--r--src/video-support/AVIMetadataLoader.vala227
-rw-r--r--src/video-support/QuickTimeAtom.vala118
-rw-r--r--src/video-support/QuicktimeMetdataLoader.vala127
-rw-r--r--src/video-support/Video.vala (renamed from src/VideoSupport.vala)716
-rw-r--r--src/video-support/VideoImportParams.vala28
-rw-r--r--src/video-support/VideoMetadata.vala51
-rw-r--r--src/video-support/VideoMetadataReaderProcess.vala66
-rw-r--r--src/video-support/VideoReader.vala317
-rw-r--r--src/video-support/VideoSourceCollection.vala175
-rw-r--r--src/video-support/meson.build36
-rw-r--r--src/video-support/util.vala13
157 files changed, 9491 insertions, 6036 deletions
diff --git a/src/AppDirs.vala b/src/AppDirs.vala
index 6c4541c..20df920 100644
--- a/src/AppDirs.vala
+++ b/src/AppDirs.vala
@@ -169,15 +169,14 @@ class AppDirs {
}
// Library folder + photo folder, based on user's preferred directory pattern.
- public static File get_baked_import_dir(time_t tm) {
+ public static File get_baked_import_dir(DateTime tm) {
string? pattern = Config.Facade.get_instance().get_directory_pattern();
if (is_string_empty(pattern))
pattern = Config.Facade.get_instance().get_directory_pattern_custom();
if (is_string_empty(pattern))
pattern = "%Y" + Path.DIR_SEPARATOR_S + "%m" + Path.DIR_SEPARATOR_S + "%d"; // default
- DateTime date = new DateTime.from_unix_local(tm);
- return File.new_for_path(get_import_dir().get_path() + Path.DIR_SEPARATOR_S + date.format(pattern));
+ return File.new_for_path(get_import_dir().get_path() + Path.DIR_SEPARATOR_S + tm.to_local().format(pattern));
}
// Returns true if the File is in or is equal to the library/import directory.
@@ -210,7 +209,7 @@ class AppDirs {
return tmp_dir;
}
-
+
public static File get_data_subdir(string name, string? subname = null) {
File subdir = get_data_dir().get_child(name);
if (subname != null)
@@ -262,7 +261,7 @@ class AppDirs {
File? install_dir = get_install_dir();
return (install_dir != null) ? install_dir.get_child("share").get_child("shotwell")
- : get_exec_dir();
+ : get_lib_dir();
}
public static File get_lib_dir() {
@@ -317,32 +316,67 @@ class AppDirs {
return f;
}
+ public static File get_metadata_helper() {
+ const string filename = "shotwell-video-metadata-handler";
+ File f = AppDirs.get_libexec_dir().get_child("video-support").get_child (filename);
+ if (!f.query_exists()) {
+ // If we're running installed.
+ f = AppDirs.get_libexec_dir () .get_child ("shotwell").get_child (filename);
+ }
+ return f;
+ }
+
public static File get_settings_migrator_bin() {
const string filename = "shotwell-settings-migrator";
- File f = AppDirs.get_libexec_dir().get_child ("settings-migrator").get_child (filename);
+ File f = AppDirs.get_libexec_dir().get_child("settings-migrator").get_child (filename);
if (!f.query_exists()) {
// If we're running installed.
f = AppDirs.get_libexec_dir () .get_child ("shotwell").get_child (filename);
}
+
+ if (!f.query_exists()) {
+ f = AppDirs.get_libexec_dir().get_parent().get_child("settings-migrator").get_child(filename);
+ }
+
return f;
}
+ public static File get_haarcascade_file() {
+ const string filename = "facedetect-haarcascade.xml";
+ var f = AppDirs.get_resources_dir().get_parent().get_child("subprojects").get_child("shotwell-facedetect").get_child (filename);
+ if (f.query_exists()) {//testing meson builddir
+ return f;
+ }
+ return get_resources_dir().get_child("facedetect-haarcascade.xml");
+ }
+
+
+#if ENABLE_FACE_DETECTION
public static File get_facedetect_bin() {
const string filename = "shotwell-facedetect";
- File f = AppDirs.get_libexec_dir().get_parent().get_child("facedetect").get_child (filename);
+ File f = AppDirs.get_libexec_dir().get_parent().get_child("subprojects").get_child(filename).get_child (filename);
if (!f.query_exists()) {
f = AppDirs.get_libexec_dir().get_child("shotwell").get_child(filename);
}
return f;
}
-
- public static File get_haarcascade_file() {
- File f = File.new_for_path(AppDirs.get_exec_dir().get_parent().get_parent().get_child("facedetect").get_child("facedetect-haarcascade.xml").get_path());
+
+ public static File get_openface_dnn_dir() {
+ return File.new_for_path(Environment.get_user_data_dir()).get_child(DEFAULT_DATA_DIR).get_child("facedetect");
+ }
+
+ public static File get_openface_dnn_system_dir() {
+ var f = File.new_for_path("/app/extra");
+ if (f.query_exists())
+ return f;
+
+ f = AppDirs.get_resources_dir().get_parent().get_child("subprojects").get_child("shotwell-facedetect");
if (f.query_exists()) {//testing meson builddir
return f;
}
- return get_resources_dir().get_child("facedetect-haarcascade.xml");
+
+ return AppDirs.get_resources_dir().get_child("facedetect");
}
+#endif
}
-
diff --git a/src/AppWindow.vala b/src/AppWindow.vala
index a5b27a4..438806c 100644
--- a/src/AppWindow.vala
+++ b/src/AppWindow.vala
@@ -6,7 +6,7 @@
public class FullscreenWindow : PageWindow {
public const int TOOLBAR_INVOCATION_MSEC = 250;
- public const int TOOLBAR_DISMISSAL_SEC = 2;
+ public const int TOOLBAR_DISMISSAL_SEC = 2 * 1000000;
public const int TOOLBAR_CHECK_DISMISSAL_MSEC = 500;
private Gtk.Overlay overlay = new Gtk.Overlay();
@@ -15,7 +15,7 @@ public class FullscreenWindow : PageWindow {
private Gtk.ToggleToolButton pin_button = new Gtk.ToggleToolButton();
private bool is_toolbar_shown = false;
private bool waiting_for_invoke = false;
- private time_t left_toolbar_time = 0;
+ private int64 left_toolbar_time = 0;
private bool switched_to = false;
private bool is_toolbar_dismissal_enabled;
@@ -246,13 +246,13 @@ public class FullscreenWindow : PageWindow {
// if this is the first time noticed, start the timer and keep checking
if (left_toolbar_time == 0) {
- left_toolbar_time = time_t();
+ left_toolbar_time = GLib.get_monotonic_time();
return true;
}
// see if enough time has elapsed
- time_t now = time_t();
+ int64 now = GLib.get_monotonic_time();
assert(now >= left_toolbar_time);
if (now - left_toolbar_time < TOOLBAR_DISMISSAL_SEC)
@@ -367,7 +367,6 @@ public abstract class PageWindow : Gtk.ApplicationWindow {
var display = get_window ().get_display ();
var cursor = new Gdk.Cursor.for_display (display, Gdk.CursorType.WATCH);
get_window().set_cursor (cursor);
- spin_event_loop();
}
public void set_normal_cursor() {
@@ -381,7 +380,6 @@ public abstract class PageWindow : Gtk.ApplicationWindow {
var display = get_window ().get_display ();
var cursor = new Gdk.Cursor.for_display (display, Gdk.CursorType.LEFT_PTR);
get_window().set_cursor (cursor);
- spin_event_loop();
}
}
@@ -415,7 +413,7 @@ public abstract class AppWindow : PageWindow {
instance = this;
title = Resources.APP_TITLE;
- set_default_icon_name("shotwell");
+ set_default_icon_name("org.gnome.Shotwell");
// restore previous size and maximization state
if (this is LibraryWindow) {
@@ -441,10 +439,6 @@ public abstract class AppWindow : PageWindow {
// with each ActionGroup while we're adding the groups to the UIManager.
add_actions ();
-
- Gtk.CssProvider provider = new Gtk.CssProvider();
- provider.load_from_resource("/org/gnome/Shotwell/misc/org.gnome.Shotwell.css");
- Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
}
private const GLib.ActionEntry[] common_actions = {
@@ -475,19 +469,6 @@ public abstract class AppWindow : PageWindow {
return fullscreen_window;
}
- public static Gtk.Builder create_builder(string glade_filename = "shotwell.ui", void *user = null) {
- Gtk.Builder builder = new Gtk.Builder();
- try {
- builder.add_from_resource(Resources.get_ui(glade_filename));
- } catch(GLib.Error error) {
- warning("Unable to create Gtk.Builder: %s\n", error.message);
- }
-
- builder.connect_signals(user);
-
- return builder;
- }
-
public static void error_message(string message, Gtk.Window? parent = null) {
error_message_with_title(Resources.APP_TITLE, message, parent);
}
@@ -560,27 +541,26 @@ public abstract class AppWindow : PageWindow {
return (Gtk.ResponseType) response;
}
- public static Gtk.ResponseType negate_affirm_all_cancel_question(string message,
- string negative, string affirmative, string affirmative_all, string? title = null,
- Gtk.Window? parent = null) {
+ public static int export_overwrite_or_replace_question(string message,
+ string alt1, string alt2, string alt3, string alt4, string alt5, 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(negative, Gtk.ResponseType.NO, affirmative, Gtk.ResponseType.YES,
- affirmative_all, Gtk.ResponseType.APPLY, _("_Cancel"), Gtk.ResponseType.CANCEL);
+ dialog.add_buttons(alt1, 1, alt2, 2, alt3, 3, alt4, 4, alt5, 5, alt6, 6);
int response = dialog.run();
dialog.destroy();
- return (Gtk.ResponseType) response;
+ return response;
}
-
- public static void database_error(DatabaseError err) {
+
+ public static void database_error(Error err) {
panic(_("A fatal error occurred when accessing Shotwell’s library. Shotwell cannot continue.\n\n%s").printf(
err.message));
}
-
+
public static void panic(string msg) {
critical(msg);
error_message(msg);
@@ -591,9 +571,13 @@ public abstract class AppWindow : PageWindow {
public abstract string get_app_role();
protected void on_about() {
- const string[] artists = { "Celler Schloss created by Hajotthu, CC BY-SA 3.0, https://commons.wikimedia.org/wiki/File:Celler_Schloss_April_2010.jpg#file", null };
+ var hash = "";
+ if (Resources.GIT_VERSION != null && Resources.GIT_VERSION != "" && Resources.GIT_VERSION != Resources.APP_VERSION) {
+ hash = " (%s)".printf(Resources.GIT_VERSION.substring(0,7));
+ }
+ string[] artists = {"Image of the Delmenhorst Town Hall by Charlie1965nrw, source: https://commons.wikimedia.org/wiki/File:Delmenhorst_Rathaus.jpg", null};
Gtk.show_about_dialog(this,
- "version", Resources.APP_VERSION + " \u2013 “Celle”",
+ "version", Resources.APP_VERSION + hash + " — Delmenhorst",
"comments", get_app_role(),
"copyright", Resources.COPYRIGHT,
"website", Resources.HOME_URL,
@@ -601,8 +585,8 @@ public abstract class AppWindow : PageWindow {
"website-label", _("Visit the Shotwell web site"),
"authors", Resources.AUTHORS,
"logo", Resources.get_icon(Resources.ICON_ABOUT_LOGO, -1),
- "artists", artists,
"translator-credits", _("translator-credits"),
+ "artists", artists,
null
);
}
diff --git a/src/Application.vala b/src/Application.vala
index 36acc41..59bae36 100644
--- a/src/Application.vala
+++ b/src/Application.vala
@@ -69,6 +69,16 @@ public class Application {
system_app.startup.connect(on_activated);
}
+ public static double get_scale() {
+ var instance = get_instance().system_app;
+ unowned GLib.List<Gtk.Window> windows = instance.get_windows();
+
+ if (windows == null)
+ return 1.0;
+
+ return windows.data.get_scale_factor();
+ }
+
/**
* @brief This is a helper for library mode that should only be
* called if we've gotten a camera mount and are _not_ the primary
@@ -104,7 +114,7 @@ public class Application {
}
/**
- * @brief Signal handler for GApplication's 'command-line' signal.
+ * @brief Signal handler for GApplication's 'activate' signal.
*
* The most likely scenario for this to be fired is if the user
* either tried to run us twice in library mode, or we've just gotten
diff --git a/src/BatchImport.vala b/src/BatchImport.vala
index 0e31441..90ccba8 100644
--- a/src/BatchImport.vala
+++ b/src/BatchImport.vala
@@ -201,10 +201,10 @@ public abstract class BatchImportJob {
return false;
}
- // returns a non-zero time_t value if this has a valid exposure time override, returns zero
+ // returns a non-null DateTime value if this has a valid exposure time override, returns zero
// otherwise
- public virtual time_t get_exposure_time_override() {
- return 0;
+ public virtual DateTime? get_exposure_time_override() {
+ return null;
}
public virtual bool recurse() {
@@ -1597,6 +1597,11 @@ private class WorkSniffer : BackgroundImportJob {
}
public void search_dir(BatchImportJob job, File dir, bool copy_to_library, bool recurse) throws Error {
+ if (dir.get_child(".nomedia").query_exists()) {
+ debug("Folder %s contains \".nomedia\" file, ignoring.", dir.get_path());
+ return;
+ }
+
FileEnumerator enumerator = dir.enumerate_children("standard::*",
FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
diff --git a/src/CheckerboardItem.vala b/src/CheckerboardItem.vala
new file mode 100644
index 0000000..a8a5e63
--- /dev/null
+++ b/src/CheckerboardItem.vala
@@ -0,0 +1,734 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public abstract class CheckerboardItem : ThumbnailView {
+ // Collection properties CheckerboardItem understands
+ // SHOW_TITLES (bool)
+ public const string PROP_SHOW_TITLES = "show-titles";
+ // SHOW_COMMENTS (bool)
+ public const string PROP_SHOW_COMMENTS = "show-comments";
+ // SHOW_SUBTITLES (bool)
+ public const string PROP_SHOW_SUBTITLES = "show-subtitles";
+
+ public const int FRAME_WIDTH = 8;
+ public const int LABEL_PADDING = 4;
+ public const int BORDER_WIDTH = 1;
+
+ public const int SHADOW_RADIUS = 4;
+ public const float SHADOW_INITIAL_ALPHA = 0.5f;
+
+ public const int TRINKET_SCALE = 12;
+ public const int TRINKET_PADDING = 1;
+
+ public const int BRIGHTEN_SHIFT = 0x18;
+
+ public Dimensions requisition = Dimensions();
+ public Gdk.Rectangle allocation = Gdk.Rectangle();
+
+ private bool exposure = false;
+ private CheckerboardItemText? title = null;
+ private bool title_visible = true;
+ private CheckerboardItemText? comment = null;
+ private bool comment_visible = true;
+ private CheckerboardItemText? subtitle = null;
+ private bool subtitle_visible = false;
+ private bool is_cursor = false;
+ private Pango.Alignment tag_alignment = Pango.Alignment.LEFT;
+ private Gee.List<Tag>? user_visible_tag_list = null;
+ private Gee.Collection<Tag> tags;
+ private Gdk.Pixbuf pixbuf = null;
+ private Gdk.Pixbuf display_pixbuf = null;
+ private Gdk.Pixbuf brightened = null;
+ private Dimensions pixbuf_dim = Dimensions();
+ private int col = -1;
+ private int row = -1;
+ private int horizontal_trinket_offset = 0;
+
+ protected CheckerboardItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, string? comment,
+ bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
+ base(source);
+
+ pixbuf_dim = initial_pixbuf_dim;
+ this.title = new CheckerboardItemText(title, alignment, marked_up);
+ // on the checkboard page we display the comment in
+ // one line, i.e., replacing all newlines with spaces.
+ // that means that the display will contain "..." if the comment
+ // is too long.
+ // warning: changes here have to be done in set_comment, too!
+ if (comment != null)
+ this.comment = new CheckerboardItemText(comment.replace("\n", " "), alignment,
+ marked_up);
+
+ // Don't calculate size here, wait for the item to be assigned to a ViewCollection
+ // (notify_membership_changed) and calculate when the collection's property settings
+ // are known
+ }
+
+ public bool has_tags { get; private set; }
+
+ public override string get_name() {
+ return (title != null) ? title.get_text() : base.get_name();
+ }
+
+ public string get_title() {
+ return (title != null) ? title.get_text() : "";
+ }
+
+ public string get_comment() {
+ return (comment != null) ? comment.get_text() : "";
+ }
+
+ public void set_title(string text, bool marked_up = false,
+ Pango.Alignment alignment = Pango.Alignment.LEFT) {
+ if (title != null && title.is_set_to(text, marked_up, alignment))
+ return;
+
+ title = new CheckerboardItemText(text, alignment, marked_up);
+
+ if (title_visible) {
+ recalc_size("set_title");
+ notify_view_altered();
+ }
+ }
+
+ public void translate_coordinates(ref int x, ref int y) {
+ x -= allocation.x + FRAME_WIDTH;
+ y -= allocation.y + FRAME_WIDTH;
+ }
+
+ public void clear_title() {
+ if (title == null)
+ return;
+
+ title = null;
+
+ if (title_visible) {
+ recalc_size("clear_title");
+ notify_view_altered();
+ }
+ }
+
+ private void set_title_visible(bool visible) {
+ if (title_visible == visible)
+ return;
+
+ title_visible = visible;
+
+ recalc_size("set_title_visible");
+ notify_view_altered();
+ }
+
+ public void set_comment(string text, bool marked_up = false,
+ Pango.Alignment alignment = Pango.Alignment.LEFT) {
+ if (comment != null && comment.is_set_to(text, marked_up, alignment))
+ return;
+
+ comment = new CheckerboardItemText(text.replace("\n", " "), alignment, marked_up);
+
+ if (comment_visible) {
+ recalc_size("set_comment");
+ notify_view_altered();
+ }
+ }
+
+ public void clear_comment() {
+ if (comment == null)
+ return;
+
+ comment = null;
+
+ if (comment_visible) {
+ recalc_size("clear_comment");
+ notify_view_altered();
+ }
+ }
+
+ private void set_comment_visible(bool visible) {
+ if (comment_visible == visible)
+ return;
+
+ comment_visible = visible;
+
+ recalc_size("set_comment_visible");
+ notify_view_altered();
+ }
+
+ public void set_tags(Gee.Collection<Tag>? tags,
+ Pango.Alignment alignment = Pango.Alignment.LEFT) {
+ has_tags = (tags != null && tags.size > 0);
+ tag_alignment = alignment;
+ string text;
+ if (has_tags) {
+ this.tags = tags;
+ user_visible_tag_list = Tag.make_user_visible_tag_list(tags);
+ text = Tag.make_tag_markup_string(user_visible_tag_list);
+ } else {
+ text = "<small>.</small>";
+ }
+
+ if (subtitle != null && subtitle.is_set_to(text, true, alignment))
+ return;
+ subtitle = new CheckerboardItemText(text, alignment, true);
+
+ if (subtitle_visible) {
+ recalc_size("set_subtitle");
+ notify_view_altered();
+ }
+ }
+
+ public void clear_tags() {
+ clear_subtitle();
+ has_tags = false;
+ user_visible_tag_list = null;
+ }
+
+ public void highlight_user_visible_tag(int index)
+ requires (user_visible_tag_list != null) {
+ string text = Tag.make_tag_markup_string(user_visible_tag_list, index);
+ subtitle = new CheckerboardItemText(text, tag_alignment, true);
+
+ if (subtitle_visible)
+ notify_view_altered();
+ }
+
+ public Tag get_user_visible_tag(int index)
+ requires (index >= 0 && index < user_visible_tag_list.size) {
+ return user_visible_tag_list.get(index);
+ }
+
+ public Pango.Layout? get_tag_list_layout() {
+ return has_tags ? subtitle.get_pango_layout() : null;
+ }
+
+ public Gdk.Rectangle get_subtitle_allocation() {
+ return subtitle.allocation;
+ }
+
+ public string get_subtitle() {
+ return (subtitle != null) ? subtitle.get_text() : "";
+ }
+
+ public void set_subtitle(string text, bool marked_up = false,
+ Pango.Alignment alignment = Pango.Alignment.LEFT) {
+ if (subtitle != null && subtitle.is_set_to(text, marked_up, alignment))
+ return;
+
+ subtitle = new CheckerboardItemText(text, alignment, marked_up);
+
+ if (subtitle_visible) {
+ recalc_size("set_subtitle");
+ notify_view_altered();
+ }
+ }
+
+ public void clear_subtitle() {
+ if (subtitle == null)
+ return;
+
+ subtitle = null;
+
+ if (subtitle_visible) {
+ recalc_size("clear_subtitle");
+ notify_view_altered();
+ }
+ }
+
+ private void set_subtitle_visible(bool visible) {
+ if (subtitle_visible == visible)
+ return;
+
+ subtitle_visible = visible;
+
+ recalc_size("set_subtitle_visible");
+ notify_view_altered();
+ }
+
+ public void set_is_cursor(bool is_cursor) {
+ this.is_cursor = is_cursor;
+ }
+
+ public bool get_is_cursor() {
+ return is_cursor;
+ }
+
+ public virtual void handle_mouse_motion(int x, int y, int height, int width) {
+
+ }
+
+ public virtual void handle_mouse_leave() {
+ unbrighten();
+ }
+
+ public virtual void handle_mouse_enter() {
+ brighten();
+ }
+
+ protected override void notify_membership_changed(DataCollection? collection) {
+ bool title_visible = (bool) get_collection_property(PROP_SHOW_TITLES, true);
+ bool comment_visible = (bool) get_collection_property(PROP_SHOW_COMMENTS, true);
+ bool subtitle_visible = (bool) get_collection_property(PROP_SHOW_SUBTITLES, false);
+
+ bool altered = false;
+ if (this.title_visible != title_visible) {
+ this.title_visible = title_visible;
+ altered = true;
+ }
+
+ if (this.comment_visible != comment_visible) {
+ this.comment_visible = comment_visible;
+ altered = true;
+ }
+
+ if (this.subtitle_visible != subtitle_visible) {
+ this.subtitle_visible = subtitle_visible;
+ altered = true;
+ }
+
+ if (altered || !requisition.has_area()) {
+ recalc_size("notify_membership_changed");
+ notify_view_altered();
+ }
+
+ base.notify_membership_changed(collection);
+ }
+
+ protected override void notify_collection_property_set(string name, Value? old, Value val) {
+ switch (name) {
+ case PROP_SHOW_TITLES:
+ set_title_visible((bool) val);
+ break;
+
+ case PROP_SHOW_COMMENTS:
+ set_comment_visible((bool) val);
+ break;
+
+ case PROP_SHOW_SUBTITLES:
+ set_subtitle_visible((bool) val);
+ break;
+ }
+
+ base.notify_collection_property_set(name, old, val);
+ }
+
+ // The alignment point is the coordinate on the y-axis (relative to the top of the
+ // CheckerboardItem) which this item should be aligned to. This allows for
+ // bottom-alignment along the bottom edge of the thumbnail.
+ public int get_alignment_point() {
+ return FRAME_WIDTH + BORDER_WIDTH + pixbuf_dim.height;
+ }
+
+ public virtual void exposed() {
+ exposure = true;
+ }
+
+ public virtual void unexposed() {
+ exposure = false;
+
+ if (title != null)
+ title.clear_pango_layout();
+
+ if (comment != null)
+ comment.clear_pango_layout();
+
+ if (subtitle != null)
+ subtitle.clear_pango_layout();
+ }
+
+ public virtual bool is_exposed() {
+ return exposure;
+ }
+
+ public bool has_image() {
+ return pixbuf != null;
+ }
+
+ public Gdk.Pixbuf? get_image() {
+ return pixbuf;
+ }
+
+ public void set_image(Gdk.Pixbuf pixbuf) {
+ this.pixbuf = pixbuf;
+ display_pixbuf = pixbuf;
+ pixbuf_dim = Dimensions.for_pixbuf(pixbuf);
+
+ recalc_size("set_image");
+ notify_view_altered();
+ }
+
+ public void clear_image(Dimensions dim) {
+ bool had_image = pixbuf != null;
+
+ pixbuf = null;
+ display_pixbuf = null;
+ pixbuf_dim = dim;
+
+ recalc_size("clear_image");
+
+ if (had_image)
+ notify_view_altered();
+ }
+
+ public static int get_max_width(int scale) {
+ // width is frame width (two sides) + frame padding (two sides) + width of pixbuf (text
+ // never wider)
+ return (FRAME_WIDTH * 2) + scale;
+ }
+
+ private void recalc_size(string reason) {
+ Dimensions old_requisition = requisition;
+
+ // only add in the text heights if they're displayed
+ int title_height = (title != null && title_visible)
+ ? title.get_height() + LABEL_PADDING : 0;
+ int comment_height = (comment != null && comment_visible)
+ ? comment.get_height() + LABEL_PADDING : 0;
+ int subtitle_height = (subtitle != null && subtitle_visible)
+ ? subtitle.get_height() + LABEL_PADDING : 0;
+
+ // width is frame width (two sides) + frame padding (two sides) + width of pixbuf
+ // (text never wider)
+ requisition.width = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2) + pixbuf_dim.width;
+
+ // height is frame width (two sides) + frame padding (two sides) + height of pixbuf
+ // + height of text + label padding (between pixbuf and text)
+ requisition.height = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2)
+ + pixbuf_dim.height + title_height + comment_height + subtitle_height;
+
+#if TRACE_REFLOW_ITEMS
+ debug("recalc_size %s: %s title_height=%d comment_height=%d subtitle_height=%d requisition=%s",
+ get_source().get_name(), reason, title_height, comment_height, subtitle_height,
+ requisition.to_string());
+#endif
+
+ if (!requisition.approx_equals(old_requisition)) {
+#if TRACE_REFLOW_ITEMS
+ debug("recalc_size %s: %s notifying geometry altered", get_source().get_name(), reason);
+#endif
+ notify_geometry_altered();
+ }
+ }
+
+ protected static Dimensions get_border_dimensions(Dimensions object_dim, int border_width) {
+ Dimensions dimensions = Dimensions();
+ dimensions.width = object_dim.width + (border_width * 2);
+ dimensions.height = object_dim.height + (border_width * 2);
+ return dimensions;
+ }
+
+ protected static Gdk.Point get_border_origin(Gdk.Point object_origin, int border_width) {
+ Gdk.Point origin = Gdk.Point();
+ origin.x = object_origin.x - border_width;
+ origin.y = object_origin.y - border_width;
+ return origin;
+ }
+
+ protected virtual void paint_shadow(Cairo.Context ctx, Dimensions dimensions, Gdk.Point origin,
+ int radius, float initial_alpha) {
+ double rgb_all = 0.0;
+
+ // top right corner
+ paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + radius, rgb_all, radius,
+ initial_alpha, -0.5 * Math.PI, 0);
+ // bottom right corner
+ paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + dimensions.height, rgb_all,
+ radius, initial_alpha, 0, 0.5 * Math.PI);
+ // bottom left corner
+ paint_shadow_in_corner(ctx, origin.x + radius, origin.y + dimensions.height, rgb_all, radius,
+ initial_alpha, 0.5 * Math.PI, Math.PI);
+
+ // left right
+ Cairo.Pattern lr = new Cairo.Pattern.linear(0, origin.y + dimensions.height,
+ 0, origin.y + dimensions.height + radius);
+ lr.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
+ lr.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
+ ctx.set_source(lr);
+ ctx.rectangle(origin.x + radius, origin.y + dimensions.height, dimensions.width - radius, radius);
+ ctx.fill();
+
+ // top down
+ Cairo.Pattern td = new Cairo.Pattern.linear(origin.x + dimensions.width,
+ 0, origin.x + dimensions.width + radius, 0);
+ td.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
+ td.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
+ ctx.set_source(td);
+ ctx.rectangle(origin.x + dimensions.width, origin.y + radius,
+ radius, dimensions.height - radius);
+ ctx.fill();
+ }
+
+ protected void paint_shadow_in_corner(Cairo.Context ctx, int x, int y,
+ double rgb_all, float radius, float initial_alpha, double arc1, double arc2) {
+ Cairo.Pattern p = new Cairo.Pattern.radial(x, y, 0, x, y, radius);
+ p.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
+ p.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0);
+ ctx.set_source(p);
+ ctx.move_to(x, y);
+ ctx.arc(x, y, radius, arc1, arc2);
+ ctx.close_path();
+ ctx.fill();
+ }
+
+ protected virtual void paint_border(Cairo.Context ctx, Dimensions object_dimensions,
+ Gdk.Point object_origin, int border_width) {
+ if (border_width == 1) {
+ ctx.rectangle(object_origin.x - border_width, object_origin.y - border_width,
+ object_dimensions.width + (border_width * 2),
+ object_dimensions.height + (border_width * 2));
+ ctx.fill();
+ } else {
+ Dimensions dimensions = get_border_dimensions(object_dimensions, border_width);
+ Gdk.Point origin = get_border_origin(object_origin, border_width);
+
+ // amount of rounding needed on corners varies by size of object
+ double scale = int.max(object_dimensions.width, object_dimensions.height);
+ draw_rounded_corners_filled(ctx, dimensions, origin, 0.25 * scale);
+ }
+ }
+
+ protected virtual void paint_image(Cairo.Context ctx, Gdk.Pixbuf pixbuf, Gdk.Point origin) {
+ paint_pixmap_with_background(ctx, pixbuf, origin.x, origin.y);
+ }
+
+ private int get_selection_border_width(int scale) {
+ return ((scale <= ((Thumbnail.MIN_SCALE + Thumbnail.MAX_SCALE) / 3)) ? 5 : 4)
+ + BORDER_WIDTH;
+ }
+
+ protected virtual Gdk.Pixbuf? get_top_left_trinket(int scale) {
+ return null;
+ }
+
+ protected virtual Gdk.Pixbuf? get_top_right_trinket(int scale) {
+ return null;
+ }
+
+ protected virtual Gdk.Pixbuf? get_bottom_left_trinket(int scale) {
+ return null;
+ }
+
+ protected virtual Gdk.Pixbuf? get_bottom_right_trinket(int scale) {
+ return null;
+ }
+
+ public void paint(Gtk.StyleContext style_context, Cairo.Context ctx, Gdk.RGBA bg_color, Gdk.RGBA selected_color,
+ Gdk.RGBA? border_color, Gdk.RGBA? focus_color) {
+ ctx.save();
+ ctx.translate(allocation.x + FRAME_WIDTH,
+ allocation.y + FRAME_WIDTH);
+ // calc the top-left point of the pixbuf
+ Gdk.Point pixbuf_origin = Gdk.Point();
+ pixbuf_origin.x = BORDER_WIDTH;
+ pixbuf_origin.y = BORDER_WIDTH;
+
+ ctx.set_line_width(FRAME_WIDTH);
+ ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
+ selected_color.alpha);
+
+ // draw shadow
+ if (border_color != null) {
+ ctx.save();
+ Dimensions shadow_dim = Dimensions();
+ shadow_dim.width = pixbuf_dim.width + BORDER_WIDTH;
+ shadow_dim.height = pixbuf_dim.height + BORDER_WIDTH;
+ paint_shadow(ctx, shadow_dim, pixbuf_origin, SHADOW_RADIUS, SHADOW_INITIAL_ALPHA);
+ ctx.restore();
+ }
+
+ // draw a border for the cursor with the selection width and normal border color
+ if (is_cursor) {
+ ctx.save();
+ ctx.set_source_rgba(focus_color.red, focus_color.green, focus_color.blue,
+ focus_color.alpha);
+ paint_border(ctx, pixbuf_dim, pixbuf_origin,
+ get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
+ ctx.restore();
+ }
+
+ // draw selection border
+ if (is_selected()) {
+ // border thickness depends on the size of the thumbnail
+ ctx.save();
+ paint_border(ctx, pixbuf_dim, pixbuf_origin,
+ get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
+ ctx.restore();
+ }
+
+ if (display_pixbuf != null) {
+ ctx.save();
+ ctx.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha);
+ paint_image(ctx, display_pixbuf, pixbuf_origin);
+ ctx.restore();
+ }
+
+ // title and subtitles are LABEL_PADDING below bottom of pixbuf
+ int text_y = pixbuf_dim.height + FRAME_WIDTH + LABEL_PADDING;
+ if (title != null && title_visible) {
+ // get the layout sized so its width is no more than the pixbuf's
+ // resize the text width to be no more than the pixbuf's
+ title.allocation.x = 0;
+ title.allocation.y = text_y;
+ title.allocation.width = pixbuf_dim.width;
+ title.allocation.height = title.get_height();
+ style_context.render_layout(ctx, title.allocation.x, title.allocation.y,
+ title.get_pango_layout(pixbuf_dim.width));
+
+ text_y += title.get_height() + LABEL_PADDING;
+ }
+
+ if (comment != null && comment_visible) {
+ comment.allocation.x = 0;
+ comment.allocation.y = text_y;
+ comment.allocation.width = pixbuf_dim.width;
+ comment.allocation.height = comment.get_height();
+ style_context.render_layout(ctx, comment.allocation.x, comment.allocation.y,
+ comment.get_pango_layout(pixbuf_dim.width));
+
+ text_y += comment.get_height() + LABEL_PADDING;
+ }
+
+ if (subtitle != null && subtitle_visible) {
+ subtitle.allocation.x = 0;
+ subtitle.allocation.y = text_y;
+ subtitle.allocation.width = pixbuf_dim.width;
+ subtitle.allocation.height = subtitle.get_height();
+
+ style_context.render_layout(ctx, subtitle.allocation.x, subtitle.allocation.y,
+ subtitle.get_pango_layout(pixbuf_dim.width));
+
+ // increment text_y if more text lines follow
+ }
+
+ ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
+ selected_color.alpha);
+
+ // draw trinkets last
+ Gdk.Pixbuf? trinket = get_bottom_left_trinket(TRINKET_SCALE);
+ if (trinket != null) {
+ int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
+ int y = pixbuf_origin.y + pixbuf_dim.height - trinket.get_height() -
+ TRINKET_PADDING;
+ Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+ ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+ ctx.fill();
+ }
+
+ trinket = get_top_left_trinket(TRINKET_SCALE);
+ if (trinket != null) {
+ int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
+ int y = pixbuf_origin.y + TRINKET_PADDING;
+ Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+ ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+ ctx.fill();
+ }
+
+ trinket = get_top_right_trinket(TRINKET_SCALE);
+ if (trinket != null) {
+ int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width -
+ get_horizontal_trinket_offset() - TRINKET_PADDING;
+ int y = pixbuf_origin.y + TRINKET_PADDING;
+ Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+ ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+ ctx.fill();
+ }
+
+ trinket = get_bottom_right_trinket(TRINKET_SCALE);
+ if (trinket != null) {
+ int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width -
+ get_horizontal_trinket_offset() - TRINKET_PADDING;
+ int y = pixbuf_origin.y + pixbuf_dim.height - trinket.height -
+ TRINKET_PADDING;
+ Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+ ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+ ctx.fill();
+ }
+ ctx.restore();
+ }
+
+ protected void set_horizontal_trinket_offset(int horizontal_trinket_offset) {
+ assert(horizontal_trinket_offset >= 0);
+ this.horizontal_trinket_offset = horizontal_trinket_offset;
+ }
+
+ protected int get_horizontal_trinket_offset() {
+ return horizontal_trinket_offset;
+ }
+
+ public void set_grid_coordinates(int col, int row) {
+ this.col = col;
+ this.row = row;
+ }
+
+ public int get_column() {
+ return col;
+ }
+
+ public int get_row() {
+ return row;
+ }
+
+ public void brighten() {
+ // "should" implies "can" and "didn't already"
+ if (brightened != null || pixbuf == null)
+ return;
+
+ // create a new lightened pixbuf to display
+ brightened = pixbuf.copy();
+ shift_colors(brightened, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, 0);
+
+ display_pixbuf = brightened;
+
+ notify_view_altered();
+ }
+
+
+ public void unbrighten() {
+ // "should", "can", "didn't already"
+ if (brightened == null || pixbuf == null)
+ return;
+
+ brightened = null;
+
+ // return to the normal image
+ display_pixbuf = pixbuf;
+
+ notify_view_altered();
+ }
+
+ public override void visibility_changed(bool visible) {
+ // if going from visible to hidden, unbrighten
+ if (!visible)
+ unbrighten();
+
+ base.visibility_changed(visible);
+ }
+
+ private bool query_tooltip_on_text(CheckerboardItemText text, Gtk.Tooltip tooltip) {
+ if (!text.get_pango_layout().is_ellipsized())
+ return false;
+
+ if (text.is_marked_up())
+ tooltip.set_markup(text.get_text());
+ else
+ tooltip.set_text(text.get_text());
+
+ return true;
+ }
+
+ public bool query_tooltip(int x, int y, Gtk.Tooltip tooltip) {
+ if (title != null && title_visible && coord_in_rectangle(x, y, title.allocation))
+ return query_tooltip_on_text(title, tooltip);
+
+ if (comment != null && comment_visible && coord_in_rectangle(x, y, comment.allocation))
+ return query_tooltip_on_text(comment, tooltip);
+
+ if (subtitle != null && subtitle_visible && coord_in_rectangle(x, y, subtitle.allocation))
+ return query_tooltip_on_text(subtitle, tooltip);
+
+ return false;
+ }
+}
+
+
diff --git a/src/CheckerboardItemText.vala b/src/CheckerboardItemText.vala
new file mode 100644
index 0000000..8924938
--- /dev/null
+++ b/src/CheckerboardItemText.vala
@@ -0,0 +1,98 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+private class CheckerboardItemText {
+ private static int one_line_height = 0;
+
+ private string text;
+ private bool marked_up;
+ private Pango.Alignment alignment;
+ private Pango.Layout layout = null;
+ private bool single_line = true;
+ private int height = 0;
+
+ public Gdk.Rectangle allocation = Gdk.Rectangle();
+
+ public CheckerboardItemText(string text, Pango.Alignment alignment = Pango.Alignment.LEFT,
+ bool marked_up = false) {
+ this.text = text;
+ this.marked_up = marked_up;
+ this.alignment = alignment;
+
+ single_line = is_single_line();
+ }
+
+ private bool is_single_line() {
+ return !String.contains_char(text, '\n');
+ }
+
+ public bool is_marked_up() {
+ return marked_up;
+ }
+
+ public bool is_set_to(string text, bool marked_up, Pango.Alignment alignment) {
+ return (this.marked_up == marked_up && this.alignment == alignment && this.text == text);
+ }
+
+ public string get_text() {
+ return text;
+ }
+
+ public int get_height() {
+ if (height == 0)
+ update_height();
+
+ return height;
+ }
+
+ public Pango.Layout get_pango_layout(int max_width = 0) {
+ if (layout == null)
+ create_pango();
+
+ if (max_width > 0)
+ layout.set_width(max_width * Pango.SCALE);
+
+ return layout;
+ }
+
+ public void clear_pango_layout() {
+ layout = null;
+ }
+
+ private void update_height() {
+ if (one_line_height != 0 && single_line)
+ height = one_line_height;
+ else
+ create_pango();
+ }
+
+ private void create_pango() {
+ // create layout for this string and ellipsize so it never extends past its laid-down width
+ layout = AppWindow.get_instance().create_pango_layout(null);
+ if (!marked_up)
+ layout.set_text(text, -1);
+ else
+ layout.set_markup(text, -1);
+
+ layout.set_ellipsize(Pango.EllipsizeMode.END);
+ layout.set_alignment(alignment);
+
+ // getting pixel size is expensive, and we only need the height, so use cached values
+ // whenever possible
+ if (one_line_height != 0 && single_line) {
+ height = one_line_height;
+ } else {
+ int width;
+ layout.get_pixel_size(out width, out height);
+
+ // cache first one-line height discovered
+ if (one_line_height == 0 && single_line)
+ one_line_height = height;
+ }
+ }
+}
+
+
diff --git a/src/CheckerboardLayout.vala b/src/CheckerboardLayout.vala
index 70e3b5c..85232f3 100644
--- a/src/CheckerboardLayout.vala
+++ b/src/CheckerboardLayout.vala
@@ -4,824 +4,6 @@
* See the COPYING file in this distribution.
*/
-private class CheckerboardItemText {
- private static int one_line_height = 0;
-
- private string text;
- private bool marked_up;
- private Pango.Alignment alignment;
- private Pango.Layout layout = null;
- private bool single_line = true;
- private int height = 0;
-
- public Gdk.Rectangle allocation = Gdk.Rectangle();
-
- public CheckerboardItemText(string text, Pango.Alignment alignment = Pango.Alignment.LEFT,
- bool marked_up = false) {
- this.text = text;
- this.marked_up = marked_up;
- this.alignment = alignment;
-
- single_line = is_single_line();
- }
-
- private bool is_single_line() {
- return !String.contains_char(text, '\n');
- }
-
- public bool is_marked_up() {
- return marked_up;
- }
-
- public bool is_set_to(string text, bool marked_up, Pango.Alignment alignment) {
- return (this.marked_up == marked_up && this.alignment == alignment && this.text == text);
- }
-
- public string get_text() {
- return text;
- }
-
- public int get_height() {
- if (height == 0)
- update_height();
-
- return height;
- }
-
- public Pango.Layout get_pango_layout(int max_width = 0) {
- if (layout == null)
- create_pango();
-
- if (max_width > 0)
- layout.set_width(max_width * Pango.SCALE);
-
- return layout;
- }
-
- public void clear_pango_layout() {
- layout = null;
- }
-
- private void update_height() {
- if (one_line_height != 0 && single_line)
- height = one_line_height;
- else
- create_pango();
- }
-
- private void create_pango() {
- // create layout for this string and ellipsize so it never extends past its laid-down width
- layout = AppWindow.get_instance().create_pango_layout(null);
- if (!marked_up)
- layout.set_text(text, -1);
- else
- layout.set_markup(text, -1);
-
- layout.set_ellipsize(Pango.EllipsizeMode.END);
- layout.set_alignment(alignment);
-
- // getting pixel size is expensive, and we only need the height, so use cached values
- // whenever possible
- if (one_line_height != 0 && single_line) {
- height = one_line_height;
- } else {
- int width;
- layout.get_pixel_size(out width, out height);
-
- // cache first one-line height discovered
- if (one_line_height == 0 && single_line)
- one_line_height = height;
- }
- }
-}
-
-public abstract class CheckerboardItem : ThumbnailView {
- // Collection properties CheckerboardItem understands
- // SHOW_TITLES (bool)
- public const string PROP_SHOW_TITLES = "show-titles";
- // SHOW_COMMENTS (bool)
- public const string PROP_SHOW_COMMENTS = "show-comments";
- // SHOW_SUBTITLES (bool)
- public const string PROP_SHOW_SUBTITLES = "show-subtitles";
-
- public const int FRAME_WIDTH = 8;
- public const int LABEL_PADDING = 4;
- public const int BORDER_WIDTH = 1;
-
- public const int SHADOW_RADIUS = 4;
- public const float SHADOW_INITIAL_ALPHA = 0.5f;
-
- public const int TRINKET_SCALE = 12;
- public const int TRINKET_PADDING = 1;
-
- public const int BRIGHTEN_SHIFT = 0x18;
-
- public Dimensions requisition = Dimensions();
- public Gdk.Rectangle allocation = Gdk.Rectangle();
-
- private bool exposure = false;
- private CheckerboardItemText? title = null;
- private bool title_visible = true;
- private CheckerboardItemText? comment = null;
- private bool comment_visible = true;
- private CheckerboardItemText? subtitle = null;
- private bool subtitle_visible = false;
- private bool is_cursor = false;
- private Pango.Alignment tag_alignment = Pango.Alignment.LEFT;
- private Gee.List<Tag>? user_visible_tag_list = null;
- private Gee.Collection<Tag> tags;
- private Gdk.Pixbuf pixbuf = null;
- private Gdk.Pixbuf display_pixbuf = null;
- private Gdk.Pixbuf brightened = null;
- private Dimensions pixbuf_dim = Dimensions();
- private int col = -1;
- private int row = -1;
- private int horizontal_trinket_offset = 0;
-
- protected CheckerboardItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, string? comment,
- bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
- base(source);
-
- pixbuf_dim = initial_pixbuf_dim;
- this.title = new CheckerboardItemText(title, alignment, marked_up);
- // on the checkboard page we display the comment in
- // one line, i.e., replacing all newlines with spaces.
- // that means that the display will contain "..." if the comment
- // is too long.
- // warning: changes here have to be done in set_comment, too!
- if (comment != null)
- this.comment = new CheckerboardItemText(comment.replace("\n", " "), alignment,
- marked_up);
-
- // Don't calculate size here, wait for the item to be assigned to a ViewCollection
- // (notify_membership_changed) and calculate when the collection's property settings
- // are known
- }
-
- public bool has_tags { get; private set; }
-
- public override string get_name() {
- return (title != null) ? title.get_text() : base.get_name();
- }
-
- public string get_title() {
- return (title != null) ? title.get_text() : "";
- }
-
- public string get_comment() {
- return (comment != null) ? comment.get_text() : "";
- }
-
- public void set_title(string text, bool marked_up = false,
- Pango.Alignment alignment = Pango.Alignment.LEFT) {
- if (title != null && title.is_set_to(text, marked_up, alignment))
- return;
-
- title = new CheckerboardItemText(text, alignment, marked_up);
-
- if (title_visible) {
- recalc_size("set_title");
- notify_view_altered();
- }
- }
-
- public void translate_coordinates(ref int x, ref int y) {
- x -= allocation.x + FRAME_WIDTH;
- y -= allocation.y + FRAME_WIDTH;
- }
-
- public void clear_title() {
- if (title == null)
- return;
-
- title = null;
-
- if (title_visible) {
- recalc_size("clear_title");
- notify_view_altered();
- }
- }
-
- private void set_title_visible(bool visible) {
- if (title_visible == visible)
- return;
-
- title_visible = visible;
-
- recalc_size("set_title_visible");
- notify_view_altered();
- }
-
- public void set_comment(string text, bool marked_up = false,
- Pango.Alignment alignment = Pango.Alignment.LEFT) {
- if (comment != null && comment.is_set_to(text, marked_up, alignment))
- return;
-
- comment = new CheckerboardItemText(text.replace("\n", " "), alignment, marked_up);
-
- if (comment_visible) {
- recalc_size("set_comment");
- notify_view_altered();
- }
- }
-
- public void clear_comment() {
- if (comment == null)
- return;
-
- comment = null;
-
- if (comment_visible) {
- recalc_size("clear_comment");
- notify_view_altered();
- }
- }
-
- private void set_comment_visible(bool visible) {
- if (comment_visible == visible)
- return;
-
- comment_visible = visible;
-
- recalc_size("set_comment_visible");
- notify_view_altered();
- }
-
- public void set_tags(Gee.Collection<Tag>? tags,
- Pango.Alignment alignment = Pango.Alignment.LEFT) {
- has_tags = (tags != null && tags.size > 0);
- tag_alignment = alignment;
- string text;
- if (has_tags) {
- this.tags = tags;
- user_visible_tag_list = Tag.make_user_visible_tag_list(tags);
- text = Tag.make_tag_markup_string(user_visible_tag_list);
- } else {
- text = "<small>.</small>";
- }
-
- if (subtitle != null && subtitle.is_set_to(text, true, alignment))
- return;
- subtitle = new CheckerboardItemText(text, alignment, true);
-
- if (subtitle_visible) {
- recalc_size("set_subtitle");
- notify_view_altered();
- }
- }
-
- public void clear_tags() {
- clear_subtitle();
- has_tags = false;
- user_visible_tag_list = null;
- }
-
- public void highlight_user_visible_tag(int index)
- requires (user_visible_tag_list != null) {
- string text = Tag.make_tag_markup_string(user_visible_tag_list, index);
- subtitle = new CheckerboardItemText(text, tag_alignment, true);
-
- if (subtitle_visible)
- notify_view_altered();
- }
-
- public Tag get_user_visible_tag(int index)
- requires (index >= 0 && index < user_visible_tag_list.size) {
- return user_visible_tag_list.get(index);
- }
-
- public Pango.Layout? get_tag_list_layout() {
- return has_tags ? subtitle.get_pango_layout() : null;
- }
-
- public Gdk.Rectangle get_subtitle_allocation() {
- return subtitle.allocation;
- }
-
- public string get_subtitle() {
- return (subtitle != null) ? subtitle.get_text() : "";
- }
-
- public void set_subtitle(string text, bool marked_up = false,
- Pango.Alignment alignment = Pango.Alignment.LEFT) {
- if (subtitle != null && subtitle.is_set_to(text, marked_up, alignment))
- return;
-
- subtitle = new CheckerboardItemText(text, alignment, marked_up);
-
- if (subtitle_visible) {
- recalc_size("set_subtitle");
- notify_view_altered();
- }
- }
-
- public void clear_subtitle() {
- if (subtitle == null)
- return;
-
- subtitle = null;
-
- if (subtitle_visible) {
- recalc_size("clear_subtitle");
- notify_view_altered();
- }
- }
-
- private void set_subtitle_visible(bool visible) {
- if (subtitle_visible == visible)
- return;
-
- subtitle_visible = visible;
-
- recalc_size("set_subtitle_visible");
- notify_view_altered();
- }
-
- public void set_is_cursor(bool is_cursor) {
- this.is_cursor = is_cursor;
- }
-
- public bool get_is_cursor() {
- return is_cursor;
- }
-
- public virtual void handle_mouse_motion(int x, int y, int height, int width) {
-
- }
-
- public virtual void handle_mouse_leave() {
- unbrighten();
- }
-
- public virtual void handle_mouse_enter() {
- brighten();
- }
-
- protected override void notify_membership_changed(DataCollection? collection) {
- bool title_visible = (bool) get_collection_property(PROP_SHOW_TITLES, true);
- bool comment_visible = (bool) get_collection_property(PROP_SHOW_COMMENTS, true);
- bool subtitle_visible = (bool) get_collection_property(PROP_SHOW_SUBTITLES, false);
-
- bool altered = false;
- if (this.title_visible != title_visible) {
- this.title_visible = title_visible;
- altered = true;
- }
-
- if (this.comment_visible != comment_visible) {
- this.comment_visible = comment_visible;
- altered = true;
- }
-
- if (this.subtitle_visible != subtitle_visible) {
- this.subtitle_visible = subtitle_visible;
- altered = true;
- }
-
- if (altered || !requisition.has_area()) {
- recalc_size("notify_membership_changed");
- notify_view_altered();
- }
-
- base.notify_membership_changed(collection);
- }
-
- protected override void notify_collection_property_set(string name, Value? old, Value val) {
- switch (name) {
- case PROP_SHOW_TITLES:
- set_title_visible((bool) val);
- break;
-
- case PROP_SHOW_COMMENTS:
- set_comment_visible((bool) val);
- break;
-
- case PROP_SHOW_SUBTITLES:
- set_subtitle_visible((bool) val);
- break;
- }
-
- base.notify_collection_property_set(name, old, val);
- }
-
- // The alignment point is the coordinate on the y-axis (relative to the top of the
- // CheckerboardItem) which this item should be aligned to. This allows for
- // bottom-alignment along the bottom edge of the thumbnail.
- public int get_alignment_point() {
- return FRAME_WIDTH + BORDER_WIDTH + pixbuf_dim.height;
- }
-
- public virtual void exposed() {
- exposure = true;
- }
-
- public virtual void unexposed() {
- exposure = false;
-
- if (title != null)
- title.clear_pango_layout();
-
- if (comment != null)
- comment.clear_pango_layout();
-
- if (subtitle != null)
- subtitle.clear_pango_layout();
- }
-
- public virtual bool is_exposed() {
- return exposure;
- }
-
- public bool has_image() {
- return pixbuf != null;
- }
-
- public Gdk.Pixbuf? get_image() {
- return pixbuf;
- }
-
- public void set_image(Gdk.Pixbuf pixbuf) {
- this.pixbuf = pixbuf;
- display_pixbuf = pixbuf;
- pixbuf_dim = Dimensions.for_pixbuf(pixbuf);
-
- recalc_size("set_image");
- notify_view_altered();
- }
-
- public void clear_image(Dimensions dim) {
- bool had_image = pixbuf != null;
-
- pixbuf = null;
- display_pixbuf = null;
- pixbuf_dim = dim;
-
- recalc_size("clear_image");
-
- if (had_image)
- notify_view_altered();
- }
-
- public static int get_max_width(int scale) {
- // width is frame width (two sides) + frame padding (two sides) + width of pixbuf (text
- // never wider)
- return (FRAME_WIDTH * 2) + scale;
- }
-
- private void recalc_size(string reason) {
- Dimensions old_requisition = requisition;
-
- // only add in the text heights if they're displayed
- int title_height = (title != null && title_visible)
- ? title.get_height() + LABEL_PADDING : 0;
- int comment_height = (comment != null && comment_visible)
- ? comment.get_height() + LABEL_PADDING : 0;
- int subtitle_height = (subtitle != null && subtitle_visible)
- ? subtitle.get_height() + LABEL_PADDING : 0;
-
- // width is frame width (two sides) + frame padding (two sides) + width of pixbuf
- // (text never wider)
- requisition.width = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2) + pixbuf_dim.width;
-
- // height is frame width (two sides) + frame padding (two sides) + height of pixbuf
- // + height of text + label padding (between pixbuf and text)
- requisition.height = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2)
- + pixbuf_dim.height + title_height + comment_height + subtitle_height;
-
-#if TRACE_REFLOW_ITEMS
- debug("recalc_size %s: %s title_height=%d comment_height=%d subtitle_height=%d requisition=%s",
- get_source().get_name(), reason, title_height, comment_height, subtitle_height,
- requisition.to_string());
-#endif
-
- if (!requisition.approx_equals(old_requisition)) {
-#if TRACE_REFLOW_ITEMS
- debug("recalc_size %s: %s notifying geometry altered", get_source().get_name(), reason);
-#endif
- notify_geometry_altered();
- }
- }
-
- protected static Dimensions get_border_dimensions(Dimensions object_dim, int border_width) {
- Dimensions dimensions = Dimensions();
- dimensions.width = object_dim.width + (border_width * 2);
- dimensions.height = object_dim.height + (border_width * 2);
- return dimensions;
- }
-
- protected static Gdk.Point get_border_origin(Gdk.Point object_origin, int border_width) {
- Gdk.Point origin = Gdk.Point();
- origin.x = object_origin.x - border_width;
- origin.y = object_origin.y - border_width;
- return origin;
- }
-
- protected virtual void paint_shadow(Cairo.Context ctx, Dimensions dimensions, Gdk.Point origin,
- int radius, float initial_alpha) {
- double rgb_all = 0.0;
-
- // top right corner
- paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + radius, rgb_all, radius,
- initial_alpha, -0.5 * Math.PI, 0);
- // bottom right corner
- paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + dimensions.height, rgb_all,
- radius, initial_alpha, 0, 0.5 * Math.PI);
- // bottom left corner
- paint_shadow_in_corner(ctx, origin.x + radius, origin.y + dimensions.height, rgb_all, radius,
- initial_alpha, 0.5 * Math.PI, Math.PI);
-
- // left right
- Cairo.Pattern lr = new Cairo.Pattern.linear(0, origin.y + dimensions.height,
- 0, origin.y + dimensions.height + radius);
- lr.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
- lr.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
- ctx.set_source(lr);
- ctx.rectangle(origin.x + radius, origin.y + dimensions.height, dimensions.width - radius, radius);
- ctx.fill();
-
- // top down
- Cairo.Pattern td = new Cairo.Pattern.linear(origin.x + dimensions.width,
- 0, origin.x + dimensions.width + radius, 0);
- td.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
- td.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
- ctx.set_source(td);
- ctx.rectangle(origin.x + dimensions.width, origin.y + radius,
- radius, dimensions.height - radius);
- ctx.fill();
- }
-
- protected void paint_shadow_in_corner(Cairo.Context ctx, int x, int y,
- double rgb_all, float radius, float initial_alpha, double arc1, double arc2) {
- Cairo.Pattern p = new Cairo.Pattern.radial(x, y, 0, x, y, radius);
- p.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
- p.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0);
- ctx.set_source(p);
- ctx.move_to(x, y);
- ctx.arc(x, y, radius, arc1, arc2);
- ctx.close_path();
- ctx.fill();
- }
-
- protected virtual void paint_border(Cairo.Context ctx, Dimensions object_dimensions,
- Gdk.Point object_origin, int border_width) {
- if (border_width == 1) {
- ctx.rectangle(object_origin.x - border_width, object_origin.y - border_width,
- object_dimensions.width + (border_width * 2),
- object_dimensions.height + (border_width * 2));
- ctx.fill();
- } else {
- Dimensions dimensions = get_border_dimensions(object_dimensions, border_width);
- Gdk.Point origin = get_border_origin(object_origin, border_width);
-
- // amount of rounding needed on corners varies by size of object
- double scale = int.max(object_dimensions.width, object_dimensions.height);
- draw_rounded_corners_filled(ctx, dimensions, origin, 0.25 * scale);
- }
- }
-
- protected virtual void paint_image(Cairo.Context ctx, Gdk.Pixbuf pixbuf, Gdk.Point origin) {
- paint_pixmap_with_background(ctx, pixbuf, origin.x, origin.y);
- }
-
- private int get_selection_border_width(int scale) {
- return ((scale <= ((Thumbnail.MIN_SCALE + Thumbnail.MAX_SCALE) / 3)) ? 5 : 4)
- + BORDER_WIDTH;
- }
-
- protected virtual Gdk.Pixbuf? get_top_left_trinket(int scale) {
- return null;
- }
-
- protected virtual Gdk.Pixbuf? get_top_right_trinket(int scale) {
- return null;
- }
-
- protected virtual Gdk.Pixbuf? get_bottom_left_trinket(int scale) {
- return null;
- }
-
- protected virtual Gdk.Pixbuf? get_bottom_right_trinket(int scale) {
- return null;
- }
-
- public void paint(Gtk.StyleContext style_context, Cairo.Context ctx, Gdk.RGBA bg_color, Gdk.RGBA selected_color,
- Gdk.RGBA? border_color, Gdk.RGBA? focus_color) {
- ctx.save();
- ctx.translate(allocation.x + FRAME_WIDTH,
- allocation.y + FRAME_WIDTH);
- // calc the top-left point of the pixbuf
- Gdk.Point pixbuf_origin = Gdk.Point();
- pixbuf_origin.x = BORDER_WIDTH;
- pixbuf_origin.y = BORDER_WIDTH;
-
- ctx.set_line_width(FRAME_WIDTH);
- ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
- selected_color.alpha);
-
- // draw shadow
- if (border_color != null) {
- ctx.save();
- Dimensions shadow_dim = Dimensions();
- shadow_dim.width = pixbuf_dim.width + BORDER_WIDTH;
- shadow_dim.height = pixbuf_dim.height + BORDER_WIDTH;
- paint_shadow(ctx, shadow_dim, pixbuf_origin, SHADOW_RADIUS, SHADOW_INITIAL_ALPHA);
- ctx.restore();
- }
-
- // draw a border for the cursor with the selection width and normal border color
- if (is_cursor) {
- ctx.save();
- ctx.set_source_rgba(focus_color.red, focus_color.green, focus_color.blue,
- focus_color.alpha);
- paint_border(ctx, pixbuf_dim, pixbuf_origin,
- get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
- ctx.restore();
- }
-
- // draw selection border
- if (is_selected()) {
- // border thickness depends on the size of the thumbnail
- ctx.save();
- paint_border(ctx, pixbuf_dim, pixbuf_origin,
- get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
- ctx.restore();
- }
-
- if (display_pixbuf != null) {
- ctx.save();
- ctx.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha);
- paint_image(ctx, display_pixbuf, pixbuf_origin);
- ctx.restore();
- }
-
- // title and subtitles are LABEL_PADDING below bottom of pixbuf
- int text_y = pixbuf_dim.height + FRAME_WIDTH + LABEL_PADDING;
- if (title != null && title_visible) {
- // get the layout sized so its width is no more than the pixbuf's
- // resize the text width to be no more than the pixbuf's
- title.allocation.x = 0;
- title.allocation.y = text_y;
- title.allocation.width = pixbuf_dim.width;
- title.allocation.height = title.get_height();
- style_context.render_layout(ctx, title.allocation.x, title.allocation.y,
- title.get_pango_layout(pixbuf_dim.width));
-
- text_y += title.get_height() + LABEL_PADDING;
- }
-
- if (comment != null && comment_visible) {
- comment.allocation.x = 0;
- comment.allocation.y = text_y;
- comment.allocation.width = pixbuf_dim.width;
- comment.allocation.height = comment.get_height();
- style_context.render_layout(ctx, comment.allocation.x, comment.allocation.y,
- comment.get_pango_layout(pixbuf_dim.width));
-
- text_y += comment.get_height() + LABEL_PADDING;
- }
-
- if (subtitle != null && subtitle_visible) {
- subtitle.allocation.x = 0;
- subtitle.allocation.y = text_y;
- subtitle.allocation.width = pixbuf_dim.width;
- subtitle.allocation.height = subtitle.get_height();
-
- style_context.render_layout(ctx, subtitle.allocation.x, subtitle.allocation.y,
- subtitle.get_pango_layout(pixbuf_dim.width));
-
- // increment text_y if more text lines follow
- }
-
- ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
- selected_color.alpha);
-
- // draw trinkets last
- Gdk.Pixbuf? trinket = get_bottom_left_trinket(TRINKET_SCALE);
- if (trinket != null) {
- int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
- int y = pixbuf_origin.y + pixbuf_dim.height - trinket.get_height() -
- TRINKET_PADDING;
- Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
- ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
- ctx.fill();
- }
-
- trinket = get_top_left_trinket(TRINKET_SCALE);
- if (trinket != null) {
- int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
- int y = pixbuf_origin.y + TRINKET_PADDING;
- Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
- ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
- ctx.fill();
- }
-
- trinket = get_top_right_trinket(TRINKET_SCALE);
- if (trinket != null) {
- int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width -
- get_horizontal_trinket_offset() - TRINKET_PADDING;
- int y = pixbuf_origin.y + TRINKET_PADDING;
- Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
- ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
- ctx.fill();
- }
-
- trinket = get_bottom_right_trinket(TRINKET_SCALE);
- if (trinket != null) {
- int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width -
- get_horizontal_trinket_offset() - TRINKET_PADDING;
- int y = pixbuf_origin.y + pixbuf_dim.height - trinket.height -
- TRINKET_PADDING;
- Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
- ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
- ctx.fill();
- }
- ctx.restore();
- }
-
- protected void set_horizontal_trinket_offset(int horizontal_trinket_offset) {
- assert(horizontal_trinket_offset >= 0);
- this.horizontal_trinket_offset = horizontal_trinket_offset;
- }
-
- protected int get_horizontal_trinket_offset() {
- return horizontal_trinket_offset;
- }
-
- public void set_grid_coordinates(int col, int row) {
- this.col = col;
- this.row = row;
- }
-
- public int get_column() {
- return col;
- }
-
- public int get_row() {
- return row;
- }
-
- public void brighten() {
- // "should" implies "can" and "didn't already"
- if (brightened != null || pixbuf == null)
- return;
-
- // create a new lightened pixbuf to display
- brightened = pixbuf.copy();
- shift_colors(brightened, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, 0);
-
- display_pixbuf = brightened;
-
- notify_view_altered();
- }
-
-
- public void unbrighten() {
- // "should", "can", "didn't already"
- if (brightened == null || pixbuf == null)
- return;
-
- brightened = null;
-
- // return to the normal image
- display_pixbuf = pixbuf;
-
- notify_view_altered();
- }
-
- public override void visibility_changed(bool visible) {
- // if going from visible to hidden, unbrighten
- if (!visible)
- unbrighten();
-
- base.visibility_changed(visible);
- }
-
- private bool query_tooltip_on_text(CheckerboardItemText text, Gtk.Tooltip tooltip) {
- if (!text.get_pango_layout().is_ellipsized())
- return false;
-
- if (text.is_marked_up())
- tooltip.set_markup(text.get_text());
- else
- tooltip.set_text(text.get_text());
-
- return true;
- }
-
- public bool query_tooltip(int x, int y, Gtk.Tooltip tooltip) {
- if (title != null && title_visible && coord_in_rectangle(x, y, title.allocation))
- return query_tooltip_on_text(title, tooltip);
-
- if (comment != null && comment_visible && coord_in_rectangle(x, y, comment.allocation))
- return query_tooltip_on_text(comment, tooltip);
-
- if (subtitle != null && subtitle_visible && coord_in_rectangle(x, y, subtitle.allocation))
- return query_tooltip_on_text(subtitle, tooltip);
-
- return false;
- }
-}
-
public class CheckerboardLayout : Gtk.DrawingArea {
public const int TOP_PADDING = 16;
public const int BOTTOM_PADDING = 16;
@@ -836,7 +18,7 @@ public class CheckerboardLayout : Gtk.DrawingArea {
// The number of pixels that the scrollbars of Gtk.ScrolledWindows allocate for themselves
// before their final size is computed. This must be taken into account when computing
// the width of this widget. This value was 0 in Gtk+ 2.x but is 1 in Gtk+ 3.x. See
- // ticket #3870 (http://redmine.yorba.org/issues/3870) for more information
+ // ticket #3870 (https://bugzilla.gnome.org/show_bug.cgi?id=717754) for more information
private const int SCROLLBAR_PLACEHOLDER_WIDTH = 1;
private class LayoutRow {
@@ -857,7 +39,6 @@ public class CheckerboardLayout : Gtk.DrawingArea {
private Gee.HashSet<CheckerboardItem> exposed_items = new Gee.HashSet<CheckerboardItem>();
private Gtk.Adjustment hadjustment = null;
private Gtk.Adjustment vadjustment = null;
- private string message = null;
private Gdk.RGBA selected_color;
private Gdk.RGBA unselected_color;
private Gdk.RGBA focus_color;
@@ -963,23 +144,18 @@ public class CheckerboardLayout : Gtk.DrawingArea {
Gtk.Allocation parent_allocation;
parent.get_allocation(out parent_allocation);
- if (message == null) {
- // set the layout's new size to be the same as the parent's width but maintain
- // it's own height
+ // set the layout's new size to be the same as the parent's width but maintain
+ // it's own height
#if TRACE_REFLOW
- debug("on_viewport_resized: due_to_reflow=%s set_size_request %dx%d",
- size_allocate_due_to_reflow.to_string(), parent_allocation.width, req.height);
+ debug("on_viewport_resized: due_to_reflow=%s set_size_request %dx%d",
+ size_allocate_due_to_reflow.to_string(), parent_allocation.width, req.height);
#endif
- // But if the current height is 0, don't request a size yet. Delay
- // it to do_reflow (bgo#766864)
- if (req.height != 0) {
- set_size_request(parent_allocation.width - SCROLLBAR_PLACEHOLDER_WIDTH, req.height);
- }
- } else {
- // set the layout's width and height to always match the parent's
- set_size_request(parent_allocation.width, parent_allocation.height);
+ // But if the current height is 0, don't request a size yet. Delay
+ // it to do_reflow (bgo#766864)
+ if (req.height != 0) {
+ set_size_request(parent_allocation.width - SCROLLBAR_PLACEHOLDER_WIDTH, req.height);
}
-
+
// possible for this widget's size_allocate not to be called, so need to update the page
// rect here
viewport_resized();
@@ -1070,8 +246,6 @@ public class CheckerboardLayout : Gtk.DrawingArea {
private void on_contents_altered(Gee.Iterable<DataObject>? added,
Gee.Iterable<DataObject>? removed) {
- if (added != null)
- message = null;
if (removed != null) {
foreach (DataObject object in removed)
@@ -1142,31 +316,6 @@ public class CheckerboardLayout : Gtk.DrawingArea {
queue_draw();
}
- public void set_message(string? text) {
- if (text == message)
- return;
-
- message = text;
-
- if (text != null) {
- // message is being set, change size to match parent's; if no parent, then the size
- // will be set later when added to the parent
- if (parent != null) {
- Gtk.Allocation parent_allocation;
- parent.get_allocation(out parent_allocation);
-
- set_size_request(parent_allocation.width, parent_allocation.height);
- }
- } else {
- // message is being cleared, layout all the items again
- need_reflow("set_message");
- }
- }
-
- public void unset_message() {
- set_message(null);
- }
-
private void update_visible_page() {
if (hadjustment != null && vadjustment != null)
visible_page = get_adjustment_page(hadjustment, vadjustment);
@@ -1185,7 +334,7 @@ public class CheckerboardLayout : Gtk.DrawingArea {
}
public CheckerboardItem? get_item_at_pixel(double xd, double yd) {
- if (message != null || item_rows == null)
+ if (item_rows == null)
return null;
int x = (int) xd;
@@ -1560,10 +709,6 @@ public class CheckerboardLayout : Gtk.DrawingArea {
private void reflow(string caller) {
reflow_needed = false;
- // if set in message mode, nothing to do here
- if (message != null)
- return;
-
Gtk.Allocation allocation;
get_allocation(out allocation);
@@ -1957,35 +1102,17 @@ public class CheckerboardLayout : Gtk.DrawingArea {
get_allocation(out allocation);
get_style_context().render_background (ctx, 0, 0, allocation.width, allocation.height);
- // watch for message mode
- if (message == null) {
#if TRACE_REFLOW
- debug("draw %s: %s", page_name, rectangle_to_string(visible_page));
+ debug("draw %s: %s", page_name, rectangle_to_string(visible_page));
#endif
-
- if (exposure_dirty)
- expose_items("draw");
-
- // have all items in the exposed area paint themselves
- foreach (CheckerboardItem item in intersection(visible_page)) {
- item.paint(get_style_context(), ctx, bg_color, item.is_selected() ? selected_color : unselected_color,
- border_color, focus_color);
- }
- } else {
- // draw the message in the center of the window
- Pango.Layout pango_layout = create_pango_layout(message);
- int text_width, text_height;
- pango_layout.get_pixel_size(out text_width, out text_height);
-
- get_allocation(out allocation);
-
- int x = allocation.width - text_width;
- x = (x > 0) ? x / 2 : 0;
-
- int y = allocation.height - text_height;
- y = (y > 0) ? y / 2 : 0;
-
- get_style_context().render_layout(ctx, x, y, pango_layout);
+
+ if (exposure_dirty)
+ expose_items("draw");
+
+ // have all items in the exposed area paint themselves
+ foreach (CheckerboardItem item in intersection(visible_page)) {
+ item.paint(get_style_context(), ctx, bg_color, item.is_selected() ? selected_color : unselected_color,
+ border_color, focus_color);
}
bool result = (base.draw != null) ? base.draw(ctx) : true;
@@ -2025,7 +1152,14 @@ public class CheckerboardLayout : Gtk.DrawingArea {
public override bool query_tooltip(int x, int y, bool keyboard_mode, Gtk.Tooltip tooltip) {
CheckerboardItem? item = get_item_at_pixel(x, y);
- return (item != null) ? item.query_tooltip(x, y, tooltip) : false;
+ // Note: X & Y allocations are relative to parents, so we need to query the item's tooltip
+ // relative to its INTERNAL coordinates, otherwise tooltips don't work
+ if (item != null) {
+ item.translate_coordinates(ref x, ref y);
+ return item.query_tooltip(x, y, tooltip);
+ } else {
+ return false;
+ }
}
private void on_colors_changed() {
diff --git a/src/CheckerboardPage.vala b/src/CheckerboardPage.vala
new file mode 100644
index 0000000..24a252a
--- /dev/null
+++ b/src/CheckerboardPage.vala
@@ -0,0 +1,758 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public abstract class CheckerboardPage : Page {
+ private const int AUTOSCROLL_PIXELS = 50;
+ private const int AUTOSCROLL_TICKS_MSEC = 50;
+
+ private CheckerboardLayout layout;
+ private Gtk.Stack stack;
+ private PageMessagePane message_pane;
+ private string item_context_menu_path = null;
+ private string page_context_menu_path = null;
+ private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
+ protected CheckerboardItem anchor = null;
+ protected CheckerboardItem cursor = null;
+ private CheckerboardItem current_hovered_item = null;
+ private bool autoscroll_scheduled = false;
+ private CheckerboardItem activated_item = null;
+ private Gee.ArrayList<CheckerboardItem> previously_selected = null;
+
+ public enum Activator {
+ KEYBOARD,
+ MOUSE
+ }
+
+ public struct KeyboardModifiers {
+ public KeyboardModifiers(Page page) {
+ ctrl_pressed = page.get_ctrl_pressed();
+ alt_pressed = page.get_alt_pressed();
+ shift_pressed = page.get_shift_pressed();
+ super_pressed = page.get_super_pressed();
+ }
+
+ public bool ctrl_pressed;
+ public bool alt_pressed;
+ public bool shift_pressed;
+ public bool super_pressed;
+ }
+
+ protected CheckerboardPage(string page_name) {
+ base (page_name);
+
+ stack = new Gtk.Stack();
+ message_pane = new PageMessagePane();
+
+ layout = new CheckerboardLayout(get_view());
+ layout.set_name(page_name);
+ stack.add_named (layout, "layout");
+ stack.add_named (message_pane, "message");
+ stack.set_visible_child(layout);
+
+ set_event_source(layout);
+
+ set_border_width(0);
+ set_shadow_type(Gtk.ShadowType.NONE);
+
+ viewport.set_border_width(0);
+ viewport.set_shadow_type(Gtk.ShadowType.NONE);
+
+ viewport.add(stack);
+
+ // want to set_adjustments before adding to ScrolledWindow to let our signal handlers
+ // run first ... otherwise, the thumbnails draw late
+ layout.set_adjustments(get_hadjustment(), get_vadjustment());
+
+ add(viewport);
+
+ // need to monitor items going hidden when dealing with anchor/cursor/highlighted items
+ get_view().items_hidden.connect(on_items_hidden);
+ get_view().contents_altered.connect(on_contents_altered);
+ get_view().items_state_changed.connect(on_items_state_changed);
+ get_view().items_visibility_changed.connect(on_items_visibility_changed);
+
+ // scrollbar policy
+ set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+ }
+
+ public void init_item_context_menu(string path) {
+ item_context_menu_path = path;
+ }
+
+ public void init_page_context_menu(string path) {
+ page_context_menu_path = path;
+ }
+
+ public Gtk.Menu? get_context_menu() {
+ // show page context menu if nothing is selected
+ return (get_view().get_selected_count() != 0) ? get_item_context_menu() :
+ get_page_context_menu();
+ }
+
+ private Gtk.Menu item_context_menu;
+ public virtual Gtk.Menu? get_item_context_menu() {
+ if (item_context_menu == null) {
+ var model = this.builder.get_object (item_context_menu_path)
+ as GLib.MenuModel;
+ item_context_menu = new Gtk.Menu.from_model (model);
+ item_context_menu.attach_to_widget (this, null);
+ }
+
+ return item_context_menu;
+ }
+
+ private Gtk.Menu page_context_menu;
+ public override Gtk.Menu? get_page_context_menu() {
+ if (page_context_menu_path == null)
+ return null;
+
+ if (page_context_menu == null) {
+ var model = this.builder.get_object (page_context_menu_path)
+ as GLib.MenuModel;
+ page_context_menu = new Gtk.Menu.from_model (model);
+ page_context_menu.attach_to_widget (this, null);
+ }
+
+ return page_context_menu;
+ }
+
+ protected override bool on_context_keypress() {
+ return popup_context_menu(get_context_menu());
+ }
+
+ protected virtual string get_view_empty_icon() {
+ return "image-x-generic-symbolic";
+ }
+
+ protected virtual string get_view_empty_message() {
+ return _("No photos/videos");
+ }
+
+ protected virtual string get_filter_no_match_message() {
+ return _("No photos/videos found which match the current filter");
+ }
+
+ protected virtual void on_item_activated(CheckerboardItem item, Activator activator,
+ KeyboardModifiers modifiers) {
+ }
+
+ public CheckerboardLayout get_checkerboard_layout() {
+ return layout;
+ }
+
+ // Gets the search view filter for this page.
+ public abstract SearchViewFilter get_search_view_filter();
+
+ public virtual Core.ViewTracker? get_view_tracker() {
+ return null;
+ }
+
+ public override void switching_from() {
+ layout.set_in_view(false);
+ get_search_view_filter().refresh.disconnect(on_view_filter_refresh);
+
+ // unselect everything so selection won't persist after page loses focus
+ get_view().unselect_all();
+
+ base.switching_from();
+ }
+
+ public void scroll_to_item(CheckerboardItem item) {
+ Gtk.Adjustment vadj = get_vadjustment();
+ if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
+ && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == AdjustmentRelation.IN_RANGE))) {
+
+ // scroll to see the new item
+ int top = 0;
+ if (item.allocation.y < vadj.get_value()) {
+ top = item.allocation.y;
+ top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+ } else {
+ top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
+ top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
+ }
+
+ vadj.set_value(top);
+
+ }
+ }
+
+ public override void switched_to() {
+ layout.set_in_view(true);
+ get_search_view_filter().refresh.connect(on_view_filter_refresh);
+ on_view_filter_refresh();
+
+ if (get_view().get_selected_count() > 0) {
+ CheckerboardItem? item = (CheckerboardItem?) get_view().get_selected_at(0);
+
+ // if item is in any way out of view, scroll to it
+ scroll_to_item(item);
+ }
+
+ base.switched_to();
+ }
+
+ private void on_view_filter_refresh() {
+ update_view_filter_message();
+ }
+
+ private void on_contents_altered(Gee.Iterable<DataObject>? added,
+ Gee.Iterable<DataObject>? removed) {
+ update_view_filter_message();
+ }
+
+ private void on_items_state_changed(Gee.Iterable<DataView> changed) {
+ update_view_filter_message();
+ }
+
+ private void on_items_visibility_changed(Gee.Collection<DataView> changed) {
+ update_view_filter_message();
+ }
+
+ private void update_view_filter_message() {
+ if (get_view().are_items_filtered_out() && get_view().get_count() == 0) {
+ set_page_message(get_filter_no_match_message());
+ } else if (get_view().get_count() == 0) {
+ set_page_message(get_view_empty_message());
+ } else {
+ unset_page_message();
+ }
+ }
+
+ public void set_page_message(string message) {
+ message_pane.label.label = message;
+ try {
+ message_pane.icon_image.icon_name = null;
+ message_pane.icon_image.gicon = Icon.new_for_string (get_view_empty_icon());
+ } catch (Error error) {
+ message_pane.icon_image.gicon = null;
+ message_pane.icon_image.icon_name = "image-x-generic-symbolic";
+ }
+ stack.set_visible_child_name ("message");
+ }
+
+ public void unset_page_message() {
+ stack.set_visible_child (layout);
+ }
+
+ public override void set_page_name(string name) {
+ base.set_page_name(name);
+
+ layout.set_name(name);
+ }
+
+ public CheckerboardItem? get_item_at_pixel(double x, double y) {
+ return layout.get_item_at_pixel(x, y);
+ }
+
+ private void on_items_hidden(Gee.Iterable<DataView> hidden) {
+ foreach (DataView view in hidden) {
+ CheckerboardItem item = (CheckerboardItem) view;
+
+ if (anchor == item)
+ anchor = null;
+
+ if (cursor == item)
+ cursor = null;
+
+ if (current_hovered_item == item)
+ current_hovered_item = null;
+ }
+ }
+
+ protected override bool key_press_event(Gdk.EventKey event) {
+ bool handled = true;
+
+ // mask out the modifiers we're interested in
+ uint state = event.state & Gdk.ModifierType.SHIFT_MASK;
+
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "Up":
+ case "KP_Up":
+ move_cursor(CompassPoint.NORTH);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Down":
+ case "KP_Down":
+ move_cursor(CompassPoint.SOUTH);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Left":
+ case "KP_Left":
+ move_cursor(CompassPoint.WEST);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Right":
+ case "KP_Right":
+ move_cursor(CompassPoint.EAST);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Home":
+ case "KP_Home":
+ CheckerboardItem? first = (CheckerboardItem?) get_view().get_first();
+ if (first != null)
+ cursor_to_item(first);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "End":
+ case "KP_End":
+ CheckerboardItem? last = (CheckerboardItem?) get_view().get_last();
+ if (last != null)
+ cursor_to_item(last);
+ select_anchor_to_cursor(state);
+ break;
+
+ case "Return":
+ case "KP_Enter":
+ if (get_view().get_selected_count() == 1)
+ on_item_activated((CheckerboardItem) get_view().get_selected_at(0),
+ Activator.KEYBOARD, KeyboardModifiers(this));
+ else
+ handled = false;
+ break;
+
+ case "space":
+ Marker marker = get_view().mark(layout.get_cursor());
+ get_view().toggle_marked(marker);
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled)
+ return true;
+
+ return (base.key_press_event != null) ? base.key_press_event(event) : true;
+ }
+
+ protected override bool on_left_click(Gdk.EventButton event) {
+ // only interested in single-click and double-clicks for now
+ if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS))
+ return false;
+
+ // mask out the modifiers we're interested in
+ uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
+
+ // use clicks for multiple selection and activation only; single selects are handled by
+ // button release, to allow for multiple items to be selected then dragged ...
+ CheckerboardItem item = get_item_at_pixel(event.x, event.y);
+ if (item != null) {
+ // ... however, there is no dragging if the user clicks on an interactive part of the
+ // CheckerboardItem (e.g. a tag)
+ if (layout.handle_left_click(item, event.x, event.y, event.state))
+ return true;
+
+ switch (state) {
+ case Gdk.ModifierType.CONTROL_MASK:
+ // with only Ctrl pressed, multiple selections are possible ... chosen item
+ // is toggled
+ Marker marker = get_view().mark(item);
+ get_view().toggle_marked(marker);
+
+ if (item.is_selected()) {
+ anchor = item;
+ cursor = item;
+ }
+ break;
+
+ case Gdk.ModifierType.SHIFT_MASK:
+ get_view().unselect_all();
+
+ if (anchor == null)
+ anchor = item;
+
+ select_between_items(anchor, item);
+
+ cursor = item;
+ break;
+
+ case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
+ // Ticket #853 - Make Ctrl + Shift + Mouse Button 1 able to start a new run
+ // of contiguous selected items without unselecting previously-selected items
+ // a la Nautilus.
+ // Same as the case for SHIFT_MASK, but don't unselect anything first.
+ if (anchor == null)
+ anchor = item;
+
+ select_between_items(anchor, item);
+
+ cursor = item;
+ break;
+
+ default:
+ if (event.type == Gdk.EventType.2BUTTON_PRESS) {
+ activated_item = item;
+ } else {
+ // if the user has selected one or more items and is preparing for a drag,
+ // don't want to blindly unselect: if they've clicked on an unselected item
+ // unselect all and select that one; if they've clicked on a previously
+ // selected item, do nothing
+ if (!item.is_selected()) {
+ Marker all = get_view().start_marking();
+ all.mark_many(get_view().get_selected());
+
+ get_view().unselect_and_select_marked(all, get_view().mark(item));
+ }
+ }
+
+ anchor = item;
+ cursor = item;
+ break;
+ }
+ layout.set_cursor(item);
+ } else {
+ // user clicked on "dead" area; only unselect if control is not pressed
+ // do we want similar behavior for shift as well?
+ if (state != Gdk.ModifierType.CONTROL_MASK)
+ get_view().unselect_all();
+
+ // grab previously marked items
+ previously_selected = new Gee.ArrayList<CheckerboardItem>();
+ foreach (DataView view in get_view().get_selected())
+ previously_selected.add((CheckerboardItem) view);
+
+ layout.set_drag_select_origin((int) event.x, (int) event.y);
+
+ return true;
+ }
+
+ // need to determine if the signal should be passed to the DnD handlers
+ // Return true to block the DnD handler, false otherwise
+
+ return get_view().get_selected_count() == 0;
+ }
+
+ protected override bool on_left_released(Gdk.EventButton event) {
+ previously_selected = null;
+
+ // if drag-selecting, stop here and do nothing else
+ if (layout.is_drag_select_active()) {
+ layout.clear_drag_select();
+ anchor = cursor;
+
+ return true;
+ }
+
+ // only interested in non-modified button releases
+ if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
+ return false;
+
+ // if the item was activated in the double-click, report it now
+ if (activated_item != null) {
+ on_item_activated(activated_item, Activator.MOUSE, KeyboardModifiers(this));
+ activated_item = null;
+
+ return true;
+ }
+
+ CheckerboardItem item = get_item_at_pixel(event.x, event.y);
+ if (item == null) {
+ // released button on "dead" area
+ return true;
+ }
+
+ if (cursor != item) {
+ // user released mouse button after moving it off the initial item, or moved from dead
+ // space onto one. either way, unselect everything
+ get_view().unselect_all();
+ } else {
+ // the idea is, if a user single-clicks on an item with no modifiers, then all other items
+ // should be deselected, however, if they single-click in order to drag one or more items,
+ // they should remain selected, hence performing this here rather than on_left_click
+ // (item may not be selected if an unimplemented modifier key was used)
+ if (item.is_selected())
+ get_view().unselect_all_but(item);
+ }
+
+ return true;
+ }
+
+ protected override bool on_right_click(Gdk.EventButton event) {
+ // only interested in single-clicks for now
+ if (event.type != Gdk.EventType.BUTTON_PRESS)
+ return false;
+
+ // get what's right-clicked upon
+ CheckerboardItem item = get_item_at_pixel(event.x, event.y);
+ if (item != null) {
+ // mask out the modifiers we're interested in
+ switch (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) {
+ case Gdk.ModifierType.CONTROL_MASK:
+ // chosen item is toggled
+ Marker marker = get_view().mark(item);
+ get_view().toggle_marked(marker);
+ break;
+
+ case Gdk.ModifierType.SHIFT_MASK:
+ // TODO
+ break;
+
+ case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
+ // TODO
+ break;
+
+ default:
+ // if the item is already selected, proceed; if item is not selected, a bare right
+ // click unselects everything else but it
+ if (!item.is_selected()) {
+ Marker all = get_view().start_marking();
+ all.mark_many(get_view().get_selected());
+
+ get_view().unselect_and_select_marked(all, get_view().mark(item));
+ }
+ break;
+ }
+ } else {
+ // clicked in "dead" space, unselect everything
+ get_view().unselect_all();
+ }
+
+ Gtk.Menu context_menu = get_context_menu();
+ return popup_context_menu(context_menu, event);
+ }
+
+ protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) {
+ if (item != null)
+ layout.handle_mouse_motion(item, x, y, mask);
+
+ // if hovering over the last hovered item, or both are null (nothing highlighted and
+ // hovering over empty space), do nothing
+ if (item == current_hovered_item)
+ return true;
+
+ // either something new is highlighted or now hovering over empty space, so dim old item
+ if (current_hovered_item != null) {
+ current_hovered_item.handle_mouse_leave();
+ current_hovered_item = null;
+ }
+
+ // if over empty space, done
+ if (item == null)
+ return true;
+
+ // brighten the new item
+ current_hovered_item = item;
+ current_hovered_item.handle_mouse_enter();
+
+ return true;
+ }
+
+ protected override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
+ // report what item the mouse is hovering over
+ if (!on_mouse_over(get_item_at_pixel(x, y), x, y, mask))
+ return false;
+
+ // go no further if not drag-selecting
+ if (!layout.is_drag_select_active())
+ return false;
+
+ // set the new endpoint of the drag selection
+ layout.set_drag_select_endpoint(x, y);
+
+ updated_selection_band();
+
+ // if out of bounds, schedule a check to auto-scroll the viewport
+ if (!autoscroll_scheduled
+ && get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) {
+ Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll);
+ autoscroll_scheduled = true;
+ }
+
+ // return true to stop a potential drag-and-drop operation
+ return true;
+ }
+
+ private void updated_selection_band() {
+ assert(layout.is_drag_select_active());
+
+ // get all items inside the selection
+ Gee.List<CheckerboardItem>? intersection = layout.items_in_selection_band();
+ if (intersection == null)
+ return;
+
+ Marker to_unselect = get_view().start_marking();
+ Marker to_select = get_view().start_marking();
+
+ // mark all selected items to be unselected
+ to_unselect.mark_many(get_view().get_selected());
+
+ // except for the items that were selected before the drag began
+ assert(previously_selected != null);
+ to_unselect.unmark_many(previously_selected);
+ to_select.mark_many(previously_selected);
+
+ // toggle selection on everything in the intersection and update the cursor
+ cursor = null;
+
+ foreach (CheckerboardItem item in intersection) {
+ if (to_select.toggle(item))
+ to_unselect.unmark(item);
+ else
+ to_unselect.mark(item);
+
+ if (cursor == null)
+ cursor = item;
+ }
+
+ get_view().select_marked(to_select);
+ get_view().unselect_marked(to_unselect);
+ }
+
+ private bool selection_autoscroll() {
+ if (!layout.is_drag_select_active()) {
+ autoscroll_scheduled = false;
+
+ return false;
+ }
+
+ // as the viewport never scrolls horizontally, only interested in vertical
+ Gtk.Adjustment vadj = get_vadjustment();
+
+ int x, y;
+ Gdk.ModifierType mask;
+ get_event_source_pointer(out x, out y, out mask);
+
+ int new_value = (int) vadj.get_value();
+ switch (get_adjustment_relation(vadj, y)) {
+ case AdjustmentRelation.BELOW:
+ // pointer above window, scroll up
+ new_value -= AUTOSCROLL_PIXELS;
+ layout.set_drag_select_endpoint(x, new_value);
+ break;
+
+ case AdjustmentRelation.ABOVE:
+ // pointer below window, scroll down, extend selection to bottom of page
+ new_value += AUTOSCROLL_PIXELS;
+ layout.set_drag_select_endpoint(x, new_value + (int) vadj.get_page_size());
+ break;
+
+ case AdjustmentRelation.IN_RANGE:
+ autoscroll_scheduled = false;
+
+ return false;
+
+ default:
+ warn_if_reached();
+ break;
+ }
+
+ // It appears that in GTK+ 2.18, the adjustment is not clamped the way it was in 2.16.
+ // This may have to do with how adjustments are different w/ scrollbars, that they're upper
+ // clamp is upper - page_size ... either way, enforce these limits here
+ vadj.set_value(new_value.clamp((int) vadj.get_lower(),
+ (int) vadj.get_upper() - (int) vadj.get_page_size()));
+
+ updated_selection_band();
+
+ return true;
+ }
+
+ public void cursor_to_item(CheckerboardItem item) {
+ assert(get_view().contains(item));
+
+ cursor = item;
+
+ if (!get_ctrl_pressed()) {
+ get_view().unselect_all();
+ Marker marker = get_view().mark(item);
+ get_view().select_marked(marker);
+ }
+ layout.set_cursor(item);
+ scroll_to_item(item);
+ }
+
+ public void move_cursor(CompassPoint point) {
+ // if no items, nothing to do
+ if (get_view().get_count() == 0)
+ return;
+
+ // if there is no better starting point, simply select the first and exit
+ // The right half of the or is related to Bug #732334, the cursor might be non-null and still not contained in
+ // the view, if the user dragged a full screen Photo off screen
+ if (cursor == null && layout.get_cursor() == null || cursor != null && !get_view().contains(cursor)) {
+ CheckerboardItem item = layout.get_item_at_coordinate(0, 0);
+ cursor_to_item(item);
+ anchor = item;
+
+ return;
+ }
+
+ if (cursor == null) {
+ cursor = layout.get_cursor() as CheckerboardItem;
+ }
+
+ // move the cursor relative to the "first" item
+ CheckerboardItem? item = layout.get_item_relative_to(cursor, point);
+ if (item != null)
+ cursor_to_item(item);
+ }
+
+ public void set_cursor(CheckerboardItem item) {
+ Marker marker = get_view().mark(item);
+ get_view().select_marked(marker);
+
+ cursor = item;
+ anchor = item;
+ }
+
+ public void select_between_items(CheckerboardItem item_start, CheckerboardItem item_end) {
+ Marker marker = get_view().start_marking();
+
+ bool passed_start = false;
+ bool passed_end = false;
+
+ foreach (DataObject object in get_view().get_all()) {
+ CheckerboardItem item = (CheckerboardItem) object;
+
+ if (item_start == item)
+ passed_start = true;
+
+ if (item_end == item)
+ passed_end = true;
+
+ if (passed_start || passed_end)
+ marker.mark((DataView) object);
+
+ if (passed_start && passed_end)
+ break;
+ }
+
+ get_view().select_marked(marker);
+ }
+
+ public void select_anchor_to_cursor(uint state) {
+ if (cursor == null || anchor == null)
+ return;
+
+ if (state == Gdk.ModifierType.SHIFT_MASK) {
+ get_view().unselect_all();
+ select_between_items(anchor, cursor);
+ } else {
+ anchor = cursor;
+ }
+ }
+
+ protected virtual void set_display_titles(bool display) {
+ get_view().freeze_notifications();
+ get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, display);
+ get_view().thaw_notifications();
+ }
+
+ protected virtual void set_display_comments(bool display) {
+ get_view().freeze_notifications();
+ get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, display);
+ get_view().thaw_notifications();
+ }
+}
+
+
diff --git a/src/Commands.vala b/src/Commands.vala
index 589ae38..76aecb4 100644
--- a/src/Commands.vala
+++ b/src/Commands.vala
@@ -1316,8 +1316,17 @@ public class AdjustDateTimePhotoCommand : SingleDataSourceCommand {
this.modify_original = modify_original;
}
+ private DateTime get_base_time() {
+ var exposure_time = dateable.get_exposure_time();
+ if (exposure_time == null) {
+ exposure_time = new DateTime.from_unix_utc(0);
+ }
+
+ return exposure_time;
+ }
+
public override void execute() {
- set_time(dateable, dateable.get_exposure_time() + (time_t) time_shift);
+ set_time(dateable, get_base_time().add_seconds(time_shift));
prev_event = dateable.get_event();
@@ -1333,12 +1342,12 @@ public class AdjustDateTimePhotoCommand : SingleDataSourceCommand {
}
public override void undo() {
- set_time(dateable, dateable.get_exposure_time() - (time_t) time_shift);
+ set_time(dateable, get_base_time().add_seconds(-1 * time_shift));
dateable.set_event(prev_event);
}
- private void set_time(Dateable dateable, time_t exposure_time) {
+ private void set_time(Dateable dateable, DateTime exposure_time) {
if (modify_original && dateable is Photo) {
try {
((Photo)dateable).set_exposure_time_persistent(exposure_time);
@@ -1358,8 +1367,8 @@ public class AdjustDateTimePhotosCommand : MultipleDataSourceCommand {
private Gee.Map<Dateable, Event?> prev_events;
// used when photos are batch changed instead of shifted uniformly
- private time_t? new_time = null;
- private Gee.HashMap<Dateable, time_t?> old_times;
+ private DateTime? new_time = null;
+ private Gee.HashMap<Dateable, DateTime?> old_times;
private Gee.ArrayList<Dateable> error_list;
public AdjustDateTimePhotosCommand(Gee.Iterable<DataView> iter, int64 time_shift,
@@ -1377,16 +1386,24 @@ public class AdjustDateTimePhotosCommand : MultipleDataSourceCommand {
// this should be replaced by a first function when we migrate to Gee's List
foreach (DataView view in iter) {
- prev_events.set(view.get_source() as Dateable, (view.get_source() as MediaSource).get_event());
+ prev_events.set(view.get_source() as Dateable, ((MediaSource) view.get_source()).get_event());
if (new_time == null) {
- new_time = ((Dateable) view.get_source()).get_exposure_time() +
- (time_t) time_shift;
+ new_time = get_base_time((Dateable)view.get_source()).add_seconds(time_shift);
break;
}
}
- old_times = new Gee.HashMap<Dateable, time_t?>();
+ old_times = new Gee.HashMap<Dateable, DateTime?>();
+ }
+
+ private DateTime get_base_time(Dateable dateable) {
+ var exposure_time = dateable.get_exposure_time();
+ if (exposure_time == null) {
+ exposure_time = new DateTime.from_unix_utc(0);
+ }
+
+ return exposure_time;
}
public override void execute() {
@@ -1425,7 +1442,7 @@ public class AdjustDateTimePhotosCommand : MultipleDataSourceCommand {
}
}
- private void set_time(Dateable dateable, time_t exposure_time) {
+ private void set_time(Dateable dateable, DateTime exposure_time) {
// set_exposure_time_persistent wouldn't work on videos,
// since we can't actually write them from inside shotwell,
// so check whether we're working on a Photo or a Video
@@ -1445,8 +1462,8 @@ public class AdjustDateTimePhotosCommand : MultipleDataSourceCommand {
public override void execute_on_source(DataSource source) {
Dateable dateable = ((Dateable) source);
- if (keep_relativity && dateable.get_exposure_time() != 0) {
- set_time(dateable, dateable.get_exposure_time() + (time_t) time_shift);
+ if (keep_relativity && dateable.get_exposure_time() != null) {
+ set_time(dateable, dateable.get_exposure_time().add_seconds(time_shift));
} else {
old_times.set(dateable, dateable.get_exposure_time());
set_time(dateable, new_time);
@@ -1470,10 +1487,10 @@ public class AdjustDateTimePhotosCommand : MultipleDataSourceCommand {
set_time(photo, old_times.get(photo));
old_times.unset(photo);
} else {
- set_time(photo, photo.get_exposure_time() - (time_t) time_shift);
+ set_time(photo, photo.get_exposure_time().add_seconds(-1 * time_shift));
}
- (source as MediaSource).set_event(prev_events.get(source as Dateable));
+ ((MediaSource) source).set_event(prev_events.get(source as Dateable));
}
}
@@ -2165,8 +2182,6 @@ public class ModifyTagsCommand : SingleDataSourceCommand {
}
foreach (string path in new_paths) {
- assert(Tag.global.exists(path));
-
SourceProxy proxy = Tag.for_path(path).get_proxy();
to_add.add(proxy);
proxy.broken.connect(on_proxy_broken);
@@ -2541,7 +2556,8 @@ public class RemoveFacesFromPhotosCommand : SimpleProxyableCommand {
face.attach_many(map_source_geometry.keys);
foreach (Gee.Map.Entry<MediaSource, string> entry in map_source_geometry.entries)
- FaceLocation.create(face.get_face_id(), ((Photo) entry.key).get_photo_id(), entry.value);
+ FaceLocation.create(face.get_face_id(), ((Photo) entry.key).get_photo_id(),
+ { entry.value, null });
}
private void on_source_destroyed(DataSource source) {
@@ -2572,6 +2588,26 @@ public class RenameFaceCommand : SimpleProxyableCommand {
}
}
+public class SetFaceRefCommand : SimpleProxyableCommand {
+ private FaceLocation face_loc;
+
+ public SetFaceRefCommand(Face face, MediaSource source) {
+ base (face, Resources.set_face_from_photo_label(face.get_name()), face.get_name());
+ Gee.Map<FaceID?, FaceLocation>? face_loc_map = FaceLocation.get_locations_by_photo((Photo)source);
+ face_loc = face_loc_map.get(face.get_face_id());
+ }
+
+ protected override void execute_on_source(DataSource source) {
+ if (!((Face) source).set_reference(face_loc))
+ AppWindow.error_message(Resources.set_face_from_photo_error());
+ }
+
+ protected override void undo_on_source(DataSource source) {
+ //if (!((Face) source).rename(old_name))
+ // AppWindow.error_message(Resources.rename_face_exists_message(old_name));
+ }
+}
+
public class DeleteFaceCommand : SimpleProxyableCommand {
private Gee.Map<PhotoID?, string> photo_geometry_map = new Gee.HashMap<PhotoID?, string>
((Gee.HashDataFunc)FaceLocation.photo_id_hash, (Gee.EqualDataFunc)FaceLocation.photo_ids_equal);
@@ -2607,7 +2643,8 @@ public class DeleteFaceCommand : SimpleProxyableCommand {
Face face = (Face) source;
face.attach(photo);
- FaceLocation.create(face.get_face_id(), entry.key, entry.value);
+ FaceLocation.create(face.get_face_id(), entry.key,
+ { entry.value, null });
}
}
}
@@ -2617,10 +2654,10 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
private MediaSource media;
private Gee.ArrayList<SourceProxy> to_add = new Gee.ArrayList<SourceProxy>();
private Gee.ArrayList<SourceProxy> to_remove = new Gee.ArrayList<SourceProxy>();
- private Gee.Map<SourceProxy, string> to_update = new Gee.HashMap<SourceProxy, string>();
- private Gee.Map<SourceProxy, string> geometries = new Gee.HashMap<SourceProxy, string>();
+ private Gee.Map<SourceProxy, FaceLocationData?> to_update = new Gee.HashMap<SourceProxy, FaceLocationData?>();
+ private Gee.Map<SourceProxy, FaceLocationData?> geometries = new Gee.HashMap<SourceProxy, FaceLocationData?>();
- public ModifyFacesCommand(MediaSource media, Gee.Map<Face, string> new_face_list) {
+ public ModifyFacesCommand(MediaSource media, Gee.Map<Face, FaceLocationData?> new_face_list) {
base (media, Resources.MODIFY_FACES_LABEL, "");
this.media = media;
@@ -2639,13 +2676,13 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
FaceLocation.get_face_location(face.get_face_id(), ((Photo) media).get_photo_id());
assert(face_location != null);
- geometries.set(proxy, face_location.get_serialized_geometry());
+ geometries.set(proxy, face_location.get_face_data());
}
}
}
// Add any face that's in the new list but not the original
- foreach (Gee.Map.Entry<Face, string> entry in new_face_list.entries) {
+ foreach (Gee.Map.Entry<Face, FaceLocationData?> entry in new_face_list.entries) {
if (original_faces == null || !original_faces.contains(entry.key)) {
SourceProxy proxy = entry.key.get_proxy();
@@ -2661,13 +2698,13 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
assert(face_location != null);
string old_geometry = face_location.get_serialized_geometry();
- if (old_geometry != entry.value) {
+ if (old_geometry != entry.value.geometry) {
SourceProxy proxy = entry.key.get_proxy();
to_update.set(proxy, entry.value);
proxy.broken.connect(on_proxy_broken);
- geometries.set(proxy, old_geometry);
+ geometries.set(proxy, face_location.get_face_data());
}
}
}
@@ -2694,7 +2731,7 @@ public class ModifyFacesCommand : SingleDataSourceCommand {
foreach (SourceProxy proxy in to_remove)
((Face) proxy.get_source()).detach(media);
- foreach (Gee.Map.Entry<SourceProxy, string> entry in to_update.entries) {
+ foreach (Gee.Map.Entry<SourceProxy, FaceLocationData?> entry in to_update.entries) {
Face face = (Face) entry.key.get_source();
FaceLocation.create(face.get_face_id(), ((Photo) media).get_photo_id(), entry.value);
}
diff --git a/src/Debug.vala b/src/Debug.vala
index f159b0d..799a94f 100644
--- a/src/Debug.vala
+++ b/src/Debug.vala
@@ -33,7 +33,7 @@ namespace Debug {
string log_file_error_msg = null;
- // logging to disk is currently off for viewer more; see http://trac.yorba.org/ticket/2078
+ // logging to disk is currently off for viewer more; see https://bugzilla.gnome.org/show_bug.cgi?id=716474
File? log_file = (log_app_version_prefix == LIBRARY_PREFIX) ? AppDirs.get_log_file() : null;
if(log_file != null) {
File log_dir = log_file.get_parent();
@@ -104,11 +104,10 @@ namespace Debug {
}
private void log(FileStream stream, string prefix, string message) {
- time_t now = time_t();
stream.printf("%s %d %s [%s] %s\n",
log_app_version_prefix,
Posix.getpid(),
- Time.local(now).to_string(),
+ new DateTime.now_local().format("%F %T"),
prefix,
message
);
diff --git a/src/DesktopIntegration.vala b/src/DesktopIntegration.vala
index 68d1ec6..754d9a1 100644
--- a/src/DesktopIntegration.vala
+++ b/src/DesktopIntegration.vala
@@ -9,7 +9,6 @@ namespace DesktopIntegration {
private const string DESKTOP_SLIDESHOW_XML_FILENAME = "wallpaper.xml";
private int init_count = 0;
-private bool send_to_installed = false;
private ExporterUI send_to_exporter = null;
private ExporterUI desktop_slideshow_exporter = null;
private double desktop_slideshow_transition = 0.0;
@@ -113,6 +112,7 @@ public async void files_send_to(File[] files) {
yield portal.compose_email(parent, {null}, null, null,
_("Send files per Mail: ") + file_names.str, null, file_paths, Xdp.EmailFlags.NONE, null);
} catch (Error e){
+ // Translators: The first %s is the name of the file, the second %s is the reason why it could not be sent
AppWindow.error_message(_("Unable to send file %s, %s").printf(
file_names.str, e.message));
}
diff --git a/src/Dialogs.vala b/src/Dialogs.vala
index b1f6e08..70dc76d 100644
--- a/src/Dialogs.vala
+++ b/src/Dialogs.vala
@@ -5,7 +5,7 @@
*/
// namespace for future migration of AppWindow alert and other question dialogs into single
-// place: http://trac.yorba.org/ticket/3452
+// place: https://bugzilla.gnome.org/show_bug.cgi?id=717659
namespace Dialogs {
public bool confirm_delete_tag(Tag tag) {
@@ -67,7 +67,7 @@ public File? choose_file(string current_file_basename) {
current_export_dir = File.new_for_path(Environment.get_home_dir());
string file_chooser_title = VideoReader.is_supported_video_filename(current_file_basename) ?
- _("Export Video") : _("Export Photo");
+ _("Export Video") : GLib.dpgettext2 (null, "Dialog Title", "Export Photo");
var chooser = new Gtk.FileChooserNative(file_chooser_title,
AppWindow.get_instance(), Gtk.FileChooserAction.SAVE, Resources.SAVE_LABEL, Resources.CANCEL_LABEL);
@@ -214,7 +214,7 @@ public string create_result_report_from_manifest(ImportManifest manifest) {
StringBuilder builder = new StringBuilder();
string header = _("Import Results Report") + " (Shotwell " + Resources.APP_VERSION + " @ " +
- TimeVal().to_iso8601() + ")\n\n";
+ new DateTime.now_utc().format_iso8601() + ")\n\n";
builder.append(header);
string subhead = (ngettext("Attempted to import %d file.", "Attempted to import %d files.",
@@ -817,8 +817,14 @@ public void multiple_object_error_dialog(Gee.ArrayList<DataObject> objects, stri
public abstract class TagsDialog : TextEntryDialogMediator {
protected TagsDialog(string title, string label, string? initial_text = null) {
- base (title, label, initial_text, HierarchicalTagIndex.get_global_index().get_all_tags(),
- ",");
+ var all = new Gee.ArrayList<string>();
+ all.add_all(HierarchicalTagIndex.get_global_index().get_all_tags());
+ var paths = HierarchicalTagIndex.get_global_index().get_all_paths();
+ foreach (var p in paths) {
+ if (p.has_prefix("/")) all.add(p);
+ }
+
+ base (title, label, initial_text, all, ",");
}
}
@@ -840,14 +846,24 @@ public class AddTagsDialog : TagsDialog {
}
protected override bool on_modify_validate(string text) {
- if (text.contains(Tag.PATH_SEPARATOR_STRING))
- return false;
-
- // Can't simply call Tag.prep_tag_names().length because of this bug:
- // https://bugzilla.gnome.org/show_bug.cgi?id=602208
string[] names = Tag.prep_tag_names(text.split(","));
-
- return names.length > 0;
+ if (names.length == 0)
+ return false;
+
+ // If allowing hierarchies, they have to start with a "/"
+ for (int i = 0; i < names.length; i++) {
+ if (names[i].contains(Tag.PATH_SEPARATOR_STRING) && !names[i].strip().has_prefix(Tag.PATH_SEPARATOR_STRING))
+ return false;
+
+ if (names[i].strip().has_prefix(Tag.PATH_SEPARATOR_STRING) && names[i].strip().length == 1)
+ return false;
+
+ if (names[i].strip().contains(Tag.PATH_SEPARATOR_STRING + Tag.PATH_SEPARATOR_STRING)) {
+ return false;
+ }
+ }
+
+ return true;
}
}
@@ -904,7 +920,26 @@ public class ModifyTagsDialog : TagsDialog {
}
protected override bool on_modify_validate(string text) {
- return (!text.contains(Tag.PATH_SEPARATOR_STRING));
+ string[] names = Tag.prep_tag_names(text.split(","));
+ if (names.length == 0)
+ return false;
+
+ // If allowing hierarchies, they have to start with a "/"
+ for (int i = 0; i < names.length; i++) {
+ if (names[i].contains(Tag.PATH_SEPARATOR_STRING) && !names[i].strip().has_prefix(Tag.PATH_SEPARATOR_STRING)) {
+ return false;
+ }
+
+ if (names[i].strip().has_prefix(Tag.PATH_SEPARATOR_STRING) && names[i].strip().length == 1)
+ return false;
+
+ if (names[i].strip().contains(Tag.PATH_SEPARATOR_STRING + Tag.PATH_SEPARATOR_STRING)) {
+ return false;
+ }
+ }
+
+ return true;
+
}
}
@@ -930,7 +965,7 @@ public Gtk.ResponseType copy_files_dialog() {
public void remove_photos_from_library(Gee.Collection<LibraryPhoto> photos) {
remove_from_app(photos, _("Remove From Library"),
- (photos.size == 1) ? _("Removing Photo From Library") : _("Removing Photos From Library"));
+ ngettext("Removing Photo From Library", "Removing Photos From Library", photos.size));
}
public void remove_from_app(Gee.Collection<MediaSource> sources, string dialog_title,
diff --git a/src/Dimensions.vala b/src/Dimensions.vala
index 3b4163c..32bf32c 100644
--- a/src/Dimensions.vala
+++ b/src/Dimensions.vala
@@ -59,8 +59,9 @@ public struct Dimensions {
public static Dimensions for_widget_allocation(Gtk.Widget widget) {
Gtk.Allocation allocation;
widget.get_allocation(out allocation);
+ var scale = widget.get_scale_factor();
- return Dimensions(allocation.width, allocation.height);
+ return Dimensions(allocation.width * scale, allocation.height * scale);
}
public static Dimensions for_rectangle(Gdk.Rectangle rect) {
diff --git a/src/DirectoryMonitor.vala b/src/DirectoryMonitor.vala
index a37b124..19992dd 100644
--- a/src/DirectoryMonitor.vala
+++ b/src/DirectoryMonitor.vala
@@ -60,7 +60,7 @@
public class DirectoryMonitor : Object {
public const int DEFAULT_PRIORITY = Priority.LOW;
public const FileQueryInfoFlags DIR_INFO_FLAGS = FileQueryInfoFlags.NONE;
- public const FileQueryInfoFlags FILE_INFO_FLAGS = FileQueryInfoFlags.NOFOLLOW_SYMLINKS;
+ public const FileQueryInfoFlags FILE_INFO_FLAGS = FileQueryInfoFlags.NONE;
// when using UNKNOWN_FILE_FLAGS, check if the resulting FileInfo's symlink status matches
// symlink support for files and directories by calling is_file_symlink_supported().
@@ -290,7 +290,7 @@ public class DirectoryMonitor : Object {
// get all the interesting matchable items from the supplied FileInfo
int64 match_size = match.get_size();
- TimeVal match_time = match.get_modification_time();
+ var match_time = match.get_modification_date_time();
foreach (File file in map.keys) {
FileInfo info = map.get(file);
@@ -303,9 +303,9 @@ public class DirectoryMonitor : Object {
if (match_size != info.get_size())
continue;
- TimeVal time = info.get_modification_time();
+ var time = info.get_modification_date_time();
- if (time.tv_sec != match_time.tv_sec)
+ if (!time.equal(match_time))
continue;
return file;
diff --git a/src/DragAndDropHandler.vala b/src/DragAndDropHandler.vala
new file mode 100644
index 0000000..ece6d9d
--- /dev/null
+++ b/src/DragAndDropHandler.vala
@@ -0,0 +1,182 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+//
+// DragAndDropHandler attaches signals to a Page to properly handle drag-and-drop requests for the
+// Page as a DnD Source. (DnD Destination handling is handled by the appropriate AppWindow, i.e.
+// LibraryWindow and DirectWindow). Assumes the Page's ViewCollection holds MediaSources.
+//
+public class DragAndDropHandler {
+ private enum TargetType {
+ XDS,
+ MEDIA_LIST
+ }
+
+ private const Gtk.TargetEntry[] SOURCE_TARGET_ENTRIES = {
+ { "XdndDirectSave0", Gtk.TargetFlags.OTHER_APP, TargetType.XDS },
+ { "shotwell/media-id-atom", Gtk.TargetFlags.SAME_APP, TargetType.MEDIA_LIST }
+ };
+
+ private static Gdk.Atom? XDS_ATOM = null;
+ private static Gdk.Atom? TEXT_ATOM = null;
+ private static uint8[]? XDS_FAKE_TARGET = null;
+
+ private weak Page page;
+ private Gtk.Widget event_source;
+ private File? drag_destination = null;
+ private ExporterUI exporter = null;
+
+ public DragAndDropHandler(Page page) {
+ this.page = page;
+ this.event_source = page.get_event_source();
+ assert(event_source != null);
+ assert(event_source.get_has_window());
+
+ // Need to do this because static member variables are not properly handled
+ if (XDS_ATOM == null)
+ XDS_ATOM = Gdk.Atom.intern_static_string("XdndDirectSave0");
+
+ if (TEXT_ATOM == null)
+ TEXT_ATOM = Gdk.Atom.intern_static_string("text/plain");
+
+ if (XDS_FAKE_TARGET == null)
+ XDS_FAKE_TARGET = string_to_uchar_array("shotwell.txt");
+
+ // register what's available on this DnD Source
+ Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES,
+ Gdk.DragAction.COPY);
+
+ // attach to the event source's DnD signals, not the Page's, which is a NO_WINDOW widget
+ // and does not emit them
+ event_source.drag_begin.connect(on_drag_begin);
+ event_source.drag_data_get.connect(on_drag_data_get);
+ event_source.drag_end.connect(on_drag_end);
+ event_source.drag_failed.connect(on_drag_failed);
+ }
+
+ ~DragAndDropHandler() {
+ if (event_source != null) {
+ event_source.drag_begin.disconnect(on_drag_begin);
+ event_source.drag_data_get.disconnect(on_drag_data_get);
+ event_source.drag_end.disconnect(on_drag_end);
+ event_source.drag_failed.disconnect(on_drag_failed);
+ }
+
+ page = null;
+ event_source = null;
+ }
+
+ private void on_drag_begin(Gdk.DragContext context) {
+ debug("on_drag_begin (%s)", page.get_page_name());
+
+ if (page == null || page.get_view().get_selected_count() == 0 || exporter != null)
+ return;
+
+ drag_destination = null;
+
+ // use the first media item as the icon
+ ThumbnailSource thumb = (ThumbnailSource) page.get_view().get_selected_at(0).get_source();
+
+ try {
+ Gdk.Pixbuf icon = thumb.get_thumbnail(AppWindow.DND_ICON_SCALE);
+ Gtk.drag_source_set_icon_pixbuf(event_source, icon);
+ } catch (Error err) {
+ warning("Unable to fetch icon for drag-and-drop from %s: %s", thumb.to_string(),
+ err.message);
+ }
+
+ // set the XDS property to indicate an XDS save is available
+ Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
+ XDS_FAKE_TARGET, 1);
+ }
+
+ private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
+ uint target_type, uint time) {
+ debug("on_drag_data_get (%s)", page.get_page_name());
+
+ if (page == null || page.get_view().get_selected_count() == 0)
+ return;
+
+ switch (target_type) {
+ case TargetType.XDS:
+ // Fetch the XDS property that has been set with the destination path
+ uchar[] data = new uchar[4096];
+ Gdk.Atom actual_type;
+ int actual_format = 0;
+ bool fetched = Gdk.property_get(context.get_source_window(), XDS_ATOM, TEXT_ATOM,
+ 0, data.length, 0, out actual_type, out actual_format, out data);
+
+ // the destination path is actually for our XDS_FAKE_TARGET, use its parent
+ // to determine where the file(s) should go
+ if (fetched && data != null && data.length > 0)
+ drag_destination = File.new_for_uri(uchar_array_to_string(data)).get_parent();
+
+ debug("on_drag_data_get (%s): %s", page.get_page_name(),
+ (drag_destination != null) ? drag_destination.get_path() : "(no path)");
+
+ // Set the property to "S" for Success or "E" for Error
+ selection_data.set(XDS_ATOM, 8,
+ string_to_uchar_array((drag_destination != null) ? "S" : "E"));
+ break;
+
+ case TargetType.MEDIA_LIST:
+ Gee.Collection<MediaSource> sources =
+ (Gee.Collection<MediaSource>) page.get_view().get_selected_sources();
+
+ // convert the selected media sources to Gdk.Atom-encoded sourceID strings for
+ // internal drag-and-drop
+ selection_data.set(Gdk.Atom.intern_static_string("SourceIDAtom"), (int) sizeof(Gdk.Atom),
+ serialize_media_sources(sources));
+ break;
+
+ default:
+ warning("on_drag_data_get (%s): unknown target type %u", page.get_page_name(),
+ target_type);
+ break;
+ }
+ }
+
+ private void on_drag_end() {
+ debug("on_drag_end (%s)", page.get_page_name());
+
+ if (page == null || page.get_view().get_selected_count() == 0 || drag_destination == null
+ || exporter != null) {
+ return;
+ }
+
+ debug("Exporting to %s", drag_destination.get_path());
+
+ // 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
+ // all transformations applied, at the image's original size).
+ 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()));
+ exporter.export(on_export_completed);
+ } else {
+ AppWindow.error_message(_("Photos cannot be exported to this directory."));
+ }
+
+ drag_destination = null;
+ }
+
+ private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
+ debug("on_drag_failed (%s): %d", page.get_page_name(), (int) drag_result);
+
+ if (page == null)
+ return false;
+
+ drag_destination = null;
+
+ return false;
+ }
+
+ private void on_export_completed() {
+ exporter = null;
+ }
+
+}
diff --git a/src/Event.vala b/src/Event.vala
index 084df97..69d27d0 100644
--- a/src/Event.vala
+++ b/src/Event.vala
@@ -100,8 +100,6 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
// In 24-hour time.
public const int EVENT_BOUNDARY_HOUR = 4;
- private const time_t TIME_T_DAY = 24 * 60 * 60;
-
private class EventSnapshot : SourceSnapshot {
private EventRow row;
private MediaSource primary_source;
@@ -303,8 +301,10 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
}
private static int64 view_comparator(void *a, void *b) {
- return ((MediaSource) ((ThumbnailView *) a)->get_source()).get_exposure_time()
- - ((MediaSource) ((ThumbnailView *) b)->get_source()).get_exposure_time() ;
+ var time_a = ((MediaSource) ((ThumbnailView *) a)->get_source()).get_exposure_time();
+ var time_b = ((MediaSource) ((ThumbnailView *) b)->get_source()).get_exposure_time();
+
+ return nullsafe_date_time_comperator(time_a, time_b);
}
private static bool view_comparator_predicate(DataObject object, Alteration alteration) {
@@ -591,7 +591,7 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
return indexable_keywords;
}
- public bool is_in_starting_day(time_t time) {
+ public bool is_in_starting_day(DateTime time) {
// it's possible the Event ref is held although it's been emptied
// (such as the user removing items during an import, when events
// are being generate on-the-fly) ... return false here and let
@@ -601,30 +601,25 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
// media sources are stored in ViewCollection from earliest to latest
MediaSource earliest_media = (MediaSource) ((DataView) view.get_at(0)).get_source();
- Time earliest_tm = Time.local(earliest_media.get_exposure_time());
+ var earliest_tm = earliest_media.get_exposure_time().to_local();
// use earliest to generate the boundary hour for that day
- Time start_boundary_tm = Time();
- start_boundary_tm.second = 0;
- start_boundary_tm.minute = 0;
- start_boundary_tm.hour = EVENT_BOUNDARY_HOUR;
- start_boundary_tm.day = earliest_tm.day;
- start_boundary_tm.month = earliest_tm.month;
- start_boundary_tm.year = earliest_tm.year;
- start_boundary_tm.isdst = -1;
-
- time_t start_boundary = start_boundary_tm.mktime();
-
+ var start_boundary = new DateTime.local(earliest_tm.get_year(),
+ earliest_tm.get_month(),
+ earliest_tm.get_day_of_month(),
+ EVENT_BOUNDARY_HOUR,
+ 0,
+ 0);
// if the earliest's exposure time was on the day but *before* the boundary hour,
// step it back a day to the prior day's boundary
- if (earliest_tm.hour < EVENT_BOUNDARY_HOUR) {
+ if (earliest_tm.get_hour() < EVENT_BOUNDARY_HOUR) {
debug("Hour before boundary, shifting back one day");
- start_boundary -= TIME_T_DAY;
+ start_boundary = start_boundary.add_days(-1);
}
- time_t end_boundary = (start_boundary + TIME_T_DAY - 1);
-
- return time >= start_boundary && time <= end_boundary;
+ var end_boundary = start_boundary.add_days(1).add_seconds(-1);
+
+ return time.compare(start_boundary) >= 0 && time.compare(end_boundary) <= 0;
}
// This method attempts to add a media source to an event in the supplied list that it would
@@ -632,9 +627,9 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
// photo). Otherwise, a new Event is generated and the source is added to it and the list.
private static Event? generate_event(MediaSource media, ViewCollection events_so_far,
string? event_name, out bool new_event) {
- time_t exposure_time = media.get_exposure_time();
+ DateTime? exposure_time = media.get_exposure_time();
- if (exposure_time == 0 && event_name == null) {
+ if (exposure_time == null && event_name == null) {
debug("Skipping event assignment to %s: no exposure time and no event name", media.to_string());
new_event = false;
@@ -754,22 +749,20 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
}
public string? get_formatted_daterange() {
- time_t start_time = get_start_time();
- time_t end_time = get_end_time();
+ DateTime? start_time = get_start_time();
+ DateTime? end_time = get_end_time();
- if (end_time == 0 && start_time == 0)
+ if (end_time == null && start_time == null)
return null;
- if (end_time == 0 && start_time != 0)
- return format_local_date(Time.local(start_time));
-
- Time start = Time.local(start_time);
- Time end = Time.local(end_time);
+ if (end_time == null && start_time != null)
+ return format_local_date(start_time.to_local());
- if (start.day == end.day && start.month == end.month && start.day == end.day)
- return format_local_date(Time.local(start_time));
+ if (start_time.get_year() == end_time.get_year() &&
+ start_time.get_day_of_year() == end_time.get_day_of_year())
+ return format_local_date(start_time.to_local());
- return format_local_datespan(start, end);
+ return format_local_datespan(start_time.to_local(), end_time.to_local());
}
public string? get_raw_name() {
@@ -811,30 +804,30 @@ public class Event : EventSource, ContainerSource, Proxyable, Indexable {
return committed;
}
- public time_t get_creation_time() {
+ public DateTime? get_creation_time() {
return event_table.get_time_created(event_id);
}
- public override time_t get_start_time() {
+ public override DateTime? get_start_time() {
// Because the ViewCollection is sorted by a DateComparator, the start time is the
// first item. However, we keep looking if it has no start time.
int count = view.get_count();
for (int i = 0; i < count; i++) {
- time_t time = ((MediaSource) (((DataView) view.get_at(i)).get_source())).get_exposure_time();
- if (time != 0)
+ var time = ((MediaSource) (((DataView) view.get_at(i)).get_source())).get_exposure_time();
+ if (time != null)
return time;
}
- return 0;
+ return null;
}
- public override time_t get_end_time() {
+ public override DateTime? get_end_time() {
int count = view.get_count();
// Because the ViewCollection is sorted by a DateComparator, the end time is the
// last item--no matter what.
if (count == 0)
- return 0;
+ return null;
return ((MediaSource) (((DataView) view.get_at(count - 1)).get_source())).get_exposure_time();
}
diff --git a/src/Exporter.vala b/src/Exporter.vala
index b9596f5..a7f7b6b 100644
--- a/src/Exporter.vala
+++ b/src/Exporter.vala
@@ -55,7 +55,9 @@ public class Exporter : Object {
YES,
NO,
CANCEL,
- REPLACE_ALL
+ REPLACE_ALL,
+ RENAME,
+ RENAME_ALL,
}
public delegate void CompletionCallback(Exporter exporter, bool is_cancelled);
@@ -116,8 +118,10 @@ public class Exporter : Object {
private unowned ProgressMonitor? monitor = null;
private Cancellable cancellable;
private bool replace_all = false;
+ private bool rename_all = false;
private bool aborted = false;
private ExportFormatParameters export_params;
+ private static File? USE_TEMPORARY_EXPORT_FOLDER = null;
public Exporter(Gee.Collection<MediaSource> to_export, File? dir, Scaling scaling,
ExportFormatParameters export_params, bool auto_replace_all = false) {
@@ -131,7 +135,7 @@ public class Exporter : Object {
public Exporter.for_temp_file(Gee.Collection<MediaSource> to_export, Scaling scaling,
ExportFormatParameters export_params) {
this.to_export.add_all(to_export);
- this.dir = null;
+ this.dir = USE_TEMPORARY_EXPORT_FOLDER;
this.scaling = scaling;
this.export_params = export_params;
}
@@ -193,6 +197,7 @@ public class Exporter : Object {
private bool process_queue() {
int submitted = 0;
+ Gee.HashSet<string> used = new Gee.HashSet<string>();
foreach (MediaSource source in to_export) {
File? use_source_file = null;
PhotoFileFormat real_export_format = PhotoFileFormat.get_system_default_format();
@@ -227,7 +232,7 @@ public class Exporter : Object {
if (export_dir == null) {
try {
bool collision;
- dest = generate_unique_file(AppDirs.get_temp_dir(), basename, out collision);
+ dest = generate_unique_file(AppDirs.get_temp_dir(), basename, out collision, used);
} catch (Error err) {
AppWindow.error_message(_("Unable to generate a temporary file for %s: %s").printf(
source.get_file().get_basename(), err.message));
@@ -236,17 +241,30 @@ public class Exporter : Object {
}
} else {
dest = dir.get_child(basename);
+ bool rename = false;
- if (!replace_all && dest.query_exists(null)) {
- switch (overwrite_callback(this, dest)) {
+ if (!replace_all && (dest.query_exists(null) || used.contains(basename))) {
+ if (rename_all) {
+ rename = true;
+ } else {
+ switch (overwrite_callback(this, dest)) {
case Overwrite.YES:
// continue
- break;
+ break;
case Overwrite.REPLACE_ALL:
replace_all = true;
- break;
-
+ break;
+
+ case Overwrite.RENAME:
+ rename = true;
+ break;
+
+ case Overwrite.RENAME_ALL:
+ rename = true;
+ rename_all = true;
+ break;
+
case Overwrite.CANCEL:
cancellable.cancel();
@@ -264,10 +282,22 @@ public class Exporter : Object {
}
continue;
+ }
+ }
+ if (rename) {
+ try {
+ bool collision;
+ dest = generate_unique_file(dir, basename, out collision, used);
+ } catch (Error err) {
+ AppWindow.error_message(_("Unable to generate a temporary file for %s: %s").printf(
+ source.get_file().get_basename(), err.message));
+ break;
+ }
}
}
}
+ used.add(dest.get_basename());
workers.enqueue(new ExportJob(this, source, dest, scaling, export_params.quality,
real_export_format, cancellable, export_params.mode == ExportFormatMode.UNMODIFIED, export_params.export_metadata));
submitted++;
@@ -315,24 +345,30 @@ public class ExporterUI {
private Exporter.Overwrite on_export_overwrite(Exporter exporter, File file) {
progress_dialog.set_modal(false);
string question = _("File %s already exists. Replace?").printf(file.get_basename());
- Gtk.ResponseType response = AppWindow.negate_affirm_all_cancel_question(question,
- _("_Skip"), _("_Replace"), _("Replace _All"), _("Export"));
+ int response = AppWindow.export_overwrite_or_replace_question(question,
+ _("_Skip"), _("Rename"), _("Rename All"),_("_Replace"), _("Replace _All"), _("_Cancel"), _("Export"));
progress_dialog.set_modal(true);
switch (response) {
- case Gtk.ResponseType.APPLY:
- return Exporter.Overwrite.REPLACE_ALL;
+ case 2:
+ return Exporter.Overwrite.RENAME;
+
+ case 3:
+ return Exporter.Overwrite.RENAME_ALL;
- case Gtk.ResponseType.YES:
- return Exporter.Overwrite.YES;
+ case 4:
+ return Exporter.Overwrite.YES;
- case Gtk.ResponseType.CANCEL:
- return Exporter.Overwrite.CANCEL;
+ case 5:
+ return Exporter.Overwrite.REPLACE_ALL;
- case Gtk.ResponseType.NO:
- default:
- return Exporter.Overwrite.NO;
+ case 6:
+ return Exporter.Overwrite.CANCEL;
+
+ case 1:
+ default:
+ return Exporter.Overwrite.NO;
}
}
@@ -340,4 +376,3 @@ public class ExporterUI {
return export_error_dialog(file, remaining > 0) != Gtk.ResponseType.CANCEL;
}
}
-
diff --git a/src/LibraryFiles.vala b/src/LibraryFiles.vala
index bbacb6c..4941742 100644
--- a/src/LibraryFiles.vala
+++ b/src/LibraryFiles.vala
@@ -26,18 +26,18 @@ public void select_copy_function() {
// Thus, when the method returns success a file may exist already, and should be overwritten.
//
// This function is thread safe.
-public File? generate_unique_file(string basename, MediaMetadata? metadata, time_t ts, out bool collision)
+public File? generate_unique_file(string basename, MediaMetadata? metadata, DateTime ts, out bool collision)
throws Error {
// use exposure timestamp over the supplied one (which probably comes from the file's
// modified time, or is simply time()), unless it's zero, in which case use current time
- time_t timestamp = ts;
+ DateTime timestamp = ts;
if (metadata != null) {
MetadataDateTime? date_time = metadata.get_creation_date_time();
if (date_time != null)
timestamp = date_time.get_timestamp();
- else if (timestamp == 0)
- timestamp = time_t();
+ else if (timestamp == null)
+ timestamp = new DateTime.now_utc();
}
// build a directory tree inside the library
@@ -71,7 +71,7 @@ public string convert_basename(string basename) {
// This function is thread-safe.
private File duplicate(File src, FileProgressCallback? progress_callback, bool blacklist) throws Error {
- time_t timestamp = 0;
+ DateTime? timestamp = null;
try {
timestamp = query_file_modified(src);
} catch (Error err) {
diff --git a/src/LibraryMonitor.vala b/src/LibraryMonitor.vala
index f9291d7..a291f15 100644
--- a/src/LibraryMonitor.vala
+++ b/src/LibraryMonitor.vala
@@ -96,7 +96,7 @@ public class LibraryMonitorPool {
public class LibraryMonitor : DirectoryMonitor {
private const int FLUSH_IMPORT_QUEUE_SEC = 3;
- private const int IMPORT_ROLL_QUIET_SEC = 5 * 60;
+ private const int IMPORT_ROLL_QUIET_SEC = 5 * 60 * 1000 * 1000;
private const int MIN_BLACKLIST_DURATION_MSEC = 5 * 1000;
private const int MAX_VERIFY_EXISTING_MEDIA_JOBS = 5;
@@ -217,7 +217,7 @@ public class LibraryMonitor : DirectoryMonitor {
private Gee.HashSet<File> pending_imports = new Gee.HashSet<File>(file_hash, file_equal);
private Gee.ArrayList<BatchImport> batch_import_queue = new Gee.ArrayList<BatchImport>();
private BatchImportRoll current_import_roll = null;
- private time_t last_import_roll_use = 0;
+ private int64 last_import_roll_use = 0;
private BatchImport current_batch_import = null;
private int checksums_completed = 0;
private int checksums_total = 0;
@@ -583,7 +583,7 @@ public class LibraryMonitor : DirectoryMonitor {
// If no import roll, or it's been over IMPORT_ROLL_QUIET_SEC since using the last one,
// create a new one. This allows for multiple files to come in back-to-back and be
// imported on the same roll.
- time_t now = (time_t) now_sec();
+ var now = GLib.get_monotonic_time();
if (current_import_roll == null || (now - last_import_roll_use) >= IMPORT_ROLL_QUIET_SEC)
current_import_roll = new BatchImportRoll();
last_import_roll_use = now;
@@ -996,7 +996,7 @@ public class LibraryMonitor : DirectoryMonitor {
}
if (!known) {
- // ressurrect tombstone if deleted
+ // resurrect tombstone if deleted
Tombstone? tombstone = Tombstone.global.locate(file);
if (tombstone != null) {
debug("Resurrecting tombstoned file %s", file.get_path());
diff --git a/src/MapWidget.vala b/src/MapWidget.vala
new file mode 100644
index 0000000..ddfae38
--- /dev/null
+++ b/src/MapWidget.vala
@@ -0,0 +1,788 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+private class MarkerImageSet {
+ public float marker_image_width;
+ public float marker_image_height;
+ public Clutter.Image? marker_image;
+ public Clutter.Image? marker_selected_image;
+ public Clutter.Image? marker_highlighted_image;
+}
+
+private enum SelectionAction {
+ SET,
+ ADD,
+ REMOVE
+}
+
+private abstract class PositionMarker : Object {
+ protected bool _highlighted = false;
+ protected bool _selected = false;
+ protected MarkerImageSet image_set;
+
+ protected PositionMarker(Champlain.Marker champlain_marker, MarkerImageSet image_set) {
+ this.champlain_marker = champlain_marker;
+ this.image_set = image_set;
+ champlain_marker.selectable = true;
+ champlain_marker.set_content(image_set.marker_image);
+ float w = image_set.marker_image_width;
+ float h = image_set.marker_image_height;
+ champlain_marker.set_size(w, h);
+ champlain_marker.set_translation(-w * MapWidget.MARKER_IMAGE_HORIZONTAL_PIN_RATIO,
+ -h * MapWidget.MARKER_IMAGE_VERTICAL_PIN_RATIO, 0);
+ }
+
+ public Champlain.Marker champlain_marker { get; protected set; }
+
+ public bool highlighted {
+ get {
+ return _highlighted;
+ }
+ set {
+ if (_highlighted == value)
+ return;
+ _highlighted = value;
+ var base_image = _selected ? image_set.marker_selected_image : image_set.marker_image;
+ champlain_marker.set_content(value ? image_set.marker_highlighted_image : base_image);
+ }
+ }
+ public bool selected {
+ get {
+ return _selected;
+ }
+ set {
+ if (_selected == value)
+ return;
+ _selected = value;
+ if (!_highlighted) {
+ var base_image = value ? image_set.marker_selected_image : image_set.marker_image;
+ champlain_marker.set_content(base_image);
+ }
+ champlain_marker.set_selected(value);
+ }
+ }
+}
+
+private class DataViewPositionMarker : PositionMarker {
+ private Gee.LinkedList<weak DataViewPositionMarker> _data_view_position_markers =
+ new Gee.LinkedList<weak DataViewPositionMarker>();
+
+ public weak DataView view { get; protected set; }
+
+ public DataViewPositionMarker(DataView view, Champlain.Marker champlain_marker,
+ MarkerImageSet image_set) {
+ base(champlain_marker, image_set);
+ this.view = view;
+
+ this._data_view_position_markers.add(this);
+ }
+
+ public void bind_mouse_events(MapWidget map_widget) {
+ champlain_marker.button_release_event.connect ((event) => {
+ if (event.button > 1)
+ return true;
+ bool mod = (bool)(event.modifier_state &
+ (Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.SHIFT_MASK));
+ SelectionAction action = SelectionAction.SET;
+ if (mod)
+ action = _selected ? SelectionAction.REMOVE : SelectionAction.ADD;
+ selected = (action != SelectionAction.REMOVE);
+ map_widget.select_data_views(_data_view_position_markers, action);
+ return true;
+ });
+ champlain_marker.enter_event.connect ((event) => {
+ highlighted = true;
+ map_widget.highlight_data_views(_data_view_position_markers);
+ return true;
+ });
+ champlain_marker.leave_event.connect ((event) => {
+ highlighted = false;
+ map_widget.unhighlight_data_views(_data_view_position_markers);
+ return true;
+ });
+ }
+}
+
+private class MarkerGroup : PositionMarker {
+ private Gee.Collection<weak DataViewPositionMarker> _data_view_position_markers =
+ new Gee.LinkedList<weak DataViewPositionMarker>();
+ private Gee.Collection<PositionMarker> _position_markers = new Gee.LinkedList<PositionMarker>();
+ private Champlain.BoundingBox bbox = new Champlain.BoundingBox();
+
+ public void bind_mouse_events(MapWidget map_widget) {
+ champlain_marker.button_release_event.connect ((event) => {
+ if (event.button > 1)
+ return true;
+ bool mod = (bool)(event.modifier_state &
+ (Clutter.ModifierType.CONTROL_MASK | Clutter.ModifierType.SHIFT_MASK));
+ SelectionAction action = SelectionAction.SET;
+ if (mod)
+ action = _selected ? SelectionAction.REMOVE : SelectionAction.ADD;
+ selected = (action != SelectionAction.REMOVE);
+ foreach (var m in _data_view_position_markers) {
+ m.selected = _selected;
+ }
+ map_widget.select_data_views(_data_view_position_markers.read_only_view, action);
+ return true;
+ });
+ champlain_marker.enter_event.connect ((event) => {
+ highlighted = true;
+ map_widget.highlight_data_views(_data_view_position_markers.read_only_view);
+ return true;
+ });
+ champlain_marker.leave_event.connect ((event) => {
+ highlighted = false;
+ map_widget.unhighlight_data_views(_data_view_position_markers.read_only_view);
+ return true;
+ });
+ }
+
+ public Gee.Collection<PositionMarker> position_markers {
+ owned get { return _position_markers.read_only_view; }
+ }
+
+ public MarkerGroup(Champlain.Marker champlain_marker, MarkerImageSet image_set) {
+ base(champlain_marker, image_set);
+ }
+
+ public void add_position_marker(PositionMarker marker) {
+ var data_view_position_marker = marker as DataViewPositionMarker;
+ if (data_view_position_marker != null)
+ _data_view_position_markers.add(data_view_position_marker);
+ var new_champlain_marker = marker.champlain_marker;
+ bbox.extend(new_champlain_marker.latitude, new_champlain_marker.longitude);
+ double lat, lon;
+ bbox.get_center(out lat, out lon);
+ champlain_marker.set_location(lat, lon);
+ _position_markers.add(marker);
+ }
+}
+
+private class MarkerGroupRaster : Object {
+ private const long MARKER_GROUP_RASTER_WIDTH_PX = 30l;
+ private const long MARKER_GROUP_RASTER_HEIGHT_PX = 30l;
+
+ private weak MapWidget map_widget;
+ private weak Champlain.View map_view;
+ private weak Champlain.MarkerLayer marker_layer;
+
+ public bool is_empty {
+ get {
+ return position_markers.is_empty;
+ }
+ }
+
+ // position_markers_tree is a two-dimensional tree for grouping position
+ // markers indexed by x (outer tree) and y (inner tree) raster coordinates.
+ // It maps coordinates to the PositionMarker (DataViewMarker or MarkerGroup)
+ // corresponding to them.
+ // If either raster index keys are empty, there is no marker within the
+ // raster cell. If both exist there are two possibilities:
+ // (1) the value is a MarkerGroup which means that multiple markers are
+ // grouped together, or (2) the value is a PositionMarker (but not a
+ // MarkerGroup) which means that there is exactly one marker in the raster
+ // cell. The tree is recreated every time the zoom level changes.
+ private Gee.TreeMap<long, Gee.TreeMap<long, unowned PositionMarker?>?> position_markers_tree =
+ new Gee.TreeMap<long, Gee.TreeMap<long, unowned PositionMarker?>?>();
+ // The marker group's collection keeps track of and owns all PositionMarkers including the marker groups
+ private Gee.Map<DataView, unowned PositionMarker> data_view_map = new Gee.HashMap<DataView, unowned PositionMarker>();
+ private Gee.Set<PositionMarker> position_markers = new Gee.HashSet<PositionMarker>();
+
+ public MarkerGroupRaster(MapWidget map_widget, Champlain.View map_view, Champlain.MarkerLayer marker_layer) {
+ this.map_widget = map_widget;
+ this.map_view = map_view;
+ this.marker_layer = marker_layer;
+ map_widget.zoom_changed.connect(regroup);
+ }
+
+ public void clear() {
+ lock (position_markers) {
+ data_view_map.clear();
+ position_markers_tree.clear();
+ position_markers.clear();
+ }
+ }
+
+ public void clear_selection() {
+ lock (position_markers) {
+ foreach (PositionMarker m in position_markers) {
+ m.selected = false;
+ }
+ }
+ }
+
+ public unowned PositionMarker? find_position_marker(DataView data_view) {
+ if (!data_view_map.has_key(data_view))
+ return null;
+ unowned PositionMarker? m;
+ lock (position_markers) {
+ m = data_view_map.get(data_view);
+ }
+ return m;
+ }
+
+ public void rasterize_marker(PositionMarker position_marker, bool already_on_map=false) {
+ var data_view_position_marker = position_marker as DataViewPositionMarker;
+ var champlain_marker = position_marker.champlain_marker;
+ long x, y;
+
+ lock (position_markers) {
+ rasterize_coords(champlain_marker.longitude, champlain_marker.latitude, out x, out y);
+ var yg = position_markers_tree.get(x);
+ if (yg == null) {
+ yg = new Gee.TreeMap<long, unowned PositionMarker?>();
+ position_markers_tree.set(x, yg);
+ }
+ var cell = yg.get(y);
+ if (cell == null) {
+ // first marker in this raster cell
+ yg.set(y, position_marker);
+ position_markers.add(position_marker);
+ if (!already_on_map)
+ marker_layer.add_marker(position_marker.champlain_marker);
+ if (data_view_position_marker != null)
+ data_view_map.set(data_view_position_marker.view, position_marker);
+
+ } else {
+ var marker_group = cell as MarkerGroup;
+ if (marker_group == null) {
+ // single marker already occupies raster cell: create new group
+ GpsCoords rasterized_gps_coords = GpsCoords() {
+ has_gps = 1,
+ longitude = map_view.x_to_longitude(x),
+ latitude = map_view.y_to_latitude(y)
+ };
+ marker_group = map_widget.create_marker_group(rasterized_gps_coords);
+ marker_group.add_position_marker(cell);
+ if (cell.selected) // group becomes selected if any contained marker is
+ marker_group.selected = true;
+ if (cell is DataViewPositionMarker)
+ data_view_map.set(((DataViewPositionMarker) cell).view, marker_group);
+ yg.set(y, marker_group);
+ position_markers.add(marker_group);
+ position_markers.remove(cell);
+ marker_layer.add_marker(marker_group.champlain_marker);
+ marker_layer.remove_marker(cell.champlain_marker);
+ }
+ // group already exists, add new marker to it
+ marker_group.add_position_marker(position_marker);
+ if (already_on_map)
+ marker_layer.remove_marker(position_marker.champlain_marker);
+ if (data_view_position_marker != null)
+ data_view_map.set(data_view_position_marker.view, marker_group);
+ }
+ }
+ }
+
+ private void rasterize_coords(double longitude, double latitude, out long x, out long y) {
+ x = (Math.lround(map_view.longitude_to_x(longitude) / MARKER_GROUP_RASTER_WIDTH_PX)) *
+ MARKER_GROUP_RASTER_WIDTH_PX + (MARKER_GROUP_RASTER_WIDTH_PX / 2);
+ y = (Math.lround(map_view.latitude_to_y(latitude) / MARKER_GROUP_RASTER_HEIGHT_PX)) *
+ MARKER_GROUP_RASTER_HEIGHT_PX + (MARKER_GROUP_RASTER_HEIGHT_PX / 2);
+ }
+
+ internal void regroup() {
+ lock (position_markers) {
+ var position_markers_current = (owned) position_markers;
+ position_markers = new Gee.HashSet<PositionMarker>();
+ position_markers_tree.clear();
+
+ foreach (var pm in position_markers_current) {
+ var marker_group = pm as MarkerGroup;
+ if (marker_group != null) {
+ marker_layer.remove_marker(marker_group.champlain_marker);
+ foreach (var position_marker in marker_group.position_markers) {
+ rasterize_marker(position_marker, false);
+ }
+ } else {
+ rasterize_marker(pm, true);
+ }
+ }
+ position_markers_current = null;
+ }
+ }
+}
+
+private class MapWidget : Gtk.Bin {
+ private const string MAPBOX_API_TOKEN = "pk.eyJ1IjoiamVuc2dlb3JnIiwiYSI6ImNqZ3FtYmhrMTBkOW8yeHBlNG8xN3hlNTAifQ.ek7i8UHeNIlkKi10fhgFgg";
+ private const uint DEFAULT_ZOOM_LEVEL = 8;
+
+ private static MapWidget instance = null;
+ private bool hide_map = false;
+
+ private GtkChamplain.Embed gtk_champlain_widget = new GtkChamplain.Embed();
+ private Champlain.View map_view = null;
+ private Champlain.Scale map_scale = new Champlain.Scale();
+ private Champlain.MarkerLayer marker_layer = new Champlain.MarkerLayer();
+ public bool map_edit_lock { get; set; }
+ private MarkerGroupRaster marker_group_raster = null;
+ private Gee.Map<DataView, unowned DataViewPositionMarker> data_view_marker_cache =
+ new Gee.HashMap<DataView, unowned DataViewPositionMarker>();
+ private weak Page? page = null;
+ private Clutter.Image? map_edit_locked_image;
+ private Clutter.Image? map_edit_unlocked_image;
+ private Clutter.Actor map_edit_lock_button = new Clutter.Actor();
+ private uint position_markers_timeout = 0;
+
+ public const float MARKER_IMAGE_HORIZONTAL_PIN_RATIO = 0.5f;
+ public const float MARKER_IMAGE_VERTICAL_PIN_RATIO = 0.825f;
+ public float map_edit_lock_image_width { get; private set; }
+ public float map_edit_lock_image_height { get; private set; }
+ public MarkerImageSet marker_image_set { get; private set; }
+ public MarkerImageSet marker_group_image_set { get; private set; }
+ public const Clutter.Color marker_point_color = { 10, 10, 255, 192 };
+
+ public signal void zoom_changed();
+
+ private MapWidget() {
+ setup_map();
+ add(gtk_champlain_widget);
+ }
+
+ public static MapWidget get_instance() {
+ if (instance == null)
+ instance = new MapWidget();
+ return instance;
+ }
+
+ public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) {
+ if (!map_edit_lock)
+ map_view.stop_go_to();
+ else
+ Gdk.drag_status(context, 0, time);
+ return true;
+ }
+
+ public override void drag_data_received(Gdk.DragContext context, int x, int y,
+ Gtk.SelectionData selection_data, uint info, uint time) {
+ bool success = false;
+ Gee.List<MediaSource>? media = unserialize_media_sources(selection_data.get_data(),
+ selection_data.get_length());
+ if (media != null && media.size > 0) {
+ double lat = map_view.y_to_latitude(y);
+ double lon = map_view.x_to_longitude(x);
+ success = internal_drop_received(media, lat, lon);
+ }
+
+ Gtk.drag_finish(context, success, false, time);
+ }
+
+ public new void set_visible(bool visible) {
+ /* hides Gtk.Widget.set_visible */
+ hide_map = !visible;
+ base.set_visible(visible);
+ }
+
+ public override void show_all() {
+ if (!hide_map)
+ base.show_all();
+ }
+
+ public void set_page(Page page) {
+ bool page_changed = false;
+ if (this.page != page) {
+ this.page = page;
+ page_changed = true;
+ clear();
+ }
+ ViewCollection view_collection = page.get_view();
+ if (view_collection == null)
+ return;
+
+ if (page_changed) {
+ data_view_marker_cache.clear();
+ foreach (DataObject view in view_collection.get_all()) {
+ if (view is DataView)
+ add_data_view((DataView) view);
+ }
+ show_position_markers();
+ }
+ // In any case, the selection did change..
+ var selected = view_collection.get_selected();
+ if (selected != null) {
+ marker_group_raster.clear_selection();
+ foreach (DataView v in view_collection.get_selected()) {
+
+ var position_marker = marker_group_raster.find_position_marker(v);
+ if (position_marker != null)
+ position_marker.selected = true;
+ if (position_marker is MarkerGroup) {
+ DataViewPositionMarker? m = data_view_marker_cache.get(v);
+ if (m != null)
+ m.selected = true;
+ }
+ }
+ }
+ }
+
+ public void clear() {
+ data_view_marker_cache.clear();
+ marker_layer.remove_all();
+ marker_group_raster.clear();
+ }
+
+ public void add_data_view(DataView view) {
+ DataSource view_source = view.get_source();
+ if (!(view_source is Positionable))
+ return;
+ Positionable p = (Positionable) view_source;
+ GpsCoords gps_coords = p.get_gps_coords();
+ if (gps_coords.has_gps <= 0)
+ return;
+ PositionMarker position_marker = create_position_marker(view);
+ marker_group_raster.rasterize_marker(position_marker);
+ }
+
+ public void show_position_markers() {
+ if (marker_group_raster.is_empty)
+ return;
+
+ map_view.stop_go_to();
+ double lat, lon;
+ var bbox = marker_layer.get_bounding_box();
+ var zoom_level = map_view.get_zoom_level();
+ var zoom_level_test = zoom_level < 2 ? 0 : zoom_level - 2;
+ bbox.get_center(out lat, out lon);
+
+ if (map_view.get_bounding_box_for_zoom_level(zoom_level_test).covers(lat, lon)) {
+ // Don't zoom in/out if target is in proximity
+ map_view.ensure_visible(bbox, true);
+ } else if (zoom_level >= DEFAULT_ZOOM_LEVEL) {
+ // zoom out to DEFAULT_ZOOM_LEVEL first, then move
+ map_view.set_zoom_level(DEFAULT_ZOOM_LEVEL);
+ map_view.ensure_visible(bbox, true);
+ } else {
+ // move first, then zoom in to DEFAULT_ZOOM_LEVEL
+ map_view.go_to(lat, lon);
+ // There seems to be a runtime issue with the animation_completed signal
+ // sig = map_view.animation_completed["go-to"].connect((v) => { ... }
+ // so we're using a timeout-based approach instead. It should be kept in sync with
+ // the animation time (500ms by default.)
+ if (position_markers_timeout > 0)
+ Source.remove(position_markers_timeout);
+ position_markers_timeout = Timeout.add(500, () => {
+ map_view.center_on(lat, lon); // ensure the timeout wasn't too fast
+ if (map_view.get_zoom_level() < DEFAULT_ZOOM_LEVEL)
+ map_view.set_zoom_level(DEFAULT_ZOOM_LEVEL);
+ map_view.ensure_visible(bbox, true);
+ position_markers_timeout = 0;
+ return Source.REMOVE;
+ });
+ }
+ }
+
+ public void select_data_views(Gee.Collection<unowned DataViewPositionMarker> ms,
+ SelectionAction action = SelectionAction.SET) {
+ if (page == null)
+ return;
+
+ ViewCollection page_view = page.get_view();
+ if (page_view != null) {
+ Marker marked = page_view.start_marking();
+ foreach (var m in ms) {
+ if (m.view is CheckerboardItem) {
+ marked.mark(m.view);
+ }
+ }
+ if (action == SelectionAction.REMOVE) {
+ page_view.unselect_marked(marked);
+ } else {
+ if (action == SelectionAction.SET)
+ page_view.unselect_all();
+ page_view.select_marked(marked);
+ }
+ }
+ }
+
+ public void highlight_data_views(Gee.Collection<unowned DataViewPositionMarker> ms) {
+ if (page == null)
+ return;
+
+ bool did_adjust_view = false;
+ foreach (var m in ms) {
+ if (!(m.view is CheckerboardItem)) {
+ continue;
+ }
+
+ CheckerboardItem item = m.view as CheckerboardItem;
+
+ if (!did_adjust_view && page is CheckerboardPage) {
+ ((CheckerboardPage) page).scroll_to_item(item);
+ did_adjust_view = true;
+ }
+ item.brighten();
+ }
+ }
+
+ public void unhighlight_data_views(Gee.Collection<unowned DataViewPositionMarker> ms) {
+ if (page == null)
+ return;
+
+ foreach (var m in ms) {
+ if (m.view is CheckerboardItem) {
+ CheckerboardItem item = (CheckerboardItem) m.view;
+ item.unbrighten();
+ }
+ }
+ }
+
+ public void highlight_position_marker(DataView v) {
+ var position_marker = marker_group_raster.find_position_marker(v);
+ if (position_marker != null) {
+ position_marker.highlighted = true;
+ }
+ }
+
+ public void unhighlight_position_marker(DataView v) {
+ var position_marker = marker_group_raster.find_position_marker(v);
+ if (position_marker != null) {
+ position_marker.highlighted = false;
+ }
+ }
+
+ public void media_source_position_changed(Gee.List<MediaSource> media, GpsCoords gps_coords) {
+ if (page == null)
+ return;
+ var view_collection = page.get_view();
+ foreach (var source in media) {
+ var view = view_collection.get_view_for_source(source);
+ if (view == null)
+ continue;
+ var marker = data_view_marker_cache.get(view);
+ if (marker != null) {
+ if (gps_coords.has_gps > 0) {
+ // update individual marker cache
+ marker.champlain_marker.set_location(gps_coords.latitude, gps_coords.longitude);
+ } else {
+ // TODO: position removal not supported by GUI
+ // remove marker from cache, map_layer
+ // remove from marker_group_raster (needs a removal method which also removes the
+ // item from the group if (marker_group_raster.find_position_marker(view) is MarkerGroup)
+ }
+ }
+ }
+ marker_group_raster.regroup();
+ }
+
+ private Champlain.MapSource create_map_source() {
+ var map_source = new Champlain.MapSourceChain();
+ var file_cache = new Champlain.FileCache.full(10 * 1024 * 1024,
+ AppDirs.get_cache_dir().get_child("tiles").get_child("mapbox-outdoors").get_path(),
+ new Champlain.ImageRenderer());
+ var memory_cache = new Champlain.MemoryCache.full(10 * 1024 * 1024, new Champlain.ImageRenderer());
+ var error_source = new Champlain.NullTileSource.full(new Champlain.ImageRenderer());
+
+ var tile_source = new Champlain.NetworkTileSource.full("mapbox-outdoors",
+ "Mapbox outdoors tiles",
+ "",
+ "",
+ 0,
+ 19,
+ 512,
+ Champlain.MapProjection.MERCATOR,
+ "https://api.mapbox.com/styles/v1/mapbox/outdoors-v11/tiles/#Z#/#X#/#Y#?access_token=" +
+ MAPBOX_API_TOKEN,
+ new Champlain.ImageRenderer());
+
+ var user_agent = "Shotwell/%s libchamplain/%s".printf(_VERSION, Champlain.VERSION_S);
+ tile_source.set_user_agent(user_agent);
+ tile_source.max_conns = 2;
+
+ map_source.push(error_source);
+ map_source.push(tile_source);
+ map_source.push(file_cache);
+ map_source.push(memory_cache);
+
+ return map_source;
+ }
+
+ private Clutter.Actor create_attribution_actor() {
+ const string IMPROVE_TEXT = N_("Improve this map");
+ var label = new Gtk.Label(null);
+ label.set_markup("<a href=\"https://www.mapbox.com/about/maps/\">© Mapbox</a> <a href=\"https://openstreetmap.org/about/\">© OpenStreetMap</a> <a href=\"https://www.mapbox.com/map-feedback/\">%s</a>".printf(IMPROVE_TEXT));
+ label.get_style_context().add_class("map-attribution");
+
+ return new GtkClutter.Actor.with_contents(label);
+ }
+
+ private void setup_map() {
+ map_view = gtk_champlain_widget.get_view();
+ map_view.add_layer(marker_layer);
+ map_view.set_map_source(create_map_source());
+
+ var map_attribution_text = create_attribution_actor();
+ map_attribution_text.content_gravity = Clutter.ContentGravity.BOTTOM_RIGHT;
+ map_attribution_text.set_x_align(Clutter.ActorAlign.END);
+ map_attribution_text.set_x_expand(true);
+ map_attribution_text.set_y_align(Clutter.ActorAlign.END);
+ map_attribution_text.set_y_expand(true);
+
+ // add lock/unlock button to top left corner of map
+ map_edit_lock_button.content_gravity = Clutter.ContentGravity.TOP_RIGHT;
+ map_edit_lock_button.reactive = true;
+ map_edit_lock_button.set_x_align(Clutter.ActorAlign.END);
+ map_edit_lock_button.set_x_expand(true);
+ map_edit_lock_button.set_y_align(Clutter.ActorAlign.START);
+ map_edit_lock_button.set_y_expand(true);
+
+ map_edit_lock_button.button_release_event.connect((a, e) => {
+ if (e.button != 1 /* CLUTTER_BUTTON_PRIMARY */)
+ return false;
+ map_edit_lock = !map_edit_lock;
+ map_edit_lock_button.set_content(map_edit_lock ?
+ map_edit_locked_image : map_edit_unlocked_image);
+ return true;
+ });
+ map_view.add_child(map_edit_lock_button);
+ map_view.add_child(map_attribution_text);
+
+ gtk_champlain_widget.has_tooltip = true;
+ gtk_champlain_widget.query_tooltip.connect((x, y, keyboard_tooltip, tooltip) => {
+ Gdk.Rectangle lock_rect = {
+ (int) map_edit_lock_button.x,
+ (int) map_edit_lock_button.y,
+ (int) map_edit_lock_button.width,
+ (int) map_edit_lock_button.height,
+ };
+ Gdk.Rectangle mouse_pos = { x, y, 1, 1 };
+ if (!lock_rect.intersect(mouse_pos, null))
+ return false;
+ tooltip.set_text(_("Lock or unlock map for geotagging by dragging pictures onto the map"));
+ return true;
+ });
+
+ // add scale to bottom left corner of the map
+ map_scale.content_gravity = Clutter.ContentGravity.BOTTOM_LEFT;
+ map_scale.connect_view(map_view);
+ map_scale.set_x_align(Clutter.ActorAlign.START);
+ map_scale.set_x_expand(true);
+ map_scale.set_y_align(Clutter.ActorAlign.END);
+ map_scale.set_y_expand(true);
+ map_view.add_child(map_scale);
+
+ map_view.set_zoom_on_double_click(false);
+ map_view.notify.connect((o, p) => {
+ if (p.name == "zoom-level")
+ zoom_changed();
+ });
+
+ Gtk.TargetEntry[] dnd_targets = {
+ LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.URI_LIST],
+ LibraryWindow.DND_TARGET_ENTRIES[LibraryWindow.TargetType.MEDIA_LIST]
+ };
+ Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, dnd_targets,
+ Gdk.DragAction.COPY | Gdk.DragAction.LINK | Gdk.DragAction.ASK);
+ button_press_event.connect(map_zoom_handler);
+ set_size_request(200, 200);
+
+ marker_group_raster = new MarkerGroupRaster(this, map_view, marker_layer);
+
+ // Load icons
+ float w, h;
+ marker_image_set = new MarkerImageSet();
+ marker_group_image_set = new MarkerImageSet();
+ marker_image_set.marker_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_GPS_MARKER, out w, out h);
+ marker_image_set.marker_image_width = w;
+ marker_image_set.marker_image_height = h;
+ marker_image_set.marker_selected_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_GPS_MARKER_SELECTED, out w, out h);
+ marker_image_set.marker_highlighted_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_GPS_MARKER_HIGHLIGHTED, out w, out h);
+
+ marker_group_image_set.marker_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_GPS_GROUP_MARKER, out w, out h);
+ marker_group_image_set.marker_image_width = w;
+ marker_group_image_set.marker_image_height = h;
+ marker_group_image_set.marker_selected_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_GPS_GROUP_MARKER_SELECTED, out w, out h);
+ marker_group_image_set.marker_highlighted_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_GPS_GROUP_MARKER_HIGHLIGHTED, out w, out h);
+
+ map_edit_locked_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_MAP_EDIT_LOCKED, out w, out h);
+ map_edit_unlocked_image = Resources.get_icon_as_clutter_image(
+ Resources.ICON_MAP_EDIT_UNLOCKED, out w, out h);
+ map_edit_lock_image_width = w;
+ map_edit_lock_image_height = h;
+ if (map_edit_locked_image == null) {
+ warning("Couldn't load map edit lock image");
+ } else {
+ map_edit_lock_button.set_content(map_edit_locked_image);
+ map_edit_lock_button.set_size(map_edit_lock_image_width, map_edit_lock_image_height);
+ map_edit_lock = true;
+ }
+ }
+
+ private Champlain.Marker create_champlain_marker(GpsCoords gps_coords) {
+ assert(gps_coords.has_gps > 0);
+
+ Champlain.Marker champlain_marker;
+ champlain_marker = new Champlain.Marker();
+ champlain_marker.set_pivot_point(0.5f, 0.5f); // set center of marker
+ champlain_marker.set_location(gps_coords.latitude, gps_coords.longitude);
+ return champlain_marker;
+ }
+
+ private DataViewPositionMarker create_position_marker(DataView view) {
+ var position_marker = data_view_marker_cache.get(view);
+ if (position_marker != null)
+ return position_marker;
+ DataSource data_source = view.get_source();
+ Positionable p = (Positionable) data_source;
+ GpsCoords gps_coords = p.get_gps_coords();
+ Champlain.Marker champlain_marker = create_champlain_marker(gps_coords);
+ position_marker = new DataViewPositionMarker(view, champlain_marker, marker_image_set);
+ position_marker.bind_mouse_events(this);
+ data_view_marker_cache.set(view, position_marker);
+ return (owned) position_marker;
+ }
+
+ internal MarkerGroup create_marker_group(GpsCoords gps_coords) {
+ Champlain.Marker champlain_marker = create_champlain_marker(gps_coords);
+ var g = new MarkerGroup(champlain_marker, marker_group_image_set);
+ g.bind_mouse_events(this);
+ return (owned) g;
+ }
+
+ private bool map_zoom_handler(Gdk.EventButton event) {
+ if (event.type == Gdk.EventType.2BUTTON_PRESS) {
+ if (event.button == 1 || event.button == 3) {
+ double lat = map_view.y_to_latitude(event.y);
+ double lon = map_view.x_to_longitude(event.x);
+ if (event.button == 1) {
+ map_view.zoom_in();
+ } else {
+ map_view.zoom_out();
+ }
+ map_view.center_on(lat, lon);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool internal_drop_received(Gee.List<MediaSource> media, double lat, double lon) {
+ if (map_edit_lock)
+ return false;
+
+ bool success = false;
+ GpsCoords gps_coords = GpsCoords() {
+ has_gps = 1,
+ latitude = lat,
+ longitude = lon
+ };
+ foreach (var m in media) {
+ Positionable p = m as Positionable;
+ if (p != null) {
+ p.set_gps_coords(gps_coords);
+ success = true;
+ }
+ }
+ media_source_position_changed(media, gps_coords);
+ return success;
+ }
+}
diff --git a/src/MediaDataRepresentation.vala b/src/MediaDataRepresentation.vala
index 6e6af00..3400577 100644
--- a/src/MediaDataRepresentation.vala
+++ b/src/MediaDataRepresentation.vala
@@ -7,10 +7,10 @@
public class BackingFileState {
public string filepath;
public int64 filesize;
- public time_t modification_time;
+ public DateTime modification_time;
public string? md5;
- public BackingFileState(string filepath, int64 filesize, time_t modification_time, string? md5) {
+ public BackingFileState(string filepath, int64 filesize, DateTime modification_time, string? md5) {
this.filepath = filepath;
this.filesize = filesize;
this.modification_time = modification_time;
@@ -154,7 +154,7 @@ public abstract class MediaSource : ThumbnailSource, Indexable {
public abstract File get_master_file();
public abstract uint64 get_master_filesize();
public abstract uint64 get_filesize();
- public abstract time_t get_timestamp();
+ public abstract DateTime? get_timestamp();
// Must return at least one, for the master file.
public abstract BackingFileState[] get_backing_files_state();
@@ -262,7 +262,7 @@ public abstract class MediaSource : ThumbnailSource, Indexable {
controller.commit();
}
- public abstract time_t get_exposure_time();
+ public abstract DateTime? get_exposure_time();
public abstract ImportID get_import_id();
}
diff --git a/src/MediaInterfaces.vala b/src/MediaInterfaces.vala
index 1a352a2..f2f570c 100644
--- a/src/MediaInterfaces.vala
+++ b/src/MediaInterfaces.vala
@@ -206,7 +206,7 @@ public interface Monitorable : MediaSource {
// from Photo to here in order to add this capability to videos. It should
// fire a "metadata:exposure-time" alteration when called.
public interface Dateable : MediaSource {
- public abstract void set_exposure_time(time_t target_time);
+ public abstract void set_exposure_time(DateTime target_time);
- public abstract time_t get_exposure_time();
+ public abstract DateTime? get_exposure_time();
}
diff --git a/src/MediaMetadata.vala b/src/MediaMetadata.vala
deleted file mode 100644
index b2ba1b7..0000000
--- a/src/MediaMetadata.vala
+++ /dev/null
@@ -1,125 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
- */
-
-public abstract class MediaMetadata {
- public abstract void read_from_file(File file) throws Error;
-
- public abstract MetadataDateTime? get_creation_date_time();
-
- public abstract string? get_title();
-
- public abstract string? get_comment();
-}
-
-public struct MetadataRational {
- public int numerator;
- public int denominator;
-
- public MetadataRational(int numerator, int denominator) {
- this.numerator = numerator;
- this.denominator = denominator;
- }
-
- private bool is_component_valid(int component) {
- return (component >= 0) && (component <= 1000000);
- }
-
- public bool is_valid() {
- return (is_component_valid(numerator) && is_component_valid(denominator));
- }
-
- public string to_string() {
- return (is_valid()) ? ("%d/%d".printf(numerator, denominator)) : "";
- }
-}
-
-public errordomain MetadataDateTimeError {
- INVALID_FORMAT,
- UNSUPPORTED_FORMAT
-}
-
-public class MetadataDateTime {
-
- private time_t timestamp;
-
- public MetadataDateTime(time_t timestamp) {
- this.timestamp = timestamp;
- }
-
- public MetadataDateTime.from_exif(string label) throws MetadataDateTimeError {
- if (!from_exif_date_time(label, out timestamp))
- throw new MetadataDateTimeError.INVALID_FORMAT("%s is not EXIF format date/time", label);
- }
-
- public MetadataDateTime.from_iptc(string date, string time) throws MetadataDateTimeError {
- // TODO: Support IPTC date/time format
- throw new MetadataDateTimeError.UNSUPPORTED_FORMAT("IPTC date/time format not currently supported");
- }
-
- public MetadataDateTime.from_xmp(string label) throws MetadataDateTimeError {
- TimeVal time_val = TimeVal();
- if (!time_val.from_iso8601(label))
- throw new MetadataDateTimeError.INVALID_FORMAT("%s is not XMP format date/time", label);
-
- timestamp = time_val.tv_sec;
- }
-
- public time_t get_timestamp() {
- return timestamp;
- }
-
- public string get_exif_label() {
- return to_exif_date_time(timestamp);
- }
-
- // TODO: get_iptc_date() and get_iptc_time()
-
- public string get_xmp_label() {
- TimeVal time_val = TimeVal();
- time_val.tv_sec = timestamp;
- time_val.tv_usec = 0;
-
- return time_val.to_iso8601();
- }
-
- public static bool from_exif_date_time(string date_time, out time_t timestamp) {
- timestamp = 0;
-
- Time tm = Time();
-
- // Check standard EXIF format
- if (date_time.scanf("%d:%d:%d %d:%d:%d",
- &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute, &tm.second) != 6) {
- // Fallback in a more generic format
- string tmp = date_time.dup();
- tmp.canon("0123456789", ' ');
- if (tmp.scanf("%4d%2d%2d%2d%2d%2d",
- &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute,&tm.second) != 6)
- return false;
- }
-
- // watch for bogosity
- if (tm.year <= 1900 || tm.month <= 0 || tm.day < 0 || tm.hour < 0 || tm.minute < 0 || tm.second < 0)
- return false;
-
- tm.year -= 1900;
- tm.month--;
- tm.isdst = -1;
-
- timestamp = tm.mktime();
-
- return true;
- }
-
- public static string to_exif_date_time(time_t timestamp) {
- return Time.local(timestamp).format("%Y:%m:%d %H:%M:%S");
- }
-
- public string to_string() {
- return to_exif_date_time(timestamp);
- }
-}
-
diff --git a/src/MediaPage.vala b/src/MediaPage.vala
index f849ac3..5fa3fca 100644
--- a/src/MediaPage.vala
+++ b/src/MediaPage.vala
@@ -90,10 +90,7 @@ public abstract class MediaPage : CheckerboardPage {
}
public static double scale_to_slider(int value) {
- assert(value >= Thumbnail.MIN_SCALE);
- assert(value <= Thumbnail.MAX_SCALE);
-
- return (double) ((value - Thumbnail.MIN_SCALE) / SLIDER_STEPPING);
+ return (double) ((value.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE) - Thumbnail.MIN_SCALE) / SLIDER_STEPPING);
}
public static int slider_to_scale(double value) {
@@ -887,7 +884,7 @@ public abstract class MediaPage : CheckerboardPage {
case SortBy.EXPOSURE_DATE:
if (ascending)
comparator = Thumbnail.exposure_time_ascending_comparator;
- else comparator = Thumbnail.exposure_time_desending_comparator;
+ else comparator = Thumbnail.exposure_time_descending_comparator;
predicate = Thumbnail.exposure_time_comparator_predicate;
break;
diff --git a/src/MetadataWriter.vala b/src/MetadataWriter.vala
index 0c23260..5fc26d1 100644
--- a/src/MetadataWriter.vala
+++ b/src/MetadataWriter.vala
@@ -15,7 +15,7 @@ public class MetadataWriter : Object {
public const uint COMMIT_DELAY_MSEC = 3000;
public const uint COMMIT_SPACING_MSEC = 50;
- private const string[] INTERESTED_PHOTO_METADATA_DETAILS = { "name", "comment", "rating", "exposure-time" };
+ private const string[] INTERESTED_PHOTO_METADATA_DETAILS = { "name", "comment", "rating", "exposure-time", "gps" };
private class CommitJob : BackgroundJob {
public LibraryPhoto photo;
@@ -108,18 +108,26 @@ public class MetadataWriter : Object {
}
// exposure date/time
- time_t current_exposure_time = photo.get_exposure_time();
- time_t metadata_exposure_time = 0;
+ DateTime? current_exposure_time = photo.get_exposure_time();
+ DateTime? metadata_exposure_time = null;
MetadataDateTime? metadata_exposure_date_time = metadata.get_exposure_date_time();
if (metadata_exposure_date_time != null)
metadata_exposure_time = metadata_exposure_date_time.get_timestamp();
if (current_exposure_time != metadata_exposure_time) {
- metadata.set_exposure_date_time(current_exposure_time != 0
+ metadata.set_exposure_date_time(current_exposure_time != null
? new MetadataDateTime(current_exposure_time)
: null);
changed = true;
}
+ // gps location
+ GpsCoords current_gps_coords = photo.get_gps_coords();
+ GpsCoords metadata_gps_coords = metadata.get_gps_coords();
+ if (!current_gps_coords.equals(ref metadata_gps_coords)) {
+ metadata.set_gps_coords(current_gps_coords);
+ changed = true;
+ }
+
// tags (keywords) ... replace (or clear) entirely rather than union or intersection
Gee.Set<string> safe_keywords = new Gee.HashSet<string>();
@@ -681,7 +689,7 @@ public class MetadataWriter : Object {
try {
job.photo.set_master_metadata_dirty(false);
- } catch (DatabaseError err) {
+ } catch (Error err) {
AppWindow.database_error(err);
}
diff --git a/src/Page.vala b/src/Page.vala
index 65b263b..6b07568 100644
--- a/src/Page.vala
+++ b/src/Page.vala
@@ -13,13 +13,13 @@ public class InjectionGroup {
}
public string name;
public string action;
- public string? accellerator;
+ public string? accelerator;
public ItemType kind;
- public Element(string name, string? action, string? accellerator, ItemType kind) {
+ public Element(string name, string? action, string? accelerator, ItemType kind) {
this.name = name;
this.action = action != null ? action : name;
- this.accellerator = accellerator;
+ this.accelerator = accelerator;
this.kind = kind;
}
}
@@ -40,8 +40,8 @@ public class InjectionGroup {
return elements;
}
- public void add_menu_item(string name, string? action = null, string? accellerator = null) {
- elements.add(new Element(name, action, accellerator, Element.ItemType.MENUITEM));
+ public void add_menu_item(string name, string? action = null, string? accelerator = null) {
+ elements.add(new Element(name, action, accelerator, Element.ItemType.MENUITEM));
}
public void add_menu(string name, string? action = null) {
@@ -68,7 +68,6 @@ public abstract class Page : Gtk.ScrolledWindow {
private string toolbar_path;
private Gdk.Rectangle last_position = Gdk.Rectangle();
private Gtk.Widget event_source = null;
- private bool dnd_enabled = false;
private ulong last_configure_ms = 0;
private bool report_move_finished = false;
private bool report_resize_finished = false;
@@ -85,6 +84,9 @@ public abstract class Page : Gtk.ScrolledWindow {
private int cursor_hide_time_cached = 0;
private bool are_actions_attached = false;
private OneShotScheduler? update_actions_scheduler = null;
+
+ protected double wheel_factor = 0.0;
+ protected double modified_wheel_factor = 1.0;
protected Page(string page_name) {
this.page_name = page_name;
@@ -193,8 +195,6 @@ public abstract class Page : Gtk.ScrolledWindow {
event_source.leave_notify_event.disconnect(on_leave_notify_event);
event_source.scroll_event.disconnect(on_mousewheel_internal);
- disable_drag_source();
-
event_source = null;
}
@@ -230,10 +230,10 @@ public abstract class Page : Gtk.ScrolledWindow {
case InjectionGroup.Element.ItemType.MENUITEM:
var item = new GLib.MenuItem (element.name,
"win." + element.action);
- if (element.accellerator != null) {
+ if (element.accelerator != null) {
item.set_attribute ("accel",
"s",
- element.accellerator);
+ element.accelerator);
}
menu.append_item (item);
@@ -592,76 +592,6 @@ public abstract class Page : Gtk.ScrolledWindow {
protected virtual void update_actions(int selected_count, int count) {
}
- // This method enables drag-and-drop on the event source and routes its events through this
- // object
- public void enable_drag_source(Gdk.DragAction actions, Gtk.TargetEntry[] source_target_entries) {
- if (dnd_enabled)
- return;
-
- assert(event_source != null);
-
- Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, source_target_entries, actions);
-
- // hook up handlers which route the event_source's DnD signals to the Page's (necessary
- // because Page is a NO_WINDOW widget and cannot support DnD on its own).
- event_source.drag_begin.connect(on_drag_begin);
- event_source.drag_data_get.connect(on_drag_data_get);
- event_source.drag_data_delete.connect(on_drag_data_delete);
- event_source.drag_end.connect(on_drag_end);
- event_source.drag_failed.connect(on_drag_failed);
-
- dnd_enabled = true;
- }
-
- public void disable_drag_source() {
- if (!dnd_enabled)
- return;
-
- assert(event_source != null);
-
- event_source.drag_begin.disconnect(on_drag_begin);
- event_source.drag_data_get.disconnect(on_drag_data_get);
- event_source.drag_data_delete.disconnect(on_drag_data_delete);
- event_source.drag_end.disconnect(on_drag_end);
- event_source.drag_failed.disconnect(on_drag_failed);
- Gtk.drag_source_unset(event_source);
-
- dnd_enabled = false;
- }
-
- public bool is_dnd_enabled() {
- return dnd_enabled;
- }
-
- private void on_drag_begin(Gdk.DragContext context) {
- drag_begin(context);
- }
-
- private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
- uint info, uint time) {
- drag_data_get(context, selection_data, info, time);
- }
-
- private void on_drag_data_delete(Gdk.DragContext context) {
- drag_data_delete(context);
- }
-
- private void on_drag_end(Gdk.DragContext context) {
- drag_end(context);
- }
-
- // wierdly, Gtk 2.16.1 doesn't supply a drag_failed virtual method in the GtkWidget impl ...
- // Vala binds to it, but it's not available in gtkwidget.h, and so gcc complains. Have to
- // makeshift one for now.
- // https://bugzilla.gnome.org/show_bug.cgi?id=584247
- public virtual bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
- return false;
- }
-
- private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
- return source_drag_failed(context, drag_result);
- }
-
// Use this function rather than GDK or GTK's get_pointer, especially if called during a
// button-down mouse drag (i.e. a window grab).
//
@@ -1032,13 +962,13 @@ public abstract class Page : Gtk.ScrolledWindow {
double dx, dy;
event.get_scroll_deltas(out dx, out dy);
- if (dy < 0)
+ if (dy < -1.0 * this.wheel_factor)
return on_mousewheel_up(event);
- else if (dy > 0)
+ else if (dy > this.wheel_factor)
return on_mousewheel_down(event);
- else if (dx < 0)
+ else if (dx < -1.0 * this.wheel_factor)
return on_mousewheel_left(event);
- else if (dx > 0)
+ else if (dx > this.wheel_factor)
return on_mousewheel_right(event);
else
return false;
@@ -1115,15 +1045,19 @@ public abstract class Page : Gtk.ScrolledWindow {
}
public void stop_cursor_hiding() {
- if (last_timeout_id != 0)
+ if (last_timeout_id != 0) {
Source.remove(last_timeout_id);
+ last_timeout_id = 0;
+ }
}
public void suspend_cursor_hiding() {
cursor_hide_time_cached = cursor_hide_msec;
- if (last_timeout_id != 0)
+ if (last_timeout_id != 0) {
Source.remove(last_timeout_id);
+ last_timeout_id = 0;
+ }
cursor_hide_msec = 0;
}
@@ -1209,1454 +1143,3 @@ public abstract class Page : Gtk.ScrolledWindow {
}
-public abstract class CheckerboardPage : Page {
- private const int AUTOSCROLL_PIXELS = 50;
- private const int AUTOSCROLL_TICKS_MSEC = 50;
-
- private CheckerboardLayout layout;
- private string item_context_menu_path = null;
- private string page_context_menu_path = null;
- private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
- protected CheckerboardItem anchor = null;
- protected CheckerboardItem cursor = null;
- private CheckerboardItem current_hovered_item = null;
- private bool autoscroll_scheduled = false;
- private CheckerboardItem activated_item = null;
- private Gee.ArrayList<CheckerboardItem> previously_selected = null;
-
- public enum Activator {
- KEYBOARD,
- MOUSE
- }
-
- public struct KeyboardModifiers {
- public KeyboardModifiers(Page page) {
- ctrl_pressed = page.get_ctrl_pressed();
- alt_pressed = page.get_alt_pressed();
- shift_pressed = page.get_shift_pressed();
- super_pressed = page.get_super_pressed();
- }
-
- public bool ctrl_pressed;
- public bool alt_pressed;
- public bool shift_pressed;
- public bool super_pressed;
- }
-
- protected CheckerboardPage(string page_name) {
- base (page_name);
-
- layout = new CheckerboardLayout(get_view());
- layout.set_name(page_name);
-
- set_event_source(layout);
-
- set_border_width(0);
- set_shadow_type(Gtk.ShadowType.NONE);
-
- viewport.set_border_width(0);
- viewport.set_shadow_type(Gtk.ShadowType.NONE);
-
- viewport.add(layout);
-
- // want to set_adjustments before adding to ScrolledWindow to let our signal handlers
- // run first ... otherwise, the thumbnails draw late
- layout.set_adjustments(get_hadjustment(), get_vadjustment());
-
- add(viewport);
-
- // need to monitor items going hidden when dealing with anchor/cursor/highlighted items
- get_view().items_hidden.connect(on_items_hidden);
- get_view().contents_altered.connect(on_contents_altered);
- get_view().items_state_changed.connect(on_items_state_changed);
- get_view().items_visibility_changed.connect(on_items_visibility_changed);
-
- // scrollbar policy
- set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
- }
-
- public void init_item_context_menu(string path) {
- item_context_menu_path = path;
- }
-
- public void init_page_context_menu(string path) {
- page_context_menu_path = path;
- }
-
- public Gtk.Menu? get_context_menu() {
- // show page context menu if nothing is selected
- return (get_view().get_selected_count() != 0) ? get_item_context_menu() :
- get_page_context_menu();
- }
-
- private Gtk.Menu item_context_menu;
- public virtual Gtk.Menu? get_item_context_menu() {
- if (item_context_menu == null) {
- var model = this.builder.get_object (item_context_menu_path)
- as GLib.MenuModel;
- item_context_menu = new Gtk.Menu.from_model (model);
- item_context_menu.attach_to_widget (this, null);
- }
-
- return item_context_menu;
- }
-
- private Gtk.Menu page_context_menu;
- public override Gtk.Menu? get_page_context_menu() {
- if (page_context_menu_path == null)
- return null;
-
- if (page_context_menu == null) {
- var model = this.builder.get_object (page_context_menu_path)
- as GLib.MenuModel;
- page_context_menu = new Gtk.Menu.from_model (model);
- page_context_menu.attach_to_widget (this, null);
- }
-
- return page_context_menu;
- }
-
- protected override bool on_context_keypress() {
- return popup_context_menu(get_context_menu());
- }
-
- protected virtual string get_view_empty_message() {
- return _("No photos/videos");
- }
-
- protected virtual string get_filter_no_match_message() {
- return _("No photos/videos found which match the current filter");
- }
-
- protected virtual void on_item_activated(CheckerboardItem item, Activator activator,
- KeyboardModifiers modifiers) {
- }
-
- public CheckerboardLayout get_checkerboard_layout() {
- return layout;
- }
-
- // Gets the search view filter for this page.
- public abstract SearchViewFilter get_search_view_filter();
-
- public virtual Core.ViewTracker? get_view_tracker() {
- return null;
- }
-
- public override void switching_from() {
- layout.set_in_view(false);
- get_search_view_filter().refresh.disconnect(on_view_filter_refresh);
-
- // unselect everything so selection won't persist after page loses focus
- get_view().unselect_all();
-
- base.switching_from();
- }
-
- public override void switched_to() {
- layout.set_in_view(true);
- get_search_view_filter().refresh.connect(on_view_filter_refresh);
- on_view_filter_refresh();
-
- if (get_view().get_selected_count() > 0) {
- CheckerboardItem? item = (CheckerboardItem?) get_view().get_selected_at(0);
-
- // if item is in any way out of view, scroll to it
- Gtk.Adjustment vadj = get_vadjustment();
- if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
- && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == AdjustmentRelation.IN_RANGE))) {
-
- // scroll to see the new item
- int top = 0;
- if (item.allocation.y < vadj.get_value()) {
- top = item.allocation.y;
- top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
- } else {
- top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
- top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
- }
-
- vadj.set_value(top);
-
- }
- }
-
- base.switched_to();
- }
-
- private void on_view_filter_refresh() {
- update_view_filter_message();
- }
-
- private void on_contents_altered(Gee.Iterable<DataObject>? added,
- Gee.Iterable<DataObject>? removed) {
- update_view_filter_message();
- }
-
- private void on_items_state_changed(Gee.Iterable<DataView> changed) {
- update_view_filter_message();
- }
-
- private void on_items_visibility_changed(Gee.Collection<DataView> changed) {
- update_view_filter_message();
- }
-
- private void update_view_filter_message() {
- if (get_view().are_items_filtered_out() && get_view().get_count() == 0) {
- set_page_message(get_filter_no_match_message());
- } else if (get_view().get_count() == 0) {
- set_page_message(get_view_empty_message());
- } else {
- unset_page_message();
- }
- }
-
- public void set_page_message(string message) {
- layout.set_message(message);
- if (is_in_view())
- layout.queue_draw();
- }
-
- public void unset_page_message() {
- layout.unset_message();
- if (is_in_view())
- layout.queue_draw();
- }
-
- public override void set_page_name(string name) {
- base.set_page_name(name);
-
- layout.set_name(name);
- }
-
- public CheckerboardItem? get_item_at_pixel(double x, double y) {
- return layout.get_item_at_pixel(x, y);
- }
-
- private void on_items_hidden(Gee.Iterable<DataView> hidden) {
- foreach (DataView view in hidden) {
- CheckerboardItem item = (CheckerboardItem) view;
-
- if (anchor == item)
- anchor = null;
-
- if (cursor == item)
- cursor = null;
-
- if (current_hovered_item == item)
- current_hovered_item = null;
- }
- }
-
- protected override bool key_press_event(Gdk.EventKey event) {
- bool handled = true;
-
- // mask out the modifiers we're interested in
- uint state = event.state & Gdk.ModifierType.SHIFT_MASK;
-
- switch (Gdk.keyval_name(event.keyval)) {
- case "Up":
- case "KP_Up":
- move_cursor(CompassPoint.NORTH);
- select_anchor_to_cursor(state);
- break;
-
- case "Down":
- case "KP_Down":
- move_cursor(CompassPoint.SOUTH);
- select_anchor_to_cursor(state);
- break;
-
- case "Left":
- case "KP_Left":
- move_cursor(CompassPoint.WEST);
- select_anchor_to_cursor(state);
- break;
-
- case "Right":
- case "KP_Right":
- move_cursor(CompassPoint.EAST);
- select_anchor_to_cursor(state);
- break;
-
- case "Home":
- case "KP_Home":
- CheckerboardItem? first = (CheckerboardItem?) get_view().get_first();
- if (first != null)
- cursor_to_item(first);
- select_anchor_to_cursor(state);
- break;
-
- case "End":
- case "KP_End":
- CheckerboardItem? last = (CheckerboardItem?) get_view().get_last();
- if (last != null)
- cursor_to_item(last);
- select_anchor_to_cursor(state);
- break;
-
- case "Return":
- case "KP_Enter":
- if (get_view().get_selected_count() == 1)
- on_item_activated((CheckerboardItem) get_view().get_selected_at(0),
- Activator.KEYBOARD, KeyboardModifiers(this));
- else
- handled = false;
- break;
-
- case "space":
- Marker marker = get_view().mark(layout.get_cursor());
- get_view().toggle_marked(marker);
- break;
-
- default:
- handled = false;
- break;
- }
-
- if (handled)
- return true;
-
- return (base.key_press_event != null) ? base.key_press_event(event) : true;
- }
-
- protected override bool on_left_click(Gdk.EventButton event) {
- // only interested in single-click and double-clicks for now
- if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS))
- return false;
-
- // mask out the modifiers we're interested in
- uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
-
- // use clicks for multiple selection and activation only; single selects are handled by
- // button release, to allow for multiple items to be selected then dragged ...
- CheckerboardItem item = get_item_at_pixel(event.x, event.y);
- if (item != null) {
- // ... however, there is no dragging if the user clicks on an interactive part of the
- // CheckerboardItem (e.g. a tag)
- if (layout.handle_left_click(item, event.x, event.y, event.state))
- return true;
-
- switch (state) {
- case Gdk.ModifierType.CONTROL_MASK:
- // with only Ctrl pressed, multiple selections are possible ... chosen item
- // is toggled
- Marker marker = get_view().mark(item);
- get_view().toggle_marked(marker);
-
- if (item.is_selected()) {
- anchor = item;
- cursor = item;
- }
- break;
-
- case Gdk.ModifierType.SHIFT_MASK:
- get_view().unselect_all();
-
- if (anchor == null)
- anchor = item;
-
- select_between_items(anchor, item);
-
- cursor = item;
- break;
-
- case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
- // Ticket #853 - Make Ctrl + Shift + Mouse Button 1 able to start a new run
- // of contiguous selected items without unselecting previously-selected items
- // a la Nautilus.
- // Same as the case for SHIFT_MASK, but don't unselect anything first.
- if (anchor == null)
- anchor = item;
-
- select_between_items(anchor, item);
-
- cursor = item;
- break;
-
- default:
- if (event.type == Gdk.EventType.2BUTTON_PRESS) {
- activated_item = item;
- } else {
- // if the user has selected one or more items and is preparing for a drag,
- // don't want to blindly unselect: if they've clicked on an unselected item
- // unselect all and select that one; if they've clicked on a previously
- // selected item, do nothing
- if (!item.is_selected()) {
- Marker all = get_view().start_marking();
- all.mark_many(get_view().get_selected());
-
- get_view().unselect_and_select_marked(all, get_view().mark(item));
- }
- }
-
- anchor = item;
- cursor = item;
- break;
- }
- layout.set_cursor(item);
- } else {
- // user clicked on "dead" area; only unselect if control is not pressed
- // do we want similar behavior for shift as well?
- if (state != Gdk.ModifierType.CONTROL_MASK)
- get_view().unselect_all();
-
- // grab previously marked items
- previously_selected = new Gee.ArrayList<CheckerboardItem>();
- foreach (DataView view in get_view().get_selected())
- previously_selected.add((CheckerboardItem) view);
-
- layout.set_drag_select_origin((int) event.x, (int) event.y);
-
- return true;
- }
-
- // need to determine if the signal should be passed to the DnD handlers
- // Return true to block the DnD handler, false otherwise
-
- return get_view().get_selected_count() == 0;
- }
-
- protected override bool on_left_released(Gdk.EventButton event) {
- previously_selected = null;
-
- // if drag-selecting, stop here and do nothing else
- if (layout.is_drag_select_active()) {
- layout.clear_drag_select();
- anchor = cursor;
-
- return true;
- }
-
- // only interested in non-modified button releases
- if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
- return false;
-
- // if the item was activated in the double-click, report it now
- if (activated_item != null) {
- on_item_activated(activated_item, Activator.MOUSE, KeyboardModifiers(this));
- activated_item = null;
-
- return true;
- }
-
- CheckerboardItem item = get_item_at_pixel(event.x, event.y);
- if (item == null) {
- // released button on "dead" area
- return true;
- }
-
- if (cursor != item) {
- // user released mouse button after moving it off the initial item, or moved from dead
- // space onto one. either way, unselect everything
- get_view().unselect_all();
- } else {
- // the idea is, if a user single-clicks on an item with no modifiers, then all other items
- // should be deselected, however, if they single-click in order to drag one or more items,
- // they should remain selected, hence performing this here rather than on_left_click
- // (item may not be selected if an unimplemented modifier key was used)
- if (item.is_selected())
- get_view().unselect_all_but(item);
- }
-
- return true;
- }
-
- protected override bool on_right_click(Gdk.EventButton event) {
- // only interested in single-clicks for now
- if (event.type != Gdk.EventType.BUTTON_PRESS)
- return false;
-
- // get what's right-clicked upon
- CheckerboardItem item = get_item_at_pixel(event.x, event.y);
- if (item != null) {
- // mask out the modifiers we're interested in
- switch (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) {
- case Gdk.ModifierType.CONTROL_MASK:
- // chosen item is toggled
- Marker marker = get_view().mark(item);
- get_view().toggle_marked(marker);
- break;
-
- case Gdk.ModifierType.SHIFT_MASK:
- // TODO
- break;
-
- case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
- // TODO
- break;
-
- default:
- // if the item is already selected, proceed; if item is not selected, a bare right
- // click unselects everything else but it
- if (!item.is_selected()) {
- Marker all = get_view().start_marking();
- all.mark_many(get_view().get_selected());
-
- get_view().unselect_and_select_marked(all, get_view().mark(item));
- }
- break;
- }
- } else {
- // clicked in "dead" space, unselect everything
- get_view().unselect_all();
- }
-
- Gtk.Menu context_menu = get_context_menu();
- return popup_context_menu(context_menu, event);
- }
-
- protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) {
- if (item != null)
- layout.handle_mouse_motion(item, x, y, mask);
-
- // if hovering over the last hovered item, or both are null (nothing highlighted and
- // hovering over empty space), do nothing
- if (item == current_hovered_item)
- return true;
-
- // either something new is highlighted or now hovering over empty space, so dim old item
- if (current_hovered_item != null) {
- current_hovered_item.handle_mouse_leave();
- current_hovered_item = null;
- }
-
- // if over empty space, done
- if (item == null)
- return true;
-
- // brighten the new item
- current_hovered_item = item;
- current_hovered_item.handle_mouse_enter();
-
- return true;
- }
-
- protected override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
- // report what item the mouse is hovering over
- if (!on_mouse_over(get_item_at_pixel(x, y), x, y, mask))
- return false;
-
- // go no further if not drag-selecting
- if (!layout.is_drag_select_active())
- return false;
-
- // set the new endpoint of the drag selection
- layout.set_drag_select_endpoint(x, y);
-
- updated_selection_band();
-
- // if out of bounds, schedule a check to auto-scroll the viewport
- if (!autoscroll_scheduled
- && get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) {
- Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll);
- autoscroll_scheduled = true;
- }
-
- // return true to stop a potential drag-and-drop operation
- return true;
- }
-
- private void updated_selection_band() {
- assert(layout.is_drag_select_active());
-
- // get all items inside the selection
- Gee.List<CheckerboardItem>? intersection = layout.items_in_selection_band();
- if (intersection == null)
- return;
-
- Marker to_unselect = get_view().start_marking();
- Marker to_select = get_view().start_marking();
-
- // mark all selected items to be unselected
- to_unselect.mark_many(get_view().get_selected());
-
- // except for the items that were selected before the drag began
- assert(previously_selected != null);
- to_unselect.unmark_many(previously_selected);
- to_select.mark_many(previously_selected);
-
- // toggle selection on everything in the intersection and update the cursor
- cursor = null;
-
- foreach (CheckerboardItem item in intersection) {
- if (to_select.toggle(item))
- to_unselect.unmark(item);
- else
- to_unselect.mark(item);
-
- if (cursor == null)
- cursor = item;
- }
-
- get_view().select_marked(to_select);
- get_view().unselect_marked(to_unselect);
- }
-
- private bool selection_autoscroll() {
- if (!layout.is_drag_select_active()) {
- autoscroll_scheduled = false;
-
- return false;
- }
-
- // as the viewport never scrolls horizontally, only interested in vertical
- Gtk.Adjustment vadj = get_vadjustment();
-
- int x, y;
- Gdk.ModifierType mask;
- get_event_source_pointer(out x, out y, out mask);
-
- int new_value = (int) vadj.get_value();
- switch (get_adjustment_relation(vadj, y)) {
- case AdjustmentRelation.BELOW:
- // pointer above window, scroll up
- new_value -= AUTOSCROLL_PIXELS;
- layout.set_drag_select_endpoint(x, new_value);
- break;
-
- case AdjustmentRelation.ABOVE:
- // pointer below window, scroll down, extend selection to bottom of page
- new_value += AUTOSCROLL_PIXELS;
- layout.set_drag_select_endpoint(x, new_value + (int) vadj.get_page_size());
- break;
-
- case AdjustmentRelation.IN_RANGE:
- autoscroll_scheduled = false;
-
- return false;
-
- default:
- warn_if_reached();
- break;
- }
-
- // It appears that in GTK+ 2.18, the adjustment is not clamped the way it was in 2.16.
- // This may have to do with how adjustments are different w/ scrollbars, that they're upper
- // clamp is upper - page_size ... either way, enforce these limits here
- vadj.set_value(new_value.clamp((int) vadj.get_lower(),
- (int) vadj.get_upper() - (int) vadj.get_page_size()));
-
- updated_selection_band();
-
- return true;
- }
-
- public void cursor_to_item(CheckerboardItem item) {
- assert(get_view().contains(item));
-
- cursor = item;
-
- if (!get_ctrl_pressed()) {
- get_view().unselect_all();
- Marker marker = get_view().mark(item);
- get_view().select_marked(marker);
- }
- layout.set_cursor(item);
-
- // if item is in any way out of view, scroll to it
- Gtk.Adjustment vadj = get_vadjustment();
- if (get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
- && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == AdjustmentRelation.IN_RANGE))
- return;
-
- // scroll to see the new item
- int top = 0;
- if (item.allocation.y < vadj.get_value()) {
- top = item.allocation.y;
- top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
- } else {
- top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
- top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
- }
-
- vadj.set_value(top);
- }
-
- public void move_cursor(CompassPoint point) {
- // if no items, nothing to do
- if (get_view().get_count() == 0)
- return;
-
- // if there is no better starting point, simply select the first and exit
- // The right half of the or is related to Bug #732334, the cursor might be non-null and still not contained in
- // the view, if the user dragged a full screen Photo off screen
- if (cursor == null && layout.get_cursor() == null || cursor != null && !get_view().contains(cursor)) {
- CheckerboardItem item = layout.get_item_at_coordinate(0, 0);
- cursor_to_item(item);
- anchor = item;
-
- return;
- }
-
- if (cursor == null) {
- cursor = layout.get_cursor() as CheckerboardItem;
- }
-
- // move the cursor relative to the "first" item
- CheckerboardItem? item = layout.get_item_relative_to(cursor, point);
- if (item != null)
- cursor_to_item(item);
- }
-
- public void set_cursor(CheckerboardItem item) {
- Marker marker = get_view().mark(item);
- get_view().select_marked(marker);
-
- cursor = item;
- anchor = item;
- }
-
- public void select_between_items(CheckerboardItem item_start, CheckerboardItem item_end) {
- Marker marker = get_view().start_marking();
-
- bool passed_start = false;
- bool passed_end = false;
-
- foreach (DataObject object in get_view().get_all()) {
- CheckerboardItem item = (CheckerboardItem) object;
-
- if (item_start == item)
- passed_start = true;
-
- if (item_end == item)
- passed_end = true;
-
- if (passed_start || passed_end)
- marker.mark((DataView) object);
-
- if (passed_start && passed_end)
- break;
- }
-
- get_view().select_marked(marker);
- }
-
- public void select_anchor_to_cursor(uint state) {
- if (cursor == null || anchor == null)
- return;
-
- if (state == Gdk.ModifierType.SHIFT_MASK) {
- get_view().unselect_all();
- select_between_items(anchor, cursor);
- } else {
- anchor = cursor;
- }
- }
-
- protected virtual void set_display_titles(bool display) {
- get_view().freeze_notifications();
- get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, display);
- get_view().thaw_notifications();
- }
-
- protected virtual void set_display_comments(bool display) {
- get_view().freeze_notifications();
- get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, display);
- get_view().thaw_notifications();
- }
-}
-
-public abstract class SinglePhotoPage : Page {
- public const Gdk.InterpType FAST_INTERP = Gdk.InterpType.NEAREST;
- public const Gdk.InterpType QUALITY_INTERP = Gdk.InterpType.BILINEAR;
- public const int KEY_REPEAT_INTERVAL_MSEC = 200;
-
- public enum UpdateReason {
- NEW_PIXBUF,
- QUALITY_IMPROVEMENT,
- RESIZED_CANVAS
- }
-
- protected Gtk.DrawingArea canvas = new Gtk.DrawingArea();
- protected Gtk.Viewport viewport = new Gtk.Viewport(null, null);
-
- private bool scale_up_to_viewport;
- private TransitionClock transition_clock;
- private int transition_duration_msec = 0;
- private Cairo.Surface pixmap = null;
- private Cairo.Context pixmap_ctx = null;
- private Cairo.Context text_ctx = null;
- private Dimensions pixmap_dim = Dimensions();
- private Gdk.Pixbuf unscaled = null;
- private Dimensions max_dim = Dimensions();
- private Gdk.Pixbuf scaled = null;
- private Gdk.Pixbuf old_scaled = null; // previous scaled image
- private Gdk.Rectangle scaled_pos = Gdk.Rectangle();
- private ZoomState static_zoom_state;
- private bool zoom_high_quality = true;
- private ZoomState saved_zoom_state;
- private bool has_saved_zoom_state = false;
- private uint32 last_nav_key = 0;
-
- protected SinglePhotoPage(string page_name, bool scale_up_to_viewport) {
- base(page_name);
-
- this.scale_up_to_viewport = scale_up_to_viewport;
-
- transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
-
- // With the current code automatically resizing the image to the viewport, scrollbars
- // should never be shown, but this may change if/when zooming is supported
- set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
-
- set_border_width(0);
- set_shadow_type(Gtk.ShadowType.NONE);
-
- viewport.set_shadow_type(Gtk.ShadowType.NONE);
- viewport.set_border_width(0);
- viewport.add(canvas);
-
- add(viewport);
-
- canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK
- | Gdk.EventMask.SUBSTRUCTURE_MASK);
-
- viewport.size_allocate.connect(on_viewport_resize);
- canvas.draw.connect(on_canvas_exposed);
-
- set_event_source(canvas);
- Config.Facade.get_instance().colors_changed.connect(on_colors_changed);
- }
-
- ~SinglePhotoPage() {
- Config.Facade.get_instance().colors_changed.disconnect(on_colors_changed);
- }
-
- public bool is_transition_in_progress() {
- return transition_clock.is_in_progress();
- }
-
- public void cancel_transition() {
- if (transition_clock.is_in_progress())
- transition_clock.cancel();
- }
-
- public void set_transition(string effect_id, int duration_msec) {
- cancel_transition();
-
- transition_clock = TransitionEffectsManager.get_instance().create_transition_clock(effect_id);
- if (transition_clock == null)
- transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
-
- transition_duration_msec = duration_msec;
- }
-
- // This method includes a call to pixmap_ctx.paint().
- private void render_zoomed_to_pixmap(ZoomState zoom_state) {
- assert(is_zoom_supported());
-
- Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle_wrt_content();
-
- Gdk.Pixbuf zoomed;
- if (get_zoom_buffer() != null) {
- zoomed = (zoom_high_quality) ? get_zoom_buffer().get_zoomed_image(zoom_state) :
- get_zoom_buffer().get_zoom_preview_image(zoom_state);
- } else {
- Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(unscaled);
-
- Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(unscaled, view_rect_proj.x,
- view_rect_proj.y, view_rect_proj.width, view_rect_proj.height);
-
- zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height,
- Gdk.InterpType.BILINEAR);
- }
-
- if (zoomed == null) {
- return;
- }
-
- int draw_x = (pixmap_dim.width - view_rect.width) / 2;
- draw_x = draw_x.clamp(0, int.MAX);
-
- int draw_y = (pixmap_dim.height - view_rect.height) / 2;
- draw_y = draw_y.clamp(0, int.MAX);
- paint_pixmap_with_background(pixmap_ctx, zoomed, draw_x, draw_y);
- }
-
- protected void on_interactive_zoom(ZoomState interactive_zoom_state) {
- assert(is_zoom_supported());
-
- set_source_color_from_string(pixmap_ctx, "#000");
- pixmap_ctx.paint();
-
- bool old_quality_setting = zoom_high_quality;
- zoom_high_quality = false;
- render_zoomed_to_pixmap(interactive_zoom_state);
- zoom_high_quality = old_quality_setting;
-
- canvas.queue_draw();
- }
-
- protected void on_interactive_pan(ZoomState interactive_zoom_state) {
- assert(is_zoom_supported());
-
- set_source_color_from_string(pixmap_ctx, "#000");
- pixmap_ctx.paint();
-
- bool old_quality_setting = zoom_high_quality;
- zoom_high_quality = true;
- render_zoomed_to_pixmap(interactive_zoom_state);
- zoom_high_quality = old_quality_setting;
-
- canvas.queue_draw();
- }
-
- protected virtual bool is_zoom_supported() {
- return false;
- }
-
- protected virtual void cancel_zoom() {
- if (pixmap != null) {
- set_source_color_from_string(pixmap_ctx, "#000");
- pixmap_ctx.paint();
- }
- }
-
- protected virtual void save_zoom_state() {
- saved_zoom_state = static_zoom_state;
- has_saved_zoom_state = true;
- }
-
- protected virtual void restore_zoom_state() {
- if (!has_saved_zoom_state)
- return;
-
- static_zoom_state = saved_zoom_state;
- repaint();
- has_saved_zoom_state = false;
- }
-
- protected virtual ZoomBuffer? get_zoom_buffer() {
- return null;
- }
-
- protected ZoomState get_saved_zoom_state() {
- return saved_zoom_state;
- }
-
- protected void set_zoom_state(ZoomState zoom_state) {
- assert(is_zoom_supported());
-
- static_zoom_state = zoom_state;
- }
-
- protected ZoomState get_zoom_state() {
- assert(is_zoom_supported());
-
- return static_zoom_state;
- }
-
- public override void switched_to() {
- base.switched_to();
-
- if (unscaled != null)
- repaint();
- }
-
- public override void set_container(Gtk.Window container) {
- base.set_container(container);
-
- // scrollbar policy in fullscreen mode needs to be auto/auto, else the pixbuf will shift
- // off the screen
- if (container is FullscreenWindow)
- set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
- }
-
- // max_dim represents the maximum size of the original pixbuf (i.e. pixbuf may be scaled and
- // the caller capable of producing larger ones depending on the viewport size). max_dim
- // is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if
- // max_dim should be ignored (i.e. scale_up_to_viewport is false).
- public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim, Direction? direction = null) {
- static_zoom_state = ZoomState(max_dim, pixmap_dim,
- static_zoom_state.get_interpolation_factor(),
- static_zoom_state.get_viewport_center());
-
- cancel_transition();
-
- this.unscaled = unscaled;
- this.max_dim = max_dim;
- this.old_scaled = scaled;
- scaled = null;
-
- // need to make sure this has happened
- canvas.realize();
-
- repaint(direction);
- }
-
- public void blank_display() {
- unscaled = null;
- max_dim = Dimensions();
- scaled = null;
- pixmap = null;
-
- // this has to have happened
- canvas.realize();
-
- // force a redraw
- invalidate_all();
- }
-
- public Cairo.Surface? get_surface() {
- return pixmap;
- }
-
- public Dimensions get_surface_dim() {
- return pixmap_dim;
- }
-
- public Cairo.Context get_cairo_context() {
- return pixmap_ctx;
- }
-
- public void paint_text(Pango.Layout pango_layout, int x, int y) {
- text_ctx.move_to(x, y);
- Pango.cairo_show_layout(text_ctx, pango_layout);
- }
-
- public Scaling get_canvas_scaling() {
- return (get_container() is FullscreenWindow) ? Scaling.for_screen(AppWindow.get_instance(), scale_up_to_viewport)
- : Scaling.for_widget(viewport, scale_up_to_viewport);
- }
-
- public Gdk.Pixbuf? get_unscaled_pixbuf() {
- return unscaled;
- }
-
- public Gdk.Pixbuf? get_scaled_pixbuf() {
- return scaled;
- }
-
- // Returns a rectangle describing the pixbuf in relation to the canvas
- public Gdk.Rectangle get_scaled_pixbuf_position() {
- return scaled_pos;
- }
-
- public bool is_inside_pixbuf(int x, int y) {
- return coord_in_rectangle(x, y, scaled_pos);
- }
-
- public void invalidate(Gdk.Rectangle rect) {
- if (canvas.get_window() != null)
- canvas.get_window().invalidate_rect(rect, false);
- }
-
- public void invalidate_all() {
- if (canvas.get_window() != null)
- canvas.get_window().invalidate_rect(null, false);
- }
-
- private void on_viewport_resize() {
- // do fast repaints while resizing
- internal_repaint(true, null);
- }
-
- protected override void on_resize_finished(Gdk.Rectangle rect) {
- base.on_resize_finished(rect);
-
- // when the resize is completed, do a high-quality repaint
- repaint();
- }
-
- private bool on_canvas_exposed(Cairo.Context exposed_ctx) {
- // draw pixmap onto canvas unless it's not been instantiated, in which case draw black
- // (so either old image or contents of another page is not left on screen)
- if (pixmap != null)
- exposed_ctx.set_source_surface(pixmap, 0, 0);
- else
- set_source_color_from_string(exposed_ctx, "#000");
-
- exposed_ctx.rectangle(0, 0, get_allocated_width(), get_allocated_height());
- exposed_ctx.paint();
-
- return true;
- }
-
- protected virtual void new_surface(Cairo.Context ctx, Dimensions ctx_dim) {
- }
-
- protected virtual void updated_pixbuf(Gdk.Pixbuf pixbuf, UpdateReason reason, Dimensions old_dim) {
- }
-
- protected virtual void paint(Cairo.Context ctx, Dimensions ctx_dim) {
- if (is_zoom_supported() && (!static_zoom_state.is_default())) {
- set_source_color_from_string(ctx, "#000");
- ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
- ctx.fill();
-
- render_zoomed_to_pixmap(static_zoom_state);
- } else if (!transition_clock.paint(ctx, ctx_dim.width, ctx_dim.height)) {
- // transition is not running, so paint the full image on a black background
- set_source_color_from_string(ctx, "#000");
-
- ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
- ctx.fill();
-
- paint_pixmap_with_background(ctx, scaled, scaled_pos.x, scaled_pos.y);
- }
- }
-
- private void repaint_pixmap() {
- if (pixmap_ctx == null)
- return;
-
- paint(pixmap_ctx, pixmap_dim);
- invalidate_all();
- }
-
- public void repaint(Direction? direction = null) {
- internal_repaint(false, direction);
- }
-
- private void internal_repaint(bool fast, Direction? direction) {
- // if not in view, assume a full repaint needed in future but do nothing more
- if (!is_in_view()) {
- pixmap = null;
- scaled = null;
-
- return;
- }
-
- // no image or window, no painting
- if (unscaled == null || canvas.get_window() == null)
- return;
-
- Gtk.Allocation allocation;
- viewport.get_allocation(out allocation);
-
- int width = allocation.width;
- int height = allocation.height;
-
- if (width <= 0 || height <= 0)
- return;
-
- bool new_pixbuf = (scaled == null);
-
- // save if reporting an image being rescaled
- Dimensions old_scaled_dim = Dimensions.for_rectangle(scaled_pos);
- Gdk.Rectangle old_scaled_pos = scaled_pos;
-
- // attempt to reuse pixmap
- if (pixmap_dim.width != width || pixmap_dim.height != height)
- pixmap = null;
-
- // if necessary, create a pixmap as large as the entire viewport
- bool new_pixmap = false;
- if (pixmap == null) {
- init_pixmap(width, height);
- new_pixmap = true;
- }
-
- if (new_pixbuf || new_pixmap) {
- Dimensions unscaled_dim = Dimensions.for_pixbuf(unscaled);
-
- // determine scaled size of pixbuf ... if a max dimensions is set and not scaling up,
- // respect it
- Dimensions scaled_dim = Dimensions();
- if (!scale_up_to_viewport && max_dim.has_area() && max_dim.width < width && max_dim.height < height)
- scaled_dim = max_dim;
- else
- scaled_dim = unscaled_dim.get_scaled_proportional(pixmap_dim);
-
- assert(width >= scaled_dim.width);
- assert(height >= scaled_dim.height);
-
- // center pixbuf on the canvas
- scaled_pos.x = (width - scaled_dim.width) / 2;
- scaled_pos.y = (height - scaled_dim.height) / 2;
- scaled_pos.width = scaled_dim.width;
- scaled_pos.height = scaled_dim.height;
- }
-
- Gdk.InterpType interp = (fast) ? FAST_INTERP : QUALITY_INTERP;
-
- // rescale if canvas rescaled or better quality is requested
- if (scaled == null) {
- scaled = resize_pixbuf(unscaled, Dimensions.for_rectangle(scaled_pos), interp);
-
- UpdateReason reason = UpdateReason.RESIZED_CANVAS;
- if (new_pixbuf)
- reason = UpdateReason.NEW_PIXBUF;
- else if (!new_pixmap && interp == QUALITY_INTERP)
- reason = UpdateReason.QUALITY_IMPROVEMENT;
-
- static_zoom_state = ZoomState(max_dim, pixmap_dim,
- static_zoom_state.get_interpolation_factor(),
- static_zoom_state.get_viewport_center());
-
- updated_pixbuf(scaled, reason, old_scaled_dim);
- }
-
- zoom_high_quality = !fast;
-
- if (direction != null && !transition_clock.is_in_progress()) {
- Spit.Transitions.Visuals visuals = new Spit.Transitions.Visuals(old_scaled,
- old_scaled_pos, scaled, scaled_pos, parse_color("#000"));
-
- transition_clock.start(visuals, direction.to_transition_direction(), transition_duration_msec,
- repaint_pixmap);
- }
-
- if (!transition_clock.is_in_progress())
- repaint_pixmap();
- }
-
- private void init_pixmap(int width, int height) {
- assert(unscaled != null);
- assert(canvas.get_window() != null);
-
- // Cairo backing surface (manual double-buffering)
- pixmap = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
- pixmap_dim = Dimensions(width, height);
-
- // Cairo context for drawing on the pixmap
- pixmap_ctx = new Cairo.Context(pixmap);
-
- // need a new pixbuf to fit this scale
- scaled = null;
-
- // Cairo context for drawing text on the pixmap
- text_ctx = new Cairo.Context(pixmap);
- set_source_color_from_string(text_ctx, "#fff");
-
-
- // no need to resize canvas, viewport does that automatically
-
- new_surface(pixmap_ctx, pixmap_dim);
- }
-
- protected override bool on_context_keypress() {
- return popup_context_menu(get_page_context_menu());
- }
-
- protected virtual void on_previous_photo() {
- }
-
- protected virtual void on_next_photo() {
- }
-
- public override bool key_press_event(Gdk.EventKey event) {
- // if the user holds the arrow keys down, we will receive a steady stream of key press
- // events for an operation that isn't designed for a rapid succession of output ...
- // we staunch the supply of new photos to under a quarter second (#533)
- bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC;
-
- bool handled = true;
- switch (Gdk.keyval_name(event.keyval)) {
- case "Left":
- case "KP_Left":
- case "BackSpace":
- if (nav_ok) {
- on_previous_photo();
- last_nav_key = event.time;
- }
- break;
-
- case "Right":
- case "KP_Right":
- case "space":
- if (nav_ok) {
- on_next_photo();
- last_nav_key = event.time;
- }
- break;
-
- default:
- handled = false;
- break;
- }
-
- if (handled)
- return true;
-
- return (base.key_press_event != null) ? base.key_press_event(event) : true;
- }
-
- private void on_colors_changed() {
- invalidate_transparent_background();
- repaint();
- }
-}
-
-//
-// DragAndDropHandler attaches signals to a Page to properly handle drag-and-drop requests for the
-// Page as a DnD Source. (DnD Destination handling is handled by the appropriate AppWindow, i.e.
-// LibraryWindow and DirectWindow). Assumes the Page's ViewCollection holds MediaSources.
-//
-public class DragAndDropHandler {
- private enum TargetType {
- XDS,
- MEDIA_LIST
- }
-
- private const Gtk.TargetEntry[] SOURCE_TARGET_ENTRIES = {
- { "XdndDirectSave0", Gtk.TargetFlags.OTHER_APP, TargetType.XDS },
- { "shotwell/media-id-atom", Gtk.TargetFlags.SAME_APP, TargetType.MEDIA_LIST }
- };
-
- private static Gdk.Atom? XDS_ATOM = null;
- private static Gdk.Atom? TEXT_ATOM = null;
- private static uint8[]? XDS_FAKE_TARGET = null;
-
- private weak Page page;
- private Gtk.Widget event_source;
- private File? drag_destination = null;
- private ExporterUI exporter = null;
-
- public DragAndDropHandler(Page page) {
- this.page = page;
- this.event_source = page.get_event_source();
- assert(event_source != null);
- assert(event_source.get_has_window());
-
- // Need to do this because static member variables are not properly handled
- if (XDS_ATOM == null)
- XDS_ATOM = Gdk.Atom.intern_static_string("XdndDirectSave0");
-
- if (TEXT_ATOM == null)
- TEXT_ATOM = Gdk.Atom.intern_static_string("text/plain");
-
- if (XDS_FAKE_TARGET == null)
- XDS_FAKE_TARGET = string_to_uchar_array("shotwell.txt");
-
- // register what's available on this DnD Source
- Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES,
- Gdk.DragAction.COPY);
-
- // attach to the event source's DnD signals, not the Page's, which is a NO_WINDOW widget
- // and does not emit them
- event_source.drag_begin.connect(on_drag_begin);
- event_source.drag_data_get.connect(on_drag_data_get);
- event_source.drag_end.connect(on_drag_end);
- event_source.drag_failed.connect(on_drag_failed);
- }
-
- ~DragAndDropHandler() {
- if (event_source != null) {
- event_source.drag_begin.disconnect(on_drag_begin);
- event_source.drag_data_get.disconnect(on_drag_data_get);
- event_source.drag_end.disconnect(on_drag_end);
- event_source.drag_failed.disconnect(on_drag_failed);
- }
-
- page = null;
- event_source = null;
- }
-
- private void on_drag_begin(Gdk.DragContext context) {
- debug("on_drag_begin (%s)", page.get_page_name());
-
- if (page == null || page.get_view().get_selected_count() == 0 || exporter != null)
- return;
-
- drag_destination = null;
-
- // use the first media item as the icon
- ThumbnailSource thumb = (ThumbnailSource) page.get_view().get_selected_at(0).get_source();
-
- try {
- Gdk.Pixbuf icon = thumb.get_thumbnail(AppWindow.DND_ICON_SCALE);
- Gtk.drag_source_set_icon_pixbuf(event_source, icon);
- } catch (Error err) {
- warning("Unable to fetch icon for drag-and-drop from %s: %s", thumb.to_string(),
- err.message);
- }
-
- // set the XDS property to indicate an XDS save is available
-#if VALA_0_20
- Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
- XDS_FAKE_TARGET, 1);
-#else
- Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE,
- XDS_FAKE_TARGET);
-#endif
- }
-
- private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
- uint target_type, uint time) {
- debug("on_drag_data_get (%s)", page.get_page_name());
-
- if (page == null || page.get_view().get_selected_count() == 0)
- return;
-
- switch (target_type) {
- case TargetType.XDS:
- // Fetch the XDS property that has been set with the destination path
- uchar[] data = new uchar[4096];
- Gdk.Atom actual_type;
- int actual_format = 0;
- bool fetched = Gdk.property_get(context.get_source_window(), XDS_ATOM, TEXT_ATOM,
- 0, data.length, 0, out actual_type, out actual_format, out data);
-
- // the destination path is actually for our XDS_FAKE_TARGET, use its parent
- // to determine where the file(s) should go
- if (fetched && data != null && data.length > 0)
- drag_destination = File.new_for_uri(uchar_array_to_string(data)).get_parent();
-
- debug("on_drag_data_get (%s): %s", page.get_page_name(),
- (drag_destination != null) ? drag_destination.get_path() : "(no path)");
-
- // Set the property to "S" for Success or "E" for Error
- selection_data.set(XDS_ATOM, 8,
- string_to_uchar_array((drag_destination != null) ? "S" : "E"));
- break;
-
- case TargetType.MEDIA_LIST:
- Gee.Collection<MediaSource> sources =
- (Gee.Collection<MediaSource>) page.get_view().get_selected_sources();
-
- // convert the selected media sources to Gdk.Atom-encoded sourceID strings for
- // internal drag-and-drop
- selection_data.set(Gdk.Atom.intern_static_string("SourceIDAtom"), (int) sizeof(Gdk.Atom),
- serialize_media_sources(sources));
- break;
-
- default:
- warning("on_drag_data_get (%s): unknown target type %u", page.get_page_name(),
- target_type);
- break;
- }
- }
-
- private void on_drag_end() {
- debug("on_drag_end (%s)", page.get_page_name());
-
- if (page == null || page.get_view().get_selected_count() == 0 || drag_destination == null
- || exporter != null) {
- return;
- }
-
- debug("Exporting to %s", drag_destination.get_path());
-
- // 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
- // all transformations applied, at the image's original size).
- 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()));
- exporter.export(on_export_completed);
- } else {
- AppWindow.error_message(_("Photos cannot be exported to this directory."));
- }
-
- drag_destination = null;
- }
-
- private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
- debug("on_drag_failed (%s): %d", page.get_page_name(), (int) drag_result);
-
- if (page == null)
- return false;
-
- drag_destination = null;
-
- return false;
- }
-
- private void on_export_completed() {
- exporter = null;
- }
-
-}
diff --git a/src/PageMessagePane.vala b/src/PageMessagePane.vala
new file mode 100644
index 0000000..e773dad
--- /dev/null
+++ b/src/PageMessagePane.vala
@@ -0,0 +1,19 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+[GtkTemplate (ui = "/org/gnome/Shotwell/ui/message_pane.ui")]
+private class PageMessagePane : Gtk.Box {
+ [GtkChild]
+ public unowned Gtk.Label label;
+
+ [GtkChild]
+ public unowned Gtk.Image icon_image;
+
+ public PageMessagePane() {
+ Object();
+ }
+}
+
diff --git a/src/Photo.vala b/src/Photo.vala
index b67457e..f31a17d 100644
--- a/src/Photo.vala
+++ b/src/Photo.vala
@@ -155,7 +155,7 @@ public enum Rating {
// particular photo without modifying the backing image file. The interface allows for
// transformations to be stored persistently elsewhere or in memory until they're committed en
// masse to an image file.
-public abstract class Photo : PhotoSource, Dateable {
+public abstract class Photo : PhotoSource, Dateable, Positionable {
// Need to use "thumb" rather than "photo" for historical reasons -- this name is used
// directly to load thumbnails from disk by already-existing filenames
public const string TYPENAME = "thumb";
@@ -183,7 +183,7 @@ public abstract class Photo : PhotoSource, Dateable {
"pns", "jps", "mpo",
// RAW extensions
- "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cap", "iiq", "eip", "dcs", "dcr", "drf",
+ "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cr3", "cap", "iiq", "eip", "dcs", "dcr", "drf",
"k25", "kdc", "dng", "erf", "fff", "mef", "mos", "mrw", "nef", "nrw", "orf", "ptx", "pef",
"pxn", "r3d", "raf", "raw", "rw2", "rwl", "rwz", "x3f", "srw"
};
@@ -210,7 +210,7 @@ public abstract class Photo : PhotoSource, Dateable {
// Here, we cache the exposure time to avoid paying to access the row every time we
// need to know it. This is initially set in the constructor, and updated whenever
// the exposure time is set (please see set_exposure_time() for details).
- private time_t cached_exposure_time;
+ private DateTime? cached_exposure_time;
public enum Exception {
NONE = 0,
@@ -640,7 +640,7 @@ public abstract class Photo : PhotoSource, Dateable {
File file = File.new_for_path(bpr.filepath);
FileInfo info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
- TimeVal timestamp = info.get_modification_time();
+ var timestamp = info.get_modification_date_time();
PhotoFileInterrogator interrogator = new PhotoFileInterrogator(
file, PhotoFileSniffer.Options.GET_ALL);
@@ -655,7 +655,7 @@ public abstract class Photo : PhotoSource, Dateable {
bpr.dim = detected.image_dim;
bpr.filesize = info.get_size();
- bpr.timestamp = timestamp.tv_sec;
+ bpr.timestamp = timestamp;
bpr.original_orientation = detected.metadata != null ? detected.metadata.get_orientation() :
Orientation.TOP_LEFT;
@@ -832,7 +832,7 @@ public abstract class Photo : PhotoSource, Dateable {
if (!developments.has_key(d))
return; // we tried!
- // Disgard changes.
+ // Discard changes.
revert_to_master(false);
// Switch master to the new photo.
@@ -1185,7 +1185,7 @@ public abstract class Photo : PhotoSource, Dateable {
return ImportResult.UNSUPPORTED_FORMAT;
}
- TimeVal timestamp = info.get_modification_time();
+ var timestamp = info.get_modification_date_time();
// if all MD5s supplied, don't sniff for them
if (params.exif_md5 != null && params.thumbnail_md5 != null && params.full_md5 != null)
@@ -1217,8 +1217,9 @@ public abstract class Photo : PhotoSource, Dateable {
}
Orientation orientation = Orientation.TOP_LEFT;
- time_t exposure_time = 0;
+ DateTime? exposure_time = null;
string title = "";
+ GpsCoords gps_coords = GpsCoords();
string comment = "";
Rating rating = Rating.UNRATED;
@@ -1234,6 +1235,7 @@ public abstract class Photo : PhotoSource, Dateable {
orientation = detected.metadata.get_orientation();
title = detected.metadata.get_title();
+ gps_coords = detected.metadata.get_gps_coords();
comment = detected.metadata.get_comment();
params.keywords = detected.metadata.get_keywords();
rating = detected.metadata.get_rating();
@@ -1255,7 +1257,7 @@ public abstract class Photo : PhotoSource, Dateable {
params.row.master.filepath = file.get_path();
params.row.master.dim = detected.image_dim;
params.row.master.filesize = info.get_size();
- params.row.master.timestamp = timestamp.tv_sec;
+ params.row.master.timestamp = timestamp;
params.row.exposure_time = exposure_time;
params.row.orientation = orientation;
params.row.master.original_orientation = orientation;
@@ -1269,6 +1271,7 @@ public abstract class Photo : PhotoSource, Dateable {
params.row.flags = 0;
params.row.master.file_format = detected.file_format;
params.row.title = title;
+ params.row.gps_coords = gps_coords;
params.row.comment = comment;
params.row.rating = rating;
@@ -1296,8 +1299,8 @@ public abstract class Photo : PhotoSource, Dateable {
params.row.master.filepath = file.get_path();
params.row.master.dim = Dimensions(0,0);
params.row.master.filesize = 0;
- params.row.master.timestamp = 0;
- params.row.exposure_time = 0;
+ params.row.master.timestamp = null;
+ params.row.exposure_time = null;
params.row.orientation = Orientation.TOP_LEFT;
params.row.master.original_orientation = Orientation.TOP_LEFT;
params.row.import_id = params.import_id;
@@ -1310,6 +1313,7 @@ public abstract class Photo : PhotoSource, Dateable {
params.row.flags = 0;
params.row.master.file_format = PhotoFileFormat.JFIF;
params.row.title = null;
+ params.row.gps_coords = GpsCoords();
params.row.comment = null;
params.row.rating = Rating.UNRATED;
@@ -1350,10 +1354,10 @@ public abstract class Photo : PhotoSource, Dateable {
return null;
}
- TimeVal modification_time = info.get_modification_time();
+ var modification_time = info.get_modification_date_time();
backing.filepath = file.get_path();
- backing.timestamp = modification_time.tv_sec;
+ backing.timestamp = modification_time;
backing.filesize = info.get_size();
backing.file_format = detected.file_format;
backing.dim = detected.image_dim;
@@ -1462,14 +1466,22 @@ public abstract class Photo : PhotoSource, Dateable {
list += "image:orientation";
updated_row.master.original_orientation = backing.original_orientation;
}
-
+
+ GpsCoords gps_coords = GpsCoords();
+
if (detected.metadata != null) {
MetadataDateTime? date_time = detected.metadata.get_exposure_date_time();
- if (date_time != null && updated_row.exposure_time != date_time.get_timestamp())
+ if (date_time != null && updated_row.exposure_time != null &&
+ !updated_row.exposure_time.equal(date_time.get_timestamp()))
list += "metadata:exposure-time";
if (updated_row.title != detected.metadata.get_title())
list += "metadata:name";
+
+ gps_coords = detected.metadata.get_gps_coords();
+ if (updated_row.gps_coords != gps_coords)
+ list += "metadata:gps";
+
if (updated_row.comment != detected.metadata.get_comment())
list += "metadata:comment";
@@ -1490,7 +1502,8 @@ public abstract class Photo : PhotoSource, Dateable {
MetadataDateTime? date_time = detected.metadata.get_exposure_date_time();
if (date_time != null)
updated_row.exposure_time = date_time.get_timestamp();
-
+
+ updated_row.gps_coords = gps_coords;
updated_row.title = detected.metadata.get_title();
updated_row.comment = detected.metadata.get_comment();
updated_row.rating = detected.metadata.get_rating();
@@ -1601,6 +1614,7 @@ public abstract class Photo : PhotoSource, Dateable {
if (reimport_state.metadata != null) {
set_title(reimport_state.metadata.get_title());
+ set_gps_coords(reimport_state.metadata.get_gps_coords());
set_comment(reimport_state.metadata.get_comment());
set_rating(reimport_state.metadata.get_rating());
apply_user_metadata_for_reimport(reimport_state.metadata);
@@ -1695,17 +1709,17 @@ public abstract class Photo : PhotoSource, Dateable {
// Use this only if the master file's modification time has been changed (i.e. touched)
public void set_master_timestamp(FileInfo info) {
- TimeVal modification = info.get_modification_time();
+ var modification = info.get_modification_date_time();
try {
lock (row) {
- if (row.master.timestamp == modification.tv_sec)
+ if (row.master.timestamp.equal(modification))
return;
- PhotoTable.get_instance().update_timestamp(row.photo_id, modification.tv_sec);
- row.master.timestamp = modification.tv_sec;
+ PhotoTable.get_instance().update_timestamp(row.photo_id, modification);
+ row.master.timestamp = modification;
}
- } catch (DatabaseError err) {
+ } catch (Error err) {
AppWindow.database_error(err);
return;
@@ -1718,15 +1732,15 @@ public abstract class Photo : PhotoSource, Dateable {
}
// Use this only if the editable file's modification time has been changed (i.e. touched)
- public void update_editable_modification_time(FileInfo info) throws DatabaseError {
- TimeVal modification = info.get_modification_time();
+ public void update_editable_modification_time(FileInfo info) throws Error {
+ var modification = info.get_modification_date_time();
bool altered = false;
lock (row) {
- if (row.editable_id.is_valid() && editable.timestamp != modification.tv_sec) {
+ if (row.editable_id.is_valid() && !editable.timestamp.equal(modification)) {
BackingPhotoTable.get_instance().update_timestamp(row.editable_id,
- modification.tv_sec);
- editable.timestamp = modification.tv_sec;
+ modification);
+ editable.timestamp = modification;
altered = true;
}
}
@@ -1739,8 +1753,13 @@ public abstract class Photo : PhotoSource, Dateable {
public static void update_many_editable_timestamps(Gee.Map<Photo, FileInfo> map)
throws DatabaseError {
DatabaseTable.begin_transaction();
- foreach (Photo photo in map.keys)
- photo.update_editable_modification_time(map.get(photo));
+ foreach (Photo photo in map.keys) {
+ try {
+ photo.update_editable_modification_time(map.get(photo));
+ } catch (Error err) {
+ debug("Failed to update modification time: %s", err.message);
+ }
+ }
DatabaseTable.commit_transaction();
}
@@ -1853,7 +1872,7 @@ public abstract class Photo : PhotoSource, Dateable {
}
}
}
- } catch (DatabaseError err) {
+ } catch (Error err) {
AppWindow.database_error(err);
}
@@ -1906,7 +1925,7 @@ public abstract class Photo : PhotoSource, Dateable {
}
}
}
- } catch (DatabaseError err) {
+ } catch (Error err) {
AppWindow.database_error(err);
}
@@ -1993,7 +2012,7 @@ public abstract class Photo : PhotoSource, Dateable {
}
}
- public override time_t get_timestamp() {
+ public override DateTime? get_timestamp() {
lock (row) {
return backing_photo_row.timestamp;
}
@@ -2169,7 +2188,7 @@ public abstract class Photo : PhotoSource, Dateable {
}
}
- public void set_master_metadata_dirty(bool dirty) throws DatabaseError {
+ public void set_master_metadata_dirty(bool dirty) throws Error {
bool committed = false;
lock (row) {
if (row.metadata_dirty != dirty) {
@@ -2277,7 +2296,7 @@ public abstract class Photo : PhotoSource, Dateable {
error("Unable to read file information for %s: %s", to_string(), err.message);
}
- TimeVal timestamp = info.get_modification_time();
+ var timestamp = info.get_modification_date_time();
// interrogate file for photo information
PhotoFileInterrogator interrogator = new PhotoFileInterrogator(file);
@@ -2297,7 +2316,7 @@ public abstract class Photo : PhotoSource, Dateable {
bool success;
lock (row) {
success = PhotoTable.get_instance().master_exif_updated(get_photo_id(), info.get_size(),
- timestamp.tv_sec, detected.md5, detected.exif_md5, detected.thumbnail_md5, row);
+ timestamp, detected.md5, detected.exif_md5, detected.thumbnail_md5, row);
}
if (success)
@@ -2324,7 +2343,7 @@ public abstract class Photo : PhotoSource, Dateable {
}
}
- public override time_t get_exposure_time() {
+ public override DateTime? get_exposure_time() {
return cached_exposure_time;
}
@@ -2362,6 +2381,29 @@ public abstract class Photo : PhotoSource, Dateable {
if (committed)
notify_altered(new Alteration("metadata", "name"));
}
+
+ public GpsCoords get_gps_coords() {
+ lock (row) {
+ return row.gps_coords;
+ }
+ }
+
+ public void set_gps_coords(GpsCoords gps_coords) {
+ DatabaseError dberr = null;
+ lock (row) {
+ try {
+ PhotoTable.get_instance().set_gps_coords(row.photo_id, gps_coords);
+ row.gps_coords = gps_coords;
+ } catch (DatabaseError err) {
+ dberr = err;
+ }
+ }
+ if (dberr == null)
+ notify_altered(new Alteration("metadata", "gps"));
+ else
+ warning("Unable to write gps coordinates for %s: %s", to_string(), dberr.message);
+ }
+
public override bool set_comment(string? comment) {
string? new_comment = prep_comment(comment);
@@ -2455,7 +2497,7 @@ public abstract class Photo : PhotoSource, Dateable {
file_exif_updated();
}
- public void set_exposure_time(time_t time) {
+ public void set_exposure_time(DateTime time) {
bool committed;
lock (row) {
committed = PhotoTable.get_instance().set_exposure_time(row.photo_id, time);
@@ -2469,7 +2511,7 @@ public abstract class Photo : PhotoSource, Dateable {
notify_altered(new Alteration("metadata", "exposure-time"));
}
- public void set_exposure_time_persistent(time_t time) throws Error {
+ public void set_exposure_time_persistent(DateTime time) throws Error {
PhotoFileReader source = get_source_reader();
// Try to write to backing file
@@ -2742,7 +2784,8 @@ public abstract class Photo : PhotoSource, Dateable {
lock (row) {
return row.transformations == null
&& (row.orientation != backing_photo_row.original_orientation
- || (date_time != null && row.exposure_time != date_time.get_timestamp()));
+ || (date_time != null && row.exposure_time != null &&
+ !row.exposure_time.equal(date_time.get_timestamp())));
}
}
@@ -2763,7 +2806,7 @@ public abstract class Photo : PhotoSource, Dateable {
// No, use file timestamp as date/time.
lock (row) {
// Did we manually set an exposure date?
- if(backing_photo_row.timestamp != row.exposure_time) {
+ if(nullsafe_date_time_comperator(backing_photo_row.timestamp, row.exposure_time) != 0) {
// Yes, we need to save this.
return true;
}
@@ -2773,7 +2816,7 @@ public abstract class Photo : PhotoSource, Dateable {
lock (row) {
return row.transformations != null
|| row.orientation != backing_photo_row.original_orientation
- || (date_time != null && row.exposure_time != date_time.get_timestamp())
+ || (date_time != null && !row.exposure_time.equal(date_time.get_timestamp()))
|| (get_comment() != comment)
|| (get_title() != title);
}
@@ -3212,6 +3255,7 @@ public abstract class Photo : PhotoSource, Dateable {
double orientation_time = 0.0;
total_timer.start();
+
#endif
// get required fields all at once, to avoid holding the row lock
@@ -3618,7 +3662,7 @@ public abstract class Photo : PhotoSource, Dateable {
debug("Updating metadata of %s", writer.get_filepath());
- if (get_exposure_time() != 0)
+ if (get_exposure_time() != null)
metadata.set_exposure_date_time(new MetadataDateTime(get_exposure_time()));
else
metadata.set_exposure_date_time(null);
@@ -3714,7 +3758,7 @@ public abstract class Photo : PhotoSource, Dateable {
metadata.set_comment(get_comment());
metadata.set_software(Resources.APP_TITLE, Resources.APP_VERSION);
- if (get_exposure_time() != 0)
+ if (get_exposure_time() != null)
metadata.set_exposure_date_time(new MetadataDateTime(get_exposure_time()));
else
metadata.set_exposure_date_time(null);
@@ -3970,15 +4014,15 @@ public abstract class Photo : PhotoSource, Dateable {
return;
}
- TimeVal timestamp = info.get_modification_time();
+ var timestamp = info.get_modification_date_time();
- BackingPhotoTable.get_instance().update_attributes(editable_id, timestamp.tv_sec,
+ BackingPhotoTable.get_instance().update_attributes(editable_id, timestamp,
info.get_size());
lock (row) {
- timestamp_changed = editable.timestamp != timestamp.tv_sec;
+ timestamp_changed = !editable.timestamp.equal(timestamp);
filesize_changed = editable.filesize != info.get_size();
- editable.timestamp = timestamp.tv_sec;
+ editable.timestamp = timestamp;
editable.filesize = info.get_size();
}
} else {
@@ -4057,7 +4101,7 @@ public abstract class Photo : PhotoSource, Dateable {
PhotoTable.get_instance().detach_editable(row);
backing_photo_row = row.master;
}
- } catch (DatabaseError err) {
+ } catch (Error err) {
warning("Unable to remove editable from PhotoTable: %s", err.message);
}
@@ -4976,7 +5020,12 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
this.import_keywords = null;
thumbnail_scheduler = new OneShotScheduler("LibraryPhoto", generate_thumbnails);
-
+ // import gps coords of photos imported with prior versions of shotwell
+ if (row.gps_coords.has_gps == -1) {
+ var gps_import_scheduler = new OneShotScheduler("LibraryPhoto", import_gps_metadata);
+ gps_import_scheduler.at_priority_idle(Priority.LOW);
+ }
+
// if marked in a state where they're held in an orphanage, rehydrate their backlinks
if ((row.flags & (FLAG_TRASH | FLAG_OFFLINE)) != 0)
rehydrate_backlinks(global, row.backlinks);
@@ -5097,7 +5146,12 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
// fire signal that thumbnails have changed
notify_thumbnail_altered();
}
-
+
+ private void import_gps_metadata() {
+ GpsCoords gps_coords = get_metadata().get_gps_coords();
+ set_gps_coords(gps_coords);
+ }
+
// These keywords are only used during import and should not be relied upon elsewhere.
public Gee.Collection<string>? get_import_keywords() {
return import_keywords;
@@ -5218,7 +5272,7 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
if (location != null) {
face.attach(dupe);
FaceLocation.create(face.get_face_id(), dupe.get_photo_id(),
- location.get_serialized_geometry());
+ location.get_face_data());
}
}
}
@@ -5332,10 +5386,14 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
PhotoMetadata? metadata = get_metadata();
if (metadata == null)
- return tags != null || tags.size > 0 || get_rating() != Rating.UNRATED;
+ return tags != null || tags.size > 0 || get_rating() != Rating.UNRATED || get_gps_coords().has_gps != 0;
if (get_rating() != metadata.get_rating())
return true;
+
+ var old_coords = metadata.get_gps_coords();
+ if (!get_gps_coords().equals(ref old_coords))
+ return true;
Gee.Set<string>? keywords = metadata.get_keywords();
int tags_count = (tags != null) ? tags.size : 0;
@@ -5366,6 +5424,7 @@ public class LibraryPhoto : Photo, Flaggable, Monitorable {
metadata.set_keywords(null);
metadata.set_rating(get_rating());
+ metadata.set_gps_coords(get_gps_coords());
}
protected override void apply_user_metadata_for_reimport(PhotoMetadata metadata) {
diff --git a/src/PhotoPage.vala b/src/PhotoPage.vala
index fd513b2..10ebb10 100644
--- a/src/PhotoPage.vala
+++ b/src/PhotoPage.vala
@@ -759,7 +759,7 @@ public abstract class EditingHostPage : SinglePhotoPage {
return false;
zoom_about_event_cursor_point(event, ZOOM_INCREMENT_SIZE);
- return false;
+ return true;
}
protected override bool on_mousewheel_down(Gdk.EventScroll event) {
@@ -767,7 +767,7 @@ public abstract class EditingHostPage : SinglePhotoPage {
return false;
zoom_about_event_cursor_point(event, -ZOOM_INCREMENT_SIZE);
- return false;
+ return true;
}
protected override void restore_zoom_state() {
@@ -1466,8 +1466,9 @@ public abstract class EditingHostPage : SinglePhotoPage {
return;
}
- if (unscaled != null)
+ if (unscaled != null) {
set_pixbuf(unscaled, max_dim);
+ }
// create the PhotoCanvas object for a two-way interface to the tool
EditingTools.PhotoCanvas photo_canvas = new EditingHostCanvas(this);
@@ -1528,8 +1529,9 @@ public abstract class EditingHostPage : SinglePhotoPage {
needs_improvement = true;
}
- if (replacement != null)
+ if (replacement != null) {
set_pixbuf(replacement, new_max_dim);
+ }
cancel_editing_pixbuf = null;
// if this is a rough pixbuf, schedule an improvement
@@ -2458,7 +2460,7 @@ public class LibraryPhotoPage : EditingHostPage {
base.add_actions (map);
map.add_action_entries (entries, this);
- (get_action ("ViewRatings") as GLib.SimpleAction).change_state (Config.Facade.get_instance ().get_display_photo_ratings ());
+ ((GLib.SimpleAction) get_action ("ViewRatings")).change_state (Config.Facade.get_instance ().get_display_photo_ratings ());
var d = Config.Facade.get_instance().get_default_raw_developer();
var action = new GLib.SimpleAction.stateful("RawDeveloper",
GLib.VariantType.STRING, d == RawDeveloper.SHOTWELL ? "Shotwell" : "Camera");
@@ -2888,7 +2890,8 @@ public class LibraryPhotoPage : EditingHostPage {
Gee.Collection<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
photos.add(photo);
- remove_from_app(photos, _("Remove From Library"), _("Removing Photo From Library"));
+ remove_from_app(photos, GLib.dpgettext2(null, "Dialog Title", "Remove From Library"),
+ GLib.dpgettext2(null, "Dialog Title", "Removing Photo From Library"));
}
private void on_move_to_trash() {
@@ -3012,7 +3015,7 @@ public class LibraryPhotoPage : EditingHostPage {
if (!has_photo())
return;
- ExportDialog export_dialog = new ExportDialog(_("Export Photo"));
+ ExportDialog export_dialog = new ExportDialog(GLib.dpgettext2(null, "Dialog Title", "Export Photo"));
int scale;
ScaleConstraint constraint;
@@ -3171,7 +3174,7 @@ public class LibraryPhotoPage : EditingHostPage {
}
protected override void insert_faces_button(Gtk.Toolbar toolbar) {
- faces_button = new Gtk.ToggleToolButton.from_stock(Resources.FACES_TOOL);
+ faces_button = new Gtk.ToggleToolButton();
faces_button.set_icon_name(Resources.ICON_FACES);
faces_button.set_label(Resources.FACES_LABEL);
faces_button.set_tooltip_text(Resources.FACES_TOOLTIP);
diff --git a/src/Printing.vala b/src/Printing.vala
index 988a456..bef3476 100644
--- a/src/Printing.vala
+++ b/src/Printing.vala
@@ -271,29 +271,29 @@ public class CustomPrintTab : Gtk.Box {
private const int CENTIMETERS_COMBO_CHOICE = 1;
[GtkChild]
- private Gtk.RadioButton standard_size_radio;
+ private unowned Gtk.RadioButton standard_size_radio;
[GtkChild]
- private Gtk.RadioButton custom_size_radio;
+ private unowned Gtk.RadioButton custom_size_radio;
[GtkChild]
- private Gtk.RadioButton image_per_page_radio;
+ private unowned Gtk.RadioButton image_per_page_radio;
[GtkChild]
- private Gtk.ComboBoxText image_per_page_combo;
+ private unowned Gtk.ComboBoxText image_per_page_combo;
[GtkChild]
- private Gtk.ComboBoxText standard_sizes_combo;
+ private unowned Gtk.ComboBoxText standard_sizes_combo;
[GtkChild]
- private Gtk.ComboBoxText units_combo;
+ private unowned Gtk.ComboBoxText units_combo;
[GtkChild]
- private Gtk.Entry custom_width_entry;
+ private unowned Gtk.Entry custom_width_entry;
[GtkChild]
- private Gtk.Entry custom_height_entry;
+ private unowned Gtk.Entry custom_height_entry;
[GtkChild]
- private Gtk.Entry ppi_entry;
+ private unowned Gtk.Entry ppi_entry;
[GtkChild]
- private Gtk.CheckButton aspect_ratio_check;
+ private unowned Gtk.CheckButton aspect_ratio_check;
[GtkChild]
- private Gtk.CheckButton title_print_check;
+ private unowned Gtk.CheckButton title_print_check;
[GtkChild]
- private Gtk.FontButton title_print_font;
+ private unowned Gtk.FontButton title_print_font;
private Measurement local_content_width = Measurement(5.0, MeasurementUnit.INCHES);
private Measurement local_content_height = Measurement(5.0, MeasurementUnit.INCHES);
@@ -683,7 +683,7 @@ public class CustomPrintTab : Gtk.Box {
}
private void set_print_titles_font(string fontname) {
- title_print_font.set_font_name(fontname);
+ ((Gtk.FontChooser) title_print_font).set_font(fontname);
}
@@ -696,7 +696,7 @@ public class CustomPrintTab : Gtk.Box {
}
private string get_print_titles_font() {
- return title_print_font.get_font_name();
+ return ((Gtk.FontChooser) title_print_font).get_font();
}
public PrintJob get_source_job() {
diff --git a/src/ProfileBrowser.vala b/src/ProfileBrowser.vala
new file mode 100644
index 0000000..4532a20
--- /dev/null
+++ b/src/ProfileBrowser.vala
@@ -0,0 +1,294 @@
+// SPDX-FileCopyrightText: Jens Georg <mail@jensge.org>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+namespace Shotwell {
+ class ProfileEditor : Gtk.Dialog {
+ public string profile_name {get; set;}
+ public string id{get; default = Uuid.string_random();}
+ public string library_folder{get; set;}
+ public string data_folder{get; set;}
+
+ public ProfileEditor() {
+ Object(use_header_bar : Resources.use_header_bar());
+ }
+
+ public override void constructed() {
+ base.constructed();
+
+ set_size_request(640, -1);
+
+ add_buttons(_("Create"), Gtk.ResponseType.OK, _("Cancel"), Gtk.ResponseType.CANCEL, null);
+ var create_button = get_widget_for_response(Gtk.ResponseType.OK);
+ create_button.get_style_context().add_class("suggested-action");
+ create_button.sensitive = false;
+ set_title(_("Create new Profile"));
+
+ data_folder = Path.build_filename(Environment.get_user_data_dir(), "shotwell", "profiles", id);
+ library_folder = Environment.get_user_special_dir(UserDirectory.PICTURES);
+
+ var grid = new Gtk.Grid();
+ grid.hexpand = true;
+ grid.vexpand = true;
+ grid.margin = 6;
+ grid.set_row_spacing(12);
+ grid.set_column_spacing(12);
+ var label = new Gtk.Label(_("Name"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ grid.attach(label, 0, 0, 1, 1);
+
+ var entry = new Gtk.Entry();
+ entry.hexpand = true;
+ entry.bind_property("text", this, "profile-name", GLib.BindingFlags.DEFAULT);
+ entry.bind_property("text", create_button, "sensitive", GLib.BindingFlags.DEFAULT, (binding, from, ref to) => {
+ to = from.get_string() != "";
+ return true;
+ });
+ grid.attach(entry, 1, 0, 2, 1);
+
+ label = new Gtk.Label(_("Library Folder"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ grid.attach(label, 0, 1, 1, 1);
+
+ entry = new Gtk.Entry();
+ entry.hexpand = true;
+ grid.attach(entry, 1, 1, 1, 1);
+ bind_property("library-folder", entry, "text", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL);
+ entry.bind_property("text", create_button, "sensitive", GLib.BindingFlags.DEFAULT, (binding, from, ref to) => {
+ to = from.get_string() != "";
+ return true;
+ });
+
+ var button = new Gtk.Button.from_icon_name("folder-symbolic", Gtk.IconSize.BUTTON);
+ button.hexpand = false;
+ button.vexpand = false;
+ button.halign = Gtk.Align.FILL;
+ button.clicked.connect(() => {
+ var dialog = new Gtk.FileChooserNative(_("Choose Library Folder"), this, Gtk.FileChooserAction.SELECT_FOLDER, _("_OK"), _("_Cancel"));
+ dialog.set_current_folder(library_folder);
+ var result = dialog.run();
+ dialog.hide();
+ if (result == Gtk.ResponseType.ACCEPT) {
+ library_folder = dialog.get_current_folder_file().get_path();
+ }
+ dialog.destroy();
+ });
+ grid.attach(button, 2, 1, 1, 1);
+
+
+ label = new Gtk.Label(_("Data Folder"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ grid.attach(label, 0, 2, 1, 1);
+
+ entry = new Gtk.Entry();
+ entry.set_text(Environment.get_user_special_dir(UserDirectory.PICTURES));
+ entry.hexpand = true;
+ bind_property("data-folder", entry, "text", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL);
+ entry.bind_property("text", create_button, "sensitive", GLib.BindingFlags.DEFAULT, (binding, from, ref to) => {
+ to = from.get_string() != "";
+ return true;
+ });
+ grid.attach(entry, 1, 2, 1, 1);
+
+ button = new Gtk.Button.from_icon_name("folder-symbolic", Gtk.IconSize.BUTTON);
+ button.hexpand = false;
+ button.vexpand = false;
+ button.halign = Gtk.Align.FILL;
+ button.clicked.connect(() => {
+ var dialog = new Gtk.FileChooserNative(_("Choose Data Folder"), this, Gtk.FileChooserAction.SELECT_FOLDER, _("_OK"), _("_Cancel"));
+ dialog.set_current_folder(data_folder);
+ var result = dialog.run();
+ dialog.hide();
+ if (result == Gtk.ResponseType.ACCEPT) {
+ data_folder = dialog.get_current_folder_file().get_path();
+ }
+ dialog.destroy();
+ });
+
+ grid.attach(button, 2, 2, 1, 1);
+
+ get_content_area().add(grid);
+
+ show_all();
+ }
+ }
+
+ private class ProfileRow : Gtk.Box {
+ public Profile profile{get; construct; }
+
+ public ProfileRow(Profile profile) {
+ Object(orientation: Gtk.Orientation.VERTICAL,
+ profile: profile, margin_top: 6, margin_bottom:6, margin_start:6, margin_end:6);
+ }
+
+ public override void constructed() {
+ base.constructed();
+ var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
+ pack_start(content, true);
+
+ var revealer = new Gtk.Revealer();
+ revealer.margin_top = 6;
+ pack_end(revealer, true);
+
+ var label = new Gtk.Label(null);
+ label.set_markup("<span weight=\"bold\">%s</span>".printf(profile.name));
+ label.halign = Gtk.Align.START;
+ content.pack_start(label, true, true, 6);
+
+ Gtk.Image image;
+ if (profile.active) {
+ image = new Gtk.Image.from_icon_name ("emblem-default-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
+ image.set_tooltip_text(_("This is the currently active profile"));
+
+ } else {
+ image = new Gtk.Image();
+ }
+ content.pack_start(image, false, false, 6);
+
+ var button = new Gtk.ToggleButton();
+ button.get_style_context().add_class("flat");
+ content.pack_start(button, false, false, 6);
+ button.bind_property("active", revealer, "reveal-child", BindingFlags.DEFAULT);
+ image = new Gtk.Image.from_icon_name("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
+ button.add(image);
+
+ // FIXME: Would love to use the facade here, but this is currently hardwired to use a fixed profile
+ // and that even is not yet initialized
+ string settings_path;
+ if (profile.id == Profile.SYSTEM) {
+ settings_path = "/org/gnome/shotwell/preferences/files/";
+ } else {
+ settings_path = "/org/gnome/shotwell/profiles/" + profile.id + "/preferences/files/";
+ }
+
+ var settings = new Settings.with_path("org.gnome.shotwell.preferences.files", settings_path);
+ var import_dir = settings.get_string("import-dir");
+ if (import_dir == "") {
+ import_dir = Environment.get_user_special_dir(UserDirectory.PICTURES);
+ }
+
+ var grid = new Gtk.Grid();
+ grid.get_style_context().add_class("content");
+ grid.set_row_spacing(12);
+ grid.set_column_spacing(6);
+ revealer.add(grid);
+ label = new Gtk.Label(_("Library Folder"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ label.margin_start = 12;
+ grid.attach(label, 0, 0, 1, 1);
+ label = new Gtk.Label(import_dir);
+ label.halign = Gtk.Align.START;
+ label.set_ellipsize(Pango.EllipsizeMode.END);
+ grid.attach(label, 1, 0, 1, 1);
+
+ label = new Gtk.Label(_("Data Folder"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ label.margin_start = 12;
+ grid.attach(label, 0, 1, 1, 1);
+ label = new Gtk.Label(profile.data_dir);
+ label.halign = Gtk.Align.START;
+ label.hexpand = true;
+ label.set_ellipsize(Pango.EllipsizeMode.END);
+ grid.attach(label, 1, 1, 1, 1);
+
+ if (profile.id != Profile.SYSTEM && !profile.active) {
+ var remove_button = new Gtk.Button.with_label(_("Remove Profile"));
+ remove_button.get_style_context().add_class("destructive-action");
+ remove_button.set_tooltip_text(_("Remove this profile"));
+ remove_button.hexpand = false;
+ remove_button.halign = Gtk.Align.END;
+ grid.attach(remove_button, 1, 2, 1, 1);
+
+ remove_button.clicked.connect(() => {
+ var flags = Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL;
+ if (Resources.use_header_bar() == 1) {
+ flags |= Gtk.DialogFlags.USE_HEADER_BAR;
+ }
+
+ var d = new Gtk.MessageDialog((Gtk.Window) this.get_toplevel(), flags, Gtk.MessageType.QUESTION, Gtk.ButtonsType.NONE, null);
+ var title = _("Remove profile “%s”").printf(profile.name);
+ var subtitle = _("None of the options will remove any of the images associated with this profile");
+ d.set_markup(_("<b><span size=\"larger\">%s</span></b>\n<span weight=\"light\">%s</span>").printf(title, subtitle));
+
+ d.add_buttons(_("Remove profile and files"), Gtk.ResponseType.OK, _("Remove profile only"), Gtk.ResponseType.ACCEPT, _("Cancel"), Gtk.ResponseType.CANCEL);
+ d.get_widget_for_response(Gtk.ResponseType.OK).get_style_context().add_class("destructive-action");
+ var response = d.run();
+ d.destroy();
+ if (response == Gtk.ResponseType.OK || response == Gtk.ResponseType.ACCEPT) {
+ ProfileManager.get_instance().remove(profile.id, response == Gtk.ResponseType.OK);
+ }
+ });
+ }
+ }
+ }
+
+ class ProfileBrowser : Gtk.Box {
+ public ProfileBrowser() {
+ Object(orientation: Gtk.Orientation.VERTICAL, vexpand: true, hexpand: true);
+ }
+
+ public signal void profile_activated(string? profile);
+
+ public override void constructed() {
+ var scrollable = new Gtk.ScrolledWindow(null, null);
+ scrollable.hexpand = true;
+ scrollable.vexpand = true;
+
+ var list_box = new Gtk.ListBox();
+ list_box.activate_on_single_click = false;
+ list_box.row_activated.connect((list_box, row) => {
+ var index = row.get_index();
+ var profile = (Profile) ProfileManager.get_instance().get_item(index);
+ if (profile.id == Profile.SYSTEM) {
+ profile_activated(null);
+ } else {
+ profile_activated(profile.name);
+ }
+ });
+ list_box.get_style_context().add_class("rich-list");
+ list_box.hexpand = true;
+ list_box.vexpand = true;
+ scrollable.add (list_box);
+ list_box.bind_model(ProfileManager.get_instance(), on_widget_create);
+ list_box.set_header_func(on_header);
+
+ var button = new Gtk.Button.with_label(_("Create new Profile"));
+ pack_start(button, false, false, 6);
+ button.clicked.connect(() => {
+ var editor = new ProfileEditor();
+ editor.set_transient_for((Gtk.Window)get_ancestor(typeof(Gtk.Window)));
+ var result = editor.run();
+ editor.hide();
+ if (result == Gtk.ResponseType.OK) {
+ debug("Request to add new profile: %s %s %s %s", editor.id, editor.profile_name, editor.library_folder, editor.data_folder);
+ ProfileManager.get_instance().add_profile(editor.id, editor.profile_name, editor.library_folder, editor.data_folder);
+ }
+ editor.destroy();
+ });
+ add(scrollable);
+ show_all();
+ }
+
+ private Gtk.Widget on_widget_create(Object item) {
+ var row = new Gtk.ListBoxRow();
+ row.add(new ProfileRow((Profile) item));
+ row.show_all();
+
+ return row;
+ }
+
+ private void on_header(Gtk.ListBoxRow row, Gtk.ListBoxRow? before) {
+ if (before == null || row.get_header() != null) {
+ return;
+ }
+
+ var separator = new Gtk.Separator(Gtk.Orientation.HORIZONTAL);
+ separator.show();
+ row.set_header(separator);
+ }
+ }
+}
diff --git a/src/Profiles.vala b/src/Profiles.vala
new file mode 100644
index 0000000..ec52800
--- /dev/null
+++ b/src/Profiles.vala
@@ -0,0 +1,303 @@
+/* Copyright 2019 Jens Georg.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+namespace Shotwell {
+ class Profile : Object {
+ public const string SYSTEM = "__shotwell_default_system";
+ public Profile(string name, string id, string data_dir, bool active) {
+ Object(name: name, id: id, data_dir: data_dir, active: active);
+ }
+ public string name {get; construct;}
+ public string id {get; construct;}
+ public string data_dir {get; construct;}
+ public bool active {get; construct;}
+ }
+
+ class ProfileManager : Object, GLib.ListModel {
+ // ListModel implementations
+ Type get_item_type() {
+ return typeof(Profile);
+ }
+
+ uint get_n_items() {
+ // All that is in the ini file plus one for the default profile
+ return profiles.get_groups().length + 1;
+ }
+
+ GLib.Object? get_item (uint position) {
+ if (position == 0) {
+ return new Profile(_("System Profile"), Profile.SYSTEM,
+ Path.build_path(Path.DIR_SEPARATOR_S, Environment.get_user_data_dir(), "shotwell"),
+ this.profile == null);
+ }
+
+ try {
+ var group = profiles.get_groups()[position - 1];
+ var id = profiles.get_value(group, "Id");
+ var name = profiles.get_value(group, "Name");
+ var active = this.profile == name;
+ return new Profile(profiles.get_value(group, "Name"),
+ id,
+ get_data_dir_for_profile(id, group),
+ active);
+ } catch (KeyFileError err) {
+ if (err is KeyFileError.GROUP_NOT_FOUND) {
+ assert_not_reached();
+ }
+
+ warning("Profile configuration file corrupt: %s", err.message);
+ }
+
+ return null;
+ }
+
+ private static ProfileManager instance;
+ public static ProfileManager get_instance() {
+ if (instance == null)
+ instance = new ProfileManager();
+
+ return instance;
+ }
+
+ private ProfileManager() {
+ Object();
+ }
+
+ private void write() {
+ try {
+ profiles.save_to_file(path);
+ } catch (Error error) {
+ critical("Failed to write profiles: %s", error.message);
+ }
+ }
+
+ private KeyFile profiles;
+ private string profile = null;
+ private string path;
+ private string group_name;
+
+ public override void constructed() {
+ base.constructed();
+
+ profiles = new KeyFile();
+ path = Path.build_filename(Environment.get_user_config_dir(), "shotwell");
+ DirUtils.create_with_parents(path, 0700);
+ path = Path.build_filename(path, "profiles.ini");
+
+ try {
+ profiles.load_from_file(path, KeyFileFlags.KEEP_COMMENTS);
+ } catch (Error error) {
+ debug("Could not read profiles: %s", error.message);
+ }
+ }
+
+ public bool has_profile (string profile, out string group_name = null) {
+ group_name = Base64.encode (profile.data);
+ return profiles.has_group(group_name);
+ }
+
+ public void set_profile(string profile) {
+ message("Using profile %s for this session", profile);
+ assert(this.profile == null);
+
+ this.profile = profile;
+
+ add_profile(Uuid.string_random(), profile, null, null);
+ }
+
+ public void add_profile(string id, string name, string? library_folder, string? data_folder) {
+ if (has_profile(name, out this.group_name)) {
+ return;
+ }
+
+ try {
+ profiles.set_string(group_name, "Name", name);
+ profiles.set_string(group_name, "Id", id);
+ if (data_folder != null) {
+ profiles.set_string(group_name, "DataDir", data_folder);
+ }
+
+ // Need to set comment after setting keys since it does not create the group
+ profiles.set_comment(group_name, null, "Profile settings for \"%s\"".printf(name));
+
+ write();
+ } catch (Error err) {
+ error("Failed to create profile: %s", err.message);
+ }
+
+ if (library_folder != null) {
+ errno = 0;
+ var f = File.new_for_commandline_arg(library_folder);
+ try {
+ f.make_directory_with_parents();
+ } catch (Error err) {
+ warning ("Failed to create library folder: %s", err.message);
+ }
+ var settings_path = "/org/gnome/shotwell/profiles/" + id + "/preferences/files/";
+
+
+ var settings = new Settings.with_path("org.gnome.shotwell.preferences.files", settings_path);
+ settings.set_string("import-dir", library_folder);
+ }
+
+ items_changed(profiles.get_groups().length, 0, 1);
+ }
+
+ public string derive_data_dir(string? data_dir) {
+ if (data_dir != null) {
+ debug ("Using user-provided data dir %s", data_dir);
+
+ try {
+ profiles.get_string(group_name, "DataDir");
+ } catch (Error error) {
+ if (profile != null && profile != "") {
+ profiles.set_string(group_name ,"DataDir", data_dir);
+ debug("Using %s as data dir for profile %s", data_dir, profile);
+ write();
+ }
+ }
+
+ return data_dir;
+ }
+
+ return Path.build_filename(Environment.get_user_data_dir(), "shotwell", "profiles", id());
+ }
+
+ public string id() {
+ // We are not running on any profile
+ if (profile == null || profile == "")
+ return "";
+
+ try {
+ return profiles.get_string(group_name, "Id");
+ } catch (Error error) {
+ assert_not_reached();
+ }
+ }
+
+ private string get_data_dir_for_profile(string id, string group) throws KeyFileError {
+ if ("DataDir" in profiles.get_keys(group)) {
+ return profiles.get_value(group, "DataDir");
+ } else {
+ return Path.build_filename(Environment.get_user_data_dir(), "shotwell", "profiles", id);
+ }
+ }
+
+ public void print_profiles() {
+ print("Available profiles:\n");
+ print("-------------------\n");
+ try {
+ foreach (var group in profiles.get_groups()) {
+ print("Profile name: %s\n", profiles.get_value(group, "Name"));
+ var id = profiles.get_value(group, "Id");
+ print("Profile Id: %s\n", id);
+ print("Data dir: %s\n", get_data_dir_for_profile(id, group));
+ print("\n");
+ }
+ } catch (Error error) {
+ print("Failed to print profiles: %s", error.message);
+ }
+ }
+
+ const string SCHEMAS[] = {
+ "sharing",
+ "printing",
+ "plugins.enable-state",
+ "preferences.ui",
+ "preferences.slideshow",
+ "preferences.window",
+ "preferences.files",
+ "preferences.editing",
+ "preferences.export",
+ };
+
+ void reset_all_keys(Settings settings) {
+ SettingsSchema schema;
+ ((Object)settings).get("settings-schema", out schema, null);
+
+ foreach (var key in schema.list_keys()) {
+ debug("Resetting key %s", key);
+ settings.reset(key);
+ }
+
+ foreach (var c in settings.list_children()) {
+ debug("Checking children %s", c);
+ var child = settings.get_child (c);
+ reset_all_keys (child);
+ }
+ }
+
+ private void remove_settings_recursively(string id) {
+ var source = SettingsSchemaSource.get_default();
+ foreach (var schema in SCHEMAS) {
+ var path = "/org/gnome/shotwell/profiles/%s/%s/".printf(id, schema.replace(".", "/"));
+ var schema_name = "org.gnome.shotwell.%s".printf(schema);
+ debug("%s @ %s", schema_name, path);
+ var schema_definition = source.lookup(schema_name, false);
+ var settings = new Settings.full (schema_definition, null, path);
+ settings.delay();
+ reset_all_keys (settings);
+ foreach (var key in schema_definition.list_keys()) {
+ debug("Resetting key %s", key);
+ settings.reset(key);
+ }
+ settings.apply();
+ Settings.sync();
+ }
+ }
+
+ public void remove(string id, bool remove_all) {
+ debug("Request to remove profile %s, with files? %s", id, remove_all.to_string());
+ int index = 1;
+ string group = null;
+
+ foreach (var g in profiles.get_groups()) {
+ try {
+ if (profiles.get_value(g, "Id") == id) {
+ group = g;
+ break;
+ }
+ index++;
+ } catch (KeyFileError error) {
+ assert_not_reached();
+ }
+ }
+
+ if (group != null) {
+ string? data_dir = null;
+
+ try {
+ data_dir = get_data_dir_for_profile(id, group);
+ // Remove profile
+ string? key = null;
+ profiles.remove_comment(group, key);
+ profiles.remove_group(group);
+ } catch (KeyFileError err) {
+ // We checked the existence of the group above.
+ assert_not_reached();
+ }
+
+ remove_settings_recursively(id);
+
+ if (remove_all) {
+ try {
+ var file = File.new_for_commandline_arg(data_dir);
+ file.trash();
+ } catch (Error error) {
+ warning("Failed to remove data folder: %s", error.message);
+ }
+ }
+
+ Idle.add(() => {
+ items_changed(index, 1, 0);
+
+ return false;
+ });
+ write();
+ }
+ }
+ }
+}
diff --git a/src/Properties.vala b/src/Properties.vala
index ad0a041..c0cf2fd 100644
--- a/src/Properties.vala
+++ b/src/Properties.vala
@@ -4,12 +4,16 @@
* See the COPYING file in this distribution.
*/
-private abstract class Properties : Gtk.Grid {
- uint line_count = 0;
+private abstract class Properties : Gtk.Box {
+ protected Gtk.Grid grid = new Gtk.Grid();
+ protected uint line_count = 0;
protected Properties() {
- row_spacing = 6;
- column_spacing = 12;
+ Object(orientation: Gtk.Orientation.VERTICAL, homogeneous : false);
+
+ grid.row_spacing = 6;
+ grid.column_spacing = 12;
+ pack_start(grid, false, false, 0);
}
protected void add_line(string label_text, string info_text, bool multi_line = false, string? href = null) {
@@ -62,18 +66,18 @@ private abstract class Properties : Gtk.Grid {
info = (Gtk.Widget) info_label;
}
- attach(label, 0, (int) line_count, 1, 1);
+ grid.attach(label, 0, (int) line_count, 1, 1);
if (multi_line) {
- attach(info, 1, (int) line_count, 1, 3);
+ grid.attach(info, 1, (int) line_count, 1, 3);
} else {
- attach(info, 1, (int) line_count, 1, 1);
+ grid.attach(info, 1, (int) line_count, 1, 1);
}
line_count++;
}
- protected string get_prettyprint_time(Time time) {
+ protected string get_prettyprint_time(DateTime time) {
string timestring = time.format(Resources.get_hh_mm_format_string());
if (timestring[0] == '0')
@@ -82,7 +86,7 @@ private abstract class Properties : Gtk.Grid {
return timestring;
}
- protected string get_prettyprint_time_with_seconds(Time time) {
+ protected string get_prettyprint_time_with_seconds(DateTime time) {
string timestring = time.format(Resources.get_hh_mm_ss_format_string());
if (timestring[0] == '0')
@@ -91,12 +95,12 @@ private abstract class Properties : Gtk.Grid {
return timestring;
}
- protected string get_prettyprint_date(Time date) {
+ protected string get_prettyprint_date(DateTime date) {
string date_string = null;
- Time today = Time.local(time_t());
- if (date.day_of_year == today.day_of_year && date.year == today.year) {
+ var today = new DateTime.now_local();
+ if (date.get_day_of_year() == today.get_day_of_year() && date.get_year() == today.get_year()) {
date_string = _("Today");
- } else if (date.day_of_year == (today.day_of_year - 1) && date.year == today.year) {
+ } else if (date.get_day_of_year() == (today.get_day_of_year() - 1) && date.get_year() == today.get_year()) {
date_string = _("Yesterday");
} else {
date_string = format_local_date(date);
@@ -140,9 +144,9 @@ private abstract class Properties : Gtk.Grid {
}
protected virtual void clear_properties() {
- foreach (Gtk.Widget child in get_children())
- remove(child);
-
+ foreach (Gtk.Widget child in grid.get_children())
+ grid.remove(child);
+
line_count = 0;
}
@@ -159,8 +163,8 @@ private abstract class Properties : Gtk.Grid {
private class BasicProperties : Properties {
private string title;
- private time_t start_time = time_t();
- private time_t end_time = time_t();
+ private DateTime? start_time = new DateTime.now_utc();
+ private DateTime? end_time = new DateTime.now_utc();
private Dimensions dimensions;
private int photo_count;
private int event_count;
@@ -173,13 +177,14 @@ private class BasicProperties : Properties {
private string raw_assoc;
public BasicProperties() {
+ base();
}
protected override void clear_properties() {
base.clear_properties();
title = "";
- start_time = 0;
- end_time = 0;
+ start_time = null;
+ end_time = null;
dimensions = Dimensions(0,0);
photo_count = -1;
event_count = -1;
@@ -269,20 +274,20 @@ private class BasicProperties : Properties {
video_count = 0;
foreach (DataView view in iter) {
DataSource source = view.get_source();
-
- if (source is PhotoSource || source is PhotoImportSource) {
- time_t exposure_time = (source is PhotoSource) ?
+
+ if (source is PhotoSource || source is PhotoImportSource) {
+ var exposure_time = (source is PhotoSource) ?
((PhotoSource) source).get_exposure_time() :
((PhotoImportSource) source).get_exposure_time();
- if (exposure_time != 0) {
- if (start_time == 0 || exposure_time < start_time)
+ if (exposure_time != null) {
+ if (start_time == null || exposure_time.compare(start_time) < 0)
start_time = exposure_time;
- if (end_time == 0 || exposure_time > end_time)
+ if (end_time == null || exposure_time.compare(end_time) > 0)
end_time = exposure_time;
}
-
+
photo_count++;
} else if (source is EventSource) {
EventSource event_source = (EventSource) source;
@@ -290,14 +295,14 @@ private class BasicProperties : Properties {
if (event_count == -1)
event_count = 0;
- if ((start_time == 0 || event_source.get_start_time() < start_time) &&
- event_source.get_start_time() != 0 ) {
+ if ((start_time == null || event_source.get_start_time().compare(start_time) < 0) &&
+ event_source.get_start_time() != null ) {
start_time = event_source.get_start_time();
}
- if ((end_time == 0 || event_source.get_end_time() > end_time) &&
- event_source.get_end_time() != 0 ) {
+ if ((end_time == null || event_source.get_end_time().compare(end_time) > 0) &&
+ event_source.get_end_time() != null ) {
end_time = event_source.get_end_time();
- } else if (end_time == 0 || event_source.get_start_time() > end_time) {
+ } else if (end_time == null || event_source.get_start_time().compare(end_time) > 0) {
end_time = event_source.get_start_time();
}
@@ -310,15 +315,15 @@ private class BasicProperties : Properties {
video_count += event_video_count;
event_count++;
} else if (source is VideoSource || source is VideoImportSource) {
- time_t exposure_time = (source is VideoSource) ?
+ var exposure_time = (source is VideoSource) ?
((VideoSource) source).get_exposure_time() :
((VideoImportSource) source).get_exposure_time();
- if (exposure_time != 0) {
- if (start_time == 0 || exposure_time < start_time)
+ if (exposure_time != null) {
+ if (start_time == null || exposure_time.compare(start_time) < 0)
start_time = exposure_time;
- if (end_time == 0 || exposure_time > end_time)
+ if (end_time == null || exposure_time.compare(end_time) > 0)
end_time = exposure_time;
}
@@ -330,9 +335,9 @@ private class BasicProperties : Properties {
protected override void get_properties(Page current_page) {
base.get_properties(current_page);
- if (end_time == 0)
+ if (end_time == null)
end_time = start_time;
- if (start_time == 0)
+ if (start_time == null)
start_time = end_time;
}
@@ -373,11 +378,11 @@ private class BasicProperties : Properties {
add_line("", video_num_string);
}
- if (start_time != 0) {
- string start_date = get_prettyprint_date(Time.local(start_time));
- string start_time = get_prettyprint_time(Time.local(start_time));
- string end_date = get_prettyprint_date(Time.local(end_time));
- string end_time = get_prettyprint_time(Time.local(end_time));
+ if (start_time != null) {
+ string start_date = get_prettyprint_date(start_time.to_local());
+ string start_time = get_prettyprint_time(start_time.to_local());
+ string end_date = get_prettyprint_date(end_time.to_local());
+ string end_time = get_prettyprint_time(end_time.to_local());
if (start_date == end_date) {
// display only one date if start and end are the same
@@ -485,7 +490,7 @@ private class ExtendedProperties : Properties {
public ExtendedProperties() {
base();
- row_spacing = 6;
+ grid.row_spacing = 6;
}
// Event stuff
@@ -574,9 +579,9 @@ private class ExtendedProperties : Properties {
copyright = metadata.get_copyright();
software = metadata.get_software();
exposure_bias = metadata.get_exposure_bias();
- time_t exposure_time_obj = metadata.get_exposure_date_time().get_timestamp();
- exposure_date = get_prettyprint_date(Time.local(exposure_time_obj));
- exposure_time = get_prettyprint_time_with_seconds(Time.local(exposure_time_obj));
+ DateTime exposure_time_obj = metadata.get_exposure_date_time().get_timestamp();
+ exposure_date = get_prettyprint_date(exposure_time_obj.to_local());
+ exposure_time = get_prettyprint_time_with_seconds(exposure_time_obj.to_local());
comment = media.get_comment();
} else if (source is EventSource) {
Event event = (Event) source;
@@ -634,7 +639,7 @@ private class ExtendedProperties : Properties {
add_line(_("GPS longitude:"), (gps_long != -1 && gps_long_ref != "" &&
gps_long_ref != null) ? "%f °%s".printf(gps_long, gps_long_ref) : NO_VALUE, false, osm_link);
- add_line(_("Artist:"), (artist != "" && artist != null) ? artist : NO_VALUE);
+ add_line(_("Artist:"), (artist != "" && artist != null) ? Markup.escape_text(artist) : NO_VALUE);
add_line(_("Copyright:"), (copyright != "" && copyright != null) ? copyright : NO_VALUE);
diff --git a/src/Resources.vala b/src/Resources.vala
index b65ec52..d03a214 100644
--- a/src/Resources.vala
+++ b/src/Resources.vala
@@ -24,6 +24,7 @@ namespace Resources {
private const string LIBEXECDIR = _LIBEXECDIR;
public const string PREFIX = _PREFIX;
+ public const string PIXBUF_LOADER_PATH = _PIXBUF_LOADER_PATH;
public const double TRANSIENT_WINDOW_OPACITY = 0.90;
@@ -69,12 +70,20 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
public const string GO_NEXT = "go-next-symbolic";
public const string GO_PREVIOUS = "go-previous-symbolic";
- public const string ICON_ABOUT_LOGO = "about-celle.jpg";
+ public const string ICON_ABOUT_LOGO = "Delmenhorst_Rathaus.jpg";
public const string ICON_GENERIC_PLUGIN = "application-x-addon-symbolic";
public const string ICON_SLIDESHOW_EXTENSION_POINT = "slideshow-extension-point";
public const int ICON_FILTER_REJECTED_OR_BETTER_FIXED_SIZE = 32;
public const int ICON_FILTER_UNRATED_OR_BETTER_FIXED_SIZE = 16;
public const int ICON_ZOOM_SCALE = 16;
+ public const string ICON_GPS_MARKER = "gps-marker";
+ public const string ICON_GPS_MARKER_HIGHLIGHTED = "gps-marker-highlighted";
+ public const string ICON_GPS_MARKER_SELECTED = "gps-marker-selected";
+ public const string ICON_GPS_GROUP_MARKER = "gps-markers-many";
+ public const string ICON_GPS_GROUP_MARKER_HIGHLIGHTED = "gps-markers-many-highlighted";
+ public const string ICON_GPS_GROUP_MARKER_SELECTED = "gps-markers-many-selected";
+ public const string ICON_MAP_EDIT_LOCKED = "map-edit-locked";
+ public const string ICON_MAP_EDIT_UNLOCKED = "map-edit-unlocked";
public const string ICON_CAMERAS = "camera-photo-symbolic";
public const string ICON_EVENTS = "multiple-events-symbolic";
@@ -403,6 +412,20 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
return ngettext ("Remove Face “%s” From Photo",
"Remove Face “%s” From Photos", count).printf(name);
}
+
+ public string set_face_from_photo_menu(string name) {
+ /* Translators: This means to teach the face to the face recognition system */
+ return _("_Train Face “%s” From Photo").printf(name);
+ }
+
+ public string set_face_from_photo_label(string name) {
+ /* Translators: This means to teach the face to the face recognition system */
+ return _("_Train Face “%s” From Photo").printf(name);
+ }
+
+ public static string set_face_from_photo_error() {
+ return "Unable to set face as reference";
+ }
public string rename_face_menu(string name) {
return _("Re_name Face “%s”…").printf(name);
@@ -777,7 +800,8 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
private string END_MULTIMONTH_DATE_FORMAT_STRING = null;
public void init () {
- get_icon_theme_engine();
+ init_icon_theme_engine();
+ init_css_provider();
// load application-wide stock icons as IconSets
generate_rating_strings();
}
@@ -969,12 +993,21 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
return noninterpretable_badge_pixbuf;
}
+
+ private void init_css_provider() {
+ Gtk.CssProvider provider = new Gtk.CssProvider();
+ provider.load_from_resource("/org/gnome/Shotwell/themes/org.gnome.Shotwell.css");
+ Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+ }
- public Gtk.IconTheme get_icon_theme_engine() {
+ private void init_icon_theme_engine() {
Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default();
icon_theme.add_resource_path("/org/gnome/Shotwell/icons");
-
- return icon_theme;
+ icon_theme.add_resource_path("/org/gnome/Shotwell/icons/hicolor");
+ icon_theme.add_resource_path("/org/gnome/Shotwell/Publishing/icons");
+ icon_theme.add_resource_path("/org/gnome/Shotwell/Publishing/icons/hicolor");
+ icon_theme.add_resource_path("/org/gnome/Shotwell/Transitions/icons");
+ icon_theme.add_resource_path("/org/gnome/Shotwell/Transitions/icons/hicolor");
}
// This method returns a reference to a cached pixbuf that may be shared throughout the system.
@@ -1038,7 +1071,7 @@ along with Shotwell; if not, write to the Free Software Foundation, Inc.,
return (scale > 0) ? scale_pixbuf(pixbuf, scale, Gdk.InterpType.BILINEAR, false) : pixbuf;
}
-
+
// Get the directory where our help files live. Returns a string
// describing the help path we want, or, if we're installed system
// -wide already, returns null.
diff --git a/src/SearchFilter.vala b/src/SearchFilter.vala
index ad8b7ec..969591f 100644
--- a/src/SearchFilter.vala
+++ b/src/SearchFilter.vala
@@ -148,7 +148,7 @@ public abstract class SearchViewFilter : ViewFilter {
}
public void set_search_filter(string? text) {
- search_filter = !is_string_empty(text) ? text.down() : null;
+ search_filter = !is_string_empty(text) ? String.remove_diacritics(text.down()) : null;
search_filter_words = search_filter != null ? search_filter.split(" ") : null;
}
@@ -775,12 +775,13 @@ public class SearchFilterToolbar : Gtk.Revealer {
switch (filter) {
case RatingFilter.REJECTED_OR_HIGHER:
- icon = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
+ var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
var image = new Gtk.Image.from_icon_name ("emblem-photos-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
image.margin_end = 2;
- (icon as Gtk.Box).pack_start(image);
+ box.pack_start(image);
image = new Gtk.Image.from_icon_name ("window-close-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
- (icon as Gtk.Box).pack_start(image);
+ box.pack_start(image);
+ icon = box;
icon.show_all();
break;
@@ -1015,7 +1016,8 @@ public class SearchFilterToolbar : Gtk.Revealer {
}
private SavedSearch get_search(Gtk.ListBoxRow row) {
- DataButton button = (row.get_children().first().data as Gtk.Box).get_children().last().data as DataButton;
+ var box = (Gtk.Box) row.get_children().first().data;
+ DataButton button = box.get_children().last().data as DataButton;
return button.search;
}
@@ -1191,7 +1193,7 @@ public class SearchFilterToolbar : Gtk.Revealer {
bool has_flagged) {
if (has_photos || has_raw)
// As a user, I would expect, that a raw photo is still a photo.
- // Let's enable the photo button even if there ar only raw photos.
+ // Let's enable the photo button even if there are only raw photos.
toolbtn_photos.set_icon_name("filter-photos-symbolic");
else
toolbtn_photos.set_icon_name("filter-photos-disabled-symbolic");
diff --git a/src/SinglePhotoPage.vala b/src/SinglePhotoPage.vala
new file mode 100644
index 0000000..754a649
--- /dev/null
+++ b/src/SinglePhotoPage.vala
@@ -0,0 +1,537 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public abstract class SinglePhotoPage : Page {
+ public const Gdk.InterpType FAST_INTERP = Gdk.InterpType.NEAREST;
+ public const Gdk.InterpType QUALITY_INTERP = Gdk.InterpType.BILINEAR;
+ public const int KEY_REPEAT_INTERVAL_MSEC = 200;
+
+ public enum UpdateReason {
+ NEW_PIXBUF,
+ QUALITY_IMPROVEMENT,
+ RESIZED_CANVAS
+ }
+
+ protected Gtk.DrawingArea canvas = new Gtk.DrawingArea();
+ protected Gtk.Viewport viewport = new Gtk.Viewport(null, null);
+
+ private bool scale_up_to_viewport;
+ private TransitionClock transition_clock;
+ private int transition_duration_msec = 0;
+ private Cairo.Surface pixmap = null;
+ private Cairo.Context pixmap_ctx = null;
+ private Cairo.Context text_ctx = null;
+ private Dimensions pixmap_dim = Dimensions();
+ private Gdk.Pixbuf unscaled = null;
+ private Dimensions max_dim = Dimensions();
+ private Gdk.Pixbuf scaled = null;
+ private Gdk.Pixbuf old_scaled = null; // previous scaled image
+ private Gdk.Rectangle scaled_pos = Gdk.Rectangle();
+ private ZoomState static_zoom_state;
+ private bool zoom_high_quality = true;
+ private ZoomState saved_zoom_state;
+ private bool has_saved_zoom_state = false;
+ private uint32 last_nav_key = 0;
+
+ protected SinglePhotoPage(string page_name, bool scale_up_to_viewport) {
+ base(page_name);
+ this.wheel_factor = 0.9999;
+
+ this.scale_up_to_viewport = scale_up_to_viewport;
+
+ transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
+
+ // With the current code automatically resizing the image to the viewport, scrollbars
+ // should never be shown, but this may change if/when zooming is supported
+ set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+
+ set_border_width(0);
+ set_shadow_type(Gtk.ShadowType.NONE);
+
+ viewport.set_shadow_type(Gtk.ShadowType.NONE);
+ viewport.set_border_width(0);
+ viewport.add(canvas);
+
+ add(viewport);
+
+ canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK
+ | Gdk.EventMask.SUBSTRUCTURE_MASK);
+
+ viewport.size_allocate.connect(on_viewport_resize);
+ canvas.draw.connect(on_canvas_exposed);
+
+ set_event_source(canvas);
+ Config.Facade.get_instance().colors_changed.connect(on_colors_changed);
+ }
+
+ ~SinglePhotoPage() {
+ Config.Facade.get_instance().colors_changed.disconnect(on_colors_changed);
+ }
+
+ public bool is_transition_in_progress() {
+ return transition_clock.is_in_progress();
+ }
+
+ public void cancel_transition() {
+ if (transition_clock.is_in_progress())
+ transition_clock.cancel();
+ }
+
+ public void set_transition(string effect_id, int duration_msec) {
+ cancel_transition();
+
+ transition_clock = TransitionEffectsManager.get_instance().create_transition_clock(effect_id);
+ if (transition_clock == null)
+ transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock();
+
+ transition_duration_msec = duration_msec;
+ }
+
+ // This method includes a call to pixmap_ctx.paint().
+ private void render_zoomed_to_pixmap(ZoomState zoom_state) {
+ assert(is_zoom_supported());
+
+ Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle_wrt_content();
+
+ Gdk.Pixbuf zoomed;
+ if (get_zoom_buffer() != null) {
+ zoomed = (zoom_high_quality) ? get_zoom_buffer().get_zoomed_image(zoom_state) :
+ get_zoom_buffer().get_zoom_preview_image(zoom_state);
+ } else {
+ Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(unscaled);
+
+ Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(unscaled, view_rect_proj.x,
+ view_rect_proj.y, view_rect_proj.width, view_rect_proj.height);
+
+ zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height,
+ Gdk.InterpType.BILINEAR);
+ }
+
+ if (zoomed == null) {
+ return;
+ }
+
+ int draw_x = (pixmap_dim.width - view_rect.width) / 2;
+ draw_x = draw_x.clamp(0, int.MAX);
+
+ int draw_y = (pixmap_dim.height - view_rect.height) / 2;
+ draw_y = draw_y.clamp(0, int.MAX);
+ paint_pixmap_with_background(pixmap_ctx, zoomed, draw_x, draw_y);
+ }
+
+ protected void on_interactive_zoom(ZoomState interactive_zoom_state) {
+ assert(is_zoom_supported());
+
+ set_source_color_from_string(pixmap_ctx, "#000");
+ pixmap_ctx.paint();
+
+ bool old_quality_setting = zoom_high_quality;
+ zoom_high_quality = false;
+ render_zoomed_to_pixmap(interactive_zoom_state);
+ zoom_high_quality = old_quality_setting;
+
+ canvas.queue_draw();
+ }
+
+ protected void on_interactive_pan(ZoomState interactive_zoom_state) {
+ assert(is_zoom_supported());
+
+ set_source_color_from_string(pixmap_ctx, "#000");
+ pixmap_ctx.paint();
+
+ bool old_quality_setting = zoom_high_quality;
+ zoom_high_quality = true;
+ render_zoomed_to_pixmap(interactive_zoom_state);
+ zoom_high_quality = old_quality_setting;
+
+ canvas.queue_draw();
+ }
+
+ protected virtual bool is_zoom_supported() {
+ return false;
+ }
+
+ protected virtual void cancel_zoom() {
+ if (pixmap != null) {
+ set_source_color_from_string(pixmap_ctx, "#000");
+ pixmap_ctx.paint();
+ }
+ }
+
+ protected virtual void save_zoom_state() {
+ saved_zoom_state = static_zoom_state;
+ has_saved_zoom_state = true;
+ }
+
+ protected virtual void restore_zoom_state() {
+ if (!has_saved_zoom_state)
+ return;
+
+ static_zoom_state = saved_zoom_state;
+ repaint();
+ has_saved_zoom_state = false;
+ }
+
+ protected virtual ZoomBuffer? get_zoom_buffer() {
+ return null;
+ }
+
+ protected ZoomState get_saved_zoom_state() {
+ return saved_zoom_state;
+ }
+
+ protected void set_zoom_state(ZoomState zoom_state) {
+ assert(is_zoom_supported());
+
+ static_zoom_state = zoom_state;
+ }
+
+ protected ZoomState get_zoom_state() {
+ assert(is_zoom_supported());
+
+ return static_zoom_state;
+ }
+
+ public override void switched_to() {
+ base.switched_to();
+
+ if (unscaled != null)
+ repaint();
+ }
+
+ public override void set_container(Gtk.Window container) {
+ base.set_container(container);
+
+ // scrollbar policy in fullscreen mode needs to be auto/auto, else the pixbuf will shift
+ // off the screen
+ if (container is FullscreenWindow)
+ set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
+ }
+
+ // max_dim represents the maximum size of the original pixbuf (i.e. pixbuf may be scaled and
+ // the caller capable of producing larger ones depending on the viewport size). max_dim
+ // is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if
+ // max_dim should be ignored (i.e. scale_up_to_viewport is false).
+ public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim, Direction? direction = null) {
+ static_zoom_state = ZoomState(max_dim, pixmap_dim,
+ static_zoom_state.get_interpolation_factor(),
+ static_zoom_state.get_viewport_center());
+
+ cancel_transition();
+
+ this.unscaled = unscaled;
+ this.max_dim = max_dim;
+ this.old_scaled = scaled;
+ scaled = null;
+
+ // need to make sure this has happened
+ canvas.realize();
+
+ repaint(direction);
+ }
+
+ public void blank_display() {
+ unscaled = null;
+ max_dim = Dimensions();
+ scaled = null;
+ pixmap = null;
+
+ // this has to have happened
+ canvas.realize();
+
+ // force a redraw
+ invalidate_all();
+ }
+
+ public Cairo.Surface? get_surface() {
+ return pixmap;
+ }
+
+ public Dimensions get_surface_dim() {
+ return pixmap_dim;
+ }
+
+ public Cairo.Context get_cairo_context() {
+ return pixmap_ctx;
+ }
+
+ public void paint_text(Pango.Layout pango_layout, int x, int y) {
+ text_ctx.move_to(x, y);
+ Pango.cairo_show_layout(text_ctx, pango_layout);
+ }
+
+ public Scaling get_canvas_scaling() {
+ return (get_container() is FullscreenWindow) ? Scaling.for_screen(AppWindow.get_instance(), scale_up_to_viewport)
+ : Scaling.for_widget(viewport, scale_up_to_viewport);
+ }
+
+ public Gdk.Pixbuf? get_unscaled_pixbuf() {
+ return unscaled;
+ }
+
+ public Gdk.Pixbuf? get_scaled_pixbuf() {
+ return scaled;
+ }
+
+ // Returns a rectangle describing the pixbuf in relation to the canvas
+ public Gdk.Rectangle get_scaled_pixbuf_position() {
+ return scaled_pos;
+ }
+
+ public bool is_inside_pixbuf(int x, int y) {
+ return coord_in_rectangle((int)Math.lround(x * Application.get_scale()),
+ (int)Math.lround(y * Application.get_scale()), scaled_pos);
+ }
+
+ public void invalidate(Gdk.Rectangle rect) {
+ if (canvas.get_window() != null)
+ canvas.get_window().invalidate_rect(rect, false);
+ }
+
+ public void invalidate_all() {
+ if (canvas.get_window() != null)
+ canvas.get_window().invalidate_rect(null, false);
+ }
+
+ private void on_viewport_resize() {
+ // do fast repaints while resizing
+ internal_repaint(true, null);
+ }
+
+ protected override void on_resize_finished(Gdk.Rectangle rect) {
+ base.on_resize_finished(rect);
+
+ // when the resize is completed, do a high-quality repaint
+ repaint();
+ }
+
+ private bool on_canvas_exposed(Cairo.Context exposed_ctx) {
+ // draw pixmap onto canvas unless it's not been instantiated, in which case draw black
+ // (so either old image or contents of another page is not left on screen)
+ if (pixmap != null) {
+ pixmap.set_device_scale(Application.get_scale(), Application.get_scale());
+ exposed_ctx.set_source_surface(pixmap, 0, 0);
+ }
+ else
+ set_source_color_from_string(exposed_ctx, "#000");
+
+ exposed_ctx.rectangle(0, 0, get_allocated_width(), get_allocated_height());
+ exposed_ctx.paint();
+
+ if (pixmap != null) {
+ pixmap.set_device_scale(1.0, 1.0);
+ }
+
+ return true;
+ }
+
+ protected virtual void new_surface(Cairo.Context ctx, Dimensions ctx_dim) {
+ }
+
+ protected virtual void updated_pixbuf(Gdk.Pixbuf pixbuf, UpdateReason reason, Dimensions old_dim) {
+ }
+
+ protected virtual void paint(Cairo.Context ctx, Dimensions ctx_dim) {
+ if (is_zoom_supported() && (!static_zoom_state.is_default())) {
+ set_source_color_from_string(ctx, "#000");
+ ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
+ ctx.fill();
+
+ render_zoomed_to_pixmap(static_zoom_state);
+ } else if (!transition_clock.paint(ctx, ctx_dim.width, ctx_dim.height)) {
+ // transition is not running, so paint the full image on a black background
+ set_source_color_from_string(ctx, "#000");
+
+ ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height);
+ ctx.fill();
+
+ //scaled.save("src%010d.png".printf(buffer_counter), "png");
+ paint_pixmap_with_background(ctx, scaled, scaled_pos.x, scaled_pos.y);
+ //pixmap.write_to_png("%010d.png".printf(buffer_counter++));
+ }
+ }
+
+ private void repaint_pixmap() {
+ if (pixmap_ctx == null)
+ return;
+
+ paint(pixmap_ctx, pixmap_dim);
+ invalidate_all();
+ }
+
+ public void repaint(Direction? direction = null) {
+ internal_repaint(false, direction);
+ }
+
+ private void internal_repaint(bool fast, Direction? direction) {
+ // if not in view, assume a full repaint needed in future but do nothing more
+ if (!is_in_view()) {
+ pixmap = null;
+ scaled = null;
+
+ return;
+ }
+
+ // no image or window, no painting
+ if (unscaled == null || canvas.get_window() == null)
+ return;
+
+ Gtk.Allocation allocation;
+ viewport.get_allocation(out allocation);
+
+ int width = allocation.width;
+ int height = allocation.height;
+
+ if (width <= 0 || height <= 0)
+ return;
+
+ bool new_pixbuf = (scaled == null);
+
+ // save if reporting an image being rescaled
+ Dimensions old_scaled_dim = Dimensions.for_rectangle(scaled_pos);
+
+ Gdk.Rectangle old_scaled_pos = scaled_pos;
+
+ // attempt to reuse pixmap
+ if (pixmap_dim.width != width || pixmap_dim.height != height)
+ pixmap = null;
+
+ // if necessary, create a pixmap as large as the entire viewport
+ bool new_pixmap = false;
+ if (pixmap == null) {
+ init_pixmap((int)Math.lround(width * Application.get_scale()), (int)Math.lround(height * Application.get_scale()));
+ new_pixmap = true;
+ }
+
+ if (new_pixbuf || new_pixmap) {
+ Dimensions unscaled_dim = Dimensions.for_pixbuf(unscaled);
+
+ // determine scaled size of pixbuf ... if a max dimensions is set and not scaling up,
+ // respect it
+ Dimensions scaled_dim = Dimensions();
+ if (!scale_up_to_viewport && max_dim.has_area() && max_dim.width < width && max_dim.height < height)
+ scaled_dim = max_dim;
+ else
+ scaled_dim = unscaled_dim.get_scaled_proportional(pixmap_dim);
+
+ // center pixbuf on the canvas
+ scaled_pos.x = (int)Math.lround(((width * Application.get_scale()) - scaled_dim.width) / 2.0);
+ scaled_pos.y = (int)Math.lround(((height * Application.get_scale()) - scaled_dim.height) / 2.0);
+ scaled_pos.width = scaled_dim.width;
+ scaled_pos.height = scaled_dim.height;
+ }
+
+ Gdk.InterpType interp = (fast) ? FAST_INTERP : QUALITY_INTERP;
+
+ // rescale if canvas rescaled or better quality is requested
+ if (scaled == null) {
+ scaled = resize_pixbuf(unscaled, Dimensions.for_rectangle(scaled_pos), interp);
+
+ UpdateReason reason = UpdateReason.RESIZED_CANVAS;
+ if (new_pixbuf)
+ reason = UpdateReason.NEW_PIXBUF;
+ else if (!new_pixmap && interp == QUALITY_INTERP)
+ reason = UpdateReason.QUALITY_IMPROVEMENT;
+
+ static_zoom_state = ZoomState(max_dim, pixmap_dim,
+ static_zoom_state.get_interpolation_factor(),
+ static_zoom_state.get_viewport_center());
+
+ updated_pixbuf(scaled, reason, old_scaled_dim);
+ }
+
+ zoom_high_quality = !fast;
+
+ if (direction != null && !transition_clock.is_in_progress()) {
+ Spit.Transitions.Visuals visuals = new Spit.Transitions.Visuals(old_scaled,
+ old_scaled_pos, scaled, scaled_pos, parse_color("#000"));
+
+ transition_clock.start(visuals, direction.to_transition_direction(), transition_duration_msec,
+ repaint_pixmap);
+ }
+
+ if (!transition_clock.is_in_progress())
+ repaint_pixmap();
+ }
+
+ private void init_pixmap(int width, int height) {
+ assert(unscaled != null);
+ assert(canvas.get_window() != null);
+
+ // Cairo backing surface (manual double-buffering)
+ pixmap = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
+ pixmap_dim = Dimensions(width, height);
+
+ // Cairo context for drawing on the pixmap
+ pixmap_ctx = new Cairo.Context(pixmap);
+
+ // need a new pixbuf to fit this scale
+ scaled = null;
+
+ // Cairo context for drawing text on the pixmap
+ text_ctx = new Cairo.Context(pixmap);
+ set_source_color_from_string(text_ctx, "#fff");
+
+
+ // no need to resize canvas, viewport does that automatically
+
+ new_surface(pixmap_ctx, pixmap_dim);
+ }
+
+ protected override bool on_context_keypress() {
+ return popup_context_menu(get_page_context_menu());
+ }
+
+ protected virtual void on_previous_photo() {
+ }
+
+ protected virtual void on_next_photo() {
+ }
+
+ public override bool key_press_event(Gdk.EventKey event) {
+ // if the user holds the arrow keys down, we will receive a steady stream of key press
+ // events for an operation that isn't designed for a rapid succession of output ...
+ // we staunch the supply of new photos to under a quarter second (#533)
+ bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC;
+
+ bool handled = true;
+ switch (Gdk.keyval_name(event.keyval)) {
+ case "Left":
+ case "KP_Left":
+ case "BackSpace":
+ if (nav_ok) {
+ on_previous_photo();
+ last_nav_key = event.time;
+ }
+ break;
+
+ case "Right":
+ case "KP_Right":
+ case "space":
+ if (nav_ok) {
+ on_next_photo();
+ last_nav_key = event.time;
+ }
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled)
+ return true;
+
+ return (base.key_press_event != null) ? base.key_press_event(event) : true;
+ }
+
+ private void on_colors_changed() {
+ invalidate_transparent_background();
+ repaint();
+ }
+}
+
+
diff --git a/src/SlideshowPage.vala b/src/SlideshowPage.vala
index 9810236..adfec7f 100644
--- a/src/SlideshowPage.vala
+++ b/src/SlideshowPage.vala
@@ -26,19 +26,19 @@ class SlideshowPage : SinglePhotoPage {
[GtkTemplate (ui = "/org/gnome/Shotwell/ui/slideshow_settings.ui")]
private class SettingsDialog : Gtk.Dialog {
[GtkChild]
- Gtk.Adjustment delay_adjustment;
+ unowned Gtk.Adjustment delay_adjustment;
[GtkChild]
- Gtk.SpinButton delay_entry;
+ unowned Gtk.SpinButton delay_entry;
[GtkChild]
- Gtk.ComboBoxText transition_effect_selector;
+ unowned Gtk.ComboBoxText transition_effect_selector;
[GtkChild]
- Gtk.Scale transition_effect_hscale;
+ unowned Gtk.Scale transition_effect_hscale;
[GtkChild]
- Gtk.SpinButton transition_effect_entry;
+ unowned Gtk.SpinButton transition_effect_entry;
[GtkChild]
- Gtk.Adjustment transition_effect_adjustment;
+ unowned Gtk.Adjustment transition_effect_adjustment;
[GtkChild]
- Gtk.CheckButton show_title_button;
+ unowned Gtk.CheckButton show_title_button;
public SettingsDialog() {
Object (use_header_bar: Resources.use_header_bar());
diff --git a/src/SortedList.vala b/src/SortedList.vala
index 20e6771..420190d 100644
--- a/src/SortedList.vala
+++ b/src/SortedList.vala
@@ -142,7 +142,7 @@ public class SortedList<G> : Object, Gee.Traversable<G>, Gee.Iterable<G>, Gee.Co
return list.get(index);
}
- private int binary_search(G search, EqualFunc? equal_func) {
+ private int binary_search(G search, EqualFunc<G>? equal_func) {
assert(cmp != null);
int min = 0;
@@ -181,7 +181,7 @@ public class SortedList<G> : Object, Gee.Traversable<G>, Gee.Iterable<G>, Gee.Co
}
// See notes at index_of for the difference between this method and it.
- public int locate(G search, bool altered, EqualFunc equal_func = direct_equal) {
+ public int locate(G search, bool altered, EqualFunc<G> equal_func = direct_equal) {
if (cmp == null || altered) {
int count = list.size;
for (int ctr = 0; ctr < count; ctr++) {
diff --git a/src/Tag.vala b/src/Tag.vala
index 46cbfaa..baf5694 100644
--- a/src/Tag.vala
+++ b/src/Tag.vala
@@ -552,11 +552,13 @@ public class Tag : DataSource, ContainerSource, Proxyable, Indexable {
// path should have already been prepared by prep_tag_name.
public static Tag for_path(string name) {
Tag? tag = global.fetch_by_name(name, true);
- if (tag == null)
+ if (tag == null) {
tag = global.restore_tag_from_holding_tank(name);
+ }
- if (tag != null)
+ if (tag != null) {
return tag;
+ }
// create a new Tag for this name
try {
diff --git a/src/Thumbnail.vala b/src/Thumbnail.vala
index f47fc69..51d2612 100644
--- a/src/Thumbnail.vala
+++ b/src/Thumbnail.vala
@@ -169,14 +169,15 @@ public class Thumbnail : MediaSourceItem {
}
public static int64 exposure_time_ascending_comparator(void *a, void *b) {
- int64 time_a = (int64) (((Thumbnail *) a)->media.get_exposure_time());
- int64 time_b = (int64) (((Thumbnail *) b)->media.get_exposure_time());
- int64 result = (time_a - time_b);
+ var time_a = (((Thumbnail *) a)->media.get_exposure_time());
+ var time_b = (((Thumbnail *) b)->media.get_exposure_time());
+
+ var result = nullsafe_date_time_comperator(time_a, time_b);
return (result != 0) ? result : filename_ascending_comparator(a, b);
}
- public static int64 exposure_time_desending_comparator(void *a, void *b) {
+ public static int64 exposure_time_descending_comparator(void *a, void *b) {
int64 result = exposure_time_ascending_comparator(b, a);
return (result != 0) ? result : filename_descending_comparator(a, b);
diff --git a/src/ThumbnailCache.vala b/src/ThumbnailCache.vala
index a0b27fd..5585708 100644
--- a/src/ThumbnailCache.vala
+++ b/src/ThumbnailCache.vala
@@ -33,7 +33,8 @@ public class ThumbnailCache : Object {
// so be careful before changing any of these values (and especially careful before arbitrarily
// manipulating a Size enum)
public enum Size {
- LARGEST = 360,
+ LARGEST = 512,
+ LARGE = 512,
BIG = 360,
MEDIUM = 128,
SMALLEST = 128;
@@ -47,11 +48,18 @@ public class ThumbnailCache : Object {
}
public static Size get_best_size(int scale) {
- return scale <= MEDIUM.get_scale() ? MEDIUM : BIG;
+ var real_scale = Application.get_scale() * scale;
+
+ if (real_scale <= MEDIUM.get_scale())
+ return MEDIUM;
+ if (real_scale <= BIG.get_scale())
+ return BIG;
+
+ return LARGE;
}
}
- private static Size[] ALL_SIZES = { Size.BIG, Size.MEDIUM };
+ private static Size[] ALL_SIZES = { Size.LARGE, Size.BIG, Size.MEDIUM };
public delegate void AsyncFetchCallback(Gdk.Pixbuf? pixbuf, Gdk.Pixbuf? unscaled, Dimensions dim,
Gdk.InterpType interp, Error? err);
@@ -167,9 +175,11 @@ public class ThumbnailCache : Object {
public const ulong MAX_BIG_CACHED_BYTES = 40 * 1024 * 1024;
public const ulong MAX_MEDIUM_CACHED_BYTES = 30 * 1024 * 1024;
+ public const ulong MAX_LARGE_CACHED_BYTES = 15 * 1024 * 1024;
private static ThumbnailCache big = null;
private static ThumbnailCache medium = null;
+ private static ThumbnailCache large = null;
private static OneShotScheduler debug_scheduler = null;
private static int cycle_fetched_thumbnails = 0;
@@ -203,6 +213,7 @@ public class ThumbnailCache : Object {
big = new ThumbnailCache(Size.BIG, MAX_BIG_CACHED_BYTES);
medium = new ThumbnailCache(Size.MEDIUM, MAX_MEDIUM_CACHED_BYTES);
+ large = new ThumbnailCache(Size.LARGE, MAX_LARGE_CACHED_BYTES);
}
public static void terminate() {
@@ -213,27 +224,33 @@ public class ThumbnailCache : Object {
debug("import from source: %s", source.to_string());
big._import_from_source(source, force);
medium._import_from_source(source, force);
+ large._import_from_source(source, force);
}
public static void import_thumbnails(ThumbnailSource source, Thumbnails thumbnails,
bool force = false) throws Error {
big._import_thumbnail(source, thumbnails.get(Size.BIG), force);
medium._import_thumbnail(source, thumbnails.get(Size.MEDIUM), force);
+ large._import_thumbnail(source, thumbnails.get(Size.LARGE), force);
}
public static void duplicate(ThumbnailSource src_source, ThumbnailSource dest_source) {
big._duplicate(src_source, dest_source);
medium._duplicate(src_source, dest_source);
+ large._duplicate(src_source, dest_source);
}
public static void remove(ThumbnailSource source) {
big._remove(source);
medium._remove(source);
+ large._remove(source);
}
private static ThumbnailCache get_best_cache(int scale) {
Size size = Size.get_best_size(scale);
- if (size == Size.BIG) {
+ if (size == Size.LARGE) {
+ return large;
+ } else if (size == Size.BIG) {
return big;
} else {
assert(size == Size.MEDIUM);
@@ -244,6 +261,9 @@ public class ThumbnailCache : Object {
private static ThumbnailCache get_cache_for(Size size) {
switch (size) {
+ case Size.LARGE:
+ return large;
+
case Size.BIG:
return big;
diff --git a/src/TimedQueue.vala b/src/TimedQueue.vala
index 47faf3c..4ea6a23 100644
--- a/src/TimedQueue.vala
+++ b/src/TimedQueue.vala
@@ -50,7 +50,7 @@ public class TimedQueue<G> {
// finding a workaround, namely using a delegate:
// https://bugzilla.gnome.org/show_bug.cgi?id=628639
public TimedQueue(uint hold_msec, DequeuedCallback<G> callback,
- owned Gee.EqualDataFunc? equal_func = null, int priority = Priority.DEFAULT) {
+ owned Gee.EqualDataFunc<G>? equal_func = null, int priority = Priority.DEFAULT) {
this.hold_msec = hold_msec;
this.callback = callback;
diff --git a/src/Upgrades.vala b/src/Upgrades.vala
index 85349ae..b06ccad 100644
--- a/src/Upgrades.vala
+++ b/src/Upgrades.vala
@@ -62,7 +62,7 @@ private interface UpgradeTask : Object{
// Deletes the mimics folder, if it still exists.
// Note: for the step count to be consistent, files cannot be written
-// to the mimcs folder for the duration of this task.
+// to the mimics folder for the duration of this task.
private class MimicsRemovalTask : Object, UpgradeTask {
// Mimics folder (to be deleted, if present)
private File mimic_dir = AppDirs.get_data_dir().get_child("mimics");
diff --git a/src/VideoMetadata.vala b/src/VideoMetadata.vala
deleted file mode 100644
index 49ba8ef..0000000
--- a/src/VideoMetadata.vala
+++ /dev/null
@@ -1,655 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
- */
-
-public class VideoMetadata : MediaMetadata {
-
- private MetadataDateTime timestamp = null;
- private string title = null;
- private string comment = null;
-
- public VideoMetadata() {
- }
-
- ~VideoMetadata() {
- }
-
- public override void read_from_file(File file) throws Error {
- QuickTimeMetadataLoader quicktime = new QuickTimeMetadataLoader(file);
- if (quicktime.is_supported()) {
- timestamp = quicktime.get_creation_date_time();
- title = quicktime.get_title();
- // TODO: is there an quicktime.get_comment ??
- comment = null;
- return;
- }
- AVIMetadataLoader avi = new AVIMetadataLoader(file);
- if (avi.is_supported()) {
- timestamp = avi.get_creation_date_time();
- title = avi.get_title();
- comment = null;
- return;
- }
-
- throw new IOError.NOT_SUPPORTED("File %s is not a supported video format", file.get_path());
- }
-
- public override MetadataDateTime? get_creation_date_time() {
- return timestamp;
- }
-
- public override string? get_title() {
- return title;
- }
-
- public override string? get_comment() {
- return comment;
- }
-
-}
-
-private class QuickTimeMetadataLoader {
-
- // Quicktime calendar date/time format is number of seconds since January 1, 1904.
- // This converts to UNIX time (66 years + 17 leap days).
- public const time_t QUICKTIME_EPOCH_ADJUSTMENT = 2082844800;
-
- private File file = null;
-
- public QuickTimeMetadataLoader(File file) {
- this.file = file;
- }
-
- public MetadataDateTime? get_creation_date_time() {
- return new MetadataDateTime((time_t) get_creation_date_time_for_quicktime());
- }
-
- public string? get_title() {
- // Not supported.
- return null;
- }
-
- // Checks if the given file is a QuickTime file.
- public bool is_supported() {
- QuickTimeAtom test = new QuickTimeAtom(file);
-
- bool ret = false;
- try {
- test.open_file();
- test.read_atom();
-
- // Look for the header.
- if ("ftyp" == test.get_current_atom_name()) {
- ret = true;
- } else {
- // Some versions of QuickTime don't have
- // an ftyp section, so we'll just look
- // for the mandatory moov section.
- while(true) {
- if ("moov" == test.get_current_atom_name()) {
- ret = true;
- break;
- }
- test.next_atom();
- test.read_atom();
- if (test.is_last_atom()) {
- break;
- }
- }
- }
- } catch (GLib.Error e) {
- debug("Error while testing for QuickTime file for %s: %s", file.get_path(), e.message);
- }
-
- try {
- test.close_file();
- } catch (GLib.Error e) {
- debug("Error while closing Quicktime file: %s", e.message);
- }
- return ret;
- }
-
- private ulong get_creation_date_time_for_quicktime() {
- QuickTimeAtom test = new QuickTimeAtom(file);
- time_t timestamp = 0;
-
- try {
- test.open_file();
- bool done = false;
- while(!done) {
- // Look for "moov" section.
- test.read_atom();
- if (test.is_last_atom()) break;
- if ("moov" == test.get_current_atom_name()) {
- QuickTimeAtom child = test.get_first_child_atom();
- while (!done) {
- // Look for "mvhd" section, or break if none is found.
- child.read_atom();
- if (child.is_last_atom() || 0 == child.section_size_remaining()) {
- done = true;
- break;
- }
-
- if ("mvhd" == child.get_current_atom_name()) {
- // Skip 4 bytes (version + flags)
- child.read_uint32();
- // Grab the timestamp.
- timestamp = child.read_uint32() - QUICKTIME_EPOCH_ADJUSTMENT;
- done = true;
- break;
- }
- child.next_atom();
- }
- }
- test.next_atom();
- }
- } catch (GLib.Error e) {
- debug("Error while testing for QuickTime file: %s", e.message);
- }
-
- try {
- test.close_file();
- } catch (GLib.Error e) {
- debug("Error while closing Quicktime file: %s", e.message);
- }
-
- // Some Android phones package videos recorded with their internal cameras in a 3GP
- // container that looks suspiciously like a QuickTime container but really isn't -- for
- // the timestamps of these Android 3GP videos are relative to the UNIX epoch
- // (January 1, 1970) instead of the QuickTime epoch (January 1, 1904). So, if we detect a
- // QuickTime movie with a negative timestamp, we can be pretty sure it isn't a valid
- // QuickTime movie that was shot before 1904 but is instead a non-compliant 3GP video
- // file. If we detect such a video, we correct its time. See this Redmine ticket
- // (http://redmine.yorba.org/issues/3314) for more information.
- if (timestamp < 0)
- timestamp += QUICKTIME_EPOCH_ADJUSTMENT;
-
- return (ulong) timestamp;
- }
-}
-
-private class QuickTimeAtom {
- private GLib.File file = null;
- private string section_name = "";
- private uint64 section_size = 0;
- private uint64 section_offset = 0;
- private GLib.DataInputStream input = null;
- private QuickTimeAtom? parent = null;
-
- public QuickTimeAtom(GLib.File file) {
- this.file = file;
- }
-
- private QuickTimeAtom.with_input_stream(GLib.DataInputStream input, QuickTimeAtom parent) {
- this.input = input;
- this.parent = parent;
- }
-
- public void open_file() throws GLib.Error {
- close_file();
- input = new GLib.DataInputStream(file.read());
- input.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
- section_size = 0;
- section_offset = 0;
- section_name = "";
- }
-
- public void close_file() throws GLib.Error {
- if (null != input) {
- input.close();
- input = null;
- }
- }
-
- private void advance_section_offset(uint64 amount) {
- section_offset += amount;
- if (null != parent) {
- parent.advance_section_offset(amount);
- }
- }
-
- public QuickTimeAtom get_first_child_atom() {
- // Child will simply have the input stream
- // but not the size/offset. This works because
- // child atoms follow immediately after a header,
- // so no skipping is required to access the child
- // from the current position.
- return new QuickTimeAtom.with_input_stream(input, this);
- }
-
- public uchar read_byte() throws GLib.Error {
- advance_section_offset(1);
- return input.read_byte();
- }
-
- public uint32 read_uint32() throws GLib.Error {
- advance_section_offset(4);
- return input.read_uint32();
- }
-
- public uint64 read_uint64() throws GLib.Error {
- advance_section_offset(8);
- return input.read_uint64();
- }
-
- public void read_atom() throws GLib.Error {
- // Read atom size.
- section_size = read_uint32();
-
- // Read atom name.
- GLib.StringBuilder sb = new GLib.StringBuilder();
- sb.append_c((char) read_byte());
- sb.append_c((char) read_byte());
- sb.append_c((char) read_byte());
- sb.append_c((char) read_byte());
- section_name = sb.str;
-
- // Check string.
- if (section_name.length != 4) {
- throw new IOError.NOT_SUPPORTED("QuickTime atom name length is invalid for %s",
- file.get_path());
- }
- for (int i = 0; i < section_name.length; i++) {
- if (!section_name[i].isprint()) {
- throw new IOError.NOT_SUPPORTED("Bad QuickTime atom in file %s", file.get_path());
- }
- }
-
- if (1 == section_size) {
- // This indicates the section size is a 64-bit
- // value, specified below the atom name.
- section_size = read_uint64();
- }
- }
-
- private void skip(uint64 skip_amount) throws GLib.Error {
- skip_uint64(input, skip_amount);
- }
-
- public uint64 section_size_remaining() {
- assert(section_size >= section_offset);
- return section_size - section_offset;
- }
-
- public void next_atom() throws GLib.Error {
- skip(section_size_remaining());
- section_size = 0;
- section_offset = 0;
- }
-
- public string get_current_atom_name() {
- return section_name;
- }
-
- public bool is_last_atom() {
- return 0 == section_size;
- }
-
-}
-
-private class AVIMetadataLoader {
-
- private File file = null;
-
- // A numerical date string, i.e 2010:01:28 14:54:25
- private const int NUMERICAL_DATE_LENGTH = 19;
-
- // Marker for timestamp section in a Nikon nctg blob.
- private const uint16 NIKON_NCTG_TIMESTAMP_MARKER = 0x13;
-
- // Size limit to ensure we don't parse forever on a bad file.
- private const int MAX_STRD_LENGTH = 100;
-
- public AVIMetadataLoader(File file) {
- this.file = file;
- }
-
- public MetadataDateTime? get_creation_date_time() {
- return new MetadataDateTime((time_t) get_creation_date_time_for_avi());
- }
-
- public string? get_title() {
- // Not supported.
- return null;
- }
-
- // Checks if the given file is an AVI file.
- public bool is_supported() {
- AVIChunk chunk = new AVIChunk(file);
- bool ret = false;
- try {
- chunk.open_file();
- chunk.read_chunk();
- // Look for the header and identifier.
- if ("RIFF" == chunk.get_current_chunk_name() &&
- "AVI " == chunk.read_name()) {
- ret = true;
- }
- } catch (GLib.Error e) {
- debug("Error while testing for AVI file: %s", e.message);
- }
-
- try {
- chunk.close_file();
- } catch (GLib.Error e) {
- debug("Error while closing AVI file: %s", e.message);
- }
- return ret;
- }
-
- // Parses a Nikon nctg tag. Based losely on avi_read_nikon() in FFmpeg.
- private string read_nikon_nctg_tag(AVIChunk chunk) throws GLib.Error {
- bool found_date = false;
- while (chunk.section_size_remaining() > sizeof(uint16)*2) {
- uint16 tag = chunk.read_uint16();
- uint16 size = chunk.read_uint16();
- if (NIKON_NCTG_TIMESTAMP_MARKER == tag) {
- found_date = true;
- break;
- }
- chunk.skip(size);
- }
-
- if (found_date) {
- // Read numerical date string, example: 2010:01:28 14:54:25
- GLib.StringBuilder sb = new GLib.StringBuilder();
- for (int i = 0; i < NUMERICAL_DATE_LENGTH; i++) {
- sb.append_c((char) chunk.read_byte());
- }
- return sb.str;
- }
- return "";
- }
-
- // Parses a Fujifilm strd tag. Based on information from:
- // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
- private string read_fuji_strd_tag(AVIChunk chunk) throws GLib.Error {
- chunk.skip(98); // Ignore 98-byte binary blob.
- chunk.skip(8); // Ignore the string "FUJIFILM"
- // Read until we find four colons, then two more chars.
- int colons = 0;
- int post_colons = 0;
- GLib.StringBuilder sb = new GLib.StringBuilder();
- // End of date is two chars past the fourth colon.
- while (colons <= 4 && post_colons < 2) {
- char c = (char) chunk.read_byte();
- if (4 == colons) {
- post_colons++;
- }
- if (':' == c) {
- colons++;
- }
- if (c.isprint()) {
- sb.append_c(c);
- }
- if (sb.len > MAX_STRD_LENGTH) {
- return ""; // Give up searching.
- }
- }
-
- if (sb.str.length < NUMERICAL_DATE_LENGTH) {
- return "";
- }
- // Date is now at the end of the string.
- return sb.str.substring(sb.str.length - NUMERICAL_DATE_LENGTH);
- }
-
- // Recursively read file until the section is found.
- private string? read_section(AVIChunk chunk) throws GLib.Error {
- while (true) {
- chunk.read_chunk();
- string name = chunk.get_current_chunk_name();
- if ("IDIT" == name) {
- return chunk.section_to_string();
- } else if ("nctg" == name) {
- return read_nikon_nctg_tag(chunk);
- } else if ("strd" == name) {
- return read_fuji_strd_tag(chunk);
- }
-
- if ("LIST" == name) {
- chunk.read_name(); // Read past list name.
- string result = read_section(chunk.get_first_child_chunk());
- if (null != result) {
- return result;
- }
- }
-
- if (chunk.is_last_chunk()) {
- break;
- }
- chunk.next_chunk();
- }
- return null;
- }
-
- // Parses a date from a string.
- // Largely based on GStreamer's avi/gstavidemux.c
- // and the information here:
- // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
- private ulong parse_date(string sdate) {
- if (sdate.length == 0) {
- return 0;
- }
-
- Date date = Date();
- uint seconds = 0;
- int year, month, day, hour, min, sec;
- char weekday[4];
- char monthstr[4];
-
- if (sdate[0].isdigit()) {
- // Format is: 2005:08:17 11:42:43
- // Format is: 2010/11/30/ 19:42
- // Format is: 2010/11/30 19:42
- string tmp = sdate.dup();
- tmp.canon("0123456789 ", ' '); // strip everything but numbers and spaces
- sec = 0;
- int result = tmp.scanf("%d %d %d %d %d %d", out year, out month, out day, out hour, out min, out sec);
- if(result < 5) {
- return 0;
- }
- date.set_dmy((DateDay) day, (DateMonth) month, (DateYear) year);
- seconds = sec + min * 60 + hour * 3600;
- } else {
- // Format is: Mon Mar 3 09:44:56 2008
- if(7 != sdate.scanf("%3s %3s %d %d:%d:%d %d", weekday, monthstr, out day, out hour,
- out min, out sec, out year)) {
- return 0; // Error
- }
- date.set_dmy((DateDay) day, month_from_string((string) monthstr), (DateYear) year);
- seconds = sec + min * 60 + hour * 3600;
- }
-
- Time time = Time();
- date.to_time(out time);
-
- // watch for overflow (happens on quasi-bogus dates, like Year 200)
- time_t tm = time.mktime();
- ulong result = tm + seconds;
- if (result < tm) {
- debug("Overflow for timestamp in video file %s", file.get_path());
-
- return 0;
- }
-
- return result;
- }
-
- private DateMonth month_from_string(string s) {
- switch (s.down()) {
- case "jan":
- return DateMonth.JANUARY;
- case "feb":
- return DateMonth.FEBRUARY;
- case "mar":
- return DateMonth.MARCH;
- case "apr":
- return DateMonth.APRIL;
- case "may":
- return DateMonth.MAY;
- case "jun":
- return DateMonth.JUNE;
- case "jul":
- return DateMonth.JULY;
- case "aug":
- return DateMonth.AUGUST;
- case "sep":
- return DateMonth.SEPTEMBER;
- case "oct":
- return DateMonth.OCTOBER;
- case "nov":
- return DateMonth.NOVEMBER;
- case "dec":
- return DateMonth.DECEMBER;
- }
- return DateMonth.BAD_MONTH;
- }
-
- private ulong get_creation_date_time_for_avi() {
- AVIChunk chunk = new AVIChunk(file);
- ulong timestamp = 0;
- try {
- chunk.open_file();
- chunk.nonsection_skip(12); // Advance past 12 byte header.
- string sdate = read_section(chunk);
- if (null != sdate) {
- timestamp = parse_date(sdate.strip());
- }
- } catch (GLib.Error e) {
- debug("Error while reading AVI file: %s", e.message);
- }
-
- try {
- chunk.close_file();
- } catch (GLib.Error e) {
- debug("Error while closing AVI file: %s", e.message);
- }
- return timestamp;
- }
-}
-
-private class AVIChunk {
- private GLib.File file = null;
- private string section_name = "";
- private uint64 section_size = 0;
- private uint64 section_offset = 0;
- private GLib.DataInputStream input = null;
- private AVIChunk? parent = null;
- private const int MAX_STRING_TO_SECTION_LENGTH = 1024;
-
- public AVIChunk(GLib.File file) {
- this.file = file;
- }
-
- private AVIChunk.with_input_stream(GLib.DataInputStream input, AVIChunk parent) {
- this.input = input;
- this.parent = parent;
- }
-
- public void open_file() throws GLib.Error {
- close_file();
- input = new GLib.DataInputStream(file.read());
- input.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN);
- section_size = 0;
- section_offset = 0;
- section_name = "";
- }
-
- public void close_file() throws GLib.Error {
- if (null != input) {
- input.close();
- input = null;
- }
- }
-
- public void nonsection_skip(uint64 skip_amount) throws GLib.Error {
- skip_uint64(input, skip_amount);
- }
-
- public void skip(uint64 skip_amount) throws GLib.Error {
- advance_section_offset(skip_amount);
- skip_uint64(input, skip_amount);
- }
-
- public AVIChunk get_first_child_chunk() {
- return new AVIChunk.with_input_stream(input, this);
- }
-
- private void advance_section_offset(uint64 amount) {
- if ((section_offset + amount) > section_size)
- amount = section_size - section_offset;
-
- section_offset += amount;
- if (null != parent) {
- parent.advance_section_offset(amount);
- }
- }
-
- public uchar read_byte() throws GLib.Error {
- advance_section_offset(1);
- return input.read_byte();
- }
-
- public uint16 read_uint16() throws GLib.Error {
- advance_section_offset(2);
- return input.read_uint16();
- }
-
- public void read_chunk() throws GLib.Error {
- // don't use checked reads here because they advance the section offset, which we're trying
- // to determine here
- GLib.StringBuilder sb = new GLib.StringBuilder();
- sb.append_c((char) input.read_byte());
- sb.append_c((char) input.read_byte());
- sb.append_c((char) input.read_byte());
- sb.append_c((char) input.read_byte());
- section_name = sb.str;
- section_size = input.read_uint32();
- section_offset = 0;
- }
-
- public string read_name() throws GLib.Error {
- GLib.StringBuilder sb = new GLib.StringBuilder();
- sb.append_c((char) read_byte());
- sb.append_c((char) read_byte());
- sb.append_c((char) read_byte());
- sb.append_c((char) read_byte());
- return sb.str;
- }
-
- public void next_chunk() throws GLib.Error {
- skip(section_size_remaining());
- section_size = 0;
- section_offset = 0;
- }
-
- public string get_current_chunk_name() {
- return section_name;
- }
-
- public bool is_last_chunk() {
- return section_size == 0;
- }
-
- public uint64 section_size_remaining() {
- assert(section_size >= section_offset);
- return section_size - section_offset;
- }
-
- // Reads section contents into a string.
- public string section_to_string() throws GLib.Error {
- GLib.StringBuilder sb = new GLib.StringBuilder();
- while (section_offset < section_size) {
- sb.append_c((char) read_byte());
- if (sb.len > MAX_STRING_TO_SECTION_LENGTH) {
- return sb.str;
- }
- }
- return sb.str;
- }
-
-}
-
diff --git a/src/camera/CameraBranch.vala b/src/camera/CameraBranch.vala
index 052f093..83e6a66 100644
--- a/src/camera/CameraBranch.vala
+++ b/src/camera/CameraBranch.vala
@@ -104,7 +104,7 @@ public class Camera.SidebarEntry : Sidebar.SimplePageEntry {
}
protected override Page create_page() {
- return new ImportPage(camera.gcamera, uri, get_sidebar_name(), get_sidebar_icon());
+ return new ImportPage(camera);
}
public string get_uri() {
diff --git a/src/camera/CameraTable.vala b/src/camera/CameraTable.vala
index 5f888ac..172c00a 100644
--- a/src/camera/CameraTable.vala
+++ b/src/camera/CameraTable.vala
@@ -4,20 +4,6 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-public class DiscoveredCamera {
- public GPhoto.Camera gcamera;
- public string uri;
- public string display_name;
- public string? icon;
-
- public DiscoveredCamera(GPhoto.Camera gcamera, string uri, string display_name, string? icon) {
- this.gcamera = gcamera;
- this.uri = uri;
- this.display_name = display_name;
- this.icon = icon;
- }
-}
-
public class CameraTable {
private const int UPDATE_DELAY_MSEC = 1000;
@@ -119,32 +105,6 @@ public class CameraTable {
return "gphoto2://[%s]/".printf(port);
}
- public static string? get_port_path(string port) {
- // Accepted format is usb:001,005
- return port.has_prefix("usb:") ?
- "/dev/bus/usb/%s".printf(port.substring(4).replace(",", "/")) : null;
- }
-
-#if HAVE_UDEV
- private string? get_name_for_uuid(string uuid) {
- foreach (Volume volume in volume_monitor.get_volumes()) {
- if (volume.get_identifier(VolumeIdentifier.UUID) == uuid) {
- return volume.get_name();
- }
- }
- return null;
- }
-
- private string? get_icon_for_uuid(string uuid) {
- foreach (Volume volume in volume_monitor.get_volumes()) {
- if (volume.get_identifier(VolumeIdentifier.UUID) == uuid) {
- return volume.get_symbolic_icon().to_string();
- }
- }
- return null;
- }
-#endif
-
private void update_camera_table() throws GPhotoError {
// need to do this because virtual ports come and go in the USB world (and probably others)
GPhoto.PortInfoList port_info_list;
@@ -217,8 +177,6 @@ public class CameraTable {
// add cameras which were not present before
foreach (string port in detected_map.keys) {
string name = detected_map.get(port);
- string display_name = null;
- string? icon = null;
string uri = get_port_uri(port);
if (camera_map.has_key(uri)) {
@@ -227,41 +185,7 @@ public class CameraTable {
continue;
}
-
-#if HAVE_UDEV
- // Get display name for camera.
- string path = get_port_path(port);
- if (null != path) {
- GUdev.Device device = client.query_by_device_file(path);
- string serial = device.get_property("ID_SERIAL_SHORT");
- if (null != serial) {
- // Try to get the name and icon.
- display_name = get_name_for_uuid(serial);
- icon = get_icon_for_uuid(serial);
- }
- if (null == display_name) {
- display_name = device.get_sysfs_attr("product");
- }
- if (null == display_name) {
- display_name = device.get_property("ID_MODEL");
- }
- }
-#endif
-
- if (port.has_prefix("disk:")) {
- try {
- var mount = File.new_for_path (port.substring(5)).find_enclosing_mount();
- var volume = mount.get_volume();
- // Translators: First %s is the name of camera as gotten from GPhoto, second is the GVolume name, e.g. Mass storage camera (510MB volume)
- display_name = _("%s (%s)").printf (name, volume.get_name ());
- icon = volume.get_symbolic_icon().to_string();
- } catch (Error e) { }
- }
- if (null == display_name) {
- // Default to GPhoto detected name.
- display_name = name;
- }
int index = port_info_list.lookup_path(port);
if (index < 0)
do_op((GPhoto.Result) index, "lookup port %s".printf(port));
@@ -283,14 +207,9 @@ public class CameraTable {
do_op(abilities_list.get_abilities(index, out camera_abilities),
"lookup camera abilities for %s".printf(name));
- GPhoto.Camera gcamera;
- do_op(GPhoto.Camera.create(out gcamera), "create camera object for %s".printf(name));
- do_op(gcamera.set_abilities(camera_abilities), "set camera abilities for %s".printf(name));
- do_op(gcamera.set_port_info(port_info), "set port info for %s on %s".printf(name, port));
-
debug("Adding to camera table: %s @ %s", name, port);
- DiscoveredCamera camera = new DiscoveredCamera(gcamera, uri, display_name, icon);
+ var camera = new DiscoveredCamera(name, port, port_info, camera_abilities);
camera_map.set(uri, camera);
camera_added(camera);
diff --git a/src/camera/DiscoveredCamera.vala b/src/camera/DiscoveredCamera.vala
new file mode 100644
index 0000000..700af8b
--- /dev/null
+++ b/src/camera/DiscoveredCamera.vala
@@ -0,0 +1,119 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class DiscoveredCamera {
+ public GPhoto.Camera gcamera;
+ public string uri;
+ public string display_name;
+ public string? icon;
+
+ private string port;
+ private string camera_name;
+ private string[] mount_uris;
+
+ public DiscoveredCamera(string name, string port, GPhoto.PortInfo port_info, GPhoto.CameraAbilities camera_abilities) throws GPhotoError {
+ this.port = port;
+ this.camera_name = name;
+ this.uri = "gphoto2://[%s]".printf(port);
+
+ this.mount_uris = new string[0];
+ this.mount_uris += this.uri;
+ this.mount_uris += "mtp://[%s]".printf(port);
+
+ var res = GPhoto.Camera.create(out this.gcamera);
+
+ if (res != GPhoto.Result.OK) {
+ throw new GPhotoError.LIBRARY("[%d] Unable to create camera object for %s: %s",
+ (int) res, name, res.as_string());
+ }
+
+ res = gcamera.set_abilities(camera_abilities);
+ if (res != GPhoto.Result.OK) {
+ throw new GPhotoError.LIBRARY("[%d] Unable to set camera abilities for %s: %s",
+ (int) res, name, res.as_string());
+ }
+
+ res = gcamera.set_port_info(port_info);
+ if (res != GPhoto.Result.OK) {
+ throw new GPhotoError.LIBRARY("[%d] Unable to set port infor for %s: %s",
+ (int) res, name, res.as_string());
+ }
+
+ var path = get_port_path(port);
+ if (path != null) {
+ var monitor = VolumeMonitor.get();
+ foreach (var volume in monitor.get_volumes()) {
+ if (volume.get_identifier(VolumeIdentifier.UNIX_DEVICE) == path) {
+ this.display_name = volume.get_name();
+ this.icon = volume.get_symbolic_icon().to_string();
+ }
+ }
+
+#if HAVE_UDEV
+ var client = new GUdev.Client(null);
+ var device = client.query_by_device_file(path);
+
+
+ // Create alternative uris (used for unmount)
+ var serial = device.get_property("ID_SERIAL");
+ this.mount_uris += "gphoto2://%s".printf(serial);
+ this.mount_uris += "mtp://%s".printf(serial);
+
+ // Look-up alternative display names
+ if (display_name == null) {
+ display_name = device.get_sysfs_attr("product");
+ }
+
+ if (display_name == null) {
+ display_name = device.get_property("ID_MODEL");
+ }
+#endif
+ }
+
+ if (port.has_prefix("disk:")) {
+ try {
+ var mount = File.new_for_path (port.substring(5)).find_enclosing_mount();
+ var volume = mount.get_volume();
+ if (volume != null) {
+ // Translators: First %s is the name of camera as gotten from GPhoto, second is the GVolume name, e.g. Mass storage camera (510MB volume)
+ display_name = _("%s (%s)").printf (name, volume.get_name ());
+ icon = volume.get_symbolic_icon().to_string();
+ } else {
+ // Translators: First %s is the name of camera as gotten from GPhoto, second is the GMount name, e.g. Mass storage camera (510MB volume)
+ display_name = _("%s (%s)").printf (name, mount.get_name ());
+ icon = mount.get_symbolic_icon().to_string();
+ }
+
+ } catch (Error e) { }
+ }
+
+ if (display_name == null) {
+ this.display_name = camera_name;
+ }
+ }
+
+ public Mount? get_mount() {
+ foreach (var uri in this.mount_uris) {
+ var f = File.new_for_uri(uri);
+ try {
+ var mount = f.find_enclosing_mount(null);
+ if (mount != null)
+ return mount;
+ } catch (Error error) {}
+ }
+
+ return null;
+ }
+
+ private string? get_port_path(string port) {
+ // Accepted format is usb:001,005
+ return port.has_prefix("usb:") ?
+ "/dev/bus/usb/%s".printf(port.substring(4).replace(",", "/")) : null;
+ }
+
+}
+
+
diff --git a/src/camera/GPhoto.vala b/src/camera/GPhoto.vala
index 9bcb151..702f307 100644
--- a/src/camera/GPhoto.vala
+++ b/src/camera/GPhoto.vala
@@ -93,7 +93,7 @@ namespace GPhoto {
}
// For CameraFileInfoFile, CameraFileInfoPreview, and CameraStorageInformation. See:
- // http://redmine.yorba.org/issues/1851
+ // https://bugzilla.gnome.org/show_bug.cgi?id=716252
// https://bugzilla.redhat.com/show_bug.cgi?id=585676
// https://sourceforge.net/tracker/?func=detail&aid=3000198&group_id=8874&atid=108874
public const int MAX_FILENAME_LENGTH = 63;
@@ -129,11 +129,10 @@ namespace GPhoto {
// Libgphoto will in some instances refuse to get metadata from a camera, but the camera is accessible as a
// filesystem. In these cases shotwell can access the file directly. See:
- // http://redmine.yorba.org/issues/2959
+ // https://bugzilla.gnome.org/show_bug.cgi?id=716915
public PhotoMetadata? get_fallback_metadata(Camera camera, Context context, string folder, string filename) {
// Fixme: Why do we need to query get_storageinfo here first?
GPhoto.CameraStorageInformation[] sifs = null;
- int count = 0;
camera.get_storageinfo(out sifs, context);
GPhoto.PortInfo port_info;
diff --git a/src/camera/ImportPage.vala b/src/camera/ImportPage.vala
index 84d7cbe..a5d3b4e 100644
--- a/src/camera/ImportPage.vala
+++ b/src/camera/ImportPage.vala
@@ -21,13 +21,13 @@ abstract class ImportSource : ThumbnailSource, Indexable {
private string folder;
private string filename;
private ulong file_size;
- private time_t modification_time;
+ private DateTime modification_time;
private Gdk.Pixbuf? preview = null;
private string? indexable_keywords = null;
protected ImportSource(string camera_name, GPhoto.Camera camera, int fsid, string folder,
- string filename, ulong file_size, time_t modification_time) {
- this.camera_name = camera_name;
+ string filename, ulong file_size, DateTime modification_time) {
+ this.camera_name =camera_name;
this.camera = camera;
this.fsid = fsid;
this.folder = folder;
@@ -65,7 +65,7 @@ abstract class ImportSource : ThumbnailSource, Indexable {
return file_size;
}
- public time_t get_modification_time() {
+ public DateTime get_modification_time() {
return modification_time;
}
@@ -73,7 +73,7 @@ abstract class ImportSource : ThumbnailSource, Indexable {
return preview;
}
- public virtual time_t get_exposure_time() {
+ public virtual DateTime get_exposure_time() {
return get_modification_time();
}
@@ -110,7 +110,7 @@ abstract class ImportSource : ThumbnailSource, Indexable {
class VideoImportSource : ImportSource {
public VideoImportSource(string camera_name, GPhoto.Camera camera, int fsid, string folder,
- string filename, ulong file_size, time_t modification_time) {
+ string filename, ulong file_size, DateTime modification_time) {
base(camera_name, camera, fsid, folder, filename, file_size, modification_time);
}
@@ -159,7 +159,7 @@ class PhotoImportSource : ImportSource {
private PhotoImportSource? associated = null; // JPEG source for RAW+JPEG
public PhotoImportSource(string camera_name, GPhoto.Camera camera, int fsid, string folder,
- string filename, ulong file_size, time_t modification_time, PhotoFileFormat file_format) {
+ string filename, ulong file_size, DateTime modification_time, PhotoFileFormat file_format) {
base(camera_name, camera, fsid, folder, filename, file_size, modification_time);
this.file_format = file_format;
}
@@ -200,7 +200,7 @@ class PhotoImportSource : ImportSource {
this.exif_md5 = exif_md5;
}
- public override time_t get_exposure_time() {
+ public override DateTime get_exposure_time() {
if (metadata == null)
return get_modification_time();
@@ -340,10 +340,10 @@ class ImportPreview : MediaSourceItem {
if (duplicated_photo_id.is_valid()) {
// Check exposure timestamp
LibraryPhoto duplicated_photo = LibraryPhoto.global.fetch(duplicated_photo_id);
- time_t photo_exposure_time = photo_import_source.get_exposure_time();
- time_t duplicated_photo_exposure_time = duplicated_photo.get_exposure_time();
+ DateTime photo_exposure_time = photo_import_source.get_exposure_time();
+ DateTime duplicated_photo_exposure_time = duplicated_photo.get_exposure_time();
- if (photo_exposure_time == duplicated_photo_exposure_time) {
+ if (photo_exposure_time.equal(duplicated_photo_exposure_time)) {
duplicated_file = DuplicatedFile.create_from_photo_id(
LibraryPhoto.global.get_basename_filesize_duplicate(
get_import_source().get_filename(), (int64) filesize));
@@ -485,7 +485,7 @@ public class ImportPage : CheckerboardPage {
private string filename;
private uint64 filesize;
private PhotoMetadata metadata;
- private time_t exposure_time;
+ private DateTime exposure_time;
private CameraImportJob? associated = null;
private BackingPhotoRow? associated_file = null;
private DuplicatedFile? duplicated_file;
@@ -503,12 +503,13 @@ public class ImportPage : CheckerboardPage {
assert(fulldir != null);
filename = import_file.get_filename();
filesize = import_file.get_filesize();
- metadata = (import_file is PhotoImportSource) ?
- (import_file as PhotoImportSource).get_metadata() : null;
+ var photo_import_source = import_file as PhotoImportSource;
+ metadata = (photo_import_source != null) ?
+ photo_import_source.get_metadata() : null;
exposure_time = import_file.get_exposure_time();
}
- public time_t get_exposure_time() {
+ public DateTime get_exposure_time() {
return exposure_time;
}
@@ -516,8 +517,8 @@ public class ImportPage : CheckerboardPage {
return duplicated_file;
}
- public override time_t get_exposure_time_override() {
- return (import_file is VideoImportSource) ? get_exposure_time() : 0;
+ public override DateTime? get_exposure_time_override() {
+ return (import_file is VideoImportSource) ? get_exposure_time() : null;
}
public override string get_dest_identifier() {
@@ -682,16 +683,13 @@ public class ImportPage : CheckerboardPage {
private Gtk.Label camera_label = new Gtk.Label(null);
private Gtk.CheckButton hide_imported;
private Gtk.ProgressBar progress_bar = new Gtk.ProgressBar();
- private GPhoto.Camera camera;
- private string uri;
+ private DiscoveredCamera dcamera;
private bool busy = false;
private bool refreshed = false;
private GPhoto.Result refresh_result = GPhoto.Result.OK;
private string refresh_error = null;
- private string camera_name;
private VolumeMonitor volume_monitor = null;
private ImportPage? local_ref = null;
- private string? icon;
private ImportPageSearchViewFilter search_filter = new ImportPageSearchViewFilter();
private HideImportedViewFilter hide_imported_filter = new HideImportedViewFilter();
private CameraViewTracker tracker;
@@ -707,28 +705,15 @@ public class ImportPage : CheckerboardPage {
LIBRARY_ERROR
}
- public ImportPage(GPhoto.Camera camera, string uri, string? display_name = null, string? icon = null) {
+ public ImportPage(DiscoveredCamera dcamera) {
base(_("Camera"));
- this.camera = camera;
- this.uri = uri;
- this.import_sources = new ImportSourceCollection("ImportSources for %s".printf(uri));
- this.icon = icon;
+ this.dcamera = dcamera;
+ this.import_sources = new ImportSourceCollection("ImportSources for %s".printf(dcamera.uri));
tracker = new CameraViewTracker(get_view());
- // Get camera name.
- if (null != display_name) {
- camera_name = display_name;
- } else {
- GPhoto.CameraAbilities abilities;
- GPhoto.Result res = camera.get_abilities(out abilities);
- if (res != GPhoto.Result.OK) {
- debug("Unable to get camera abilities: %s", res.to_full_string());
- camera_name = _("Camera");
- }
- }
- camera_label.set_text(camera_name);
- set_page_name(camera_name);
+ camera_label.set_text(dcamera.display_name);
+ set_page_name(dcamera.display_name);
// Mount.unmounted signal is *only* fired when a VolumeMonitor has been instantiated.
this.volume_monitor = VolumeMonitor.get();
@@ -846,6 +831,14 @@ public class ImportPage : CheckerboardPage {
return tracker;
}
+ protected override string get_view_empty_icon() {
+ if (this.dcamera.icon != null) {
+ return this.dcamera.icon;
+ }
+
+ return "camera-photo-symbolic";
+ }
+
protected override string get_view_empty_message() {
return _("The camera seems to be empty. No photos/videos found to import");
}
@@ -855,8 +848,8 @@ public class ImportPage : CheckerboardPage {
}
private static int64 preview_comparator(void *a, void *b) {
- return ((ImportPreview *) a)->get_import_source().get_exposure_time()
- - ((ImportPreview *) b)->get_import_source().get_exposure_time();
+ return nullsafe_date_time_comperator(((ImportPreview *) a)->get_import_source().get_exposure_time(),
+ ((ImportPreview *) b)->get_import_source().get_exposure_time());
}
private static bool preview_comparator_predicate(DataObject object, Alteration alteration) {
@@ -864,7 +857,7 @@ public class ImportPage : CheckerboardPage {
}
private int64 import_job_comparator(void *a, void *b) {
- return ((CameraImportJob *) a)->get_exposure_time() - ((CameraImportJob *) b)->get_exposure_time();
+ return nullsafe_date_time_comperator(((CameraImportJob *) a)->get_exposure_time(), ((CameraImportJob *) b)->get_exposure_time());
}
protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
@@ -896,11 +889,11 @@ public class ImportPage : CheckerboardPage {
}
public GPhoto.Camera get_camera() {
- return camera;
+ return dcamera.gcamera;
}
public string get_uri() {
- return uri;
+ return dcamera.uri;
}
public bool is_busy() {
@@ -998,27 +991,9 @@ public class ImportPage : CheckerboardPage {
}
// if locked because it's mounted, offer to unmount
- debug("Checking if %s is mounted…", uri);
+ debug("Checking if %s is mounted…", dcamera.uri);
- File uri = File.new_for_uri(uri);
-
- Mount mount = null;
- try {
- mount = uri.find_enclosing_mount(null);
- } catch (Error err) {
- // error means not mounted
- }
-
- // Could not find mount for gphoto2://, re-try with mtp://
- // It seems some devices are mounted using MTP and not gphoto2 daemon
- if (mount == null && this.uri.has_prefix("gphoto2")) {
- uri = File.new_for_uri("mtp" + this.uri.substring(7));
- try {
- mount = uri.find_enclosing_mount(null);
- } catch (Error err) {
- // error means not mounted
- }
- }
+ var mount = dcamera.get_mount();
if (mount != null) {
// it's mounted, offer to unmount for the user
@@ -1128,7 +1103,7 @@ public class ImportPage : CheckerboardPage {
* @param search_target The name of the directory to look for.
*/
private bool check_directory_exists(int fsid, string dir, string search_target) {
- string? fulldir = get_fulldir(camera, camera_name, fsid, dir);
+ string? fulldir = get_fulldir(dcamera.gcamera, dcamera.display_name, fsid, dir);
GPhoto.Result result;
GPhoto.CameraList folders;
@@ -1138,7 +1113,7 @@ public class ImportPage : CheckerboardPage {
return false;
}
- result = camera.list_folders(fulldir, folders, spin_idle_context.context);
+ result = dcamera.gcamera.list_folders(fulldir, folders, spin_idle_context.context);
if (result != GPhoto.Result.OK) {
// fetching the list failed - can't determine whether specified dir is present
return false;
@@ -1167,7 +1142,7 @@ public class ImportPage : CheckerboardPage {
update_status(busy, false);
refresh_error = null;
- refresh_result = camera.init(spin_idle_context.context);
+ refresh_result = dcamera.gcamera.init(spin_idle_context.context);
// If we fail to claim the device, we might have run into a conflict
// with gvfs-gphoto2-volume-monitor. Back off, try again after
@@ -1209,7 +1184,7 @@ public class ImportPage : CheckerboardPage {
Gee.ArrayList<ImportSource> import_list = new Gee.ArrayList<ImportSource>();
GPhoto.CameraStorageInformation[] sifs = null;
- refresh_result = camera.get_storageinfo(out sifs, spin_idle_context.context);
+ refresh_result = dcamera.gcamera.get_storageinfo(out sifs, spin_idle_context.context);
if (refresh_result == GPhoto.Result.OK) {
for (int fsid = 0; fsid < sifs.length; fsid++) {
// Check well-known video and image paths first to prevent accidental
@@ -1302,7 +1277,7 @@ public class ImportPage : CheckerboardPage {
progress_bar.set_text("");
progress_bar.set_fraction(0.0);
- GPhoto.Result res = camera.exit(spin_idle_context.context);
+ GPhoto.Result res = dcamera.gcamera.exit(spin_idle_context.context);
if (res != GPhoto.Result.OK) {
// log but don't fail
warning("Unable to unlock camera: %s", res.to_full_string());
@@ -1386,7 +1361,7 @@ public class ImportPage : CheckerboardPage {
}
private bool enumerate_files(int fsid, string dir, Gee.ArrayList<ImportSource> import_list) {
- string? fulldir = get_fulldir(camera, camera_name, fsid, dir);
+ string? fulldir = get_fulldir(dcamera.gcamera, dcamera.display_name, fsid, dir);
if (fulldir == null) {
warning("Skipping enumerating %s: invalid folder name", dir);
@@ -1401,7 +1376,7 @@ public class ImportPage : CheckerboardPage {
return false;
}
- refresh_result = camera.list_files(fulldir, files, spin_idle_context.context);
+ refresh_result = dcamera.gcamera.list_files(fulldir, files, spin_idle_context.context);
if (refresh_result != GPhoto.Result.OK) {
warning("Unable to list files in %s: %s", fulldir, refresh_result.to_full_string());
@@ -1424,7 +1399,7 @@ public class ImportPage : CheckerboardPage {
try {
GPhoto.CameraFileInfo info;
- if (!GPhoto.get_info(spin_idle_context.context, camera, fulldir, filename, out info)) {
+ if (!GPhoto.get_info(spin_idle_context.context, dcamera.gcamera, fulldir, filename, out info)) {
warning("Skipping import of %s/%s: name too long", fulldir, filename);
continue;
@@ -1438,8 +1413,8 @@ public class ImportPage : CheckerboardPage {
}
if (VideoReader.is_supported_video_filename(filename)) {
- VideoImportSource video_source = new VideoImportSource(camera_name, camera,
- fsid, dir, filename, info.file.size, info.file.mtime);
+ VideoImportSource video_source = new VideoImportSource(dcamera.display_name, dcamera.gcamera,
+ fsid, dir, filename, info.file.size, new DateTime.from_unix_utc(info.file.mtime));
import_list.add(video_source);
} else {
// determine file format from type, and then from file extension
@@ -1454,8 +1429,8 @@ public class ImportPage : CheckerboardPage {
continue;
}
}
- import_list.add(new PhotoImportSource(camera_name, camera, fsid, dir, filename,
- info.file.size, info.file.mtime, file_format));
+ import_list.add(new PhotoImportSource(dcamera.display_name, dcamera.gcamera, fsid, dir, filename,
+ info.file.size, new DateTime.from_unix_utc(info.file.mtime), file_format));
}
progress_bar.pulse();
@@ -1479,7 +1454,7 @@ public class ImportPage : CheckerboardPage {
return false;
}
- refresh_result = camera.list_folders(fulldir, folders, spin_idle_context.context);
+ refresh_result = dcamera.gcamera.list_folders(fulldir, folders, spin_idle_context.context);
if (refresh_result != GPhoto.Result.OK) {
warning("Unable to list folders in %s: %s", fulldir, refresh_result.to_full_string());
@@ -1498,8 +1473,12 @@ public class ImportPage : CheckerboardPage {
return false;
}
- if (!enumerate_files(fsid, append_path(dir, subdir), import_list))
- return false;
+ if (subdir.has_prefix(".")) {
+ debug("Skipping hidden sub-folder %s in %s", subdir, dir);
+ } else {
+ if (!enumerate_files(fsid, append_path(dir, subdir), import_list))
+ return false;
+ }
}
return true;
@@ -1575,7 +1554,7 @@ public class ImportPage : CheckerboardPage {
PhotoMetadata? metadata = null;
if (!VideoReader.is_supported_video_filename(filename)) {
try {
- metadata = GPhoto.load_metadata(spin_idle_context.context, camera, fulldir,
+ metadata = GPhoto.load_metadata(spin_idle_context.context, dcamera.gcamera, fulldir,
filename);
} catch (Error err) {
warning("Unable to fetch metadata for %s/%s: %s", fulldir, filename,
@@ -1604,7 +1583,7 @@ public class ImportPage : CheckerboardPage {
preview_fulldir = associated.get_fulldir();
preview_filename = associated.get_filename();
}
- preview = GPhoto.load_preview(spin_idle_context.context, camera, preview_fulldir,
+ preview = GPhoto.load_preview(spin_idle_context.context, dcamera.gcamera, preview_fulldir,
preview_filename, out preview_md5);
} catch (Error err) {
// only issue the warning message if we're not reading a video. GPhoto is capable
@@ -1621,17 +1600,18 @@ public class ImportPage : CheckerboardPage {
debug("camera MD5 %s: exif=%s preview=%s", filename, exif_only_md5, preview_md5);
#endif
- if (import_source is VideoImportSource)
- (import_source as VideoImportSource).update(preview);
+ var video_import_source = import_source as VideoImportSource;
+ if (video_import_source != null)
+ video_import_source.update(preview);
- if (import_source is PhotoImportSource)
- (import_source as PhotoImportSource).update(preview, preview_md5, metadata,
- exif_only_md5);
+ var photo_import_source = import_source as PhotoImportSource;
+ if (photo_import_source != null)
+ photo_import_source.update(preview, preview_md5, metadata, exif_only_md5);
if (associated != null) {
try {
PhotoMetadata? associated_metadata = GPhoto.load_metadata(spin_idle_context.context,
- camera, associated.get_fulldir(), associated.get_filename());
+ dcamera.gcamera, associated.get_fulldir(), associated.get_filename());
associated.update(preview, preview_md5, associated_metadata, null);
} catch (Error err) {
warning("Unable to fetch metadata for %s/%s: %s", associated.get_fulldir(),
@@ -1671,7 +1651,7 @@ public class ImportPage : CheckerboardPage {
}
private void import(Gee.Iterable<DataObject> items) {
- GPhoto.Result res = camera.init(spin_idle_context.context);
+ GPhoto.Result res = dcamera.gcamera.init(spin_idle_context.context);
if (res != GPhoto.Result.OK) {
AppWindow.error_message(_("Unable to lock camera: %s").printf(res.to_full_string()));
@@ -1712,14 +1692,14 @@ public class ImportPage : CheckerboardPage {
jobs.add(import_job);
}
- debug("Importing %d files from %s", jobs.size, camera_name);
+ debug("Importing %d files from %s", jobs.size, dcamera.display_name);
if (jobs.size > 0) {
// see import_reporter() to see why this is held during the duration of the import
assert(local_ref == null);
local_ref = this;
- BatchImport batch_import = new BatchImport(jobs, camera_name, import_reporter,
+ BatchImport batch_import = new BatchImport(jobs, dcamera.display_name, import_reporter,
null, already_imported);
batch_import.import_job_failed.connect(on_import_job_failed);
batch_import.import_complete.connect(close_import);
@@ -1811,7 +1791,7 @@ public class ImportPage : CheckerboardPage {
}
private void close_import() {
- GPhoto.Result res = camera.exit(spin_idle_context.context);
+ GPhoto.Result res = dcamera.gcamera.exit(spin_idle_context.context);
if (res != GPhoto.Result.OK) {
// log but don't fail
message("Unable to unlock camera: %s", res.to_full_string());
diff --git a/src/config/Config.vala b/src/config/Config.vala
index 0e2798a..3081ff0 100644
--- a/src/config/Config.vala
+++ b/src/config/Config.vala
@@ -26,7 +26,7 @@ public class Facade : ConfigurationFacade {
public signal void colors_changed();
private Facade() {
- base(new GSettingsConfigurationEngine());
+ base(new GSettingsConfigurationEngine(Shotwell.ProfileManager.get_instance().id()));
transparent_background_type_changed.connect(on_color_name_changed);
transparent_background_color_changed.connect(on_color_name_changed);
diff --git a/src/config/ConfigurationInterfaces.vala b/src/config/ConfigurationInterfaces.vala
index a8d8192..12c7da1 100644
--- a/src/config/ConfigurationInterfaces.vala
+++ b/src/config/ConfigurationInterfaces.vala
@@ -39,6 +39,7 @@ public enum ConfigurableProperty {
DISPLAY_EXTENDED_PROPERTIES,
DISPLAY_SIDEBAR,
DISPLAY_TOOLBAR,
+ DISPLAY_MAP_WIDGET,
DISPLAY_SEARCH_BAR,
DISPLAY_PHOTO_RATINGS,
DISPLAY_PHOTO_TAGS,
@@ -149,7 +150,10 @@ public enum ConfigurableProperty {
case DISPLAY_TOOLBAR:
return "DISPLAY_TOOLBAR";
-
+
+ case DISPLAY_MAP_WIDGET:
+ return "DISPLAY_MAP_WIDGET";
+
case DISPLAY_SEARCH_BAR:
return "DISPLAY_SEARCH_BAR";
@@ -400,6 +404,9 @@ public abstract class ConfigurationFacade : Object {
case ConfigurableProperty.IMPORT_DIR:
import_directory_changed();
break;
+ default:
+ // We do not support notification for the rest of the properties
+ break;
}
}
@@ -718,7 +725,6 @@ public abstract class ConfigurationFacade : Object {
on_configuration_error(err);
}
}
-
//
// display toolbar
@@ -742,6 +748,26 @@ public abstract class ConfigurationFacade : Object {
}
//
+ // display map widget
+ //
+ public virtual bool get_display_map_widget() {
+ try {
+ return get_engine().get_bool_property(ConfigurableProperty.DISPLAY_MAP_WIDGET);
+ } catch (ConfigurationError err) {
+ on_configuration_error(err);
+
+ return false;
+ }
+ }
+ public virtual void set_display_map_widget(bool display) {
+ try {
+ get_engine().set_bool_property(ConfigurableProperty.DISPLAY_MAP_WIDGET, display);
+ } catch (ConfigurationError err) {
+ on_configuration_error(err);
+ }
+ }
+
+ //
// display search & filter toolbar
//
public virtual bool get_display_search_bar() {
diff --git a/src/config/GSettingsEngine.vala b/src/config/GSettingsEngine.vala
index d35eb93..d4d95c6 100644
--- a/src/config/GSettingsEngine.vala
+++ b/src/config/GSettingsEngine.vala
@@ -5,7 +5,7 @@
*/
public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
- private const string ROOT_SCHEMA_NAME = "org.yorba.shotwell";
+ private const string ROOT_SCHEMA_NAME = "org.gnome.shotwell";
private const string PREFS_SCHEMA_NAME = ROOT_SCHEMA_NAME + ".preferences";
private const string UI_PREFS_SCHEMA_NAME = PREFS_SCHEMA_NAME + ".ui";
private const string SLIDESHOW_PREFS_SCHEMA_NAME = PREFS_SCHEMA_NAME + ".slideshow";
@@ -25,8 +25,11 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
private string[] schema_names;
private string[] key_names;
private Gee.HashMap<string, Settings> settings_cache = new Gee.HashMap<string, Settings>();
-
- public GSettingsConfigurationEngine() {
+
+ private string profile = "";
+
+ public GSettingsConfigurationEngine(string? profile) {
+ this.profile = profile == null ? "" : profile;
schema_names = new string[ConfigurableProperty.NUM_PROPERTIES];
schema_names[ConfigurableProperty.AUTO_IMPORT_FROM_LIBRARY] = FILES_PREFS_SCHEMA_NAME;
@@ -47,6 +50,7 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
schema_names[ConfigurableProperty.DISPLAY_EXTENDED_PROPERTIES] = UI_PREFS_SCHEMA_NAME;
schema_names[ConfigurableProperty.DISPLAY_SIDEBAR] = UI_PREFS_SCHEMA_NAME;
schema_names[ConfigurableProperty.DISPLAY_TOOLBAR] = UI_PREFS_SCHEMA_NAME;
+ schema_names[ConfigurableProperty.DISPLAY_MAP_WIDGET] = UI_PREFS_SCHEMA_NAME;
schema_names[ConfigurableProperty.DISPLAY_SEARCH_BAR] = UI_PREFS_SCHEMA_NAME;
schema_names[ConfigurableProperty.DISPLAY_PHOTO_RATINGS] = UI_PREFS_SCHEMA_NAME;
schema_names[ConfigurableProperty.DISPLAY_PHOTO_TAGS] = UI_PREFS_SCHEMA_NAME;
@@ -120,6 +124,7 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
key_names[ConfigurableProperty.DISPLAY_EXTENDED_PROPERTIES] = "display-extended-properties";
key_names[ConfigurableProperty.DISPLAY_SIDEBAR] = "display-sidebar";
key_names[ConfigurableProperty.DISPLAY_TOOLBAR] = "display-toolbar";
+ key_names[ConfigurableProperty.DISPLAY_MAP_WIDGET] = "display-map-widget";
key_names[ConfigurableProperty.DISPLAY_SEARCH_BAR] = "display-search-bar";
key_names[ConfigurableProperty.DISPLAY_PHOTO_RATINGS] = "display-photo-ratings";
key_names[ConfigurableProperty.DISPLAY_PHOTO_TAGS] = "display-photo-tags";
@@ -176,7 +181,14 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
private Settings get_settings(string schema) {
if (!this.settings_cache.has_key(schema)) {
- this.settings_cache[schema] = new Settings(schema);
+ if (schema.has_prefix (ROOT_SCHEMA_NAME)) {
+ var path = schema.replace(ROOT_SCHEMA_NAME, "");
+ path = "/org/gnome/shotwell/%s%s/".printf(profile == "" ? "" : "profiles/" + profile, path.replace(".", "/"));
+ path = path.replace("//", "/");
+ this.settings_cache[schema] = new Settings.with_path (schema, path);
+ } else {
+ this.settings_cache[schema] = new Settings(schema);
+ }
}
return this.settings_cache[schema];
@@ -229,7 +241,9 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
Settings schema_object = get_settings(schema);
- return schema_object.get_int(key);
+ var v = schema_object.get_int(key);
+
+ return v;
}
private void set_gs_int(string schema, string key, int value) throws ConfigurationError {
@@ -292,7 +306,7 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
if (cleaned_id == null)
cleaned_id = "default";
- cleaned_id = cleaned_id.replace("org.yorba.shotwell.", "");
+ cleaned_id = cleaned_id.replace("org.gnome.shotwell.", "");
cleaned_id = cleaned_id.replace(".", "-");
return cleaned_id;
@@ -304,7 +318,7 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
cleaned_id = "default";
cleaned_id = cleaned_id.replace(".", "-");
- return "org.yorba.shotwell.%s.%s".printf(domain, cleaned_id);
+ return "org.gnome.shotwell.%s.%s".printf(domain, cleaned_id);
}
private static string make_gsettings_key(string gconf_key) {
@@ -513,4 +527,64 @@ public class GSettingsConfigurationEngine : ConfigurationEngine, GLib.Object {
}
}
+ /*! @brief Migrates settings data over from old-style /org/yorba/ paths to /org/gnome/ ones.
+ * Should only be called ONCE, during DB upgrading; otherwise, stale data may be copied
+ * over newer data by accident.
+ */
+ public static void run_gsettings_migrator_v2() {
+ var source = SettingsSchemaSource.get_default();
+ var schema = source.lookup("org.yorba.shotwell", true);
+ var settings = new Settings.full(schema, null, null);
+
+ copy_schema(settings);
+
+ Settings.sync();
+ }
+
+ static void copy_schema(Settings settings) {
+ SettingsSchema schema;
+ ((Object)settings).get("settings-schema", out schema, null);
+ var id = schema.get_id();
+ var path = schema.get_path();
+
+ var new_id = id.replace("org.yorba.shotwell", "org.gnome.shotwell");
+ var new_path = path.replace("/org/yorba/shotwell", "/org/gnome/shotwell");
+
+ var new_schema = SettingsSchemaSource.get_default().lookup(new_id, true);
+
+ // If we cannot find this schema, we cannot migrate the keys anyway, so skip it
+ if (new_schema != null) {
+ var new_settings = new Settings.with_path(new_id, new_path);
+ new_settings.delay();
+
+ foreach (var k in schema.list_keys()) {
+ var key = schema.get_key(k);
+ var default_value = key.get_default_value();
+ var val = settings.get_value(k);
+ if (val.equal(default_value)) {
+ debug("%s is default value, skipping", k);
+ continue;
+ }
+
+ if (!new_schema.has_key(k)) {
+ debug("Cannot migrate %s as it does not exist", k);
+ continue;
+ }
+
+ debug("Will migrate %s %s @ %s -> %s:%s %s", k, id, path, new_id, new_path, val.print(true));
+ if (!new_settings.set_value(k, val)) {
+ debug(" Failed...");
+ }
+ }
+ new_settings.apply();
+ }
+ else {
+ debug("%s does not exist, skipping\n", new_id);
+ }
+
+ foreach (var c in schema.list_children()) {
+ var child = settings.get_child(c);
+ copy_schema(child);
+ }
+ }
}
diff --git a/src/core/DataCollection.vala b/src/core/DataCollection.vala
index 83a216d..044f7b6 100644
--- a/src/core/DataCollection.vala
+++ b/src/core/DataCollection.vala
@@ -559,7 +559,7 @@ public class DataCollection {
if (!properties.unset(name))
return;
- // only notify if the propery was unset (that is, was set to begin with)
+ // only notify if the property was unset (that is, was set to begin with)
notify_property_cleared(name);
// notify all items
diff --git a/src/core/DataSourceTypes.vala b/src/core/DataSourceTypes.vala
index a79264f..1baf387 100644
--- a/src/core/DataSourceTypes.vala
+++ b/src/core/DataSourceTypes.vala
@@ -72,9 +72,9 @@ public abstract class EventSource : ThumbnailSource {
base (object_id);
}
- public abstract time_t get_start_time();
+ public abstract DateTime? get_start_time();
- public abstract time_t get_end_time();
+ public abstract DateTime? get_end_time();
public abstract uint64 get_total_filesize();
diff --git a/src/core/SourceInterfaces.vala b/src/core/SourceInterfaces.vala
index 91a8aca..6e0c149 100644
--- a/src/core/SourceInterfaces.vala
+++ b/src/core/SourceInterfaces.vala
@@ -42,3 +42,18 @@ public interface Indexable : DataSource {
}
}
+// Positionable DataSources provide a globally locatable point in longitude and latitude degrees
+
+public struct GpsCoords {
+ public int has_gps;
+ public double latitude;
+ public double longitude;
+ public bool equals(ref GpsCoords gps) {
+ return (has_gps == 0 && gps.has_gps == 0) || (latitude == gps.latitude && longitude == gps.longitude);
+ }
+}
+
+public interface Positionable : DataSource {
+ public abstract GpsCoords get_gps_coords();
+ public abstract void set_gps_coords(GpsCoords gps_coords);
+}
diff --git a/src/core/util.vala b/src/core/util.vala
index 9507895..461d2c0 100644
--- a/src/core/util.vala
+++ b/src/core/util.vala
@@ -190,7 +190,12 @@ public bool null_progress_monitor(uint64 count, uint64 total) {
return true;
}
+public static int64 nullsafe_date_time_comperator(DateTime? time_a, DateTime? time_b) {
+ if (time_a == null && time_b == null) return 0;
-double degrees_to_radians(double theta) {
- return (theta * (GLib.Math.PI / 180.0));
-}
+ if (time_a == null && time_b != null) return -1;
+ if (time_a != null && time_b == null) return 1;
+
+ return time_a.compare(time_b);
+
+} \ No newline at end of file
diff --git a/src/data_imports/DataImportJob.vala b/src/data_imports/DataImportJob.vala
index 4035ae6..eeaec40 100644
--- a/src/data_imports/DataImportJob.vala
+++ b/src/data_imports/DataImportJob.vala
@@ -13,7 +13,7 @@ public class DataImportJob : BatchImportJob {
private DataImportSource import_source;
private File? src_file;
private uint64 filesize;
- private time_t exposure_time;
+ private DateTime? exposure_time;
private DataImportJob? associated = null;
private HierarchicalTagIndex? detected_htags = null;
@@ -48,7 +48,7 @@ public class DataImportJob : BatchImportJob {
return (detected_htags.size > 0) ? HierarchicalTagIndex.from_paths(detected_htags) : null;
}
- public time_t get_exposure_time() {
+ public DateTime get_exposure_time() {
return exposure_time;
}
@@ -158,7 +158,7 @@ public class DataImportJob : BatchImportJob {
if (title != null)
photo.set_title(title);
// exposure time
- time_t? date_time = src_photo.get_exposure_time();
+ var date_time = src_photo.get_exposure_time();
if (date_time != null)
photo.set_exposure_time(date_time);
// import ID
diff --git a/src/data_imports/DataImportSource.vala b/src/data_imports/DataImportSource.vala
index ba00be3..012abdc 100644
--- a/src/data_imports/DataImportSource.vala
+++ b/src/data_imports/DataImportSource.vala
@@ -20,7 +20,7 @@ public class DataImportSource {
private string? title = null;
private string? preview_md5 = null;
private uint64 file_size;
- private time_t modification_time;
+ private DateTime modification_time;
private MetadataDateTime? exposure_time;
public DataImportSource(ImportableMediaItem db_photo) {
@@ -52,7 +52,7 @@ public class DataImportSource {
if (title == null) {
title = (metadata != null) ? metadata.get_title() : null;
}
- time_t? date_time = db_photo.get_exposure_time();
+ var date_time = db_photo.get_exposure_time();
if (date_time != null) {
exposure_time = new MetadataDateTime(date_time);
} else {
@@ -110,7 +110,7 @@ public class DataImportSource {
return get_name();
}
- public time_t get_exposure_time() {
+ public DateTime get_exposure_time() {
return (exposure_time != null) ? exposure_time.get_timestamp() : modification_time;
}
diff --git a/src/data_imports/DataImports.vala b/src/data_imports/DataImports.vala
index a98c91b..258a653 100644
--- a/src/data_imports/DataImports.vala
+++ b/src/data_imports/DataImports.vala
@@ -17,7 +17,7 @@ namespace DataImports {
public void init() throws Error {
string[] core_ids = new string[0];
- core_ids += "org.yorba.shotwell.dataimports.fspot";
+ core_ids += "org.gnome.shotwell.dataimports.fspot";
Plugins.register_extension_point(typeof(Spit.DataImports.Service), _("Data Imports"),
Resources.IMPORT, core_ids);
diff --git a/src/data_imports/DataImportsPluginHost.vala b/src/data_imports/DataImportsPluginHost.vala
index 158b8f4..46cfa46 100644
--- a/src/data_imports/DataImportsPluginHost.vala
+++ b/src/data_imports/DataImportsPluginHost.vala
@@ -474,8 +474,9 @@ private void data_import_reporter(ImportManifest manifest, BatchImportRoll impor
}
private int64 import_job_comparator(void *a, void *b) {
- return ((DataImportJob *) a)->get_exposure_time()
- - ((DataImportJob *) b)->get_exposure_time();
+
+ return nullsafe_date_time_comperator(((DataImportJob *) a)->get_exposure_time(),
+ ((DataImportJob *) b)->get_exposure_time());
}
}
diff --git a/src/data_imports/DataImportsUI.vala b/src/data_imports/DataImportsUI.vala
index 29791a4..6fb7158 100644
--- a/src/data_imports/DataImportsUI.vala
+++ b/src/data_imports/DataImportsUI.vala
@@ -34,7 +34,7 @@ public class ConcreteDialogPane : Spit.DataImports.DialogPane, GLib.Object {
public class StaticMessagePane : ConcreteDialogPane {
public StaticMessagePane(string message_string) {
Gtk.Label message_label = new Gtk.Label(message_string);
- (get_widget() as Gtk.Box).pack_start(message_label, true, true, 0);
+ ((Gtk.Box) get_widget()).pack_start(message_label, true, true, 0);
}
public StaticMessagePane.with_pango(string msg) {
@@ -42,7 +42,7 @@ public class StaticMessagePane : ConcreteDialogPane {
label.set_markup(msg);
label.set_line_wrap(true);
- (get_widget() as Gtk.Box).pack_start(label, true, true, 0);
+ ((Gtk.Box) get_widget()).pack_start(label, true, true, 0);
}
}
@@ -123,7 +123,7 @@ public class LibrarySelectionPane : ConcreteDialogPane {
button_box.add(import_button);
content_box.pack_end(button_box, true, false, 6);
- (get_widget() as Gtk.Box).pack_start(content_box, true, true, 0);
+ ((Gtk.Box) get_widget()).pack_start(content_box, true, true, 0);
set_import_button_sensitivity();
}
@@ -177,7 +177,7 @@ public class ProgressPane : ConcreteDialogPane {
progress_label = new Gtk.Label("");
content_box.pack_start(progress_label, false, true, 6);
- (get_widget() as Gtk.Container).add(content_box);
+ ((Gtk.Container) get_widget()).add(content_box);
}
public void update_progress(double progress, string? progress_message) {
@@ -285,7 +285,7 @@ public class DataImportsDialog : Gtk.Dialog {
}
}
- // Intall the central area in all cases
+ // Install the central area in all cases
central_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
((Gtk.Box) get_content_area()).pack_start(central_area_layouter, true, true, 0);
diff --git a/src/db/DatabaseTable.vala b/src/db/DatabaseTable.vala
index 5ec5be1..dea797a 100644
--- a/src/db/DatabaseTable.vala
+++ b/src/db/DatabaseTable.vala
@@ -21,12 +21,12 @@ public abstract class DatabaseTable {
* tables are created on demand and tables and columns are easily ignored when already present.
* However, the change should be noted in upgrade_database() as a comment.
***/
- public const int SCHEMA_VERSION = 20;
-
+ public const int SCHEMA_VERSION = 24;
+
protected static Sqlite.Database db;
-
+
private static int in_transaction = 0;
-
+
public string table_name = null;
private static void prepare_db(string filename) {
@@ -287,7 +287,19 @@ public abstract class DatabaseTable {
if (res != Sqlite.DONE)
throw_error("DatabaseTable.update_int64_by_id_2 %s.%s".printf(table_name, column), res);
}
-
+
+ protected void update_double_by_id_2(int64 id, string column, double value) throws DatabaseError {
+ Sqlite.Statement stmt;
+ prepare_update_by_id(id, column, out stmt);
+
+ int res = stmt.bind_double(1, value);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("DatabaseTable.update_double_by_id_2 %s.%s".printf(table_name, column), res);
+ }
+
protected void delete_by_id(int64 id) throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2("DELETE FROM %s WHERE id=?".printf(table_name), -1, out stmt);
diff --git a/src/db/Db.vala b/src/db/Db.vala
index 3eca8ce..5072967 100644
--- a/src/db/Db.vala
+++ b/src/db/Db.vala
@@ -293,7 +293,7 @@ private VerifyResult upgrade_database(int input_version) {
}
version = 16;
-
+
//
// Version 17:
// * Added comment column to PhotoTable and VideoTable
@@ -349,11 +349,72 @@ private VerifyResult upgrade_database(int input_version) {
//
version = 20;
+
//
- // Finalize the upgrade process
+ // Version 21:
+ // * Add has_gps, gps_lat and gps_lon columns to PhotoTable
+
+ if (!DatabaseTable.ensure_column("PhotoTable", "has_gps", "INTEGER DEFAULT -1",
+ "upgrade_database: adding gps_lat column to PhotoTable")
+ || !DatabaseTable.ensure_column("PhotoTable", "gps_lat", "REAL",
+ "upgrade_database: adding gps_lat column to PhotoTable")
+ || !DatabaseTable.ensure_column("PhotoTable", "gps_lon", "REAL",
+ "upgrade_database: adding gps_lon column to PhotoTable")) {
+ return VerifyResult.UPGRADE_ERROR;
+ }
+
+ version = 21;
+
+ //
+ // Version 22:
+ // * Create face detection tables even if feasture is not enabled
+ // * Added face pixels column to FaceLocationTable
+ // * Added face vector column to FaceTable
//
+ FaceTable.get_instance();
+ FaceLocationTable.get_instance();
+ if (!DatabaseTable.has_column("FaceLocationTable", "vec")) {
+ message("upgrade_database: adding vec column to FaceLocationTable");
+ if (!DatabaseTable.add_column("FaceLocationTable", "vec", "TEXT"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+ if (!DatabaseTable.has_column("FaceLocationTable", "guess")) {
+ message("upgrade_database: adding guess column to FaceLocationTable");
+ if (!DatabaseTable.add_column("FaceLocationTable", "guess", "INTEGER DEFAULT 0"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+ if (!DatabaseTable.has_column("FaceTable", "ref")) {
+ message("upgrade_database: adding ref column to FaceTable");
+ if (!DatabaseTable.add_column("FaceTable", "ref", "INTEGER DEFAULT -1"))
+ return VerifyResult.UPGRADE_ERROR;
+ }
+ version = 22;
+
+ //
+ // Finalize the upgrade process
+ //
+
+ if (input_version < 23) {
+ // Run the settings migrator to copy settings data from /org/yorba/shotwell to /org/gnome/shotwell
+ GSettingsConfigurationEngine.run_gsettings_migrator_v2();
+ }
+
+ version = 23;
+
+ if (input_version < 24) {
+ // Convert timestamp 0 to NULL to represent unset date and free 0 to be 1.1.1970 00:00
+ message("upgrade_database: Shifting times from 0 to null for unset times");
+ try {
+ PhotoTable.upgrade_for_unset_timestamp();
+ VideoTable.upgrade_for_unset_timestamp();
+ version = 24;
+ } catch (DatabaseError err) {
+ critical("Failed to upgrade database to version 24: %s", err.message);
+ }
+ }
+
assert(version == DatabaseTable.SCHEMA_VERSION);
VersionTable.get_instance().update_version(version, Resources.APP_VERSION);
diff --git a/src/db/EventTable.vala b/src/db/EventTable.vala
index 593d51c..3b7df17 100644
--- a/src/db/EventTable.vala
+++ b/src/db/EventTable.vala
@@ -25,7 +25,7 @@ public struct EventID {
public class EventRow {
public EventID event_id;
public string? name;
- public time_t time_created;
+ public int64 time_created;
public string? primary_source_id;
public string? comment;
}
@@ -80,7 +80,7 @@ public class EventTable : DatabaseTable {
-1, out stmt);
assert(res == Sqlite.OK);
- time_t time_created = (time_t) now_sec();
+ int64 time_created = now_sec();
res = stmt.bind_text(1, primary_source_id);
assert(res == Sqlite.OK);
@@ -151,7 +151,7 @@ public class EventTable : DatabaseTable {
if (row.name != null && row.name.length == 0)
row.name = null;
row.primary_source_id = source_id_upgrade(stmt.column_int64(1), stmt.column_text(2));
- row.time_created = (time_t) stmt.column_int64(3);
+ row.time_created = stmt.column_int64(3);
row.comment = stmt.column_text(4);
return row;
@@ -183,7 +183,7 @@ public class EventTable : DatabaseTable {
row.event_id = EventID(stmt.column_int64(0));
row.name = stmt.column_text(1);
row.primary_source_id = source_id_upgrade(stmt.column_int64(2), stmt.column_text(3));
- row.time_created = (time_t) stmt.column_int64(4);
+ row.time_created = stmt.column_int64(4);
row.comment = stmt.column_text(5);
event_rows.add(row);
@@ -218,12 +218,12 @@ public class EventTable : DatabaseTable {
return update_text_by_id(event_id.id, "primary_source_id", primary_source_id);
}
- public time_t get_time_created(EventID event_id) {
+ public DateTime? get_time_created(EventID event_id) {
Sqlite.Statement stmt;
if (!select_by_id(event_id.id, "time_created", out stmt))
- return 0;
+ return null;
- return (time_t) stmt.column_int64(0);
+ return new DateTime.from_unix_utc(stmt.column_int64(0));
}
public bool set_comment(EventID event_id, string new_comment) {
diff --git a/src/db/FaceLocationTable.vala b/src/db/FaceLocationTable.vala
index 8398616..f4c88d7 100644
--- a/src/db/FaceLocationTable.vala
+++ b/src/db/FaceLocationTable.vala
@@ -27,6 +27,7 @@ public class FaceLocationRow {
public FaceID face_id;
public PhotoID photo_id;
public string geometry;
+ public string vec;
}
public class FaceLocationTable : DatabaseTable {
@@ -42,7 +43,9 @@ public class FaceLocationTable : DatabaseTable {
+ "id INTEGER NOT NULL PRIMARY KEY, "
+ "face_id INTEGER NOT NULL, "
+ "photo_id INTEGER NOT NULL, "
- + "geometry TEXT"
+ + "geometry TEXT, "
+ + "vec TEXT, "
+ + "guess INTEGER DEFAULT 0"
+ ")", -1, out stmt);
assert(res == Sqlite.OK);
@@ -58,10 +61,10 @@ public class FaceLocationTable : DatabaseTable {
return instance;
}
- public FaceLocationRow add(FaceID face_id, PhotoID photo_id, string geometry) throws DatabaseError {
+ public FaceLocationRow add(FaceID face_id, PhotoID photo_id, string geometry, string? vec = null) throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2(
- "INSERT INTO FaceLocationTable (face_id, photo_id, geometry) VALUES (?, ?, ?)",
+ "INSERT INTO FaceLocationTable (face_id, photo_id, geometry, vec) VALUES (?, ?, ?, ?)",
-1, out stmt);
assert(res == Sqlite.OK);
@@ -71,6 +74,9 @@ public class FaceLocationTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_text(3, geometry);
assert(res == Sqlite.OK);
+ if (vec == null) vec = "";
+ res = stmt.bind_text(4, vec);
+ assert(res == Sqlite.OK);
res = stmt.step();
if (res != Sqlite.DONE)
@@ -81,6 +87,7 @@ public class FaceLocationTable : DatabaseTable {
row.face_id = face_id;
row.photo_id = photo_id;
row.geometry = geometry;
+ row.vec = vec;
return row;
}
@@ -88,7 +95,7 @@ public class FaceLocationTable : DatabaseTable {
public Gee.List<FaceLocationRow?> get_all_rows() throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2(
- "SELECT id, face_id, photo_id, geometry FROM FaceLocationTable",
+ "SELECT id, face_id, photo_id, geometry, vec FROM FaceLocationTable",
-1, out stmt);
assert(res == Sqlite.OK);
@@ -107,6 +114,7 @@ public class FaceLocationTable : DatabaseTable {
row.face_id = FaceID(stmt.column_int64(1));
row.photo_id = PhotoID(stmt.column_int64(2));
row.geometry = stmt.column_text(3);
+ row.vec = stmt.column_text(4);
rows.add(row);
}
@@ -195,4 +203,63 @@ public class FaceLocationTable : DatabaseTable {
if (res != Sqlite.DONE)
throw_error("FaceLocationTable.update_face_location_serialized_geometry", res);
}
+
+ public void update_face_location_face_data(FaceLocation face_location)
+ throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE FaceLocationTable SET geometry=?, vec=? WHERE id=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+
+ FaceLocationData face_data = face_location.get_face_data();
+ res = stmt.bind_text(1, face_data.geometry);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(2, face_data.vec);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(3, face_location.get_face_location_id().id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("FaceLocationTable.update_face_location_serialized_geometry", res);
+ }
+ public Gee.List<FaceLocationRow?> get_face_ref_vecs(Gee.List<FaceRow?> face_rows)
+ throws DatabaseError {
+ Sqlite.Statement stmt;
+
+ string[] where_in = {};
+ foreach (var r in face_rows) {
+ if (r != null) where_in += "?";
+ }
+ int res = db.prepare_v2(
+ "SELECT id, face_id, photo_id, geometry, vec FROM FaceLocationTable WHERE photo_id IN (%s)"
+ .printf(string.joinv(",", where_in)),
+ -1, out stmt);
+ assert(res == Sqlite.OK);
+ int c = 1;
+ foreach (var r in face_rows) {
+ if (r != null) {
+ res = stmt.bind_int64(c, r.ref.id);
+ assert(res == Sqlite.OK);
+ }
+ c++;
+ }
+
+ Gee.List<FaceLocationRow?> rows = new Gee.ArrayList<FaceLocationRow?>();
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("FaceLocationTable.get_face_ref_vecs", res);
+
+ FaceLocationRow row = new FaceLocationRow();
+ row.face_location_id = FaceLocationID(stmt.column_int64(0));
+ row.face_id = FaceID(stmt.column_int64(1));
+ row.photo_id = PhotoID(stmt.column_int64(2));
+ row.geometry = stmt.column_text(3);
+ row.vec = stmt.column_text(4);
+ rows.add(row);
+ }
+ return rows;
+ }
}
diff --git a/src/db/FaceTable.vala b/src/db/FaceTable.vala
index 4836910..e799f97 100644
--- a/src/db/FaceTable.vala
+++ b/src/db/FaceTable.vala
@@ -25,7 +25,9 @@ public struct FaceID {
public class FaceRow {
public FaceID face_id;
public string name;
- public time_t time_created;
+ public int64 time_created;
+ public PhotoID ref;
+ public string vec;
}
public class FaceTable : DatabaseTable {
@@ -40,7 +42,8 @@ public class FaceTable : DatabaseTable {
+ "("
+ "id INTEGER NOT NULL PRIMARY KEY, "
+ "name TEXT NOT NULL, "
- + "time_created TIMESTAMP"
+ + "time_created TIMESTAMP, "
+ + "ref INTEGER DEFAULT -1"
+ ")", -1, out stmt);
assert(res == Sqlite.OK);
@@ -62,7 +65,7 @@ public class FaceTable : DatabaseTable {
out stmt);
assert(res == Sqlite.OK);
- time_t time_created = (time_t) now_sec();
+ var time_created = now_sec();
res = stmt.bind_text(1, name);
assert(res == Sqlite.OK);
@@ -129,7 +132,7 @@ public class FaceTable : DatabaseTable {
FaceRow row = new FaceRow();
row.face_id = face_id;
row.name = stmt.column_text(0);
- row.time_created = (time_t) stmt.column_int64(1);
+ row.time_created = stmt.column_int64(1);
return row;
}
@@ -153,7 +156,7 @@ public class FaceTable : DatabaseTable {
FaceRow row = new FaceRow();
row.face_id = FaceID(stmt.column_int64(0));
row.name = stmt.column_text(1);
- row.time_created = (time_t) stmt.column_int64(2);
+ row.time_created = stmt.column_int64(2);
rows.add(row);
}
@@ -164,4 +167,47 @@ public class FaceTable : DatabaseTable {
public void rename(FaceID face_id, string new_name) throws DatabaseError {
update_text_by_id_2(face_id.id, "name", new_name);
}
+
+ public void set_reference(FaceID face_id, PhotoID photo_id)
+ throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE FaceTable SET ref=? WHERE id=?", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(1, photo_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(2, face_id.id);
+ assert(res == Sqlite.OK);
+
+ res = stmt.step();
+ if (res != Sqlite.DONE)
+ throw_error("FaceTable.set_reference", res);
+ }
+
+ public Gee.List<FaceRow?> get_ref_rows() throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("SELECT id, name, time_created, ref FROM FaceTable WHERE ref != -1", -1,
+ out stmt);
+ assert(res == Sqlite.OK);
+
+ Gee.List<FaceRow?> rows = new Gee.ArrayList<FaceRow?>();
+
+ for (;;) {
+ res = stmt.step();
+ if (res == Sqlite.DONE)
+ break;
+ else if (res != Sqlite.ROW)
+ throw_error("FaceTable.get_all_rows", res);
+
+ // res == Sqlite.ROW
+ FaceRow row = new FaceRow();
+ row.face_id = FaceID(stmt.column_int64(0));
+ row.name = stmt.column_text(1);
+ row.time_created = stmt.column_int64(2);
+ row.ref = PhotoID(stmt.column_int64(3));
+
+ rows.add(row);
+ }
+
+ return rows;
+ }
}
diff --git a/src/db/PhotoTable.vala b/src/db/PhotoTable.vala
index 24cec86..4e3f672 100644
--- a/src/db/PhotoTable.vala
+++ b/src/db/PhotoTable.vala
@@ -44,9 +44,7 @@ public struct ImportID {
}
public static ImportID generate() {
- TimeVal timestamp = TimeVal();
- timestamp.get_current_time();
- int64 id = timestamp.tv_sec;
+ int64 id = GLib.get_real_time () / Util.USEC_PER_SEC;
return ImportID(id);
}
@@ -72,7 +70,7 @@ public struct ImportID {
public class PhotoRow {
public PhotoID photo_id;
public BackingPhotoRow master;
- public time_t exposure_time;
+ public DateTime? exposure_time;
public ImportID import_id;
public EventID event_id;
public Orientation orientation;
@@ -80,13 +78,14 @@ public class PhotoRow {
public string md5;
public string thumbnail_md5;
public string exif_md5;
- public time_t time_created;
+ public int64 time_created;
public uint64 flags;
public Rating rating;
public string title;
+ public GpsCoords gps_coords;
public string comment;
public string? backlinks;
- public time_t time_reimported;
+ public int64 time_reimported;
public BackingPhotoID editable_id;
public bool metadata_dirty;
@@ -103,6 +102,10 @@ public class PhotoRow {
development_ids = new BackingPhotoID[RawDeveloper.as_array().length];
foreach (RawDeveloper d in RawDeveloper.as_array())
development_ids[d] = BackingPhotoID();
+ gps_coords = GpsCoords();
+ development_ids = new BackingPhotoID[RawDeveloper.as_array().length];
+ foreach (RawDeveloper d in RawDeveloper.as_array())
+ development_ids[d] = BackingPhotoID();
}
}
@@ -140,6 +143,9 @@ public class PhotoTable : DatabaseTable {
+ "develop_shotwell_id INTEGER DEFAULT -1, "
+ "develop_camera_id INTEGER DEFAULT -1, "
+ "develop_embedded_id INTEGER DEFAULT -1, "
+ + "has_gps INTEGER DEFAULT -1, "
+ + "gps_lat REAL, "
+ + "gps_lon REAL, "
+ "comment TEXT"
+ ")", -1, out stmt);
assert(res == Sqlite.OK);
@@ -209,12 +215,12 @@ public class PhotoTable : DatabaseTable {
int res = db.prepare_v2(
"INSERT INTO PhotoTable (filename, width, height, filesize, timestamp, exposure_time, "
+ "orientation, original_orientation, import_id, event_id, md5, thumbnail_md5, "
- + "exif_md5, time_created, file_format, title, rating, editable_id, developer, comment) "
- + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ + "exif_md5, time_created, file_format, title, rating, editable_id, developer, has_gps, gps_lat, gps_lon, comment) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-1, out stmt);
assert(res == Sqlite.OK);
- ulong time_created = now_sec();
+ var time_created = now_sec();
res = stmt.bind_text(1, photo_row.master.filepath);
assert(res == Sqlite.OK);
@@ -224,9 +230,17 @@ public class PhotoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_int64(4, photo_row.master.filesize);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(5, photo_row.master.timestamp);
+ if (photo_row.master.timestamp == null) {
+ res = stmt.bind_null(5);
+ } else {
+ res = stmt.bind_int64(5, photo_row.master.timestamp.to_unix());
+ }
assert(res == Sqlite.OK);
- res = stmt.bind_int64(6, photo_row.exposure_time);
+ if (photo_row.exposure_time == null) {
+ res = stmt.bind_null(6);
+ } else {
+ res = stmt.bind_int64(6, photo_row.exposure_time.to_unix());
+ }
assert(res == Sqlite.OK);
res = stmt.bind_int(7, photo_row.master.original_orientation);
assert(res == Sqlite.OK);
@@ -254,7 +268,13 @@ public class PhotoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_text(19, photo_row.developer.to_string());
assert(res == Sqlite.OK);
- res = stmt.bind_text(20, photo_row.comment);
+ res = stmt.bind_int(20, photo_row.gps_coords.has_gps);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_double(21, photo_row.gps_coords.latitude);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_double(22, photo_row.gps_coords.longitude);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(23, photo_row.comment);
assert(res == Sqlite.OK);
res = stmt.step();
@@ -269,7 +289,7 @@ public class PhotoTable : DatabaseTable {
photo_row.photo_id = PhotoID(db.last_insert_rowid());
photo_row.orientation = photo_row.master.original_orientation;
photo_row.event_id = EventID();
- photo_row.time_created = (time_t) time_created;
+ photo_row.time_created = time_created;
photo_row.flags = 0;
return photo_row.photo_id;
@@ -285,11 +305,12 @@ public class PhotoTable : DatabaseTable {
int res = db.prepare_v2(
"UPDATE PhotoTable SET width = ?, height = ?, filesize = ?, timestamp = ?, "
+ "exposure_time = ?, orientation = ?, original_orientation = ?, md5 = ?, "
- + "exif_md5 = ?, thumbnail_md5 = ?, file_format = ?, title = ?, time_reimported = ? "
+ + "exif_md5 = ?, thumbnail_md5 = ?, file_format = ?, title = ?, "
+ + "has_gps = ?, gps_lat = ?, gps_lon = ?, time_reimported = ? "
+ "WHERE id = ?", -1, out stmt);
assert(res == Sqlite.OK);
- time_t time_reimported = (time_t) now_sec();
+ var time_reimported = now_sec();
res = stmt.bind_int(1, row.master.dim.width);
assert(res == Sqlite.OK);
@@ -297,9 +318,13 @@ public class PhotoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_int64(3, row.master.filesize);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(4, row.master.timestamp);
+ res = stmt.bind_int64(4, row.master.timestamp.to_unix());
assert(res == Sqlite.OK);
- res = stmt.bind_int64(5, row.exposure_time);
+ if (row.exposure_time == null) {
+ res = stmt.bind_null(5);
+ } else {
+ res = stmt.bind_int64(5, row.exposure_time.to_unix());
+ }
assert(res == Sqlite.OK);
res = stmt.bind_int(6, row.master.original_orientation);
assert(res == Sqlite.OK);
@@ -315,9 +340,15 @@ public class PhotoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_text(12, row.title);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(13, time_reimported);
+ res = stmt.bind_int(13, row.gps_coords.has_gps);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_double(14, row.gps_coords.latitude);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_double(15, row.gps_coords.longitude);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(16, time_reimported);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(14, row.photo_id.id);
+ res = stmt.bind_int64(17, row.photo_id.id);
assert(res == Sqlite.OK);
res = stmt.step();
@@ -328,7 +359,7 @@ public class PhotoTable : DatabaseTable {
row.orientation = row.master.original_orientation;
}
- public bool master_exif_updated(PhotoID photoID, int64 filesize, long timestamp,
+ public bool master_exif_updated(PhotoID photoID, int64 filesize, DateTime timestamp,
string md5, string? exif_md5, string? thumbnail_md5, PhotoRow row) {
Sqlite.Statement stmt;
int res = db.prepare_v2(
@@ -338,7 +369,7 @@ public class PhotoTable : DatabaseTable {
res = stmt.bind_int64(1, filesize);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(2, timestamp);
+ res = stmt.bind_int64(2, timestamp.to_unix());
assert(res == Sqlite.OK);
res = stmt.bind_text(3, md5);
assert(res == Sqlite.OK);
@@ -372,7 +403,7 @@ public class PhotoTable : DatabaseTable {
// the DB as a zero due to Vala 0.14 breaking the way it handled
// objects passed as 'ref' arguments to methods.
//
- // For further details, please see http://redmine.yorba.org/issues/4354 and
+ // For further details, please see https://bugzilla.gnome.org/show_bug.cgi?id=718194 and
// https://bugzilla.gnome.org/show_bug.cgi?id=663818 .
private void validate_orientation(PhotoRow row) {
if ((row.orientation < Orientation.MIN) ||
@@ -390,7 +421,7 @@ public class PhotoTable : DatabaseTable {
+ "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
+ "exif_md5, time_created, flags, rating, file_format, title, backlinks, "
+ "time_reimported, editable_id, metadata_dirty, developer, develop_shotwell_id, "
- + "develop_camera_id, develop_embedded_id, comment "
+ + "develop_camera_id, develop_embedded_id, has_gps, gps_lat, gps_lon, comment "
+ "FROM PhotoTable WHERE id=?",
-1, out stmt);
assert(res == Sqlite.OK);
@@ -406,8 +437,12 @@ public class PhotoTable : DatabaseTable {
row.master.filepath = stmt.column_text(0);
row.master.dim = Dimensions(stmt.column_int(1), stmt.column_int(2));
row.master.filesize = stmt.column_int64(3);
- row.master.timestamp = (time_t) stmt.column_int64(4);
- row.exposure_time = (time_t) stmt.column_int64(5);
+ row.master.timestamp = new DateTime.from_unix_utc(stmt.column_int64(4));
+ if (stmt.column_type(5) == Sqlite.NULL) {
+ row.exposure_time = null;
+ } else {
+ row.exposure_time = new DateTime.from_unix_utc(stmt.column_int64(5));
+ }
row.orientation = (Orientation) stmt.column_int(6);
row.master.original_orientation = (Orientation) stmt.column_int(7);
row.import_id.id = stmt.column_int64(8);
@@ -416,13 +451,13 @@ public class PhotoTable : DatabaseTable {
row.md5 = stmt.column_text(11);
row.thumbnail_md5 = stmt.column_text(12);
row.exif_md5 = stmt.column_text(13);
- row.time_created = (time_t) stmt.column_int64(14);
+ row.time_created = stmt.column_int64(14);
row.flags = stmt.column_int64(15);
row.rating = Rating.unserialize(stmt.column_int(16));
row.master.file_format = PhotoFileFormat.unserialize(stmt.column_int(17));
row.title = stmt.column_text(18);
row.backlinks = stmt.column_text(19);
- row.time_reimported = (time_t) stmt.column_int64(20);
+ row.time_reimported = stmt.column_int64(20);
row.editable_id = BackingPhotoID(stmt.column_int64(21));
row.metadata_dirty = stmt.column_int(22) != 0;
row.developer = stmt.column_text(23) != null ? RawDeveloper.from_string(stmt.column_text(23)) :
@@ -430,7 +465,10 @@ public class PhotoTable : DatabaseTable {
row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(24));
row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(25));
row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(26));
- row.comment = stmt.column_text(27);
+ row.gps_coords.has_gps = stmt.column_int(27);
+ row.gps_coords.latitude = stmt.column_double(28);
+ row.gps_coords.longitude = stmt.column_double(29);
+ row.comment = stmt.column_text(30);
return row;
}
@@ -442,7 +480,7 @@ public class PhotoTable : DatabaseTable {
+ "original_orientation, import_id, event_id, transformations, md5, thumbnail_md5, "
+ "exif_md5, time_created, flags, rating, file_format, title, backlinks, time_reimported, "
+ "editable_id, metadata_dirty, developer, develop_shotwell_id, develop_camera_id, "
- + "develop_embedded_id, comment FROM PhotoTable",
+ + "develop_embedded_id, has_gps, gps_lat, gps_lon, comment FROM PhotoTable",
-1, out stmt);
assert(res == Sqlite.OK);
@@ -454,8 +492,12 @@ public class PhotoTable : DatabaseTable {
row.master.filepath = stmt.column_text(1);
row.master.dim = Dimensions(stmt.column_int(2), stmt.column_int(3));
row.master.filesize = stmt.column_int64(4);
- row.master.timestamp = (time_t) stmt.column_int64(5);
- row.exposure_time = (time_t) stmt.column_int64(6);
+ row.master.timestamp = new DateTime.from_unix_utc(stmt.column_int64(5));
+ if (stmt.column_type(6) == Sqlite.NULL) {
+ row.exposure_time = null;
+ } else {
+ row.exposure_time = new DateTime.from_unix_utc(stmt.column_int64(6));
+ }
row.orientation = (Orientation) stmt.column_int(7);
row.master.original_orientation = (Orientation) stmt.column_int(8);
row.import_id.id = stmt.column_int64(9);
@@ -464,13 +506,13 @@ public class PhotoTable : DatabaseTable {
row.md5 = stmt.column_text(12);
row.thumbnail_md5 = stmt.column_text(13);
row.exif_md5 = stmt.column_text(14);
- row.time_created = (time_t) stmt.column_int64(15);
+ row.time_created = stmt.column_int64(15);
row.flags = stmt.column_int64(16);
row.rating = Rating.unserialize(stmt.column_int(17));
row.master.file_format = PhotoFileFormat.unserialize(stmt.column_int(18));
row.title = stmt.column_text(19);
row.backlinks = stmt.column_text(20);
- row.time_reimported = (time_t) stmt.column_int64(21);
+ row.time_reimported = stmt.column_int64(21);
row.editable_id = BackingPhotoID(stmt.column_int64(22));
row.metadata_dirty = stmt.column_int(23) != 0;
row.developer = stmt.column_text(24) != null ? RawDeveloper.from_string(stmt.column_text(24)) :
@@ -478,7 +520,10 @@ public class PhotoTable : DatabaseTable {
row.development_ids[RawDeveloper.SHOTWELL] = BackingPhotoID(stmt.column_int64(25));
row.development_ids[RawDeveloper.CAMERA] = BackingPhotoID(stmt.column_int64(26));
row.development_ids[RawDeveloper.EMBEDDED] = BackingPhotoID(stmt.column_int64(27));
- row.comment = stmt.column_text(28);
+ row.gps_coords.has_gps = stmt.column_int(28);
+ row.gps_coords.latitude = stmt.column_double(29);
+ row.gps_coords.longitude = stmt.column_double(30);
+ row.comment = stmt.column_text(31);
validate_orientation(row);
@@ -500,9 +545,9 @@ public class PhotoTable : DatabaseTable {
int res = db.prepare_v2("INSERT INTO PhotoTable (filename, width, height, filesize, "
+ "timestamp, exposure_time, orientation, original_orientation, import_id, event_id, "
+ "transformations, md5, thumbnail_md5, exif_md5, time_created, flags, rating, "
- + "file_format, title, editable_id, developer, develop_shotwell_id, develop_camera_id, "
- + "develop_embedded_id, comment) "
- + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ + "file_format, title, has_gps, gps_lat, gps_lon, editable_id, developer, "
+ + "develop_shotwell_id, develop_camera_id, develop_embedded_id, comment) "
+ + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-1, out stmt);
assert(res == Sqlite.OK);
@@ -514,9 +559,13 @@ public class PhotoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_int64(4, original.master.filesize);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(5, original.master.timestamp);
+ res = stmt.bind_int64(5, original.master.timestamp.to_unix());
assert(res == Sqlite.OK);
- res = stmt.bind_int64(6, original.exposure_time);
+ if (original.exposure_time == null) {
+ res = stmt.bind_null(6);
+ } else {
+ res = stmt.bind_int64(6, original.exposure_time.to_unix());
+ }
assert(res == Sqlite.OK);
res = stmt.bind_int(7, original.orientation);
assert(res == Sqlite.OK);
@@ -544,18 +593,23 @@ public class PhotoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_text(19, original.title);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(20, editable_id.id);
+ res = stmt.bind_int(20, original.gps_coords.has_gps);
assert(res == Sqlite.OK);
-
- res = stmt.bind_text(21, original.developer.to_string());
+ res = stmt.bind_double(21, original.gps_coords.latitude);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(22, develop_shotwell.id);
+ res = stmt.bind_double(22, original.gps_coords.longitude);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(23, develop_camera_id.id);
+ res = stmt.bind_int64(23, editable_id.id);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(24, develop_embedded_id.id);
+ res = stmt.bind_text(24, original.developer.to_string());
assert(res == Sqlite.OK);
- res = stmt.bind_text(25, original.comment);
+ res = stmt.bind_int64(25, develop_shotwell.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(26, develop_camera_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_int64(27, develop_embedded_id.id);
+ assert(res == Sqlite.OK);
+ res = stmt.bind_text(28, original.comment);
assert(res == Sqlite.OK);
res = stmt.step();
@@ -572,7 +626,15 @@ public class PhotoTable : DatabaseTable {
public bool set_title(PhotoID photo_id, string? new_title) {
return update_text_by_id(photo_id.id, "title", new_title != null ? new_title : "");
}
-
+
+ public void set_gps_coords(PhotoID photo_id, GpsCoords new_gps_coords) throws DatabaseError {
+ update_int_by_id_2(photo_id.id, "has_gps", new_gps_coords.has_gps);
+ if (new_gps_coords.has_gps > 0) {
+ update_double_by_id_2(photo_id.id, "gps_lat", new_gps_coords.latitude);
+ update_double_by_id_2(photo_id.id, "gps_lon", new_gps_coords.longitude);
+ }
+ }
+
public bool set_comment(PhotoID photo_id, string? new_comment) {
return update_text_by_id(photo_id.id, "comment", new_comment != null ? new_comment : "");
}
@@ -581,12 +643,12 @@ public class PhotoTable : DatabaseTable {
update_text_by_id_2(photo_id.id, "filename", filepath);
}
- public void update_timestamp(PhotoID photo_id, time_t timestamp) throws DatabaseError {
- update_int64_by_id_2(photo_id.id, "timestamp", timestamp);
+ public void update_timestamp(PhotoID photo_id, DateTime timestamp) throws DatabaseError {
+ update_int64_by_id_2(photo_id.id, "timestamp", timestamp.to_unix());
}
- public bool set_exposure_time(PhotoID photo_id, time_t time) {
- return update_int64_by_id(photo_id.id, "exposure_time", (int64) time);
+ public bool set_exposure_time(PhotoID photo_id, DateTime time) {
+ return update_int64_by_id(photo_id.id, "exposure_time", time.to_unix());
}
public void set_import_id(PhotoID photo_id, ImportID import_id) throws DatabaseError {
@@ -1051,6 +1113,16 @@ public class PhotoTable : DatabaseTable {
public void remove_development(PhotoRow row, RawDeveloper rd) throws DatabaseError {
update_raw_development(row, rd, BackingPhotoID());
}
+
+ public static void upgrade_for_unset_timestamp() throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE PhotoTable SET exposure_time = NULL WHERE exposure_time = '0'", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ throw_error("PhotoTable.upgrade_for_unset_timestamp", res);
+ }
+ }
}
@@ -1084,10 +1156,10 @@ public struct BackingPhotoID {
public class BackingPhotoRow {
public BackingPhotoID id;
- public time_t time_created;
+ public int64 time_created;
public string? filepath = null;
public int64 filesize;
- public time_t timestamp;
+ public DateTime? timestamp;
public PhotoFileFormat file_format;
public Dimensions dim;
public Orientation original_orientation;
@@ -1095,15 +1167,21 @@ public class BackingPhotoRow {
public bool matches_file_info(FileInfo info) {
if (filesize != info.get_size())
return false;
+
+ if (timestamp == null)
+ return false;
- return timestamp == info.get_modification_time().tv_sec;
+ return timestamp.equal(info.get_modification_date_time());
}
public bool is_touched(FileInfo info) {
if (filesize != info.get_size())
return false;
+
+ if (timestamp == null)
+ return true;
- return timestamp != info.get_modification_time().tv_sec;
+ return !timestamp.equal(info.get_modification_date_time());
}
// Copies another backing photo row into this one.
@@ -1162,11 +1240,11 @@ public class BackingPhotoTable : DatabaseTable {
-1, out stmt);
assert(res == Sqlite.OK);
- time_t time_created = (time_t) now_sec();
+ var time_created = now_sec();
res = stmt.bind_text(1, state.filepath);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(2, state.timestamp);
+ res = stmt.bind_int64(2, state.timestamp.to_unix());
assert(res == Sqlite.OK);
res = stmt.bind_int64(3, state.filesize);
assert(res == Sqlite.OK);
@@ -1208,12 +1286,12 @@ public class BackingPhotoTable : DatabaseTable {
BackingPhotoRow row = new BackingPhotoRow();
row.id = id;
row.filepath = stmt.column_text(0);
- row.timestamp = (time_t) stmt.column_int64(1);
+ row.timestamp = new DateTime.from_unix_utc(stmt.column_int64(1));
row.filesize = stmt.column_int64(2);
row.dim = Dimensions(stmt.column_int(3), stmt.column_int(4));
row.original_orientation = (Orientation) stmt.column_int(5);
row.file_format = PhotoFileFormat.unserialize(stmt.column_int(6));
- row.time_created = (time_t) stmt.column_int64(7);
+ row.time_created = stmt.column_int64(7);
return row;
}
@@ -1227,7 +1305,7 @@ public class BackingPhotoTable : DatabaseTable {
-1, out stmt);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(1, row.timestamp);
+ res = stmt.bind_int64(1, row.timestamp.to_unix());
assert(res == Sqlite.OK);
res = stmt.bind_int64(2, row.filesize);
assert(res == Sqlite.OK);
@@ -1247,13 +1325,13 @@ public class BackingPhotoTable : DatabaseTable {
throw_error("BackingPhotoTable.update", res);
}
- public void update_attributes(BackingPhotoID id, time_t timestamp, int64 filesize) throws DatabaseError {
+ public void update_attributes(BackingPhotoID id, DateTime timestamp, int64 filesize) throws DatabaseError {
Sqlite.Statement stmt;
int res = db.prepare_v2("UPDATE BackingPhotoTable SET timestamp=?, filesize=? WHERE id=?",
-1, out stmt);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(1, timestamp);
+ res = stmt.bind_int64(1, timestamp.to_unix());
assert(res == Sqlite.OK);
res = stmt.bind_int64(2, filesize);
assert(res == Sqlite.OK);
@@ -1273,8 +1351,8 @@ public class BackingPhotoTable : DatabaseTable {
update_text_by_id_2(id.id, "filepath", filepath);
}
- public void update_timestamp(BackingPhotoID id, time_t timestamp) throws DatabaseError {
- update_int64_by_id_2(id.id, "timestamp", timestamp);
+ public void update_timestamp(BackingPhotoID id, DateTime timestamp) throws DatabaseError {
+ update_int64_by_id_2(id.id, "timestamp", timestamp.to_unix());
}
}
diff --git a/src/db/TagTable.vala b/src/db/TagTable.vala
index d650641..ce191c1 100644
--- a/src/db/TagTable.vala
+++ b/src/db/TagTable.vala
@@ -26,7 +26,7 @@ public class TagRow {
public TagID tag_id;
public string name;
public Gee.Set<string>? source_id_list;
- public time_t time_created;
+ public int64 time_created;
}
public class TagTable : DatabaseTable {
@@ -79,7 +79,7 @@ public class TagTable : DatabaseTable {
out stmt);
assert(res == Sqlite.OK);
- time_t time_created = (time_t) now_sec();
+ var time_created = now_sec();
res = stmt.bind_text(1, name);
assert(res == Sqlite.OK);
@@ -151,7 +151,7 @@ public class TagTable : DatabaseTable {
row.tag_id = tag_id;
row.name = stmt.column_text(0);
row.source_id_list = unserialize_source_ids(stmt.column_text(1));
- row.time_created = (time_t) stmt.column_int64(2);
+ row.time_created = stmt.column_int64(2);
return row;
}
@@ -176,7 +176,7 @@ public class TagTable : DatabaseTable {
row.tag_id = TagID(stmt.column_int64(0));
row.name = stmt.column_text(1);
row.source_id_list = unserialize_source_ids(stmt.column_text(2));
- row.time_created = (time_t) stmt.column_int64(3);
+ row.time_created = stmt.column_int64(3);
rows.add(row);
}
diff --git a/src/db/TombstoneTable.vala b/src/db/TombstoneTable.vala
index 892198f..5c19c5c 100644
--- a/src/db/TombstoneTable.vala
+++ b/src/db/TombstoneTable.vala
@@ -27,7 +27,7 @@ public class TombstoneRow {
public string filepath;
public int64 filesize;
public string? md5;
- public time_t time_created;
+ public int64 time_created;
public Tombstone.Reason reason;
}
@@ -71,7 +71,7 @@ public class TombstoneTable : DatabaseTable {
-1, out stmt);
assert(res == Sqlite.OK);
- time_t time_created = (time_t) now_sec();
+ var time_created = now_sec();
res = stmt.bind_text(1, filepath);
assert(res == Sqlite.OK);
@@ -124,7 +124,7 @@ public class TombstoneTable : DatabaseTable {
row.filepath = stmt.column_text(1);
row.filesize = stmt.column_int64(2);
row.md5 = stmt.column_text(3);
- row.time_created = (time_t) stmt.column_int64(4);
+ row.time_created = stmt.column_int64(4);
row.reason = Tombstone.Reason.unserialize(stmt.column_int(5));
rows[index++] = row;
diff --git a/src/db/VideoTable.vala b/src/db/VideoTable.vala
index 7bd1bb7..8af1278 100644
--- a/src/db/VideoTable.vala
+++ b/src/db/VideoTable.vala
@@ -38,20 +38,20 @@ public class VideoRow {
public VideoID video_id;
public string filepath;
public int64 filesize;
- public time_t timestamp;
+ public DateTime timestamp;
public int width;
public int height;
public double clip_duration;
public bool is_interpretable;
- public time_t exposure_time;
+ public DateTime? exposure_time;
public ImportID import_id;
public EventID event_id;
public string md5;
- public time_t time_created;
+ public int64 time_created;
public Rating rating;
public string title;
public string? backlinks;
- public time_t time_reimported;
+ public int64 time_reimported;
public uint64 flags;
public string comment;
}
@@ -119,7 +119,7 @@ public class VideoTable : DatabaseTable {
-1, out stmt);
assert(res == Sqlite.OK);
- ulong time_created = now_sec();
+ var time_created = now_sec();
res = stmt.bind_text(1, video_row.filepath);
assert(res == Sqlite.OK);
@@ -133,9 +133,13 @@ public class VideoTable : DatabaseTable {
assert(res == Sqlite.OK);
res = stmt.bind_int64(6, video_row.filesize);
assert(res == Sqlite.OK);
- res = stmt.bind_int64(7, video_row.timestamp);
+ res = stmt.bind_int64(7, video_row.timestamp.to_unix());
assert(res == Sqlite.OK);
- res = stmt.bind_int64(8, video_row.exposure_time);
+ if (video_row.exposure_time == null) {
+ stmt.bind_null(8);
+ } else {
+ res = stmt.bind_int64(8, video_row.exposure_time.to_unix());
+ }
assert(res == Sqlite.OK);
res = stmt.bind_int64(9, video_row.import_id.id);
assert(res == Sqlite.OK);
@@ -159,7 +163,7 @@ public class VideoTable : DatabaseTable {
// fill in ignored fields with database values
video_row.video_id = VideoID(db.last_insert_rowid());
video_row.event_id = EventID();
- video_row.time_created = (time_t) time_created;
+ video_row.time_created = time_created;
video_row.flags = 0;
return video_row.video_id;
@@ -208,16 +212,19 @@ public class VideoTable : DatabaseTable {
row.clip_duration = stmt.column_double(3);
row.is_interpretable = (stmt.column_int(4) == 1);
row.filesize = stmt.column_int64(5);
- row.timestamp = (time_t) stmt.column_int64(6);
- row.exposure_time = (time_t) stmt.column_int64(7);
+ if (stmt.column_type(6) == Sqlite.NULL) {
+ row.exposure_time = null;
+ } else {
+ row.exposure_time = new DateTime.from_unix_utc(stmt.column_int64(6));
+ }
row.import_id.id = stmt.column_int64(8);
row.event_id.id = stmt.column_int64(9);
row.md5 = stmt.column_text(10);
- row.time_created = (time_t) stmt.column_int64(11);
+ row.time_created = stmt.column_int64(11);
row.rating = Rating.unserialize(stmt.column_int(12));
row.title = stmt.column_text(13);
row.backlinks = stmt.column_text(14);
- row.time_reimported = (time_t) stmt.column_int64(15);
+ row.time_reimported = stmt.column_int64(15);
row.flags = stmt.column_int64(16);
row.comment = stmt.column_text(17);
@@ -244,16 +251,20 @@ public class VideoTable : DatabaseTable {
row.clip_duration = stmt.column_double(4);
row.is_interpretable = (stmt.column_int(5) == 1);
row.filesize = stmt.column_int64(6);
- row.timestamp = (time_t) stmt.column_int64(7);
- row.exposure_time = (time_t) stmt.column_int64(8);
- row.import_id.id = stmt.column_int64(9);
+ row.timestamp = new DateTime.from_unix_utc(stmt.column_int64(7));
+ if (stmt.column_type(8) == Sqlite.NULL) {
+ row.exposure_time = null;
+ } else {
+ row.exposure_time = new DateTime.from_unix_utc(stmt.column_int64(8));
+ }
+ row.import_id.id = stmt.column_int64(9);
row.event_id.id = stmt.column_int64(10);
row.md5 = stmt.column_text(11);
- row.time_created = (time_t) stmt.column_int64(12);
+ row.time_created = stmt.column_int64(12);
row.rating = Rating.unserialize(stmt.column_int(13));
row.title = stmt.column_text(14);
row.backlinks = stmt.column_text(15);
- row.time_reimported = (time_t) stmt.column_int64(16);
+ row.time_reimported = stmt.column_int64(16);
row.flags = stmt.column_int64(17);
row.comment = stmt.column_text(18);
@@ -275,8 +286,8 @@ public class VideoTable : DatabaseTable {
update_text_by_id_2(video_id.id, "comment", new_comment != null ? new_comment : "");
}
- public void set_exposure_time(VideoID video_id, time_t time) throws DatabaseError {
- update_int64_by_id_2(video_id.id, "exposure_time", (int64) time);
+ public void set_exposure_time(VideoID video_id, DateTime time) throws DatabaseError {
+ update_int64_by_id_2(video_id.id, "exposure_time", time.to_unix());
}
public void set_rating(VideoID video_id, Rating rating) throws DatabaseError {
@@ -455,8 +466,19 @@ public class VideoTable : DatabaseTable {
return result;
}
- public void set_timestamp(VideoID video_id, time_t timestamp) throws DatabaseError {
- update_int64_by_id_2(video_id.id, "timestamp", (int64) timestamp);
+ public void set_timestamp(VideoID video_id, DateTime timestamp) throws DatabaseError {
+ update_int64_by_id_2(video_id.id, "timestamp", timestamp.to_unix());
}
+
+ public static void upgrade_for_unset_timestamp() throws DatabaseError {
+ Sqlite.Statement stmt;
+ int res = db.prepare_v2("UPDATE VideoTable SET exposure_time = NULL WHERE exposure_time = '0'", -1, out stmt);
+ assert(res == Sqlite.OK);
+ res = stmt.step();
+ if (res != Sqlite.DONE) {
+ throw_error("VideoTable.upgrade_for_unset_timestamp", res);
+ }
+ }
+
}
diff --git a/src/dialogs/AdjustDateTimeDialog.vala b/src/dialogs/AdjustDateTimeDialog.vala
index fc08a3f..f475773 100644
--- a/src/dialogs/AdjustDateTimeDialog.vala
+++ b/src/dialogs/AdjustDateTimeDialog.vala
@@ -14,7 +14,7 @@ public class AdjustDateTimeDialog : Gtk.Dialog {
private const int CALENDAR_THUMBNAIL_SCALE = 1;
- time_t original_time;
+ DateTime? original_time;
Gtk.Label original_time_label;
Gtk.Calendar calendar;
Gtk.SpinButton hour;
@@ -182,32 +182,33 @@ public class AdjustDateTimeDialog : Gtk.Dialog {
original_time = source.get_exposure_time();
- if (original_time == 0) {
- original_time = time_t();
+ if (original_time == null) {
+ // This came from
+ original_time = new DateTime.now_utc();
no_original_time = true;
}
- set_time(Time.local(original_time));
+ set_time(original_time.to_local());
set_original_time_label(Config.Facade.get_instance().get_use_24_hour_time());
}
- private void set_time(Time time) {
- calendar.select_month(time.month, time.year + YEAR_OFFSET);
- calendar.select_day(time.day);
+ private void set_time(DateTime time) {
+ calendar.select_month(time.get_month() - 1, time.get_year());
+ calendar.select_day(time.get_day_of_month());
calendar.notify_property("year");
calendar.notify_property("month");
if (Config.Facade.get_instance().get_use_24_hour_time()) {
system.set_active(TimeSystem.24HR);
- hour.set_value(time.hour);
+ hour.set_value(time.get_hour());
} else {
- int AMPM_hour = time.hour % 12;
+ int AMPM_hour = time.get_hour() % 12;
hour.set_value((AMPM_hour == 0) ? 12 : AMPM_hour);
- system.set_active((time.hour >= 12) ? TimeSystem.PM : TimeSystem.AM);
+ system.set_active((time.get_hour() >= 12) ? TimeSystem.PM : TimeSystem.AM);
}
- minute.set_value(time.minute);
- second.set_value(time.second);
+ minute.set_value(time.get_minute());
+ second.set_value(time.get_second());
previous_time_system = (TimeSystem) system.get_active();
}
@@ -217,43 +218,35 @@ public class AdjustDateTimeDialog : Gtk.Dialog {
return;
original_time_label.set_text(_("Original: ") +
- Time.local(original_time).format(use_24_hr_format ? _("%m/%d/%Y, %H:%M:%S") :
+ original_time.to_local().format(use_24_hr_format ? _("%m/%d/%Y, %H:%M:%S") :
_("%m/%d/%Y, %I:%M:%S %p")));
}
- private time_t get_time() {
- Time time = Time();
-
- time.second = (int) second.get_value();
- time.minute = (int) minute.get_value();
-
+ private DateTime get_time() {
// convert to 24 hr
int hour = (int) hour.get_value();
- time.hour = (hour == 12 && system.get_active() != TimeSystem.24HR) ? 0 : hour;
- time.hour += ((system.get_active() == TimeSystem.PM) ? 12 : 0);
+ hour = (hour == 12 && system.get_active() != TimeSystem.24HR) ? 0 : hour;
+ hour += ((system.get_active() == TimeSystem.PM) ? 12 : 0);
uint year, month, day;
calendar.get_date(out year, out month, out day);
- time.year = ((int) year) - YEAR_OFFSET;
- time.month = (int) month;
- time.day = (int) day;
- time.isdst = -1;
-
- return time.mktime();
+ return new DateTime.local((int)year, (int)month + 1, (int)day, hour, (int)minute.get_value(), (int)second.get_value());
}
- public bool execute(out int64 time_shift, out bool keep_relativity,
+ public bool execute(out TimeSpan time_shift, out bool keep_relativity,
out bool modify_originals) {
show_all();
bool response = false;
if (run() == Gtk.ResponseType.OK) {
- if (no_original_time)
- time_shift = (int64) get_time();
- else
- time_shift = (int64) (get_time() - original_time);
+ // Difference returns microseconds, so divide by 1000000, we need seconds
+ if (no_original_time) {
+ time_shift = get_time().difference(new DateTime.from_unix_utc(0)) / 1000 / 1000;
+ } else {
+ time_shift = (get_time().difference(original_time)) / 1000 / 1000;
+ }
keep_relativity = relativity_radio_button.get_active();
@@ -286,7 +279,7 @@ public class AdjustDateTimeDialog : Gtk.Dialog {
}
private void on_time_changed() {
- int64 time_shift = ((int64) get_time() - (int64) original_time);
+ var time_shift = get_time().difference (original_time);
calendar.notify_property("year");
calendar.notify_property("month");
@@ -301,12 +294,12 @@ public class AdjustDateTimeDialog : Gtk.Dialog {
time_shift = time_shift.abs();
- days = (int) (time_shift / SECONDS_IN_DAY);
- time_shift = time_shift % SECONDS_IN_DAY;
- hours = (int) (time_shift / SECONDS_IN_HOUR);
- time_shift = time_shift % SECONDS_IN_HOUR;
- minutes = (int) (time_shift / SECONDS_IN_MINUTE);
- seconds = (int) (time_shift % SECONDS_IN_MINUTE);
+ days = (int) (time_shift / TimeSpan.DAY);
+ time_shift = time_shift % TimeSpan.DAY;
+ hours = (int) (time_shift / TimeSpan.HOUR);
+ time_shift = time_shift % TimeSpan.HOUR;
+ minutes = (int) (time_shift / TimeSpan.MINUTE);
+ seconds = (int) ((time_shift % TimeSpan.MINUTE) / TimeSpan.SECOND);
string shift_status = (forward) ?
_("Exposure time will be shifted forward by\n%d %s, %d %s, %d %s, and %d %s.") :
diff --git a/src/dialogs/MultiTextEntryDialog.vala b/src/dialogs/MultiTextEntryDialog.vala
index 42e5318..ddbd59b 100644
--- a/src/dialogs/MultiTextEntryDialog.vala
+++ b/src/dialogs/MultiTextEntryDialog.vala
@@ -11,7 +11,7 @@ public class MultiTextEntryDialog : Gtk.Dialog {
private unowned OnModifyValidateType on_modify_validate;
[GtkChild]
- private Gtk.TextView entry;
+ private unowned Gtk.TextView entry;
public MultiTextEntryDialog() {
Object (use_header_bar: Resources.use_header_bar());
diff --git a/src/dialogs/Preferences.vala b/src/dialogs/Preferences.vala
index 17b16cf..efd9589 100644
--- a/src/dialogs/Preferences.vala
+++ b/src/dialogs/Preferences.vala
@@ -19,49 +19,49 @@ public class PreferencesDialog : Gtk.Dialog {
private static PreferencesDialog preferences_dialog;
[GtkChild]
- private Gtk.Switch switch_dark;
+ private unowned Gtk.Switch switch_dark;
[GtkChild]
- private Gtk.ComboBox photo_editor_combo;
+ private unowned Gtk.ComboBox photo_editor_combo;
[GtkChild]
- private Gtk.ComboBox raw_editor_combo;
+ private unowned Gtk.ComboBox raw_editor_combo;
private SortedList<AppInfo> external_raw_apps;
private SortedList<AppInfo> external_photo_apps;
[GtkChild]
- private Gtk.FileChooserButton library_dir_button;
+ private unowned Gtk.FileChooserButton library_dir_button;
[GtkChild]
- private Gtk.ComboBoxText dir_pattern_combo;
+ private unowned Gtk.ComboBoxText dir_pattern_combo;
[GtkChild]
- private Gtk.Entry dir_pattern_entry;
+ private unowned Gtk.Entry dir_pattern_entry;
[GtkChild]
- private Gtk.Label dir_pattern_example;
+ private unowned Gtk.Label dir_pattern_example;
private bool allow_closing = false;
private string? lib_dir = null;
private Gee.ArrayList<PathFormat> path_formats = new Gee.ArrayList<PathFormat>();
private GLib.DateTime example_date = new GLib.DateTime.local(2009, 3, 10, 18, 16, 11);
[GtkChild]
- private Gtk.CheckButton lowercase;
+ private unowned Gtk.CheckButton lowercase;
private Plugins.ManifestWidgetMediator plugins_mediator = new Plugins.ManifestWidgetMediator();
[GtkChild]
- private Gtk.ComboBoxText default_raw_developer_combo;
+ private unowned Gtk.ComboBoxText default_raw_developer_combo;
[GtkChild]
- private Gtk.CheckButton autoimport;
+ private unowned Gtk.CheckButton autoimport;
[GtkChild]
- private Gtk.CheckButton write_metadata;
+ private unowned Gtk.CheckButton write_metadata;
[GtkChild]
- private Gtk.Label pattern_help;
+ private unowned Gtk.Label pattern_help;
[GtkChild]
- private Gtk.Notebook preferences_notebook;
+ private unowned Gtk.Stack preferences_stack;
[GtkChild]
- private Gtk.RadioButton transparent_checker_radio;
+ private unowned Gtk.RadioButton transparent_checker_radio;
[GtkChild]
- private Gtk.RadioButton transparent_solid_radio;
+ private unowned Gtk.RadioButton transparent_solid_radio;
[GtkChild]
- private Gtk.ColorButton transparent_solid_color;
+ private unowned Gtk.ColorButton transparent_solid_color;
[GtkChild]
- private Gtk.RadioButton transparent_none_radio;
+ private unowned Gtk.RadioButton transparent_none_radio;
private PreferencesDialog() {
Object (use_header_bar: Resources.use_header_bar());
@@ -81,7 +81,7 @@ public class PreferencesDialog : Gtk.Dialog {
Gdk.RGBA color = Gdk.RGBA();
color.parse(Config.Facade.get_instance().get_transparent_background_color());
- (transparent_solid_color as Gtk.ColorChooser).rgba = color;
+ ((Gtk.ColorChooser) transparent_solid_color).rgba = color;
transparent_solid_color.color_set.connect(on_color_changed);
switch (Config.Facade.get_instance().get_transparent_background_type()) {
@@ -105,11 +105,11 @@ public class PreferencesDialog : Gtk.Dialog {
if (help_path == null) {
// We're installed system-wide, so use the system help.
- pattern_help.set_markup("<a href=\"" + Resources.DIR_PATTERN_URI_SYSWIDE + "\">" + _("(Help)") + "</a>");
+ pattern_help.set_markup("<a href=\"%s\">%s</a>".printf(Resources.DIR_PATTERN_URI_SYSWIDE, _("(Help)")));
} else {
// We're being run from the build directory; we'll have to handle clicks to this
// link manually ourselves, due to a limitation of help: URIs.
- pattern_help.set_markup("<a href=\"dummy:\">" + _("(Help)") + "</a>");
+ pattern_help.set_markup("<a href=\"dummy:\">%s</a>".printf(_("(Help)")));
pattern_help.activate_link.connect(on_local_pattern_help);
}
@@ -126,7 +126,9 @@ public class PreferencesDialog : Gtk.Dialog {
lowercase.toggled.connect(on_lowercase_toggled);
- (preferences_notebook.get_nth_page (2) as Gtk.Container).add (plugins_mediator);
+ ((Gtk.Box)preferences_stack.get_child_by_name("plugins")).add(plugins_mediator);
+ ((Gtk.Box)preferences_stack.get_child_by_name("profiles")).add(new Shotwell.ProfileBrowser());
+
populate_preference_options();
@@ -177,7 +179,7 @@ public class PreferencesDialog : Gtk.Dialog {
}
private void on_color_changed() {
- var color = (transparent_solid_color as Gtk.ColorChooser).rgba.to_string();
+ var color = ((Gtk.ColorChooser) transparent_solid_color).rgba.to_string();
Config.Facade.get_instance().set_transparent_background_color(color);
}
diff --git a/src/dialogs/SetBackground.vala b/src/dialogs/SetBackground.vala
index d9a77c4..ec56502 100644
--- a/src/dialogs/SetBackground.vala
+++ b/src/dialogs/SetBackground.vala
@@ -8,9 +8,9 @@
[GtkTemplate (ui = "/org/gnome/Shotwell/ui/set_background_dialog.ui")]
public class SetBackgroundPhotoDialog : Gtk.Dialog {
[GtkChild]
- private Gtk.CheckButton desktop_background_checkbox;
+ private unowned Gtk.CheckButton desktop_background_checkbox;
[GtkChild]
- private Gtk.CheckButton screensaver_checkbox;
+ private unowned Gtk.CheckButton screensaver_checkbox;
public SetBackgroundPhotoDialog() {
Object(use_header_bar: Resources.use_header_bar());
diff --git a/src/dialogs/SetBackgroundSlideshow.vala b/src/dialogs/SetBackgroundSlideshow.vala
index 914af76..479b0c7 100644
--- a/src/dialogs/SetBackgroundSlideshow.vala
+++ b/src/dialogs/SetBackgroundSlideshow.vala
@@ -8,13 +8,13 @@
[GtkTemplate (ui = "/org/gnome/Shotwell/ui/set_background_slideshow_dialog.ui")]
public class SetBackgroundSlideshowDialog : Gtk.Dialog {
[GtkChild]
- private Gtk.CheckButton desktop_background_checkbox;
+ private unowned Gtk.CheckButton desktop_background_checkbox;
[GtkChild]
- private Gtk.CheckButton screensaver_checkbox;
+ private unowned Gtk.CheckButton screensaver_checkbox;
[GtkChild]
- private Gtk.Scale delay_scale;
+ private unowned Gtk.Scale delay_scale;
[GtkChild]
- private Gtk.Label delay_value_label;
+ private unowned Gtk.Label delay_value_label;
private int delay_value = 0;
diff --git a/src/dialogs/TextEntry.vala b/src/dialogs/TextEntry.vala
index d82fdbd..a2e4653 100644
--- a/src/dialogs/TextEntry.vala
+++ b/src/dialogs/TextEntry.vala
@@ -12,10 +12,10 @@ public class TextEntryDialog : Gtk.Dialog {
private unowned OnModifyValidateType on_modify_validate;
[GtkChild]
- private Gtk.Entry entry;
+ private unowned Gtk.Entry entry;
[GtkChild]
- private Gtk.Label label;
+ private unowned Gtk.Label label;
public TextEntryDialog() {
Object (use_header_bar: Resources.use_header_bar());
diff --git a/src/dialogs/WelcomeDialog.vala b/src/dialogs/WelcomeDialog.vala
index e40686d..7fa0b7c 100644
--- a/src/dialogs/WelcomeDialog.vala
+++ b/src/dialogs/WelcomeDialog.vala
@@ -60,11 +60,9 @@ public class WelcomeDialog : Gtk.Dialog {
Gtk.Label instructions = new Gtk.Label("");
string indent_prefix = " "; // we can't tell what the indent prefix is going to be so assume we need one
- string arrow_glyph = (get_direction() == Gtk.TextDirection.RTL) ? "◂" : "▸";
-
instructions.set_markup(((indent_prefix + "&#8226; %s\n") + (indent_prefix + "&#8226; %s\n")
+ (indent_prefix + "&#8226; %s")).printf(
- _("Choose <span weight=\"bold\">File %s Import From Folder</span>").printf(arrow_glyph),
+ _("Choose “Import From Folder” from the File menu"),
_("Drag and drop photos onto the Shotwell window"),
_("Connect a camera to your computer and import")));
instructions.xalign = 0.0f;
diff --git a/src/direct/DirectPhotoPage.vala b/src/direct/DirectPhotoPage.vala
index 39a87f1..cc7186c 100644
--- a/src/direct/DirectPhotoPage.vala
+++ b/src/direct/DirectPhotoPage.vala
@@ -219,8 +219,9 @@ public class DirectPhotoPage : EditingHostPage {
return true;
} else {
- if (get_container() is DirectWindow) {
- (get_container() as DirectWindow).do_fullscreen();
+ var direct_window = get_container() as DirectWindow;
+ if (direct_window != null) {
+ direct_window.do_fullscreen();
return true;
}
diff --git a/src/editing_tools/EditingTools.vala b/src/editing_tools/EditingTools.vala
index 82fef0f..0042d57 100644
--- a/src/editing_tools/EditingTools.vala
+++ b/src/editing_tools/EditingTools.vala
@@ -87,7 +87,8 @@ public abstract class EditingToolWindow : Gtk.Window {
}
public override void realize() {
- (this as Gtk.Widget).set_opacity(Resources.TRANSIENT_WINDOW_OPACITY);
+ // Force the use of gtk_widget_set_opacity; gtk_window_set_opacity is deprecated
+ ((Gtk.Widget) this).set_opacity(Resources.TRANSIENT_WINDOW_OPACITY);
base.realize();
}
@@ -381,12 +382,13 @@ public abstract class PhotoCanvas {
}
public void erase_horizontal_line(int x, int y, int width) {
+ var scale = Application.get_scale();
default_ctx.save();
default_ctx.set_operator(Cairo.Operator.SOURCE);
default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
default_ctx.rectangle(scaled_position.x + x, scaled_position.y + y,
- width - 1, 1);
+ width - 1, 1 * scale);
default_ctx.fill();
default_ctx.restore();
@@ -404,6 +406,8 @@ public abstract class PhotoCanvas {
public void erase_vertical_line(int x, int y, int height) {
default_ctx.save();
+ var scale = Application.get_scale();
+
// Ticket #3146 - artifacting when moving the crop box or
// enlarging it from the lower right.
// We now no longer subtract one from the height before choosing
@@ -411,7 +415,7 @@ public abstract class PhotoCanvas {
default_ctx.set_operator(Cairo.Operator.SOURCE);
default_ctx.set_source_surface(scaled, scaled_position.x, scaled_position.y);
default_ctx.rectangle(scaled_position.x + x, scaled_position.y + y,
- 1, height);
+ 1 * scale, height);
default_ctx.fill();
default_ctx.restore();
@@ -427,12 +431,19 @@ public abstract class PhotoCanvas {
public void invalidate_area(Box area) {
Gdk.Rectangle rect = area.get_rectangle();
+
rect.x += scaled_position.x;
rect.y += scaled_position.y;
drawing_window.invalidate_rect(rect, false);
}
+ public void set_cursor(Gdk.CursorType cursor_type) {
+ var display = get_drawing_window().get_display();
+ var cursor = new Gdk.Cursor.for_display (display, cursor_type);
+ get_drawing_window().set_cursor(cursor);
+ }
+
private Cairo.Surface pixbuf_to_surface(Cairo.Context default_ctx, Gdk.Pixbuf pixbuf,
Gdk.Rectangle pos) {
Cairo.Surface surface = new Cairo.Surface.similar(default_ctx.get_target(),
@@ -1220,11 +1231,7 @@ public class CropTool : EditingTool {
// make sure the cursor isn't set to a modify indicator
if (canvas != null) {
- var drawing_window = canvas.get_drawing_window ();
- var display = drawing_window.get_display ();
- var cursor = new Gdk.Cursor.for_display (display,
- Gdk.CursorType.LEFT_PTR);
- drawing_window.set_cursor (cursor);
+ canvas.set_cursor (Gdk.CursorType.LEFT_PTR);
}
crop_surface = null;
@@ -1244,20 +1251,22 @@ public class CropTool : EditingTool {
}
private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
+ var scale = Application.get_scale();
wide_black_ctx = new Cairo.Context(ctx.get_target());
set_source_color_from_string(wide_black_ctx, "#000");
- wide_black_ctx.set_line_width(1);
+ wide_black_ctx.set_line_width(1 * scale);
wide_white_ctx = new Cairo.Context(ctx.get_target());
set_source_color_from_string(wide_white_ctx, "#FFF");
- wide_white_ctx.set_line_width(1);
+ wide_white_ctx.set_line_width(1 * scale);
thin_white_ctx = new Cairo.Context(ctx.get_target());
set_source_color_from_string(thin_white_ctx, "#FFF");
- thin_white_ctx.set_line_width(0.5);
+ thin_white_ctx.set_line_width(0.5 * scale);
text_ctx = new Cairo.Context(ctx.get_target());
text_ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ text_ctx.set_font_size(10.0 * scale);
}
private void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled, Gdk.Rectangle scaled_position) {
@@ -1286,7 +1295,8 @@ public class CropTool : EditingTool {
Box offset_scaled_crop = scaled_crop.get_offset(scaled_pixbuf_pos.x, scaled_pixbuf_pos.y);
// determine where the mouse down landed and store for future events
- in_manipulation = offset_scaled_crop.approx_location(x, y);
+ 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;
@@ -1314,19 +1324,21 @@ public class CropTool : EditingTool {
// only deal with manipulating the crop tool when click-and-dragging one of the edges
// or the interior
if (in_manipulation != BoxLocation.OUTSIDE)
- on_canvas_manipulation(x, y);
+ on_canvas_manipulation((int)Math.lround(x * Application.get_scale()),
+ (int)Math.lround(y * Application.get_scale()));
update_cursor(x, y);
canvas.repaint();
}
public override void paint(Cairo.Context default_ctx) {
+ var scale = Application.get_scale();
// fill region behind the crop surface with neutral color
int w = canvas.get_drawing_window().get_width();
int h = canvas.get_drawing_window().get_height();
default_ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0);
- default_ctx.rectangle(0, 0, w, h);
+ default_ctx.rectangle(0, 0, w * scale, h * scale);
default_ctx.fill();
default_ctx.paint();
@@ -1376,7 +1388,8 @@ public class CropTool : EditingTool {
Box offset_scaled_crop = scaled_crop.get_offset(scaled_pos.x, scaled_pos.y);
Gdk.CursorType cursor_type = Gdk.CursorType.LEFT_PTR;
- switch (offset_scaled_crop.approx_location(x, y)) {
+ switch (offset_scaled_crop.approx_location((int)Math.lround(x * Application.get_scale()),
+ (int)Math.lround(y * Application.get_scale()))) {
case BoxLocation.LEFT_SIDE:
cursor_type = Gdk.CursorType.LEFT_SIDE;
break;
@@ -1419,10 +1432,7 @@ public class CropTool : EditingTool {
}
if (cursor_type != current_cursor_type) {
- var drawing_window = canvas.get_drawing_window ();
- var display = drawing_window.get_display ();
- var cursor = new Gdk.Cursor.for_display (display, cursor_type);
- drawing_window.set_cursor (cursor);
+ canvas.set_cursor(cursor_type);
current_cursor_type = cursor_type;
}
}
@@ -1893,8 +1903,6 @@ public class RedeyeTool : EditingTool {
private bool is_reticle_move_in_progress = false;
private Gdk.Point reticle_move_mouse_start_point;
private Gdk.Point reticle_move_anchor;
- private Gdk.Cursor cached_arrow_cursor;
- private Gdk.Cursor cached_grab_cursor;
private Gdk.Rectangle old_scaled_pixbuf_position;
private Gdk.Pixbuf current_pixbuf = null;
@@ -1928,13 +1936,14 @@ public class RedeyeTool : EditingTool {
}
private void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
+ var scale = Application.get_scale();
wider_gray_ctx = new Cairo.Context(ctx.get_target());
set_source_color_from_string(wider_gray_ctx, "#111");
- wider_gray_ctx.set_line_width(3);
+ wider_gray_ctx.set_line_width(3 * scale);
thin_white_ctx = new Cairo.Context(ctx.get_target());
set_source_color_from_string(thin_white_ctx, "#FFF");
- thin_white_ctx.set_line_width(1);
+ thin_white_ctx.set_line_width(1 * scale);
}
private void draw_redeye_instance(RedeyeInstance inst) {
@@ -2044,10 +2053,6 @@ public class RedeyeTool : EditingTool {
bind_window_handlers();
- var display = canvas.get_drawing_window().get_display();
- cached_arrow_cursor = new Gdk.Cursor.for_display(display, Gdk.CursorType.LEFT_PTR);
- cached_grab_cursor = new Gdk.Cursor.for_display(display, Gdk.CursorType.FLEUR);
-
DataCollection? owner = canvas.get_photo().get_membership();
if (owner != null)
owner.items_altered.connect(on_photos_altered);
@@ -2112,13 +2117,17 @@ public class RedeyeTool : EditingTool {
}
public override void on_left_click(int x, int y) {
+ var scale = Application.get_scale();
+
Gdk.Rectangle bounds_rect =
RedeyeInstance.to_bounds_rect(user_interaction_instance);
- if (coord_in_rectangle(x, y, bounds_rect)) {
+
+ if (coord_in_rectangle((int)Math.lround(x * scale), (int)Math.lround(y * scale), bounds_rect)) {
+ print("Motion in progress!!\n");
is_reticle_move_in_progress = true;
- reticle_move_mouse_start_point.x = x;
- reticle_move_mouse_start_point.y = y;
+ reticle_move_mouse_start_point.x = (int)Math.lround(x * scale);
+ reticle_move_mouse_start_point.y = (int)Math.lround(y * scale);
reticle_move_anchor = user_interaction_instance.center;
}
}
@@ -2128,6 +2137,8 @@ public class RedeyeTool : EditingTool {
}
public override void on_motion(int x, int y, Gdk.ModifierType mask) {
+ var scale = Application.get_scale();
+
if (is_reticle_move_in_progress) {
Gdk.Rectangle active_region_rect =
@@ -2144,8 +2155,8 @@ public class RedeyeTool : EditingTool {
active_region_rect.y + active_region_rect.height -
user_interaction_instance.radius - 1;
- int delta_x = x - reticle_move_mouse_start_point.x;
- int delta_y = y - reticle_move_mouse_start_point.y;
+ int delta_x = (int)Math.lround(x * scale) - reticle_move_mouse_start_point.x;
+ int delta_y = (int)Math.lround(y * scale) - reticle_move_mouse_start_point.y;
user_interaction_instance.center.x = reticle_move_anchor.x +
delta_x;
@@ -2164,10 +2175,10 @@ public class RedeyeTool : EditingTool {
Gdk.Rectangle bounds =
RedeyeInstance.to_bounds_rect(user_interaction_instance);
- if (coord_in_rectangle(x, y, bounds)) {
- canvas.get_drawing_window().set_cursor(cached_grab_cursor);
+ if (coord_in_rectangle((int)Math.lround(x * scale), (int)Math.lround(y * scale), bounds)) {
+ canvas.set_cursor(Gdk.CursorType.FLEUR);
} else {
- canvas.get_drawing_window().set_cursor(cached_arrow_cursor);
+ canvas.set_cursor(Gdk.CursorType.LEFT_PTR);
}
}
}
diff --git a/src/editing_tools/StraightenTool.vala b/src/editing_tools/StraightenTool.vala
index f427b99..2b0591a 100644
--- a/src/editing_tools/StraightenTool.vala
+++ b/src/editing_tools/StraightenTool.vala
@@ -80,13 +80,13 @@ public class StraightenTool : EditingTool {
// different backgrounds.
ctx.set_source_rgba(0.0, 0.0, 0.0, alpha);
ctx.set_dash(GUIDE_DASH, GUIDE_DASH[0] / 2);
- ctx.move_to(x[0] + 0.5, y[0] + 0.5);
- ctx.line_to(x[1] + 0.5, y[1] + 0.5);
+ ctx.move_to(x[0] * Application.get_scale() + 0.5, y[0]* Application.get_scale() + 0.5);
+ ctx.line_to(x[1] * Application.get_scale()+ 0.5, y[1]* Application.get_scale() + 0.5);
ctx.stroke();
ctx.set_dash(GUIDE_DASH, -GUIDE_DASH[0] / 2);
ctx.set_source_rgba(1.0, 1.0, 1.0, alpha);
- ctx.move_to(x[0] + 0.5, y[0] + 0.5);
- ctx.line_to(x[1] + 0.5, y[1] + 0.5);
+ ctx.move_to(x[0] * Application.get_scale()+ 0.5, y[0]* Application.get_scale() + 0.5);
+ ctx.line_to(x[1] * Application.get_scale()+ 0.5, y[1] * Application.get_scale()+ 0.5);
ctx.stroke();
}
}
@@ -456,7 +456,7 @@ public class StraightenTool : EditingTool {
*/
private void update_rotated_surface() {
draw_rotated_source(photo_surf, rotate_ctx, view_width, view_height, photo_angle);
- rotate_ctx.set_line_width(1.0);
+ rotate_ctx.set_line_width(1.0 * Application.get_scale());
draw_superimposed_grid(rotate_ctx, view_width, view_height);
}
@@ -468,8 +468,8 @@ public class StraightenTool : EditingTool {
* it's not used.
*/
public override void paint(Cairo.Context ctx) {
- int w = canvas.get_drawing_window().get_width();
- int h = canvas.get_drawing_window().get_height();
+ var w = canvas.get_drawing_window().get_width() * Application.get_scale();
+ var h = canvas.get_drawing_window().get_height() * Application.get_scale();
// fill region behind the rotation surface with neutral color.
canvas.get_default_ctx().identity_matrix();
diff --git a/src/events/EventDirectoryItem.vala b/src/events/EventDirectoryItem.vala
index 5b177fb..dbab1b1 100644
--- a/src/events/EventDirectoryItem.vala
+++ b/src/events/EventDirectoryItem.vala
@@ -60,7 +60,7 @@ class EventDirectoryItem : CheckerboardItem {
pixbuf = media.get_preview_pixbuf(squared_scaling);
} catch (Error error) {
ThumbnailCache.fetch_async_scaled(media, ThumbnailCache.Size.BIG,
- new Dimensions(ThumbnailCache.Size.BIG, ThumbnailCache.Size.BIG),
+ Dimensions(ThumbnailCache.Size.BIG, ThumbnailCache.Size.BIG),
ThumbnailCache.DEFAULT_INTERP, () => {});
if (media is LibraryPhoto) {
LibraryPhoto photo = (LibraryPhoto) media;
diff --git a/src/events/EventsBranch.vala b/src/events/EventsBranch.vala
index 097a664..0550eb7 100644
--- a/src/events/EventsBranch.vala
+++ b/src/events/EventsBranch.vala
@@ -133,8 +133,8 @@ public class Events.Branch : Sidebar.Branch {
b = swap;
}
- int64 result = ((Events.EventEntry) a).get_event().get_start_time()
- - ((Events.EventEntry) b).get_event().get_start_time();
+ int64 result = nullsafe_date_time_comperator(((Events.EventEntry) a).get_event().get_start_time(),
+ ((Events.EventEntry) b).get_event().get_start_time());
// to stabilize sort (events with the same start time are allowed)
if (result == 0) {
@@ -215,14 +215,14 @@ public class Events.Branch : Sidebar.Branch {
}
private void add_event(Event event) {
- time_t event_time = event.get_start_time();
- if (event_time == 0) {
+ DateTime? event_time = event.get_start_time();
+ if (event_time == null) {
add_undated_event(event);
return;
}
- Time event_tm = Time.local(event_time);
+ var event_tm = event_time.to_local();
Sidebar.Entry? year;
Sidebar.Entry? month = find_event_month(event, event_tm, out year);
@@ -246,14 +246,14 @@ public class Events.Branch : Sidebar.Branch {
}
private void move_event(Event event) {
- time_t event_time = event.get_start_time();
- if (event_time == 0) {
+ DateTime? event_time = event.get_start_time();
+ if (event_time == null) {
move_to_undated_event(event);
return;
}
- Time event_tm = Time.local(event_time);
+ var event_tm = event_time.to_local();
Sidebar.Entry? year;
Sidebar.Entry? month = find_event_month(event, event_tm, out year);
@@ -296,13 +296,13 @@ public class Events.Branch : Sidebar.Branch {
}
}
- private Sidebar.Entry? find_event_month(Event event, Time event_tm, out Sidebar.Entry found_year) {
+ private Sidebar.Entry? find_event_month(Event event, DateTime event_tm, out Sidebar.Entry found_year) {
// find the year first
found_year = find_event_year(event, event_tm);
if (found_year == null)
return null;
- int event_month = event_tm.month + 1;
+ int event_month = event_tm.get_month();
// found the year, traverse the months
return find_first_child(found_year, (entry) => {
@@ -310,8 +310,8 @@ public class Events.Branch : Sidebar.Branch {
});
}
- private Sidebar.Entry? find_event_year(Event event, Time event_tm) {
- int event_year = event_tm.year + 1900;
+ private Sidebar.Entry? find_event_year(Event event, DateTime event_tm) {
+ int event_year = event_tm.get_year();
return find_first_child(get_root(), (entry) => {
if ((entry is Events.UndatedDirectoryEntry) || (entry is Events.NoEventEntry) ||
@@ -400,9 +400,9 @@ public class Events.MasterDirectoryEntry : Events.DirectoryEntry {
public class Events.YearDirectoryEntry : Events.DirectoryEntry {
private string name;
- private Time tm;
+ private DateTime tm;
- public YearDirectoryEntry(string name, Time tm) {
+ public YearDirectoryEntry(string name, DateTime tm) {
this.name = name;
this.tm = tm;
}
@@ -412,7 +412,7 @@ public class Events.YearDirectoryEntry : Events.DirectoryEntry {
}
public int get_year() {
- return tm.year + 1900;
+ return tm.get_year();
}
protected override Page create_page() {
@@ -422,9 +422,9 @@ public class Events.YearDirectoryEntry : Events.DirectoryEntry {
public class Events.MonthDirectoryEntry : Events.DirectoryEntry {
private string name;
- private Time tm;
+ private DateTime tm;
- public MonthDirectoryEntry(string name, Time tm) {
+ public MonthDirectoryEntry(string name, DateTime tm) {
this.name = name;
this.tm = tm;
}
@@ -434,11 +434,11 @@ public class Events.MonthDirectoryEntry : Events.DirectoryEntry {
}
public int get_year() {
- return tm.year + 1900;
+ return tm.get_year();
}
public int get_month() {
- return tm.month + 1;
+ return tm.get_month();
}
protected override Page create_page() {
@@ -456,7 +456,7 @@ public class Events.UndatedDirectoryEntry : Events.DirectoryEntry {
protected override Page create_page() {
return new SubEventsDirectoryPage(SubEventsDirectoryPage.DirectoryType.UNDATED,
- Time.local(0));
+ new DateTime.now_local());
}
}
diff --git a/src/events/EventsDirectoryPage.vala b/src/events/EventsDirectoryPage.vala
index 7ead1a0..c00e4bf 100644
--- a/src/events/EventsDirectoryPage.vala
+++ b/src/events/EventsDirectoryPage.vala
@@ -88,10 +88,10 @@ public abstract class EventsDirectoryPage : CheckerboardPage {
}
private static int64 event_ascending_comparator(void *a, void *b) {
- time_t start_a = ((EventDirectoryItem *) a)->event.get_start_time();
- time_t start_b = ((EventDirectoryItem *) b)->event.get_start_time();
+ DateTime start_a = ((EventDirectoryItem *) a)->event.get_start_time();
+ DateTime start_b = ((EventDirectoryItem *) b)->event.get_start_time();
- return start_a - start_b;
+ return start_a.compare(start_b);
}
private static int64 event_descending_comparator(void *a, void *b) {
@@ -239,21 +239,21 @@ public class SubEventsDirectoryPage : EventsDirectoryPage {
}
public const string UNDATED_PAGE_NAME = _("Undated");
- public const string YEAR_FORMAT = _("%Y");
- public const string MONTH_FORMAT = _("%B");
+ public const string YEAR_FORMAT = "%Y";
+ public const string MONTH_FORMAT = "%0B";
private class SubEventDirectoryManager : EventsDirectoryPage.EventDirectoryManager {
private int month = 0;
private int year = 0;
DirectoryType type;
- public SubEventDirectoryManager(DirectoryType type, Time time) {
+ public SubEventDirectoryManager(DirectoryType type, DateTime time) {
base();
if (type == DirectoryType.MONTH)
- month = time.month;
+ month = time.get_month();
this.type = type;
- year = time.year;
+ year = time.get_year();
}
public override bool include_in_view(DataSource source) {
@@ -261,10 +261,10 @@ public class SubEventsDirectoryPage : EventsDirectoryPage {
return false;
EventSource event = (EventSource) source;
- Time event_time = Time.local(event.get_start_time());
- if (event_time.year == year) {
+ var event_time = event.get_start_time().to_local();
+ if (event_time.get_year() == year) {
if (type == DirectoryType.MONTH) {
- return (event_time.month == month);
+ return (event_time.get_month() == month);
}
return true;
}
@@ -284,12 +284,26 @@ public class SubEventsDirectoryPage : EventsDirectoryPage {
}
}
- public SubEventsDirectoryPage(DirectoryType type, Time time) {
+ public SubEventsDirectoryPage(DirectoryType type, DateTime time) {
string page_name;
if (type == SubEventsDirectoryPage.DirectoryType.UNDATED) {
page_name = UNDATED_PAGE_NAME;
} else {
- page_name = time.format((type == DirectoryType.YEAR) ? YEAR_FORMAT : MONTH_FORMAT);
+ switch (type) {
+ case DirectoryType.MONTH: {
+ page_name = time.format(MONTH_FORMAT);
+ if (page_name.index_of("%0B") != -1) {
+ page_name = time.format("%B");
+ }
+ }
+ break;
+ case DirectoryType.YEAR: {
+ page_name = time.format(YEAR_FORMAT);
+ }
+ break;
+ default:
+ assert_not_reached();
+ }
}
base(page_name, new SubEventDirectoryManager(type, time), null);
diff --git a/src/faces/Face.vala b/src/faces/Face.vala
index 9304023..cdccc1b 100644
--- a/src/faces/Face.vala
+++ b/src/faces/Face.vala
@@ -345,9 +345,19 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable {
// add them all at once to the SourceCollection
global.add_many(faces);
global.init_add_many_unlinked(unlinked);
+
+#if ENABLE_FACE_DETECTION
+ // Start the face detection background process
+ // FaceTool talks to it over DBus
+ start_facedetect_process();
+#endif
}
public static void terminate() {
+ try {
+ if (FaceDetect.face_detect_proxy != null)
+ FaceDetect.face_detect_proxy.terminate();
+ } catch(Error e) {}
}
public static int compare_names(void *a, void *b) {
@@ -365,6 +375,14 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable {
public static bool equal_name_strings(void *a, void *b) {
return String.collated_equals(a, b);
}
+
+#if ENABLE_FACE_DETECTION
+ private static void start_facedetect_process() {
+ message("Launching facedetect process: %s", AppDirs.get_facedetect_bin().get_path());
+ // Start the watcher, process started via DBus service
+ FaceDetect.init(AppDirs.get_openface_dnn_system_dir().get_path() + ":" + AppDirs.get_openface_dnn_dir().get_path());
+ }
+#endif
// Returns a Face for the name, creating a new empty one if it does not already exist.
// name should have already been prepared by prep_face_name.
@@ -387,7 +405,7 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable {
return face;
}
-
+
// Utility function to cleanup a face name that comes from user input and prepare it for use
// in the system and storage in the database. Returns null if the name is unacceptable.
public static string? prep_face_name(string name) {
@@ -574,6 +592,16 @@ public class Face : DataSource, ContainerSource, Proxyable, Indexable {
return true;
}
+
+ public bool set_reference(FaceLocation face_loc) {
+ try {
+ FaceTable.get_instance().set_reference(row.face_id, face_loc.get_photo_id());
+ } catch (DatabaseError err) {
+ AppWindow.database_error(err);
+ return false;
+ }
+ return true;
+ }
public bool contains(MediaSource source) {
return media_views.has_view_for_source(source);
diff --git a/src/faces/FaceDetect.vala b/src/faces/FaceDetect.vala
new file mode 100644
index 0000000..83caa4d
--- /dev/null
+++ b/src/faces/FaceDetect.vala
@@ -0,0 +1,146 @@
+/**
+ * Face detection and recognition functions
+ * Copyright 2018 Narendra A (narendra_m_a(at)yahoo(dot)com)
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+// DBus face_detect_proxy definition
+public struct FaceRect {
+ public double x;
+ public double y;
+ public double width;
+ public double height;
+ public double[] vec;
+}
+
+[DBus (name = "org.gnome.Shotwell.Faces1")]
+public interface FaceDetectInterface : DBusProxy {
+ public abstract FaceRect[] detect_faces(string inputName, string cascadeName, double scale, bool infer)
+ throws IOError, DBusError;
+ public abstract bool load_net(string netFile)
+ throws IOError, DBusError;
+ public abstract void terminate() throws IOError, DBusError;
+}
+
+// Class to communicate with facedetect process over DBus
+public class FaceDetect {
+ public const string DBUS_NAME = "org.gnome.Shotwell.Faces1";
+ public const string DBUS_PATH = "/org/gnome/shotwell/faces";
+ public static bool connected = false;
+ public static string net_file;
+ public const string ERROR_MESSAGE = "Unable to connect to facedetect service";
+
+ public static FaceDetectInterface face_detect_proxy;
+
+#if FACEDETECT_BUS_PRIVATE
+ private static GLib.DBusServer dbus_server;
+ private static Subprocess process;
+#endif
+
+ public static void create_face_detect_proxy(DBusConnection connection, string bus_name, string owner) {
+ if (bus_name == DBUS_NAME) {
+ message("Dbus name %s available", bus_name);
+
+ try {
+ // Service file should automatically run the facedetect binary
+ face_detect_proxy = Bus.get_proxy_sync (BusType.SESSION, DBUS_NAME, DBUS_PATH);
+ face_detect_proxy.load_net(net_file);
+ connected = true;
+ } catch(IOError e) {
+ AppWindow.error_message(ERROR_MESSAGE);
+ } catch(DBusError e) {
+ AppWindow.error_message(ERROR_MESSAGE);
+ }
+ }
+ }
+
+ public static void interface_gone(DBusConnection connection, string bus_name) {
+ message("Dbus name %s gone", bus_name);
+ connected = false;
+ face_detect_proxy = null;
+ }
+
+#if FACEDETECT_BUS_PRIVATE
+ private static bool on_new_connection(DBusServer server, DBusConnection connection) {
+ try {
+ face_detect_proxy = connection.get_proxy_sync(null, DBUS_PATH,
+ DBusProxyFlags.DO_NOT_LOAD_PROPERTIES
+ | DBusProxyFlags.DO_NOT_CONNECT_SIGNALS,
+ null);
+ Idle.add(() => {
+ try {
+ face_detect_proxy.load_net(net_file);
+ connected = true;
+ } catch (Error error) {
+ critical("Failed to call load_net: %s", error.message);
+ AppWindow.error_message(ERROR_MESSAGE);
+ }
+ return false;
+ });
+
+ return true;
+ } catch (Error error) {
+ critical("Failed to create face_detect_proxy for face detect: %s", error.message);
+ AppWindow.error_message(ERROR_MESSAGE);
+
+ return false;
+ }
+ }
+#endif
+
+ public static void init(string net_file) {
+ FaceDetect.net_file = net_file;
+#if FACEDETECT_BUS_PRIVATE
+ var address = "unix:tmpdir=%s".printf(Environment.get_tmp_dir());
+ var observer = new DBusAuthObserver();
+ observer.authorize_authenticated_peer.connect((stream, credentials) => {
+ debug("Observer trying to authorize for %s", credentials.to_string());
+ if (credentials == null)
+ return false;
+
+ try {
+ if (!credentials.is_same_user(new Credentials()))
+ return false;
+ return true;
+ } catch (Error error) {
+ return false;
+ }
+ });
+
+ try {
+ dbus_server = new GLib.DBusServer.sync(address, DBusServerFlags.NONE, DBus.generate_guid(), observer, null);
+ dbus_server.new_connection.connect(on_new_connection);
+ dbus_server.start();
+ process = new Subprocess(SubprocessFlags.NONE, AppDirs.get_facedetect_bin().get_path(),
+ "--address=" + dbus_server.get_client_address());
+
+ } catch (Error error) {
+ warning("Failed to create private DBus server: %s", error.message);
+ AppWindow.error_message(ERROR_MESSAGE);
+ }
+#else
+ Bus.watch_name(BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.NONE,
+ create_face_detect_proxy, interface_gone);
+#endif
+ }
+
+}
diff --git a/src/faces/FaceLocation.vala b/src/faces/FaceLocation.vala
index e143b2e..0f4e383 100644
--- a/src/faces/FaceLocation.vala
+++ b/src/faces/FaceLocation.vala
@@ -4,6 +4,11 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
+// Encapsulate geometry and pixels of a Face
+public struct FaceLocationData {
+ public string geometry;
+ public string vec;
+}
public class FaceLocation : Object {
private static Gee.Map<FaceID?, Gee.Map<PhotoID?, FaceLocation>> face_photos_map;
@@ -12,17 +17,17 @@ public class FaceLocation : Object {
private FaceLocationID face_location_id;
private FaceID face_id;
private PhotoID photo_id;
- private string geometry;
-
+ private FaceLocationData face_data;
+
private FaceLocation(FaceLocationID face_location_id, FaceID face_id, PhotoID photo_id,
- string geometry) {
+ FaceLocationData face_data) {
this.face_location_id = face_location_id;
this.face_id = face_id;
this.photo_id = photo_id;
- this.geometry = geometry;
+ this.face_data = face_data;
}
- public static FaceLocation create(FaceID face_id, PhotoID photo_id, string geometry) {
+ public static FaceLocation create(FaceID face_id, PhotoID photo_id, FaceLocationData face_data) {
FaceLocation face_location = null;
// Test if that FaceLocation already exists (that face in that photo) ...
@@ -33,12 +38,11 @@ public class FaceLocation : Object {
face_location = faces_map.get(face_id);
- if (face_location.get_serialized_geometry() != geometry) {
- face_location.set_serialized_geometry(geometry);
+ if (face_location.get_serialized_geometry() != face_data.geometry) {
+ face_location.set_face_data(face_data);
try {
- FaceLocationTable.get_instance().update_face_location_serialized_geometry(
- face_location);
+ FaceLocationTable.get_instance().update_face_location_face_data(face_location);
} catch (DatabaseError err) {
AppWindow.database_error(err);
}
@@ -51,7 +55,7 @@ public class FaceLocation : Object {
try {
face_location =
FaceLocation.add_from_row(
- FaceLocationTable.get_instance().add(face_id, photo_id, geometry));
+ FaceLocationTable.get_instance().add(face_id, photo_id, face_data.geometry, face_data.vec));
} catch (DatabaseError err) {
AppWindow.database_error(err);
}
@@ -84,7 +88,8 @@ public class FaceLocation : Object {
public static FaceLocation add_from_row(FaceLocationRow row) {
FaceLocation face_location =
- new FaceLocation(row.face_location_id, row.face_id, row.photo_id, row.geometry);
+ new FaceLocation(row.face_location_id, row.face_id, row.photo_id,
+ { row.geometry, row.vec });
Gee.Map<PhotoID?, FaceLocation> photos_map = face_photos_map.get(row.face_id);
if (photos_map == null) {photos_map = new Gee.HashMap<PhotoID?, FaceLocation>
@@ -196,10 +201,22 @@ public class FaceLocation : Object {
}
public string get_serialized_geometry() {
- return geometry;
+ return face_data.geometry;
+ }
+
+ public string get_serialized_vec() {
+ return face_data.vec;
+ }
+
+ public FaceLocationData get_face_data() {
+ return face_data;
+ }
+
+ public PhotoID get_photo_id() {
+ return photo_id;
}
- private void set_serialized_geometry(string geometry) {
- this.geometry = geometry;
+ private void set_face_data(FaceLocationData face_data) {
+ this.face_data = face_data;
}
}
diff --git a/src/faces/FacePage.vala b/src/faces/FacePage.vala
index f2512d5..1766b91 100644
--- a/src/faces/FacePage.vala
+++ b/src/faces/FacePage.vala
@@ -44,6 +44,7 @@ public class FacePage : CollectionPage {
{ "DeleteFace", on_delete_face },
{ "RenameFace", on_rename_face },
{ "RemoveFaceFromPhotos", on_remove_face_from_photos },
+ { "SetFaceRefFromPhoto", on_set_face_ref },
{ "DeleteFaceSidebar", on_delete_face },
{ "RenameFaceSidebar", on_rename_face }
};
@@ -74,6 +75,7 @@ public class FacePage : CollectionPage {
menuFaces.add_menu_item(Resources.remove_face_from_photos_menu(this.face.get_name(), get_view().get_count()), "RemoveFaceFromPhotos", "<Primary>r");
menuFaces.add_menu_item(Resources.rename_face_menu(this.face.get_name()), "RenameFace", "<Primary>e");
+ menuFaces.add_menu_item(Resources.set_face_from_photo_menu(this.face.get_name()), "SetFaceRefFromPhoto", null);
menuFaces.add_menu_item(Resources.delete_face_menu(this.face.get_name()), "DeleteFace", "<Primary>t");
return menuFaces;
@@ -102,6 +104,11 @@ public class FacePage : CollectionPage {
null,
selected_count > 0);
+ set_action_details("SetFaceRefFromPhoto",
+ Resources.set_face_from_photo_menu(face.get_name()),
+ null,
+ selected_count == 1);
+
base.update_actions(selected_count, count);
}
@@ -120,4 +127,11 @@ public class FacePage : CollectionPage {
(Gee.Collection<MediaSource>) get_view().get_selected_sources()));
}
}
+
+ private void on_set_face_ref() {
+ if (get_view().get_selected_count() == 1) {
+ get_command_manager().execute(new SetFaceRefCommand(face,
+ (MediaSource) get_view().get_selected_at(0).get_source()));
+ }
+ }
}
diff --git a/src/faces/FaceShape.vala b/src/faces/FaceShape.vala
index 1ff01fd..f90f254 100644
--- a/src/faces/FaceShape.vala
+++ b/src/faces/FaceShape.vala
@@ -18,14 +18,16 @@ public abstract class FaceShape : Object {
protected Gdk.CursorType current_cursor_type = Gdk.CursorType.BOTTOM_RIGHT_CORNER;
protected EditingTools.PhotoCanvas canvas;
protected string serialized = null;
+ protected double[] face_vec;
private bool editable = true;
private bool visible = true;
private bool known = true;
+ private double guess = 0.0;
private weak FacesTool.FaceWidget face_widget = null;
- protected FaceShape(EditingTools.PhotoCanvas canvas) {
+ protected FaceShape(EditingTools.PhotoCanvas canvas, double[] vec) {
this.canvas = canvas;
this.canvas.new_surface.connect(prepare_ctx);
@@ -37,19 +39,21 @@ public abstract class FaceShape : Object {
face_window.show_all();
face_window.hide();
- this.canvas.get_drawing_window().set_cursor(new Gdk.Cursor(current_cursor_type));
+ this.face_vec = vec;
+ this.canvas.set_cursor(current_cursor_type);
}
~FaceShape() {
- if (visible)
+ if (visible) {
erase();
+ }
face_window.destroy();
canvas.new_surface.disconnect(prepare_ctx);
// make sure the cursor isn't set to a modify indicator
- canvas.get_drawing_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR));
+ canvas.set_cursor(Gdk.CursorType.LEFT_PTR);
}
public static FaceShape from_serialized(EditingTools.PhotoCanvas canvas, string serialized)
@@ -88,7 +92,15 @@ public abstract class FaceShape : Object {
public bool get_known() {
return known;
}
+
+ public void set_guess(double guess) {
+ this.guess = guess;
+ }
+ public double get_guess() {
+ return guess;
+ }
+
public void set_widget(FacesTool.FaceWidget face_widget) {
this.face_widget = face_widget;
}
@@ -107,7 +119,7 @@ public abstract class FaceShape : Object {
face_window.hide();
// make sure the cursor isn't set to a modify indicator
- canvas.get_drawing_window().set_cursor(new Gdk.Cursor(Gdk.CursorType.LEFT_PTR));
+ canvas.set_cursor(Gdk.CursorType.LEFT_PTR);
}
public void show() {
@@ -160,7 +172,7 @@ public abstract class FaceShape : Object {
return true;
}
- public abstract string serialize();
+ public abstract string serialize(bool geometry_only = false);
public abstract void update_face_window_position();
public abstract void prepare_ctx(Cairo.Context ctx, Dimensions dim);
public abstract void on_resized_pixbuf(Dimensions old_dim, Gdk.Pixbuf scaled);
@@ -170,6 +182,7 @@ public abstract class FaceShape : Object {
public abstract bool cursor_is_over(int x, int y);
public abstract bool equals(FaceShape face_shape);
public abstract double get_distance(int x, int y);
+ public abstract double[] get_face_vec();
protected abstract void paint();
protected abstract void erase();
@@ -186,13 +199,17 @@ public class FaceRectangle : FaceShape {
private BoxLocation in_manipulation = BoxLocation.OUTSIDE;
private Cairo.Context wide_black_ctx = null;
private Cairo.Context wide_white_ctx = null;
- private Cairo.Context thin_white_ctx = null;
private int last_grab_x = -1;
private int last_grab_y = -1;
public FaceRectangle(EditingTools.PhotoCanvas canvas, int x, int y,
- int half_width = NULL_SIZE, int half_height = NULL_SIZE) {
- base(canvas);
+ int half_width = NULL_SIZE, int half_height = NULL_SIZE, double[] vec = {}) {
+ double[] int_vec;
+ if (vec.length == 0)
+ int_vec = create_empty_vec();
+ else
+ int_vec = vec;
+ base(canvas, int_vec);
Gdk.Rectangle scaled_pixbuf_pos = canvas.get_scaled_pixbuf_position();
x -= scaled_pixbuf_pos.x;
@@ -219,6 +236,14 @@ public class FaceRectangle : FaceShape {
if (!is_editable())
erase_label();
}
+
+ public static double[] create_empty_vec() {
+ double[] empty_vec = new double[128];
+ for (int i = 0; i < 128; i++) {
+ empty_vec[i] = 0;
+ }
+ return empty_vec;
+ }
public static new FaceRectangle from_serialized(EditingTools.PhotoCanvas canvas, string[] args)
throws FaceShapeError {
@@ -226,7 +251,9 @@ public class FaceRectangle : FaceShape {
Photo photo = canvas.get_photo();
Dimensions raw_dim = photo.get_raw_dimensions();
-
+
+ // 1, 2 is the center of the rectangle, 3, 4 is the half width / height of the rectangle,
+ // normalized
int x = (int) (raw_dim.width * double.parse(args[1]));
int y = (int) (raw_dim.height * double.parse(args[2]));
int half_width = (int) (raw_dim.width * double.parse(args[3]));
@@ -265,9 +292,21 @@ public class FaceRectangle : FaceShape {
if (half_width < FACE_MIN_SIZE || half_height < FACE_MIN_SIZE)
throw new FaceShapeError.CANT_CREATE("FaceShape is out of cropped photo area");
-
+
+ string[] vec_str;
+ if (args.length == 6)
+ vec_str = args[5].split(",");
+ else
+ vec_str = {};
+ double[] vec = new double[128];
+ for (int i = 0; i < 128; i++) {
+ if (vec_str.length > i)
+ vec[i] = double.parse(vec_str[i]);
+ else
+ vec[i] = 0;
+ }
return new FaceRectangle(canvas, box.left + half_width, box.top + half_height,
- half_width, half_height);
+ half_width, half_height, vec);
}
public override void update_face_window_position() {
@@ -283,32 +322,35 @@ public class FaceRectangle : FaceShape {
face_window.get_allocation(out face_window_alloc);
- x += scaled_pixbuf_pos.x + box.left + ((box.get_width() - face_window_alloc.width) >> 1);
- y += scaled_pixbuf_pos.y + box.bottom + FACE_WINDOW_MARGIN;
+ var scale = Application.get_scale();
+ var left = (int)Math.lround((scaled_pixbuf_pos.x + box.left) / scale);
+ var width = (int)Math.lround(box.get_width() / scale);
+ var top = (int)Math.lround((scaled_pixbuf_pos.y + box.bottom) / scale);
+ x += (left + ((width - face_window_alloc.width) >> 1));
+ y += top + FACE_WINDOW_MARGIN;
face_window.move(x, y);
}
protected override void paint() {
+ // The box is in image coordinates. Need to scale down to device coordinates
canvas.draw_box(wide_black_ctx, box);
canvas.draw_box(wide_white_ctx, box.get_reduced(1));
canvas.draw_box(wide_white_ctx, box.get_reduced(2));
- canvas.invalidate_area(box);
+ //canvas.invalidate_area(box);
if (!is_editable())
paint_label();
}
protected override void erase() {
- canvas.erase_box(box);
- canvas.erase_box(box.get_reduced(1));
- canvas.erase_box(box.get_reduced(2));
-
canvas.invalidate_area(box);
if (!is_editable())
erase_label();
+
+// canvas.repaint();
}
private void paint_label() {
@@ -317,6 +359,9 @@ public class FaceRectangle : FaceShape {
ctx.save();
+ ctx.select_font_face("Sans", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ ctx.set_font_size(10.0 * Application.get_scale());
+
Cairo.TextExtents text_extents = Cairo.TextExtents();
ctx.text_extents(get_name(), out text_extents);
@@ -368,7 +413,7 @@ public class FaceRectangle : FaceShape {
ctx.restore();
}
- public override string serialize() {
+ public override string serialize(bool geometry_only = false) {
if (serialized != null)
return serialized;
@@ -378,10 +423,15 @@ public class FaceRectangle : FaceShape {
double half_height;
get_geometry(out x, out y, out half_width, out half_height);
-
- serialized = "%s;%s;%s;%s;%s".printf(SHAPE_TYPE, x.to_string(),
+ serialized = "%s;%s;%s;%s;%s;".printf(SHAPE_TYPE, x.to_string(),
y.to_string(), half_width.to_string(), half_height.to_string());
-
+ if (!geometry_only) {
+ string face_vec_str = "";
+ foreach (var d in face_vec[0:-2])
+ face_vec_str += d.to_string() + ",";
+ face_vec_str += face_vec[-1].to_string();
+ serialized += face_vec_str;
+ }
return serialized;
}
@@ -425,23 +475,23 @@ public class FaceRectangle : FaceShape {
half_width = (width_right_end - width_left_end) / 2;
half_height = (height_bottom_end - height_top_end) / 2;
}
+
+ public override double[] get_face_vec() {
+ return face_vec;
+ }
public override bool equals(FaceShape face_shape) {
- return serialize() == face_shape.serialize();
+ return serialize(true) == face_shape.serialize(true);
}
public override void prepare_ctx(Cairo.Context ctx, Dimensions dim) {
wide_black_ctx = new Cairo.Context(ctx.get_target());
set_source_color_from_string(wide_black_ctx, "#000");
- wide_black_ctx.set_line_width(1);
+ wide_black_ctx.set_line_width(1 * Application.get_scale());
wide_white_ctx = new Cairo.Context(ctx.get_target());
- set_source_color_from_string(wide_black_ctx, "#FFF");
- wide_white_ctx.set_line_width(1);
-
- thin_white_ctx = new Cairo.Context(ctx.get_target());
- set_source_color_from_string(wide_black_ctx, "#FFF");
- thin_white_ctx.set_line_width(0.5);
+ set_source_color_from_string(wide_white_ctx, "#FFF");
+ wide_white_ctx.set_line_width(1 * Application.get_scale());
}
private bool on_canvas_manipulation(int x, int y) {
@@ -620,17 +670,20 @@ public class FaceRectangle : FaceShape {
Box new_box = Box(left, top, right, bottom);
if (!box.equals(new_box)) {
- erase();
+ canvas.invalidate_area(box);
if (in_manipulation != BoxLocation.INSIDE)
check_resized_box(new_box);
box = new_box;
paint();
+ canvas.invalidate_area(new_box);
}
if (is_editable())
update_face_window_position();
+
+ canvas.repaint();
serialized = null;
@@ -698,8 +751,7 @@ public class FaceRectangle : FaceShape {
}
if (cursor_type != current_cursor_type) {
- Gdk.Cursor cursor = new Gdk.Cursor(cursor_type);
- canvas.get_drawing_window().set_cursor(cursor);
+ canvas.set_cursor(cursor_type);
current_cursor_type = cursor_type;
}
}
diff --git a/src/faces/FacesTool.vala b/src/faces/FacesTool.vala
index 9803787..d399b38 100644
--- a/src/faces/FacesTool.vala
+++ b/src/faces/FacesTool.vala
@@ -119,7 +119,7 @@ public class FacesTool : EditingTools.EditingTool {
private EditingPhase editing_phase = EditingPhase.NOT_EDITING;
private Gtk.Box help_layout = null;
private Gtk.Box response_layout = null;
- private Gtk.HSeparator buttons_text_separator = null;
+ private Gtk.Separator buttons_text_separator = null;
private Gtk.Label help_text = null;
private Gtk.Box face_widgets_layout = null;
private Gtk.Box layout = null;
@@ -163,7 +163,7 @@ public class FacesTool : EditingTools.EditingTool {
layout = new Gtk.Box(Gtk.Orientation.VERTICAL, CONTROL_SPACING);
layout.pack_start(face_widgets_layout, false);
layout.pack_start(help_layout, false);
- layout.pack_start(new Gtk.HSeparator(), false);
+ layout.pack_start(new Gtk.Separator(Gtk.Orientation.HORIZONTAL), false);
layout.pack_start(response_layout, false);
add(layout);
@@ -178,7 +178,7 @@ public class FacesTool : EditingTools.EditingTool {
case EditingPhase.CLICK_TO_EDIT:
assert(face_shape != null);
- help_text.set_markup(Markup.printf_escaped(_("Click to edit face <i>%s</i>"),
+ help_text.set_markup(Markup.printf_escaped(_("Click to edit face “%s”"),
face_shape.get_name()));
break;
@@ -254,7 +254,7 @@ public class FacesTool : EditingTools.EditingTool {
face_widgets_layout.pack_start(event_box, false);
if (buttons_text_separator == null) {
- buttons_text_separator = new Gtk.HSeparator();
+ buttons_text_separator = new Gtk.Separator(Gtk.Orientation.HORIZONTAL);
face_widgets_layout.pack_end(buttons_text_separator, false);
}
@@ -315,121 +315,49 @@ public class FacesTool : EditingTools.EditingTool {
private class FaceDetectionJob : BackgroundJob {
private Gee.Queue<string> faces = null;
private string image_path;
- private string output;
- public SpawnError? spawnError;
+ private float scale;
+ public string? spawnError;
- public FaceDetectionJob(FacesToolWindow owner, string image_path,
+ public FaceDetectionJob(FacesToolWindow owner, string image_path, float scale,
CompletionCallback completion_callback, Cancellable cancellable,
CancellationCallback cancellation_callback) {
base(owner, completion_callback, cancellable, cancellation_callback);
this.image_path = image_path;
+ this.scale = scale;
}
public override void execute() {
+ if (!FaceDetect.connected) {
+ spawnError = "Face detect process not connected!\n";
+ return;
+ }
+ FaceRect[] rects;
try {
- string[] argv = {
- AppDirs.get_facedetect_bin().get_path(),
- "--cascade=" + AppDirs.get_haarcascade_file().get_path(),
- "--scale=1.2",
- image_path
- };
- Process.spawn_sync(null, argv, null, SpawnFlags.STDERR_TO_DEV_NULL, null, out output);
-
- } catch (SpawnError e) {
- spawnError = e;
- critical(e.message);
-
+ rects = FaceDetect.face_detect_proxy.detect_faces(image_path,
+ AppDirs.get_haarcascade_file().get_path(), scale, true);
+ } catch(Error e) {
+ spawnError = "DBus error: " + e.message + "!\n";
return;
}
-
faces = new Gee.PriorityQueue<string>();
- string[] lines = output.split("\n");
- foreach (string line in lines) {
- if (line.length == 0)
- continue;
-
- debug("shotwell-facedetect: %s", line);
-
- string[] type_and_serialized = line.split(";");
- if (type_and_serialized.length != 2) {
- // Pass on external helper log output as our debug log
- continue;
- }
-
- switch (type_and_serialized[0]) {
- case "face":
- StringBuilder serialized_geometry = new StringBuilder();
- serialized_geometry.append(FaceRectangle.SHAPE_TYPE);
- serialized_geometry.append(";");
- serialized_geometry.append(parse_serialized_geometry(type_and_serialized[1]));
-
- faces.add(serialized_geometry.str);
- break;
-
- case "warning":
- warning("%s\n", type_and_serialized[1]);
- break;
-
- case "error":
- critical("%s\n", type_and_serialized[1]);
- assert_not_reached();
-
- default:
- break;
+ for (int i = 0; i < rects.length; i++) {
+ double rect_x, rect_y, rect_w, rect_h;
+ string face_vec_str = "";
+ rect_w = rects[i].width / 2;
+ rect_h = rects[i].height / 2;
+ rect_x = rects[i].x + rect_w;
+ rect_y = rects[i].y + rect_h;
+ if (rects[i].vec != null) {
+ foreach (var d in rects[i].vec) { face_vec_str += d.to_string() + ","; }
}
+ string serialized = "%s;%f;%f;%f;%f;%s".printf(FaceRectangle.SHAPE_TYPE,
+ rect_x, rect_y, rect_w, rect_h,
+ face_vec_str);
+ faces.add(serialized);
}
}
- private string parse_serialized_geometry(string serialized_geometry) {
- string[] serialized_geometry_pieces = serialized_geometry.split("&");
- if (serialized_geometry_pieces.length != 4) {
- critical("Wrong serialized line in face detection program output.");
- assert_not_reached();
- }
-
- double x = 0;
- double y = 0;
- double width = 0;
- double height = 0;
- foreach (string piece in serialized_geometry_pieces) {
-
- string[] name_and_value = piece.split("=");
- if (name_and_value.length != 2) {
- critical("Wrong serialized line in face detection program output.");
- assert_not_reached();
- }
-
- switch (name_and_value[0]) {
- case "x":
- x = name_and_value[1].to_double();
- break;
-
- case "y":
- y = name_and_value[1].to_double();
- break;
-
- case "width":
- width = name_and_value[1].to_double();
- break;
-
- case "height":
- height = name_and_value[1].to_double();
- break;
-
- default:
- critical("Wrong serialized line in face detection program output.");
- assert_not_reached();
- }
- }
-
- double half_width = width / 2;
- double half_height = height / 2;
-
- return "%s;%s;%s;%s".printf((x + half_width).to_string(), (y + half_height).to_string(),
- half_width.to_string(), half_height.to_string());
- }
-
public string? get_next() {
if (faces == null)
return null;
@@ -450,6 +378,7 @@ public class FacesTool : EditingTools.EditingTool {
private Workers workers;
private FaceShape editing_face_shape = null;
private FacesToolWindow faces_tool_window = null;
+ private const int FACE_DETECT_MAX_WIDTH = 1200;
private FacesTool() {
base("FacesTool");
@@ -481,8 +410,10 @@ public class FacesTool : EditingTools.EditingTool {
foreach (Gee.Map.Entry<FaceID?, FaceLocation> entry in face_locations.entries) {
FaceShape new_face_shape;
string serialized_geometry = entry.value.get_serialized_geometry();
+ string serialized_vec = entry.value.get_serialized_vec();
+ string face_shape_str = serialized_geometry + ";" + serialized_vec;
try {
- new_face_shape = FaceShape.from_serialized(canvas, serialized_geometry);
+ new_face_shape = FaceShape.from_serialized(canvas, face_shape_str);
} catch (FaceShapeError e) {
if (e is FaceShapeError.CANT_CREATE)
continue;
@@ -502,9 +433,12 @@ public class FacesTool : EditingTools.EditingTool {
face_detection_cancellable = new Cancellable();
workers = new Workers(1, false);
+ Dimensions dimensions = canvas.get_photo().get_dimensions();
+ float scale_factor = (float)dimensions.width / FACE_DETECT_MAX_WIDTH;
face_detection = new FaceDetectionJob(faces_tool_window,
- canvas.get_photo().get_file().get_path(), on_faces_detected,
- face_detection_cancellable, on_detection_cancelled);
+ canvas.get_photo().get_file().get_path(), scale_factor,
+ on_faces_detected,
+ face_detection_cancellable, on_detection_cancelled);
bind_window_handlers();
@@ -591,6 +525,10 @@ public class FacesTool : EditingTools.EditingTool {
}
public override void on_left_click(int x, int y) {
+ var scale = Application.get_scale();
+ x = (int) Math.lround(x * scale);
+ y = (int) Math.lround(y * scale);
+
if (editing_face_shape != null && editing_face_shape.on_left_click(x, y))
return;
@@ -607,6 +545,10 @@ public class FacesTool : EditingTools.EditingTool {
}
public override void on_left_released(int x, int y) {
+ var scale = Application.get_scale();
+ x = (int) Math.lround(x * scale);
+ y = (int) Math.lround(y * scale);
+
if (editing_face_shape != null) {
editing_face_shape.on_left_released(x, y);
@@ -616,6 +558,10 @@ public class FacesTool : EditingTools.EditingTool {
}
public override void on_motion(int x, int y, Gdk.ModifierType mask) {
+ var scale = Application.get_scale();
+ x = (int) Math.lround(x * scale);
+ y = (int) Math.lround(y * scale);
+
if (editing_face_shape == null) {
FaceShape to_show = null;
double distance = 0;
@@ -784,14 +730,21 @@ public class FacesTool : EditingTools.EditingTool {
if (face_shapes == null)
return;
- Gee.Map<Face, string> new_faces = new Gee.HashMap<Face, string>();
+ Gee.Map<Face, FaceLocationData?> new_faces = new Gee.HashMap<Face, FaceLocationData?>();
foreach (FaceShape face_shape in face_shapes.values) {
if (!face_shape.get_known())
continue;
Face new_face = Face.for_name(face_shape.get_name());
-
- new_faces.set(new_face, face_shape.serialize());
+ string[] face_string = face_shape.serialize().split(";");
+ string face_vec_str, face_geometry;
+ face_geometry = string.joinv(";", face_string[0:5]);
+ face_vec_str = face_string[5];
+ FaceLocationData face_data =
+ {
+ face_geometry, face_vec_str
+ };
+ new_faces.set(new_face, face_data);
}
ModifyFacesCommand command = new ModifyFacesCommand(canvas.get_photo(), new_faces);
@@ -848,7 +801,7 @@ public class FacesTool : EditingTools.EditingTool {
private void delete_face(string face_name) {
face_shapes.unset(face_name);
- // It is posible to have two visible faces at the same time, this happens
+ // It is possible to have two visible faces at the same time, this happens
// if you are editing one face and you move the pointer around the
// FaceWidgets area in FacesToolWindow. And you can delete one of that
// faces, so the other visible face must be repainted.
@@ -908,7 +861,6 @@ public class FacesTool : EditingTools.EditingTool {
private void detect_faces() {
faces_tool_window.detection_button.set_sensitive(false);
faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES);
-
workers.enqueue(face_detection);
}
@@ -945,19 +897,71 @@ public class FacesTool : EditingTools.EditingTool {
continue;
c++;
+ // Reference faces to match with
+ Face? guess = get_face_match(face_shape, 0.7);
- face_shape.set_name("Unknown face #%d".printf(c));
- face_shape.set_known(false);
+ if (guess == null) {
+ face_shape.set_name("Unknown face #%d".printf(c));
+ face_shape.set_known(false);
+ } else {
+ string name_str;
+ name_str = "%s (%0.2f%%)".printf(guess.get_name(), face_shape.get_guess() * 100);
+ face_shape.set_name(name_str);
+ face_shape.set_known(true);
+ }
add_face(face_shape);
}
}
+ private double dot_product(double[] vec1, double[] vec2) {
+ if (vec1.length != vec2.length) {
+ return 0;
+ }
+
+ double ret = 0;
+ for (var i = 0; i < vec1.length; i++) {
+ ret += vec1[i] * vec2[i];
+ }
+ return ret;
+ }
+
+ private Face? get_face_match(FaceShape face_shape, double threshold) {
+ Gee.List<FaceLocationRow?> face_vecs;
+ try {
+ Gee.List<FaceRow?> face_rows = FaceTable.get_instance().get_ref_rows();
+ face_vecs = FaceLocationTable.get_instance().get_face_ref_vecs(face_rows);
+ } catch(DatabaseError err) {
+ warning("Cannot get reference faces from DB");
+ return null;
+ }
+ FaceID? guess_id = null;
+ double max_product = threshold;
+ foreach (var row in face_vecs) {
+ string[] vec_str = row.vec.split(",");
+ double[] vec = {};
+ foreach (var d in vec_str) vec += double.parse(d);
+ double product = dot_product(face_shape.get_face_vec(), vec[0:128]);
+ if (product > max_product) {
+ max_product = product;
+ guess_id = row.face_id;
+ }
+ }
+
+ Face? face = null;
+ if (guess_id != null) {
+ face = Face.global.fetch(guess_id);
+ face_shape.set_guess(max_product);
+ assert(face != null);
+ }
+ return face;
+ }
+
private void on_faces_detected() {
face_detection_cancellable.reset();
if (face_detection.spawnError != null){
string spawnErrorMessage = _("Error trying to spawn face detection program:\n");
- AppWindow.error_message(spawnErrorMessage + face_detection.spawnError.message + "\n");
+ AppWindow.error_message(spawnErrorMessage + face_detection.spawnError + "\n");
faces_tool_window.set_editing_phase(EditingPhase.DETECTING_FACES_FINISHED);
} else
pick_faces_from_autodetected();
diff --git a/src/import-roll/ImportRollBranch.vala b/src/import-roll/ImportRollBranch.vala
index 32337cc..0c582ac 100644
--- a/src/import-roll/ImportRollBranch.vala
+++ b/src/import-roll/ImportRollBranch.vala
@@ -6,8 +6,7 @@ public class ImportRoll.Branch : Sidebar.Branch {
Sidebar.Branch.Options.HIDE_IF_EMPTY,
ImportRoll.Branch.comparator);
- this.entries = new Gee.HashMap<int64?, ImportRoll.SidebarEntry>((Gee.HashDataFunc<int64?>)GLib.int64_hash,
- (Gee.EqualDataFunc<int64?>)GLib.int64_equal);
+ this.entries = new Gee.HashMap<int64?, ImportRoll.SidebarEntry>(int64_hash, int64_equal);
foreach (var source in MediaCollectionRegistry.get_instance().get_all()) {
on_import_rolls_altered(source);
diff --git a/src/library/BackgroundProgressBar.vala b/src/library/BackgroundProgressBar.vala
new file mode 100644
index 0000000..8ad7185
--- /dev/null
+++ b/src/library/BackgroundProgressBar.vala
@@ -0,0 +1,109 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+internal class BackgroundProgressBar : Gtk.ProgressBar {
+ public enum Priority {
+ NONE = 0,
+ STARTUP_SCAN = 35,
+ REALTIME_UPDATE = 40,
+ REALTIME_IMPORT = 50,
+ METADATA_WRITER = 30
+ }
+
+ public bool should_be_visible { get; private set; default = false; }
+
+#if UNITY_SUPPORT
+ // UnityProgressBar: init
+ private UnityProgressBar uniprobar = UnityProgressBar.get_instance();
+#endif
+
+ private const int PULSE_MSEC = 250;
+
+ public BackgroundProgressBar() {
+ Object(show_text: true);
+ }
+
+ private Priority current_priority = Priority.NONE;
+ private uint pulse_id = 0;
+
+ public void start(string label, Priority priority) {
+ if (priority < current_priority)
+ return;
+
+ stop(priority, false);
+
+ current_priority = priority;
+ set_text(label);
+ pulse();
+ should_be_visible = true;
+ pulse_id = Timeout.add(PULSE_MSEC, on_pulse_timeout);
+ }
+
+ public void stop(Priority priority, bool clear) {
+ if (priority < current_priority)
+ return;
+
+ if (pulse_id != 0) {
+ Source.remove(pulse_id);
+ pulse_id = 0;
+ }
+
+ if (clear)
+ this.clear(priority);
+ }
+
+ public bool update(string label, Priority priority, double count, double total) {
+ if (priority < current_priority)
+ return false;
+
+ stop(priority, false);
+
+ if (count <= 0.0 || total <= 0.0 || count >= total) {
+ clear(priority);
+
+ return false;
+ }
+
+ current_priority = priority;
+
+ double fraction = count / total;
+ set_fraction(fraction);
+ set_text(_("%s (%d%%)").printf(label, (int) (fraction * 100.0)));
+ should_be_visible = true;
+
+#if UNITY_SUPPORT
+ // UnityProgressBar: try to draw & set progress
+ uniprobar.set_visible(true);
+ uniprobar.set_progress(fraction);
+#endif
+
+ return true;
+ }
+
+ public void clear(Priority priority) {
+ if (priority < current_priority)
+ return;
+
+ stop(priority, false);
+
+ current_priority = 0;
+
+ set_fraction(0.0);
+ set_text("");
+ should_be_visible = false;
+
+#if UNITY_SUPPORT
+ // UnityProgressBar: reset
+ uniprobar.reset();
+#endif
+ }
+
+ private bool on_pulse_timeout() {
+ pulse();
+
+ return true;
+ }
+}
diff --git a/src/library/LibraryWindow.vala b/src/library/LibraryWindow.vala
index 53b3a7b..849ae2e 100644
--- a/src/library/LibraryWindow.vala
+++ b/src/library/LibraryWindow.vala
@@ -24,18 +24,10 @@ public class LibraryWindow : AppWindow {
"mtp:"
};
- private const int BACKGROUND_PROGRESS_PULSE_MSEC = 250;
// If we're not operating on at least this many files, don't display the progress
// bar at all; otherwise, it'll go by too quickly, giving the appearance of a glitch.
- const int MIN_PROGRESS_BAR_FILES = 20;
-
- // these values reflect the priority various background operations have when reporting
- // progress to the LibraryWindow progress bar ... higher values give priority to those reports
- private const int STARTUP_SCAN_PROGRESS_PRIORITY = 35;
- private const int REALTIME_UPDATE_PROGRESS_PRIORITY = 40;
- private const int REALTIME_IMPORT_PROGRESS_PRIORITY = 50;
- private const int METADATA_WRITER_PROGRESS_PRIORITY = 30;
+ const int MIN_PROGRESS_BAR_FILES = 1;
// This lists the order of the toplevel items in the sidebar. New toplevel items should be
// added here in the position they should appear in the sidebar. To re-order, simply move
@@ -136,11 +128,12 @@ public class LibraryWindow : AppWindow {
private SearchFilterToolbar search_toolbar;
private Gtk.Box top_section = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
- private Gtk.Frame background_progress_frame = new Gtk.Frame(null);
- private Gtk.ProgressBar background_progress_bar = new Gtk.ProgressBar();
- private bool background_progress_displayed = false;
+ private Gtk.Revealer background_progress_frame = new Gtk.Revealer();
+ private BackgroundProgressBar background_progress_bar = new BackgroundProgressBar();
- private BasicProperties basic_properties = new BasicProperties();
+ // Instantiate later in constructor because the map support loads its icons in there and we need
+ // to have the global app instance available for that
+ private BasicProperties basic_properties;
private ExtendedProperties extended_properties = new ExtendedProperties();
private Gtk.Revealer extended_properties_revealer = new Gtk.Revealer();
@@ -149,14 +142,6 @@ public class LibraryWindow : AppWindow {
private Gtk.Box right_vbox;
private Gtk.Revealer toolbar_revealer = new Gtk.Revealer ();
- private int current_progress_priority = 0;
- private uint background_progress_pulse_id = 0;
-
-#if UNITY_SUPPORT
- //UnityProgressBar: init
- UnityProgressBar uniprobar = UnityProgressBar.get_instance();
-#endif
-
public LibraryWindow(ProgressMonitor progress_monitor) {
base();
@@ -180,11 +165,12 @@ public class LibraryWindow : AppWindow {
properties_scheduler = new OneShotScheduler("LibraryWindow properties",
on_update_properties_now);
-
+
// setup search bar and add its accelerators to the window
search_toolbar = new SearchFilterToolbar(search_actions);
// create the main layout & start at the Library page
+ basic_properties = new BasicProperties();
create_layout(library_branch.photos_entry.get_page());
// settings that should persist between sessions
@@ -217,8 +203,6 @@ public class LibraryWindow : AppWindow {
CameraTable.get_instance().camera_added.connect(on_camera_added);
- background_progress_bar.set_show_text(true);
-
// Need to re-install F8 here as it will overwrite the binding created
// by the menu
const string[] accels = { "<Primary>f", "F8", null };
@@ -282,7 +266,7 @@ public class LibraryWindow : AppWindow {
{ "CommonFind", on_find },
{ "CommonNewSearch", on_new_search },
- // Toogle actions
+ // Toggle actions
{ "CommonDisplayBasicProperties", on_action_toggle, null, "false", on_display_basic_properties },
{ "CommonDisplayExtendedProperties", on_action_toggle, null, "false", on_display_extended_properties },
@@ -770,7 +754,7 @@ public class LibraryWindow : AppWindow {
if (AppDirs.get_import_dir().get_path() == Environment.get_home_dir() && notify_library_is_home_dir) {
Gtk.ResponseType response = AppWindow.affirm_cancel_question(
_("Shotwell is configured to import photos to your home directory.\n" +
- "We recommend changing this in <span weight=\"bold\">Edit %s Preferences</span>.\n" +
+ "We recommend changing this in Edit %s Preferences.\n" +
"Do you want to continue importing photos?").printf("▸"),
_("_Import"), _("Library Location"), AppWindow.get_instance());
@@ -949,15 +933,9 @@ public class LibraryWindow : AppWindow {
return;
ImportPage page = (ImportPage) entry.get_page();
- File uri_file = File.new_for_uri(camera.uri);
// find the VFS mount point
- Mount mount = null;
- try {
- mount = uri_file.find_enclosing_mount(null);
- } catch (Error err) {
- // error means not mounted
- }
+ var mount = camera.get_mount();
// don't unmount mass storage cameras, as they are then unavailable to gPhoto
if (mount != null && !camera.uri.has_prefix("file://")) {
@@ -1012,135 +990,40 @@ public class LibraryWindow : AppWindow {
sort_events_action.change_state (event_sort_val);
}
-
- private void start_pulse_background_progress_bar(string label, int priority) {
- if (priority < current_progress_priority)
- return;
-
- stop_pulse_background_progress_bar(priority, false);
-
- current_progress_priority = priority;
-
- background_progress_bar.set_text(label);
- background_progress_bar.pulse();
- show_background_progress_bar();
-
- background_progress_pulse_id = Timeout.add(BACKGROUND_PROGRESS_PULSE_MSEC,
- on_pulse_background_progress_bar);
- }
-
- private bool on_pulse_background_progress_bar() {
- background_progress_bar.pulse();
-
- return true;
- }
-
- private void stop_pulse_background_progress_bar(int priority, bool clear) {
- if (priority < current_progress_priority)
- return;
-
- if (background_progress_pulse_id != 0) {
- Source.remove(background_progress_pulse_id);
- background_progress_pulse_id = 0;
- }
-
- if (clear)
- clear_background_progress_bar(priority);
- }
-
- private void update_background_progress_bar(string label, int priority, double count,
- double total) {
- if (priority < current_progress_priority)
- return;
-
- stop_pulse_background_progress_bar(priority, false);
-
- if (count <= 0.0 || total <= 0.0 || count >= total) {
- clear_background_progress_bar(priority);
-
- return;
- }
-
- current_progress_priority = priority;
-
- double fraction = count / total;
- background_progress_bar.set_fraction(fraction);
- background_progress_bar.set_text(_("%s (%d%%)").printf(label, (int) (fraction * 100.0)));
- show_background_progress_bar();
-
-#if UNITY_SUPPORT
- //UnityProgressBar: try to draw & set progress
- uniprobar.set_visible(true);
- uniprobar.set_progress(fraction);
-#endif
- }
-
- private void clear_background_progress_bar(int priority) {
- if (priority < current_progress_priority)
- return;
-
- stop_pulse_background_progress_bar(priority, false);
-
- current_progress_priority = 0;
-
- background_progress_bar.set_fraction(0.0);
- background_progress_bar.set_text("");
- hide_background_progress_bar();
-
-#if UNITY_SUPPORT
- //UnityProgressBar: reset
- uniprobar.reset();
-#endif
- }
-
- private void show_background_progress_bar() {
- if (!background_progress_displayed) {
- top_section.pack_end(background_progress_frame, false, false, 0);
- background_progress_frame.show_all();
- background_progress_displayed = true;
- }
- }
-
- private void hide_background_progress_bar() {
- if (background_progress_displayed) {
- top_section.remove(background_progress_frame);
- background_progress_displayed = false;
- }
- }
-
+
private void on_library_monitor_discovery_started() {
- start_pulse_background_progress_bar(_("Updating library…"), STARTUP_SCAN_PROGRESS_PRIORITY);
+ background_progress_bar.start(_("Updating library…"), BackgroundProgressBar.Priority.STARTUP_SCAN);
}
private void on_library_monitor_discovery_completed() {
- stop_pulse_background_progress_bar(STARTUP_SCAN_PROGRESS_PRIORITY, true);
+ background_progress_bar.stop(BackgroundProgressBar.Priority.STARTUP_SCAN, true);
}
private void on_library_monitor_auto_update_progress(int completed_files, int total_files) {
if (total_files < MIN_PROGRESS_BAR_FILES)
- clear_background_progress_bar(REALTIME_UPDATE_PROGRESS_PRIORITY);
+ background_progress_bar.clear(BackgroundProgressBar.Priority.REALTIME_UPDATE);
else {
- update_background_progress_bar(_("Updating library…"), REALTIME_UPDATE_PROGRESS_PRIORITY,
+ background_progress_bar.update(_("Updating library…"), BackgroundProgressBar.Priority.REALTIME_UPDATE,
completed_files, total_files);
}
}
private void on_library_monitor_auto_import_preparing() {
- start_pulse_background_progress_bar(_("Preparing to auto-import photos…"),
- REALTIME_IMPORT_PROGRESS_PRIORITY);
+ background_progress_bar.start(_("Preparing to auto-import photos…"),
+ BackgroundProgressBar.Priority.REALTIME_UPDATE);
}
private void on_library_monitor_auto_import_progress(uint64 completed_bytes, uint64 total_bytes) {
- update_background_progress_bar(_("Auto-importing photos…"),
- REALTIME_IMPORT_PROGRESS_PRIORITY, completed_bytes, total_bytes);
+ background_progress_bar.update(_("Auto-importing photos…"),
+ BackgroundProgressBar.Priority.REALTIME_UPDATE, completed_bytes, total_bytes);
}
private void on_metadata_writer_progress(uint completed, uint total) {
if (total < MIN_PROGRESS_BAR_FILES)
- clear_background_progress_bar(METADATA_WRITER_PROGRESS_PRIORITY);
+ background_progress_bar.clear(BackgroundProgressBar.Priority.METADATA_WRITER);
else {
- update_background_progress_bar(_("Writing metadata to files…"),
- METADATA_WRITER_PROGRESS_PRIORITY, completed, total);
+ background_progress_bar.update(_("Writing metadata to files…"),
+ BackgroundProgressBar.Priority.METADATA_WRITER, completed, total);
}
}
@@ -1153,17 +1036,22 @@ public class LibraryWindow : AppWindow {
background_progress_frame.set_border_width(2);
background_progress_frame.add(background_progress_bar);
- background_progress_frame.get_style_context().remove_class("frame");
+ background_progress_frame.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP);
+ background_progress_frame.halign = Gtk.Align.FILL;
+ background_progress_frame.valign = Gtk.Align.END;
+ background_progress_frame.vexpand = false;
+ background_progress_frame.hexpand = true;
+ background_progress_bar.bind_property("should-be-visible", background_progress_frame, "reveal-child", GLib.BindingFlags.DEFAULT);
// pad the bottom frame (properties)
basic_properties.halign = Gtk.Align.FILL;
basic_properties.valign = Gtk.Align.CENTER;
basic_properties.hexpand = true;
- basic_properties.vexpand = false;
+ basic_properties.vexpand = true;
basic_properties.margin_top = 10;
basic_properties.margin_bottom = 10;
basic_properties.margin_start = 6;
- basic_properties.margin_end = 0;
+ basic_properties.margin_end = 6;
bottom_frame.add(basic_properties);
bottom_frame.get_style_context().remove_class("frame");
@@ -1171,6 +1059,7 @@ public class LibraryWindow : AppWindow {
// "attach" the progress bar to the sidebar tree, so the movable ridge is to resize the
// top two and the basic information pane
top_section.pack_start(scrolled_sidebar, true, true, 0);
+ top_section.pack_end(background_progress_frame, false, false, 0);
sidebar_paned.pack1(top_section, true, false);
sidebar_paned.pack2(bottom_frame, false, false);
diff --git a/src/library/TrashPage.vala b/src/library/TrashPage.vala
index 35dee4e..1e72f07 100644
--- a/src/library/TrashPage.vala
+++ b/src/library/TrashPage.vala
@@ -105,6 +105,10 @@ public class TrashPage : CheckerboardPage {
protected override string get_view_empty_message() {
return _("Trash is empty");
}
+
+ protected override string get_view_empty_icon() {
+ return "user-trash-symbolic";
+ }
private void on_delete() {
remove_from_app((Gee.Collection<MediaSource>) get_view().get_selected_sources(), _("Delete"),
diff --git a/src/libshotwell.deps b/src/libshotwell.deps
deleted file mode 100644
index 62f5abe..0000000
--- a/src/libshotwell.deps
+++ /dev/null
@@ -1,20 +0,0 @@
-atk
-gdk-3.0
-gee-0.8
-gexiv2
-gio-unix-2.0
-glib-2.0
-gmodule-2.0
-gnome-vfs-2.0
-gstreamer-0.10
-gstreamer-base-0.10
-gtk+-3.0
-gudev-1.0
-libexif
-libraw
-libgphoto2
-libsoup-2.4
-libxml-2.0
-sqlite3
-unique-1.0
-webkit-1.0
diff --git a/src/main.vala b/src/main.vala
index d0cb246..cdc9b27 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -72,6 +72,10 @@ void library_exec(string[] mounts) {
result.to_string());
break;
}
+
+ // Need to set this before anything else, but _after_ setting the profile
+ var use_dark = Config.Facade.get_instance().get_gtk_theme_variant();
+ Gtk.Settings.get_default().gtk_application_prefer_dark_theme = use_dark;
if (errormsg != null) {
Gtk.MessageDialog dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL,
@@ -107,7 +111,7 @@ void library_exec(string[] mounts) {
progress_dialog.update_display_every(100);
progress_dialog.set_minimum_on_screen_time_msec(250);
try {
- progress_dialog.icon = new Gdk.Pixbuf.from_resource("/org/gnome/Shotwell/icons/shotwell.svg");
+ progress_dialog.icon = new Gdk.Pixbuf.from_resource("/org/gnome/Shotwell/icons/hicolor/scalable/org.gnome.Shotwell.svg");
} catch (Error err) {
debug("Warning - could not load application icon for loading window: %s", err.message);
}
@@ -253,13 +257,43 @@ private void report_system_pictures_import(ImportManifest manifest, BatchImportR
ImportUI.report_manifest(manifest, true);
}
+void dump_tags (GExiv2.Metadata metadata, string[] tags) throws Error {
+ foreach (string tag in tags) {
+ try {
+ print("%-64s%s\n",
+ tag,
+ metadata.try_get_tag_interpreted_string (tag));
+ } catch (Error err) {
+ print("Failed to get tag %s: %s\n", tag, err.message);
+ }
+ }
+}
+
+void dump_metadata (string filename) {
+ try {
+ var metadata = new GExiv2.Metadata();
+ var file = File.new_for_commandline_arg(filename);
+ metadata.from_stream (file.read());
+
+ dump_tags(metadata, metadata.get_exif_tags());
+ dump_tags(metadata, metadata.get_iptc_tags());
+ dump_tags(metadata, metadata.get_xmp_tags());
+ } catch (Error err) {
+ stderr.printf("Unable to dump metadata for %s: %s\n", filename, err.message);
+ }
+}
+
void editing_exec(string filename, bool fullscreen) {
File initial_file = File.new_for_commandline_arg(filename);
// preconfigure units
Direct.preconfigure(initial_file);
Db.preconfigure(null);
-
+
+ // Need to set this before anything else, but _after_ setting the profile
+ var use_dark = Config.Facade.get_instance().get_gtk_theme_variant();
+ Gtk.Settings.get_default().gtk_application_prefer_dark_theme = use_dark;
+
// initialize units for direct-edit mode
try {
Direct.app_init();
@@ -278,6 +312,7 @@ void editing_exec(string filename, bool fullscreen) {
DirectWindow direct_window = new DirectWindow(initial_file);
direct_window.show_all();
+ direct_window.maximize();
debug("%lf seconds to Gtk.main()", startup_timer.elapsed());
@@ -299,43 +334,29 @@ void editing_exec(string filename, bool fullscreen) {
namespace CommandlineOptions {
bool no_startup_progress = false;
-string data_dir = null;
+string? data_dir = null;
bool show_version = false;
bool no_runtime_monitoring = false;
bool fullscreen = false;
-
-private OptionEntry[]? entries = null;
-
-public OptionEntry[] get_options() {
- if (entries != null)
- return entries;
-
- OptionEntry datadir = { "datadir", 'd', 0, OptionArg.FILENAME, &data_dir,
- _("Path to Shotwell’s private data"), _("DIRECTORY") };
- entries += datadir;
-
- OptionEntry no_monitoring = { "no-runtime-monitoring", 0, 0, OptionArg.NONE, &no_runtime_monitoring,
- _("Do not monitor library directory at runtime for changes"), null };
- entries += no_monitoring;
-
- OptionEntry no_startup = { "no-startup-progress", 0, 0, OptionArg.NONE, &no_startup_progress,
- _("Don’t display startup progress meter"), null };
- entries += no_startup;
-
- OptionEntry version = { "version", 'V', 0, OptionArg.NONE, &show_version,
- _("Show the application’s version"), null };
- entries += version;
-
- OptionEntry fullscreen = { "fullscreen", 'f', 0, OptionArg.NONE,
- &fullscreen, _("Start the application in fullscreen mode"), null };
- entries += fullscreen;
-
- OptionEntry terminator = { null, 0, 0, 0, null, null, null };
- entries += terminator;
-
- return entries;
-}
-
+bool show_metadata = false;
+string? profile = null;
+bool create_profile = false;
+bool list_profiles = false;
+bool browse_profiles = false;
+
+const OptionEntry[] entries = {
+ { "datadir", 'd', 0, OptionArg.FILENAME, ref data_dir, N_("Path to Shotwell’s private data"), N_("DIRECTORY") },
+ { "no-runtime-monitoring", 0, 0, OptionArg.NONE, ref no_runtime_monitoring, N_("Do not monitor library directory at runtime for changes"), null },
+ { "no-startup-progress", 0, 0, OptionArg.NONE, ref no_startup_progress, N_("Don’t display startup progress meter"), null },
+ { "version", 'V', 0, OptionArg.NONE, ref show_version, N_("Show the application’s version") },
+ { "fullscreen", 'f', 0, OptionArg.NONE, ref fullscreen, N_("Start the application in fullscreen mode"), null },
+ { "show-metadata", 'p', 0, OptionArg.NONE, ref show_metadata, N_("Print the metadata of the image file"), null },
+ { "profile", 'i', 0, OptionArg.STRING, ref profile, N_("Name for a custom profile"), N_("PROFILE") },
+ { "profile-browser", 'b', 0, OptionArg.NONE, ref browse_profiles, N_("Start with a browser of available profiles"), null },
+ { "create", 'c', 0, OptionArg.NONE, ref create_profile, N_("If PROFILE given with --profile does not exist, create it"), null },
+ { "list-profiles", 'l', 0, OptionArg.NONE, ref list_profiles, N_("Show available profiles"), null },
+ { null, 0, 0, 0, null, null, null }
+};
}
void main(string[] args) {
@@ -345,7 +366,7 @@ void main(string[] args) {
// This has to be done before the AppWindow is created in order to ensure the XMP
// parser is initialized in a thread-safe fashion; please see
- // http://redmine.yorba.org/issues/4120 for details.
+ // https://bugzilla.gnome.org/show_bug.cgi?id=717931 for details.
GExiv2.initialize();
GExiv2.log_use_glib_logging();
@@ -353,22 +374,30 @@ void main(string[] args) {
// logging mechanisms
GExiv2.log_set_level(GExiv2.LogLevel.DEBUG);
+ // If set to non-empty, initialize GdkPixbuf with an additional loader path
+ if (Resources.PIXBUF_LOADER_PATH != "") {
+ debug("Trying to set module path to %s", Resources.PIXBUF_LOADER_PATH);
+ try {
+ Gdk.Pixbuf.init_modules(Resources.PIXBUF_LOADER_PATH);
+ } catch (Error err) {
+ message("Failed to set additional pixbuf loader path: %s", err.message);
+ }
+ }
+
// following the GIO programming guidelines at http://developer.gnome.org/gio/2.26/ch03.html,
// set the GSETTINGS_SCHEMA_DIR environment variable to allow us to load GSettings schemas from
// the build directory. this allows us to access local GSettings schemas without having to
// muck with the user's XDG_... directories, which is seriously frowned upon
if (AppDirs.get_install_dir() == null) {
GLib.Environment.set_variable("GSETTINGS_SCHEMA_DIR", AppDirs.get_lib_dir().get_path() +
- "/misc", true);
+ "/data/gsettings", true);
}
-
+
// init GTK (valac has already called g_threads_init())
try {
- Gtk.init_with_args(ref args, _("[FILE]"), CommandlineOptions.get_options(),
+ Gtk.init_with_args(ref args, _("[FILE]"), CommandlineOptions.entries,
Resources.APP_GETTEXT_PACKAGE);
- var use_dark = Config.Facade.get_instance().get_gtk_theme_variant();
- Gtk.Settings.get_default().gtk_application_prefer_dark_theme = use_dark;
} catch (Error e) {
print(e.message + "\n");
print(_("Run “%s --help” to see a full list of available command line options.\n"), args[0]);
@@ -376,6 +405,41 @@ void main(string[] args) {
return;
}
+ if (CommandlineOptions.browse_profiles) {
+ var window = new Gtk.Dialog();
+ window.set_title (_("Choose Shotwell's profile"));
+ var browser = new Shotwell.ProfileBrowser();
+ browser.profile_activated.connect((profile) => {
+ CommandlineOptions.profile = profile;
+ window.response(Gtk.ResponseType.OK);
+ });
+ window.get_content_area().add(browser);
+ window.set_size_request(430, 560);
+ var response = window.run();
+ window.destroy();
+ // Anything else than selecting an entry in the list will stop shotwell from starting
+ if (response != Gtk.ResponseType.OK) {
+ return;
+ }
+ }
+
+ // Setup profile manager
+ if (CommandlineOptions.profile != null) {
+ var manager = Shotwell.ProfileManager.get_instance();
+ if (!manager.has_profile (CommandlineOptions.profile)) {
+ if (!CommandlineOptions.create_profile) {
+ print(_("Profile %s does not exist. Did you mean to pass --create as well?"),
+ CommandlineOptions.profile);
+ AppDirs.terminate();
+ return;
+ }
+ }
+ manager.set_profile(CommandlineOptions.profile);
+ CommandlineOptions.data_dir = manager.derive_data_dir(CommandlineOptions.data_dir);
+ } else {
+ message("Starting session with system profile");
+ }
+
if (CommandlineOptions.show_version) {
if (Resources.GIT_VERSION != "")
print("%s %s (%s)\n", Resources.APP_TITLE, Resources.APP_VERSION, Resources.GIT_VERSION);
@@ -386,7 +450,16 @@ void main(string[] args) {
return;
}
-
+
+ if (CommandlineOptions.list_profiles) {
+ var manager = Shotwell.ProfileManager.get_instance();
+ manager.print_profiles();
+
+ AppDirs.terminate();
+
+ return;
+ }
+
// init debug prior to anything else (except Gtk, which it relies on, and AppDirs, which needs
// to be set ASAP) ... since we need to know what mode we're in, examine the command-line
// first
@@ -397,15 +470,21 @@ void main(string[] args) {
string[] mounts = new string[0];
string filename = null;
- for (int ctr = 1; ctr < args.length; ctr++) {
- string arg = args[ctr];
-
+ foreach (var arg in args[1:args.length]) {
if (LibraryWindow.is_mount_uri_supported(arg)) {
mounts += arg;
} else if (is_string_empty(filename) && !arg.contains("://")) {
filename = arg;
}
}
+
+ if (CommandlineOptions.show_metadata) {
+ dump_metadata (filename);
+
+ AppDirs.terminate();
+
+ return;
+ }
Debug.init(is_string_empty(filename) ? Debug.LIBRARY_PREFIX : Debug.VIEWER_PREFIX);
diff --git a/src/meson.build b/src/meson.build
index a532eec..460092e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -17,234 +17,272 @@ processor = executable('shotwell-graphics-processor',
dependencies: [gio, gdk, gee],
link_with: sw_graphics_processor)
+shotwell_deps = [gio, gee, sqlite, gtk, sqlite, posix, gphoto2,
+ gstreamer_pbu, gudev, gexiv2, gmodule,
+ libraw, libexif, sw_plugin]
+
+shotwell_libs = [sw_graphics_processor]
+
face_sources = (['faces/FacesBranch.vala',
- 'faces/FaceLocation.vala',
'faces/FacePage.vala',
'faces/FaceShape.vala',
+ 'faces/FaceDetect.vala',
'faces/Faces.vala',
- 'faces/Face.vala',
- 'db/FaceLocationTable.vala',
- 'db/FaceTable.vala',
'faces/FacesTool.vala'])
shotwell_deps = [gio, gee, sqlite, gtk, sqlite, posix, gphoto2,
- gstreamer_pbu, gio_unix, gudev, gexiv2, gmodule,
- libraw, libexif, sw_plugin, portal, version]
-if unity_available
- shotwell_deps += [unity]
-endif
-executable('shotwell',
- ['unit/Unit.vala',
- 'util/Util.vala',
- 'util/file.vala',
- 'util/image.vala',
- 'util/misc.vala',
- 'util/string.vala',
- 'util/system.vala',
- 'util/ui.vala',
- 'threads/Threads.vala',
- 'threads/Workers.vala',
- 'threads/BackgroundJob.vala',
- 'threads/Semaphore.vala',
- 'db/Db.vala',
- 'db/DatabaseTable.vala',
- 'db/PhotoTable.vala',
- 'db/EventTable.vala',
- 'db/TagTable.vala',
- 'db/TombstoneTable.vala',
- 'db/VideoTable.vala',
- 'db/VersionTable.vala',
- 'db/SavedSearchDBTable.vala',
- 'editing_tools/EditingTools.vala',
- 'editing_tools/RGBHistogramManipulator.vala',
- 'editing_tools/StraightenTool.vala',
- 'slideshow/Slideshow.vala',
- 'slideshow/TransitionEffects.vala',
- 'photos/Photos.vala',
- 'photos/PhotoFileAdapter.vala',
- 'photos/PhotoFileFormat.vala',
- 'photos/PhotoFileSniffer.vala',
- 'photos/PhotoMetadata.vala',
- 'photos/GRaw.vala',
- 'photos/GdkSupport.vala',
- 'photos/GifSupport.vala',
- 'photos/JfifSupport.vala',
- 'photos/BmpSupport.vala',
- 'photos/RawSupport.vala',
- 'photos/PngSupport.vala',
- 'photos/TiffSupport.vala',
- 'plugins/Plugins.vala',
- 'plugins/StandardHostInterface.vala',
- 'plugins/ManifestWidget.vala',
- 'publishing/Publishing.vala',
- 'publishing/PublishingUI.vala',
- 'publishing/PublishingPluginHost.vala',
- 'publishing/APIGlue.vala',
- 'library/Library.vala',
- 'library/LibraryWindow.vala',
- 'library/LibraryBranch.vala',
- 'library/TrashSidebarEntry.vala',
- 'library/OfflineSidebarEntry.vala',
- 'library/FlaggedSidebarEntry.vala',
- 'library/LastImportSidebarEntry.vala',
- 'library/ImportQueueSidebarEntry.vala',
- 'library/FlaggedPage.vala',
- 'library/ImportQueuePage.vala',
- 'library/LastImportPage.vala',
- 'library/OfflinePage.vala',
- 'library/TrashPage.vala',
- 'direct/Direct.vala',
- 'direct/DirectWindow.vala',
- 'direct/DirectPhoto.vala',
- 'direct/DirectPhotoPage.vala',
- 'direct/DirectView.vala',
- 'core/Core.vala',
- 'core/DataCollection.vala',
- 'core/DataSet.vala',
- 'core/util.vala',
- 'core/SourceCollection.vala',
- 'core/SourceHoldingTank.vala',
- 'core/DatabaseSourceCollection.vala',
- 'core/ContainerSourceCollection.vala',
- 'core/ViewCollection.vala',
- 'core/DataObject.vala',
- 'core/Alteration.vala',
- 'core/DataSource.vala',
- 'core/DataSourceTypes.vala',
- 'core/DataView.vala',
- 'core/DataViewTypes.vala',
- 'core/Tracker.vala',
- 'core/SourceInterfaces.vala',
- 'sidebar/Sidebar.vala',
- 'sidebar/Branch.vala',
- 'sidebar/Entry.vala',
- 'sidebar/Tree.vala',
- 'sidebar/common.vala',
- 'events/Events.vala',
- 'events/EventsBranch.vala',
- 'events/EventsDirectoryPage.vala',
- 'events/EventPage.vala',
- 'events/EventDirectoryItem.vala',
- 'tags/Tags.vala',
- 'tags/TagsBranch.vala',
- 'tags/TagPage.vala',
- 'tags/HierarchicalTagIndex.vala',
- 'tags/HierarchicalTagUtilities.vala',
- 'camera/Camera.vala',
- 'camera/CameraBranch.vala',
- 'camera/CameraTable.vala',
- 'camera/GPhoto.vala',
- 'camera/ImportPage.vala',
- 'searches/Searches.vala',
- 'searches/SearchesBranch.vala',
- 'searches/SearchBoolean.vala',
- 'searches/SavedSearchPage.vala',
- 'searches/SavedSearchDialog.vala',
- 'config/Config.vala',
- 'config/ConfigurationInterfaces.vala',
- 'config/GSettingsEngine.vala',
- 'data_imports/DataImports.vala',
- 'data_imports/DataImportsPluginHost.vala',
- 'data_imports/DataImportsUI.vala',
- 'data_imports/DataImportJob.vala',
- 'data_imports/DataImportSource.vala',
- 'folders/Folders.vala',
- 'folders/FoldersBranch.vala',
- 'folders/FoldersPage.vala',
- 'import-roll/ImportRollBranch.vala',
- 'import-roll/ImportRollEntry.vala',
- 'main.vala',
- 'AppWindow.vala',
- 'CollectionPage.vala',
- 'NaturalCollate.vala',
- 'Thumbnail.vala',
- 'ThumbnailCache.vala',
- 'CheckerboardLayout.vala',
- 'PhotoPage.vala',
- 'Page.vala',
- 'SortedList.vala',
- 'Dimensions.vala',
- 'Box.vala',
- 'Photo.vala',
- 'Orientation.vala',
- 'BatchImport.vala',
- 'Dialogs.vala',
- 'Resources.vala',
- 'Debug.vala',
- 'Properties.vala',
- 'Event.vala',
- 'International.vala',
- 'AppDirs.vala',
- 'PixbufCache.vala',
- 'CommandManager.vala',
- 'Commands.vala',
- 'SlideshowPage.vala',
- 'LibraryFiles.vala',
- 'Printing.vala',
- 'Tag.vala',
- 'Screensaver.vala',
- 'Exporter.vala',
- 'DirectoryMonitor.vala',
- 'LibraryMonitor.vala',
- 'VideoSupport.vala',
- 'Tombstone.vala',
- 'MetadataWriter.vala',
- 'Application.vala',
- 'TimedQueue.vala',
- 'MediaPage.vala',
- 'MediaDataRepresentation.vala',
- 'DesktopIntegration.vala',
- 'MediaInterfaces.vala',
- 'MediaMetadata.vala',
- 'VideoMetadata.vala',
- 'MediaMonitor.vala',
- 'PhotoMonitor.vala',
- 'VideoMonitor.vala',
- 'SearchFilter.vala',
- 'MediaViewTracker.vala',
- 'UnityProgressBar.vala',
- 'Upgrades.vala',
- 'dialogs/AdjustDateTimeDialog.vala',
- 'dialogs/EntryMultiCompletion.vala',
- 'dialogs/ExportDialog.vala',
- 'dialogs/MultiTextEntryDialog.vala',
- 'dialogs/Preferences.vala',
- 'dialogs/ProgressDialog.vala',
- 'dialogs/SetBackgroundSlideshow.vala',
- 'dialogs/SetBackground.vala',
- 'dialogs/TextEntry.vala',
- 'dialogs/WelcomeDialog.vala',
- '.unitize/_UnitInternals.vala',
- '.unitize/_UtilInternals.vala',
- '.unitize/_ThreadsInternals.vala',
- '.unitize/_DbInternals.vala',
- '.unitize/_EditingToolsInternals.vala',
- '.unitize/_PluginsInternals.vala',
- '.unitize/_SlideshowInternals.vala',
- '.unitize/_PhotosInternals.vala',
- '.unitize/_PublishingInternals.vala',
- '.unitize/_LibraryInternals.vala',
- '.unitize/_DirectInternals.vala',
- '.unitize/_CoreInternals.vala',
- '.unitize/_SidebarInternals.vala',
- '.unitize/_EventsInternals.vala',
- '.unitize/_TagsInternals.vala',
- '.unitize/_CameraInternals.vala',
- '.unitize/_SearchesInternals.vala',
- '.unitize/_ConfigInternals.vala',
- '.unitize/_DataImportsInternals.vala',
- '.unitize/_FoldersInternals.vala',
- '.unitize/_Library_unitize_entry.vala',
- '.unitize/_Direct_unitize_entry.vala'] + shotwell_resources + face_sources,
- include_directories : vapi_incdir,
- dependencies : shotwell_deps,
- vala_args : ['--pkg', 'libgphoto2',
- '--pkg', 'libraw',
- '--pkg', 'libexif',
- '--pkg', 'version',
- '--gresources',
- join_paths(meson.source_root(),
- 'org.gnome.Shotwell.gresource.xml')
- ],
- link_with: [sw_graphics_processor],
- install : true)
+ gstreamer_pbu, gudev, gexiv2, gmodule, unity,
+ libraw, libexif, sw_plugin, webpdemux, webp, version,
+ portal]
+
+subdir('metadata')
+subdir('publishing')
+subdir('video-support')
+
+executable(
+ 'shotwell',
+ [
+ 'unit/Unit.vala',
+ 'util/Util.vala',
+ 'util/file.vala',
+ 'util/image.vala',
+ 'util/misc.vala',
+ 'util/string.vala',
+ 'util/system.vala',
+ 'util/ui.vala',
+ 'threads/Threads.vala',
+ 'threads/Workers.vala',
+ 'threads/BackgroundJob.vala',
+ 'threads/Semaphore.vala',
+ 'db/Db.vala',
+ 'db/DatabaseTable.vala',
+ 'db/PhotoTable.vala',
+ 'db/EventTable.vala',
+ 'db/FaceLocationTable.vala',
+ 'db/FaceTable.vala',
+ 'db/TagTable.vala',
+ 'db/TombstoneTable.vala',
+ 'db/VideoTable.vala',
+ 'db/VersionTable.vala',
+ 'db/SavedSearchDBTable.vala',
+ 'editing_tools/EditingTools.vala',
+ 'editing_tools/RGBHistogramManipulator.vala',
+ 'editing_tools/StraightenTool.vala',
+ 'faces/Face.vala',
+ 'faces/FaceLocation.vala',
+ 'slideshow/Slideshow.vala',
+ 'slideshow/TransitionEffects.vala',
+ 'photos/Photos.vala',
+ 'photos/PhotoFileAdapter.vala',
+ 'photos/PhotoFileFormat.vala',
+ 'photos/PhotoFileSniffer.vala',
+ 'photos/PhotoMetadata.vala',
+ 'photos/GRaw.vala',
+ 'photos/GdkSupport.vala',
+ 'photos/GifSupport.vala',
+ 'photos/JfifSupport.vala',
+ 'photos/BmpSupport.vala',
+ 'photos/RawSupport.vala',
+ 'photos/PngSupport.vala',
+ 'photos/TiffSupport.vala',
+ 'photos/WebPSupport.vala',
+ 'photos/AvifSupport.vala',
+ 'photos/HeifSupport.vala',
+ 'photos/JpegXLSupport.vala',
+ 'plugins/Plugins.vala',
+ 'plugins/StandardHostInterface.vala',
+ 'plugins/ManifestWidget.vala',
+ 'publishing/Publishing.vala',
+ 'publishing/PublishingUI.vala',
+ 'publishing/PublishingPluginHost.vala',
+ 'publishing/APIGlue.vala',
+ 'library/BackgroundProgressBar.vala',
+ 'library/Library.vala',
+ 'library/LibraryWindow.vala',
+ 'library/LibraryBranch.vala',
+ 'library/TrashSidebarEntry.vala',
+ 'library/OfflineSidebarEntry.vala',
+ 'library/FlaggedSidebarEntry.vala',
+ 'library/LastImportSidebarEntry.vala',
+ 'library/ImportQueueSidebarEntry.vala',
+ 'library/FlaggedPage.vala',
+ 'library/ImportQueuePage.vala',
+ 'library/LastImportPage.vala',
+ 'library/OfflinePage.vala',
+ 'library/TrashPage.vala',
+ 'direct/Direct.vala',
+ 'direct/DirectWindow.vala',
+ 'direct/DirectPhoto.vala',
+ 'direct/DirectPhotoPage.vala',
+ 'direct/DirectView.vala',
+ 'core/Core.vala',
+ 'core/DataCollection.vala',
+ 'core/DataSet.vala',
+ 'core/util.vala',
+ 'core/SourceCollection.vala',
+ 'core/SourceHoldingTank.vala',
+ 'core/DatabaseSourceCollection.vala',
+ 'core/ContainerSourceCollection.vala',
+ 'core/ViewCollection.vala',
+ 'core/DataObject.vala',
+ 'core/Alteration.vala',
+ 'core/DataSource.vala',
+ 'core/DataSourceTypes.vala',
+ 'core/DataView.vala',
+ 'core/DataViewTypes.vala',
+ 'core/Tracker.vala',
+ 'core/SourceInterfaces.vala',
+ 'sidebar/Sidebar.vala',
+ 'sidebar/Branch.vala',
+ 'sidebar/Entry.vala',
+ 'sidebar/Tree.vala',
+ 'sidebar/common.vala',
+ 'events/Events.vala',
+ 'events/EventsBranch.vala',
+ 'events/EventsDirectoryPage.vala',
+ 'events/EventPage.vala',
+ 'events/EventDirectoryItem.vala',
+ 'tags/Tags.vala',
+ 'tags/TagsBranch.vala',
+ 'tags/TagPage.vala',
+ 'tags/HierarchicalTagIndex.vala',
+ 'tags/HierarchicalTagUtilities.vala',
+ 'camera/Camera.vala',
+ 'camera/CameraBranch.vala',
+ 'camera/CameraTable.vala',
+ 'camera/DiscoveredCamera.vala',
+ 'camera/GPhoto.vala',
+ 'camera/ImportPage.vala',
+ 'searches/Searches.vala',
+ 'searches/SearchesBranch.vala',
+ 'searches/SearchBoolean.vala',
+ 'searches/SavedSearchPage.vala',
+ 'searches/SavedSearchDialog.vala',
+ 'config/Config.vala',
+ 'config/ConfigurationInterfaces.vala',
+ 'config/GSettingsEngine.vala',
+ 'data_imports/DataImports.vala',
+ 'data_imports/DataImportsPluginHost.vala',
+ 'data_imports/DataImportsUI.vala',
+ 'data_imports/DataImportJob.vala',
+ 'data_imports/DataImportSource.vala',
+ 'folders/Folders.vala',
+ 'folders/FoldersBranch.vala',
+ 'folders/FoldersPage.vala',
+ 'import-roll/ImportRollBranch.vala',
+ 'import-roll/ImportRollEntry.vala',
+ 'main.vala',
+ 'AppWindow.vala',
+ 'CollectionPage.vala',
+ 'NaturalCollate.vala',
+ 'Thumbnail.vala',
+ 'ThumbnailCache.vala',
+ 'CheckerboardItem.vala',
+ 'CheckerboardItemText.vala',
+ 'CheckerboardLayout.vala',
+ 'PhotoPage.vala',
+ 'Page.vala',
+ 'SinglePhotoPage.vala',
+ 'CheckerboardPage.vala',
+ 'DragAndDropHandler.vala',
+ 'PageMessagePane.vala',
+ 'SortedList.vala',
+ 'Dimensions.vala',
+ 'Box.vala',
+ 'Photo.vala',
+ 'Orientation.vala',
+ 'BatchImport.vala',
+ 'Dialogs.vala',
+ 'Resources.vala',
+ 'Debug.vala',
+ 'Properties.vala',
+ 'Event.vala',
+ 'International.vala',
+ 'AppDirs.vala',
+ 'PixbufCache.vala',
+ 'CommandManager.vala',
+ 'Commands.vala',
+ 'SlideshowPage.vala',
+ 'LibraryFiles.vala',
+ 'Printing.vala',
+ 'Tag.vala',
+ 'Screensaver.vala',
+ 'Exporter.vala',
+ 'DirectoryMonitor.vala',
+ 'LibraryMonitor.vala',
+ 'Tombstone.vala',
+ 'MetadataWriter.vala',
+ 'Application.vala',
+ 'TimedQueue.vala',
+ 'MediaPage.vala',
+ 'MediaDataRepresentation.vala',
+ 'DesktopIntegration.vala',
+ 'MediaInterfaces.vala',
+ 'MediaMonitor.vala',
+ 'PhotoMonitor.vala',
+ 'VideoMonitor.vala',
+ 'SearchFilter.vala',
+ 'MediaViewTracker.vala',
+ 'UnityProgressBar.vala',
+ 'Upgrades.vala',
+ 'dialogs/AdjustDateTimeDialog.vala',
+ 'dialogs/EntryMultiCompletion.vala',
+ 'dialogs/ExportDialog.vala',
+ 'dialogs/MultiTextEntryDialog.vala',
+ 'dialogs/Preferences.vala',
+ 'dialogs/ProgressDialog.vala',
+ 'dialogs/SetBackgroundSlideshow.vala',
+ 'dialogs/SetBackground.vala',
+ 'dialogs/TextEntry.vala',
+ 'dialogs/WelcomeDialog.vala',
+ 'Profiles.vala',
+ 'ProfileBrowser.vala',
+ '.unitize/_UnitInternals.vala',
+ '.unitize/_UtilInternals.vala',
+ '.unitize/_ThreadsInternals.vala',
+ '.unitize/_DbInternals.vala',
+ '.unitize/_EditingToolsInternals.vala',
+ '.unitize/_PluginsInternals.vala',
+ '.unitize/_SlideshowInternals.vala',
+ '.unitize/_PhotosInternals.vala',
+ '.unitize/_PublishingInternals.vala',
+ '.unitize/_LibraryInternals.vala',
+ '.unitize/_DirectInternals.vala',
+ '.unitize/_CoreInternals.vala',
+ '.unitize/_SidebarInternals.vala',
+ '.unitize/_EventsInternals.vala',
+ '.unitize/_TagsInternals.vala',
+ '.unitize/_CameraInternals.vala',
+ '.unitize/_SearchesInternals.vala',
+ '.unitize/_ConfigInternals.vala',
+ '.unitize/_DataImportsInternals.vala',
+ '.unitize/_FoldersInternals.vala',
+ '.unitize/_Library_unitize_entry.vala',
+ '.unitize/_Direct_unitize_entry.vala',
+ 'video-support/VideoReader.vala',
+ 'video-support/VideoImportParams.vala',
+ 'video-support/Video.vala',
+ 'video-support/VideoSourceCollection.vala',
+ 'video-support/VideoMetadata.vala'
+ ] + shotwell_resources + face_sources,
+ include_directories : vapi_incdir,
+ dependencies : [
+ shotwell_deps,
+ sw_publishing_gui,
+ metadata,
+ metadata_handling
+ ],
+ vala_args : [
+ '--pkg', 'libgphoto2',
+ '--pkg', 'libraw',
+ '--pkg', 'libexif',
+ '--pkg', 'version',
+ '--gresources',
+ join_paths(meson.project_source_root(), 'data',
+ 'org.gnome.Shotwell.gresource.xml')
+ ],
+ link_with: [
+ sw_graphics_processor
+ ],
+ install : true
+)
diff --git a/src/metadata/MediaMetadata.vala b/src/metadata/MediaMetadata.vala
new file mode 100644
index 0000000..a329cb1
--- /dev/null
+++ b/src/metadata/MediaMetadata.vala
@@ -0,0 +1,15 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public abstract class MediaMetadata {
+ public abstract void read_from_file(File file) throws Error;
+
+ public abstract MetadataDateTime? get_creation_date_time();
+
+ public abstract string? get_title();
+
+ public abstract string? get_comment();
+}
diff --git a/src/metadata/MetadataDateTime.vala b/src/metadata/MetadataDateTime.vala
new file mode 100644
index 0000000..9dae99b
--- /dev/null
+++ b/src/metadata/MetadataDateTime.vala
@@ -0,0 +1,78 @@
+public errordomain MetadataDateTimeError {
+ INVALID_FORMAT,
+ UNSUPPORTED_FORMAT
+}
+
+public class MetadataDateTime {
+
+ private DateTime timestamp;
+
+ public MetadataDateTime(DateTime timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public MetadataDateTime.from_exif(string label) throws MetadataDateTimeError {
+ if (!from_exif_date_time(label, out timestamp))
+ throw new MetadataDateTimeError.INVALID_FORMAT("%s is not EXIF format date/time", label);
+ }
+
+ public MetadataDateTime.from_iptc(string date, string time) throws MetadataDateTimeError {
+ // TODO: Support IPTC date/time format
+ throw new MetadataDateTimeError.UNSUPPORTED_FORMAT("IPTC date/time format not currently supported");
+ }
+
+ public MetadataDateTime.from_xmp(string label) throws MetadataDateTimeError {
+ var dt = new DateTime.from_iso8601(label, null);
+ if (dt == null)
+ throw new MetadataDateTimeError.INVALID_FORMAT("%s is not XMP format date/time", label);
+
+ timestamp = dt;
+ }
+
+ public DateTime? get_timestamp() {
+ return timestamp;
+ }
+
+ public string get_exif_label() {
+ return to_exif_date_time(timestamp);
+ }
+
+ // TODO: get_iptc_date() and get_iptc_time()
+
+ public string get_xmp_label() {
+ return timestamp.format_iso8601();
+ }
+
+ public static bool from_exif_date_time(string date_time, out DateTime? timestamp) {
+ timestamp = null;
+
+ Time tm = Time();
+
+ // Check standard EXIF format
+ if (date_time.scanf("%d:%d:%d %d:%d:%d",
+ &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute, &tm.second) != 6) {
+ // Fallback in a more generic format
+ string tmp = date_time.dup();
+ tmp.canon("0123456789", ' ');
+ if (tmp.scanf("%4d%2d%2d%2d%2d%2d",
+ &tm.year, &tm.month, &tm.day, &tm.hour, &tm.minute,&tm.second) != 6)
+ return false;
+ }
+
+ // watch for bogosity
+ if (tm.year <= 1900 || tm.month <= 0 || tm.day < 0 || tm.hour < 0 || tm.minute < 0 || tm.second < 0)
+ return false;
+
+ timestamp = new DateTime.local(tm.year, tm.month, tm.day, tm.hour, tm.minute, tm.second);
+
+ return true;
+ }
+
+ public static string to_exif_date_time(DateTime timestamp) {
+ return timestamp.to_local().format("%Y:%m:%d %H:%M:%S");
+ }
+
+ public string to_string() {
+ return to_exif_date_time(timestamp);
+ }
+}
diff --git a/src/metadata/MetadataRational.vala b/src/metadata/MetadataRational.vala
new file mode 100644
index 0000000..ec3ac17
--- /dev/null
+++ b/src/metadata/MetadataRational.vala
@@ -0,0 +1,26 @@
+public struct MetadataRational {
+ public int numerator;
+ public int denominator;
+
+ public MetadataRational.invalid() {
+ this.numerator = -1;
+ this.denominator = -1;
+ }
+
+ public MetadataRational(int numerator, int denominator) {
+ this.numerator = numerator;
+ this.denominator = denominator;
+ }
+
+ private bool is_component_valid(int component) {
+ return (component >= 0) && (component <= 1000000);
+ }
+
+ public bool is_valid() {
+ return (is_component_valid(numerator) && is_component_valid(denominator));
+ }
+
+ public string to_string() {
+ return (is_valid()) ? ("%d/%d".printf(numerator, denominator)) : "";
+ }
+}
diff --git a/src/metadata/meson.build b/src/metadata/meson.build
new file mode 100644
index 0000000..7f322ca
--- /dev/null
+++ b/src/metadata/meson.build
@@ -0,0 +1,16 @@
+libmetadata = static_library(
+ 'metadata',
+ [
+ 'MediaMetadata.vala',
+ 'MetadataDateTime.vala',
+ 'MetadataRational.vala'
+ ],
+ dependencies : [
+ gio
+ ]
+)
+
+metadata = declare_dependency(
+ include_directories : include_directories('.'),
+ link_with : libmetadata
+)
diff --git a/src/photos/AvifSupport.vala b/src/photos/AvifSupport.vala
new file mode 100644
index 0000000..842f0fc
--- /dev/null
+++ b/src/photos/AvifSupport.vala
@@ -0,0 +1,140 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+class AvifFileFormatProperties : PhotoFileFormatProperties {
+ private static string[] KNOWN_EXTENSIONS = { "avif" };
+ private static string[] KNOWN_MIME_TYPES = { "image/avif" };
+
+ private static AvifFileFormatProperties instance = null;
+
+ public static void init() {
+ instance = new AvifFileFormatProperties();
+ }
+
+ public static AvifFileFormatProperties get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormat get_file_format() {
+ return PhotoFileFormat.AVIF;
+ }
+
+ public override PhotoFileFormatFlags get_flags() {
+ return PhotoFileFormatFlags.NONE;
+ }
+
+ public override string get_user_visible_name() {
+ return _("AVIF");
+ }
+
+ public override string get_default_extension() {
+ return KNOWN_EXTENSIONS[0];
+ }
+
+ public override string[] get_known_extensions() {
+ return KNOWN_EXTENSIONS;
+ }
+
+ public override string get_default_mime_type() {
+ return KNOWN_MIME_TYPES[0];
+ }
+
+ public override string[] get_mime_types() {
+ return KNOWN_MIME_TYPES;
+ }
+}
+
+public class AvifSniffer : GdkSniffer {
+ public AvifSniffer(File file, PhotoFileSniffer.Options options) {
+ base (file, options);
+ }
+
+ public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
+ // Rely on GdkSniffer to detect corruption
+ is_corrupted = false;
+
+ if (!is_supported_bmff_with_variants(file, {"avif", "avis"}))
+ return null;
+
+ DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
+ if (detected == null)
+ return null;
+
+ return (detected.file_format == PhotoFileFormat.AVIF) ? detected : null;
+ }
+}
+
+public class AvifReader : GdkReader {
+ public AvifReader(string filepath) {
+ base (filepath, PhotoFileFormat.AVIF);
+ }
+}
+
+public class AvifWriter : PhotoFileWriter {
+ public AvifWriter(string filepath) {
+ base (filepath, PhotoFileFormat.AVIF);
+ }
+
+ public override void write(Gdk.Pixbuf pixbuf, Jpeg.Quality quality) throws Error {
+ pixbuf.save(get_filepath(), "avif", "quality", "90", null);
+ }
+}
+
+public class AvifMetadataWriter : PhotoFileMetadataWriter {
+ public AvifMetadataWriter(string filepath) {
+ base (filepath, PhotoFileFormat.AVIF);
+ }
+
+ public override void write_metadata(PhotoMetadata metadata) throws Error {
+ metadata.write_to_file(get_file());
+ }
+}
+
+public class AvifFileFormatDriver : PhotoFileFormatDriver {
+ private static AvifFileFormatDriver instance = null;
+
+ public static void init() {
+ instance = new AvifFileFormatDriver();
+ AvifFileFormatProperties.init();
+ }
+
+ public static AvifFileFormatDriver get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormatProperties get_properties() {
+ return AvifFileFormatProperties.get_instance();
+ }
+
+ public override PhotoFileReader create_reader(string filepath) {
+ return new AvifReader(filepath);
+ }
+
+ public override bool can_write_image() {
+ return true;
+ }
+
+ public override bool can_write_metadata() {
+ return true;
+ }
+
+ public override PhotoFileWriter? create_writer(string filepath) {
+ return new AvifWriter(filepath);
+ }
+
+ public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
+ return new AvifMetadataWriter(filepath);
+ }
+
+ public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
+ return new AvifSniffer(file, options);
+ }
+
+ public override PhotoMetadata create_metadata() {
+ return new PhotoMetadata();
+ }
+}
+
diff --git a/src/photos/BmpSupport.vala b/src/photos/BmpSupport.vala
index a59a4d9..26ec911 100644
--- a/src/photos/BmpSupport.vala
+++ b/src/photos/BmpSupport.vala
@@ -90,33 +90,6 @@ public class BmpReader : GdkReader {
public BmpReader(string filepath) {
base (filepath, PhotoFileFormat.BMP);
}
-
- public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error {
- Gdk.Pixbuf result = null;
- /* if we encounter a situation where there are two orders of magnitude or more of
- difference between the full image size and the scaled size, and if the full image
- size has five or more decimal digits of precision, Gdk.Pixbuf.from_file_at_scale( ) can
- fail due to what appear to be floating-point round-off issues. This isn't surprising,
- since 32-bit floats only have 6-7 decimal digits of precision in their mantissa. In
- this case, we prefetch the image at a larger scale and then downsample it to the
- desired scale as a post-process step. This short-circuits Gdk.Pixbuf's buggy
- scaling code. */
- if (((full.width > 9999) || (full.height > 9999)) && ((scaled.width < 100) ||
- (scaled.height < 100))) {
- Dimensions prefetch_dimensions = full.get_scaled_by_constraint(1000,
- ScaleConstraint.DIMENSIONS);
-
- result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), prefetch_dimensions.width,
- prefetch_dimensions.height, false);
-
- result = result.scale_simple(scaled.width, scaled.height, Gdk.InterpType.HYPER);
- } else {
- result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width,
- scaled.height, false);
- }
-
- return result;
- }
}
public class BmpWriter : PhotoFileWriter {
diff --git a/src/photos/GdkSupport.vala b/src/photos/GdkSupport.vala
index f7e18d5..64a08d6 100644
--- a/src/photos/GdkSupport.vala
+++ b/src/photos/GdkSupport.vala
@@ -21,7 +21,30 @@ public abstract class GdkReader : PhotoFileReader {
}
public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error {
- return new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width, scaled.height, false);
+ Gdk.Pixbuf result = null;
+ /* if we encounter a situation where there are two orders of magnitude or more of
+ difference between the full image size and the scaled size, and if the full image
+ size has five or more decimal digits of precision, Gdk.Pixbuf.from_file_at_scale( ) can
+ fail due to what appear to be floating-point round-off issues. This isn't surprising,
+ since 32-bit floats only have 6-7 decimal digits of precision in their mantissa. In
+ this case, we prefetch the image at a larger scale and then downsample it to the
+ desired scale as a post-process step. This short-circuits Gdk.Pixbuf's buggy
+ scaling code. */
+ if (((full.width > 9999) || (full.height > 9999)) && ((scaled.width < 100) ||
+ (scaled.height < 100))) {
+ Dimensions prefetch_dimensions = full.get_scaled_by_constraint(1000,
+ ScaleConstraint.DIMENSIONS);
+
+ result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), prefetch_dimensions.width,
+ prefetch_dimensions.height, false);
+
+ result = result.scale_simple(scaled.width, scaled.height, Gdk.InterpType.HYPER);
+ } else {
+ result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width,
+ scaled.height, false);
+ }
+
+ return result;
}
}
@@ -112,13 +135,14 @@ public abstract class GdkSniffer : PhotoFileSniffer {
Gdk.Pixbuf? pixbuf = pixbuf_loader.get_pixbuf();
if (pixbuf == null)
return;
-
+
detected.colorspace = pixbuf.get_colorspace();
detected.channels = pixbuf.get_n_channels();
detected.bits_per_channel = pixbuf.get_bits_per_sample();
unowned Gdk.PixbufFormat format = pixbuf_loader.get_format();
detected.format_name = format.get_name();
+ debug("Pixbuf detected format name: %s", detected.format_name);
detected.file_format = PhotoFileFormat.from_pixbuf_name(detected.format_name);
area_prepared = true;
diff --git a/src/photos/GifSupport.vala b/src/photos/GifSupport.vala
index bd6ef6a..b49b4f2 100644
--- a/src/photos/GifSupport.vala
+++ b/src/photos/GifSupport.vala
@@ -86,33 +86,6 @@ public class GifReader : GdkReader {
public GifReader(string filepath) {
base (filepath, PhotoFileFormat.PNG);
}
-
- public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error {
- Gdk.Pixbuf result = null;
- /* if we encounter a situation where there are two orders of magnitude or more of
- difference between the full image size and the scaled size, and if the full image
- size has five or more decimal digits of precision, Gdk.Pixbuf.from_file_at_scale( ) can
- fail due to what appear to be floating-point round-off issues. This isn't surprising,
- since 32-bit floats only have 6-7 decimal digits of precision in their mantissa. In
- this case, we prefetch the image at a larger scale and then downsample it to the
- desired scale as a post-process step. This short-circuits Gdk.Pixbuf's buggy
- scaling code. */
- if (((full.width > 9999) || (full.height > 9999)) && ((scaled.width < 100) ||
- (scaled.height < 100))) {
- Dimensions prefetch_dimensions = full.get_scaled_by_constraint(1000,
- ScaleConstraint.DIMENSIONS);
-
- result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), prefetch_dimensions.width,
- prefetch_dimensions.height, false);
-
- result = result.scale_simple(scaled.width, scaled.height, Gdk.InterpType.HYPER);
- } else {
- result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width,
- scaled.height, false);
- }
-
- return result;
- }
}
public class GifMetadataWriter : PhotoFileMetadataWriter {
diff --git a/src/photos/HeifSupport.vala b/src/photos/HeifSupport.vala
new file mode 100644
index 0000000..0c05e02
--- /dev/null
+++ b/src/photos/HeifSupport.vala
@@ -0,0 +1,150 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+class HeifFileFormatProperties : PhotoFileFormatProperties {
+ private static string[] KNOWN_EXTENSIONS = { "heif", "heic" };
+ private static string[] KNOWN_MIME_TYPES = { "image/heif" };
+
+ private static HeifFileFormatProperties instance = null;
+
+ public static void init() {
+ instance = new HeifFileFormatProperties();
+ }
+
+ public static HeifFileFormatProperties get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormat get_file_format() {
+ return PhotoFileFormat.HEIF;
+ }
+
+ public override PhotoFileFormatFlags get_flags() {
+ return PhotoFileFormatFlags.NONE;
+ }
+
+ public override string get_user_visible_name() {
+ return _("HEIF");
+ }
+
+ public override string get_default_extension() {
+ return KNOWN_EXTENSIONS[0];
+ }
+
+ public override string[] get_known_extensions() {
+ return KNOWN_EXTENSIONS;
+ }
+
+ public override string get_default_mime_type() {
+ return KNOWN_MIME_TYPES[0];
+ }
+
+ public override string[] get_mime_types() {
+ return KNOWN_MIME_TYPES;
+ }
+}
+
+public class HeifSniffer : GdkSniffer {
+ private const string[] MAGIC_SEQUENCES = { "heic", "heix", "hevc", "heim", "heis", "hevm", "hevs", "mif1", "msf1"};
+
+ public HeifSniffer(File file, PhotoFileSniffer.Options options) {
+ base (file, options);
+ }
+
+ public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
+ // Rely on GdkSniffer to detect corruption
+ is_corrupted = false;
+
+ if (!is_supported_bmff_with_variants(file, MAGIC_SEQUENCES))
+ return null;
+
+ DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
+ if (detected == null)
+ return null;
+
+ if (detected.file_format == PhotoFileFormat.AVIF)
+ detected.file_format = PhotoFileFormat.HEIF;
+
+ // Heif contains its own rotation information, so we need to ignore the EXIF rotation+
+ if (detected.metadata != null) {
+ detected.metadata.set_orientation(Orientation.TOP_LEFT);
+ }
+
+ return (detected.file_format == PhotoFileFormat.HEIF) ? detected : null;
+ }
+
+}
+
+public class HeifReader : GdkReader {
+ public HeifReader(string filepath) {
+ base (filepath, PhotoFileFormat.HEIF);
+ }
+
+ public override PhotoMetadata read_metadata() throws Error {
+ PhotoMetadata metadata = new PhotoMetadata();
+ metadata.read_from_file(get_file());
+ // Heif contains its own rotation information, so we need to ignore the EXIF rotation
+ metadata.set_orientation(Orientation.TOP_LEFT);
+ return metadata;
+ }
+
+}
+
+public class HeifMetadataWriter : PhotoFileMetadataWriter {
+ public HeifMetadataWriter(string filepath) {
+ base (filepath, PhotoFileFormat.HEIF);
+ }
+
+ public override void write_metadata(PhotoMetadata metadata) throws Error {
+ metadata.write_to_file(get_file());
+ }
+}
+
+public class HeifFileFormatDriver : PhotoFileFormatDriver {
+ private static HeifFileFormatDriver instance = null;
+
+ public static void init() {
+ instance = new HeifFileFormatDriver();
+ HeifFileFormatProperties.init();
+ }
+
+ public static HeifFileFormatDriver get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormatProperties get_properties() {
+ return HeifFileFormatProperties.get_instance();
+ }
+
+ public override PhotoFileReader create_reader(string filepath) {
+ return new HeifReader(filepath);
+ }
+
+ public override bool can_write_image() {
+ return false;
+ }
+
+ public override bool can_write_metadata() {
+ return true;
+ }
+
+ public override PhotoFileWriter? create_writer(string filepath) {
+ return null;
+ }
+
+ public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
+ return new HeifMetadataWriter(filepath);
+ }
+
+ public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
+ return new HeifSniffer(file, options);
+ }
+
+ public override PhotoMetadata create_metadata() {
+ return new PhotoMetadata();
+ }
+}
+
diff --git a/src/photos/JfifSupport.vala b/src/photos/JfifSupport.vala
index 5ea64a5..0de45f8 100644
--- a/src/photos/JfifSupport.vala
+++ b/src/photos/JfifSupport.vala
@@ -103,17 +103,78 @@ public class JfifSniffer : GdkSniffer {
}
public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
- // Rely on GdkSniffer to detect corruption
is_corrupted = false;
-
- if (!Jpeg.is_jpeg(file))
- return null;
-
- DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
- if (detected == null)
+ if (!calc_md5) {
+ return fast_sniff (out is_corrupted);
+ } else {
+ if (!Jpeg.is_jpeg(file)) {
+ return null;
+ }
+
+ // Rely on GdkSniffer to detect corruption
+
+ DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
+ if (detected == null)
+ return null;
+
+ return (detected.file_format == PhotoFileFormat.JFIF) ? detected : null;
+ }
+ }
+
+ private DetectedPhotoInformation? fast_sniff(out bool is_corrupted) throws Error {
+ is_corrupted = false;
+ var detected = new DetectedPhotoInformation();
+
+ detected.metadata = new PhotoMetadata();
+ try {
+ detected.metadata.read_from_file(file);
+ } catch (Error err) {
+ // no metadata detected
+ detected.metadata = null;
+ }
+
+ var fins = file.read(null);
+ var dins = new DataInputStream(fins);
+ dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
+ var seekable = (Seekable) dins;
+
+ var marker = Jpeg.Marker.INVALID;
+ var length = Jpeg.read_marker_2(dins, out marker);
+
+ if (marker != Jpeg.Marker.SOI) {
return null;
-
- return (detected.file_format == PhotoFileFormat.JFIF) ? detected : null;
+ }
+
+ length = Jpeg.read_marker_2(dins, out marker);
+ while (!marker.is_sof() && length > 0) {
+ seekable.seek(length, SeekType.CUR, null);
+ length = Jpeg.read_marker_2(dins, out marker);
+ }
+
+ if (marker.is_sof()) {
+ if (length < 6) {
+ is_corrupted = true;
+ return null;
+ }
+
+ // Skip precision
+ dins.read_byte();
+
+ // Next two 16 bytes are image dimensions
+ uint16 height = dins.read_uint16();
+ uint16 width = dins.read_uint16();
+
+ detected.image_dim = Dimensions(width, height);
+ detected.colorspace = Gdk.Colorspace.RGB;
+ detected.channels = 3;
+ detected.bits_per_channel = 8;
+ detected.format_name = "jpeg";
+ detected.file_format = PhotoFileFormat.from_pixbuf_name(detected.format_name);
+ } else {
+ is_corrupted = true;
+ }
+
+ return detected;
}
}
@@ -159,6 +220,16 @@ namespace Jpeg {
public uint8 get_byte() {
return (uint8) this;
}
+
+ public bool is_sof() {
+ // FFCn is SOF unless n is a multiple of 4 > 0 (FFC4, FFC8, FFCC)
+ if ((this & 0xC0) != 0xC0) {
+ return false;
+ }
+
+ var variant = this & 0x0F;
+ return variant == 0 || variant % 4 != 0;
+ }
}
public enum Quality {
@@ -219,12 +290,9 @@ namespace Jpeg {
return is_jpeg_stream(mins);
}
- private int read_marker(InputStream fins, out Jpeg.Marker marker) throws Error {
+ private int32 read_marker_2(DataInputStream dins, out Jpeg.Marker marker) throws Error {
marker = Jpeg.Marker.INVALID;
-
- DataInputStream dins = new DataInputStream(fins);
- dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
-
+
if (dins.read_byte() != Jpeg.MARKER_PREFIX)
return -1;
@@ -235,9 +303,10 @@ namespace Jpeg {
}
uint16 length = dins.read_uint16();
- if (length < 2 && fins is Seekable) {
+ var seekable = dins as Seekable;
+ if (length < 2 && dins != null) {
debug("Invalid length %Xh at ofs %" + int64.FORMAT + "Xh", length,
- (fins as Seekable).tell() - 2);
+ seekable.tell() - 2);
return -1;
}
@@ -245,5 +314,12 @@ namespace Jpeg {
// account for two length bytes already read
return length - 2;
}
+
+ private int read_marker(InputStream fins, out Jpeg.Marker marker) throws Error {
+ DataInputStream dins = new DataInputStream(fins);
+ dins.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
+
+ return read_marker_2(dins, out marker);
+ }
}
diff --git a/src/photos/JpegXLSupport.vala b/src/photos/JpegXLSupport.vala
new file mode 100644
index 0000000..eed220c
--- /dev/null
+++ b/src/photos/JpegXLSupport.vala
@@ -0,0 +1,149 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+class JpegXLFileFormatProperties : PhotoFileFormatProperties {
+ private static string[] KNOWN_EXTENSIONS = { "jxl", "jpegxl" };
+ private static string[] KNOWN_MIME_TYPES = { "image/jxl" };
+
+ private static JpegXLFileFormatProperties instance = null;
+
+ public static void init() {
+ instance = new JpegXLFileFormatProperties();
+ }
+
+ public static JpegXLFileFormatProperties get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormat get_file_format() {
+ return PhotoFileFormat.JPEGXL;
+ }
+
+ public override PhotoFileFormatFlags get_flags() {
+ return PhotoFileFormatFlags.NONE;
+ }
+
+ public override string get_user_visible_name() {
+ return _("JPEGXL");
+ }
+
+ public override string get_default_extension() {
+ return KNOWN_EXTENSIONS[0];
+ }
+
+ public override string[] get_known_extensions() {
+ return KNOWN_EXTENSIONS;
+ }
+
+ public override string get_default_mime_type() {
+ return KNOWN_MIME_TYPES[0];
+ }
+
+ public override string[] get_mime_types() {
+ return KNOWN_MIME_TYPES;
+ }
+}
+
+public class JpegXLSniffer : GdkSniffer {
+ // See https://github.com/ImageMagick/jpeg-xl/blob/main/doc/format_overview.md#file-format
+ private const uint8[] CODESTREAM_MAGIC_SEQUENCE = { 0xff, 0x0a };
+ private const uint8[] BMFF_MAGIC_SEQUENCE = {0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A};
+
+
+ public JpegXLSniffer(File file, PhotoFileSniffer.Options options) {
+ base (file, options);
+ }
+
+ private static bool is_jpegxl_file(File file) throws Error {
+ FileInputStream instream = file.read(null);
+
+ // Read out first four bytes
+ uint8[] file_lead_sequence = new uint8[BMFF_MAGIC_SEQUENCE.length];
+
+ var size = instream.read(file_lead_sequence, null);
+
+ return size == BMFF_MAGIC_SEQUENCE.length && (Memory.cmp(CODESTREAM_MAGIC_SEQUENCE, file_lead_sequence, CODESTREAM_MAGIC_SEQUENCE.length) == 0 ||
+ Memory.cmp(BMFF_MAGIC_SEQUENCE, file_lead_sequence, BMFF_MAGIC_SEQUENCE.length) == 0);
+
+ }
+
+ public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
+ // Rely on GdkSniffer to detect corruption
+ is_corrupted = false;
+
+ if (!is_jpegxl_file(file))
+ return null;
+
+ DetectedPhotoInformation? detected = base.sniff(out is_corrupted);
+ if (detected == null)
+ return null;
+
+ return (detected.file_format == PhotoFileFormat.JPEGXL) ? detected : null;
+ }
+
+}
+
+public class JpegXLReader : GdkReader {
+ public JpegXLReader(string filepath) {
+ base (filepath, PhotoFileFormat.JPEGXL);
+ }
+}
+
+public class JpegXLMetadataWriter : PhotoFileMetadataWriter {
+ public JpegXLMetadataWriter(string filepath) {
+ base (filepath, PhotoFileFormat.JPEGXL);
+ }
+
+ public override void write_metadata(PhotoMetadata metadata) throws Error {
+ metadata.write_to_file(get_file());
+ }
+}
+
+public class JpegXLFileFormatDriver : PhotoFileFormatDriver {
+ private static JpegXLFileFormatDriver instance = null;
+
+ public static void init() {
+ instance = new JpegXLFileFormatDriver();
+ JpegXLFileFormatProperties.init();
+ }
+
+ public static JpegXLFileFormatDriver get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormatProperties get_properties() {
+ return JpegXLFileFormatProperties.get_instance();
+ }
+
+ public override PhotoFileReader create_reader(string filepath) {
+ return new JpegXLReader(filepath);
+ }
+
+ public override bool can_write_image() {
+ return false;
+ }
+
+ public override bool can_write_metadata() {
+ return true;
+ }
+
+ public override PhotoFileWriter? create_writer(string filepath) {
+ return null;
+ }
+
+ public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
+ return new JpegXLMetadataWriter(filepath);
+ }
+
+ public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
+ return new JpegXLSniffer(file, options);
+ }
+
+ public override PhotoMetadata create_metadata() {
+ return new PhotoMetadata();
+ }
+}
+
diff --git a/src/photos/PhotoFileFormat.vala b/src/photos/PhotoFileFormat.vala
index e642008..4c69de3 100644
--- a/src/photos/PhotoFileFormat.vala
+++ b/src/photos/PhotoFileFormat.vala
@@ -58,12 +58,16 @@ public enum PhotoFileFormat {
TIFF,
BMP,
GIF,
+ WEBP,
+ AVIF,
+ HEIF,
+ JPEGXL,
UNKNOWN;
// This is currently listed in the order of detection, that is, the file is examined from
// left to right. (See PhotoFileInterrogator.)
public static PhotoFileFormat[] get_supported() {
- return { JFIF, RAW, PNG, TIFF, BMP, GIF };
+ return { JFIF, RAW, PNG, TIFF, BMP, GIF, WEBP, AVIF, HEIF, JPEGXL };
}
public static PhotoFileFormat[] get_writeable() {
@@ -141,7 +145,19 @@ public enum PhotoFileFormat {
case GIF:
return 5;
-
+
+ case WEBP:
+ return 6;
+
+ case AVIF:
+ return 7;
+
+ case HEIF:
+ return 8;
+
+ case JPEGXL:
+ return 9;
+
case UNKNOWN:
default:
return -1;
@@ -169,6 +185,18 @@ public enum PhotoFileFormat {
case 5:
return GIF;
+ case 6:
+ return WEBP;
+
+ case 7:
+ return AVIF;
+
+ case 8:
+ return HEIF;
+
+ case 9:
+ return JPEGXL;
+
default:
return UNKNOWN;
}
@@ -217,7 +245,17 @@ public enum PhotoFileFormat {
case "gif":
return PhotoFileFormat.GIF;
-
+
+ case "heif/avif":
+ case "avif":
+ return PhotoFileFormat.AVIF;
+
+ case "heif":
+ return PhotoFileFormat.HEIF;
+
+ case "jxl":
+ return PhotoFileFormat.JPEGXL;
+
default:
return PhotoFileFormat.UNKNOWN;
}
@@ -249,6 +287,22 @@ public enum PhotoFileFormat {
Photos.GifFileFormatDriver.init();
break;
+ case WEBP:
+ Photos.WebpFileFormatDriver.init();
+ break;
+
+ case AVIF:
+ AvifFileFormatDriver.init();
+ break;
+
+ case HEIF:
+ HeifFileFormatDriver.init();
+ break;
+
+ case JPEGXL:
+ JpegXLFileFormatDriver.init();
+ break;
+
default:
error("Unsupported file format %s", this.to_string());
}
@@ -274,6 +328,18 @@ public enum PhotoFileFormat {
case GIF:
return Photos.GifFileFormatDriver.get_instance();
+ case WEBP:
+ return Photos.WebpFileFormatDriver.get_instance();
+
+ case AVIF:
+ return AvifFileFormatDriver.get_instance();
+
+ case HEIF:
+ return HeifFileFormatDriver.get_instance();
+
+ case JPEGXL:
+ return JpegXLFileFormatDriver.get_instance();
+
default:
error("Unsupported file format %s", this.to_string());
}
diff --git a/src/photos/PhotoFileSniffer.vala b/src/photos/PhotoFileSniffer.vala
index 7442fde..6358920 100644
--- a/src/photos/PhotoFileSniffer.vala
+++ b/src/photos/PhotoFileSniffer.vala
@@ -47,6 +47,34 @@ public abstract class PhotoFileSniffer {
}
public abstract DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error;
+
+ protected static bool is_supported_bmff_with_variants(File file, string[] variants) throws Error {
+
+ FileInputStream instream = file.read(null);
+
+ // Skip the first four bytes
+ if (instream.skip(4) != 4) {
+ return false;
+ }
+
+ // The next four bytes need to be ftyp
+ var buf = new uint8[4];
+ if (instream.read(buf, null) != 4) {
+ return false;
+ }
+
+ if (Memory.cmp("ftyp".data, buf, 4) != 0) {
+ return false;
+ }
+
+ if (instream.read(buf, null) != 4) {
+ return false;
+ }
+
+ buf += '\0';
+
+ return (string)buf in variants;
+ }
}
//
diff --git a/src/photos/PhotoMetadata.vala b/src/photos/PhotoMetadata.vala
index a9b7457..3bf77d6 100644
--- a/src/photos/PhotoMetadata.vala
+++ b/src/photos/PhotoMetadata.vala
@@ -241,9 +241,13 @@ public class PhotoMetadata : MediaMetadata {
public override Bytes flatten() throws Error {
unowned GExiv2.PreviewProperties?[] props = owner.exiv2.get_preview_properties();
assert(props != null && props.length > number);
-
- return new
- Bytes(owner.exiv2.get_preview_image(props[number]).get_data());
+
+ try {
+ return new
+ Bytes(owner.exiv2.try_get_preview_image(props[number]).get_data());
+ } catch (Error err) {
+ return new Bytes(null);
+ }
}
}
@@ -278,12 +282,8 @@ public class PhotoMetadata : MediaMetadata {
exiv2 = new GExiv2.Metadata();
exif = null;
-#if NEW_GEXIV2_API
exiv2.open_buf(buffer[0:length]);
-#else
- exiv2.open_buf(buffer, length);
-#endif
- exif = Exif.Data.new_from_data(buffer);
+ exif = Exif.Data.new_from_data(buffer[0:length]);
source_name = "<memory buffer %d bytes>".printf(length);
}
@@ -291,11 +291,8 @@ public class PhotoMetadata : MediaMetadata {
exiv2 = new GExiv2.Metadata();
exif = null;
-#if NEW_GEXIV2_API
exiv2.from_app1_segment(buffer.get_data());
-#else
exif = Exif.Data.new_from_data(buffer.get_data());
-#endif
source_name = "<app1 segment %zu bytes>".printf(buffer.get_size());
}
@@ -371,7 +368,11 @@ public class PhotoMetadata : MediaMetadata {
}
public bool has_tag(string tag) {
- return exiv2.has_tag(tag);
+ try {
+ return exiv2.try_has_tag(tag);
+ } catch (Error error) {
+ return false;
+ }
}
private Gee.Set<string> create_string_set(owned CompareDataFunc<string>? compare_func) {
@@ -397,6 +398,9 @@ public class PhotoMetadata : MediaMetadata {
case MetadataDomain.IPTC:
tags = exiv2.get_iptc_tags();
break;
+ default:
+ // Just ignore any other unknown tags
+ break;
}
if (tags == null || tags.length == 0)
@@ -429,19 +433,35 @@ public class PhotoMetadata : MediaMetadata {
}
public string? get_tag_label(string tag) {
- return GExiv2.Metadata.get_tag_label(tag);
+ try {
+ return GExiv2.Metadata.try_get_tag_label(tag);
+ } catch (Error error) {
+ return null;
+ }
}
public string? get_tag_description(string tag) {
- return GExiv2.Metadata.get_tag_description(tag);
+ try {
+ return GExiv2.Metadata.try_get_tag_description(tag);
+ } catch (Error error) {
+ return null;
+ }
}
public string? get_string(string tag, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS) {
- return prepare_input_text(exiv2.get_tag_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH);
+ try {
+ return prepare_input_text(exiv2.try_get_tag_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH);
+ } catch (Error error) {
+ return null;
+ }
}
public string? get_string_interpreted(string tag, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS) {
- return prepare_input_text(exiv2.get_tag_interpreted_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH);
+ try {
+ return prepare_input_text(exiv2.try_get_tag_interpreted_string(tag), options, DEFAULT_USER_TEXT_INPUT_LENGTH);
+ } catch (Error error) {
+ return null;
+ }
}
public string? get_first_string(string[] tags) {
@@ -469,26 +489,30 @@ public class PhotoMetadata : MediaMetadata {
// NOTE: get_tag_multiple() in gexiv2 currently does not work with EXIF tags (as EXIF can
// never return a list of strings). It will quietly return NULL if attempted. Until fixed
// (there or here), don't use this function to access EXIF. See:
- // http://trac.yorba.org/ticket/2966
+ // https://gitlab.gnome.org/GNOME/gexiv2/issues/10
public Gee.List<string>? get_string_multiple(string tag) {
- string[] values = exiv2.get_tag_multiple(tag);
- if (values == null || values.length == 0)
- return null;
-
- Gee.List<string> list = new Gee.ArrayList<string>();
-
- Gee.HashSet<string> collection = new Gee.HashSet<string>();
- foreach (string value in values) {
- string? prepped = prepare_input_text(value, PREPARE_STRING_OPTIONS,
- DEFAULT_USER_TEXT_INPUT_LENGTH);
-
- if (prepped != null && !collection.contains(prepped)) {
- list.add(prepped);
- collection.add(prepped);
+ try {
+ string[] values = exiv2.try_get_tag_multiple(tag);
+ if (values == null || values.length == 0)
+ return null;
+
+ Gee.List<string> list = new Gee.ArrayList<string>();
+
+ Gee.HashSet<string> collection = new Gee.HashSet<string>();
+ foreach (string value in values) {
+ string? prepped = prepare_input_text(value, PREPARE_STRING_OPTIONS,
+ DEFAULT_USER_TEXT_INPUT_LENGTH);
+
+ if (prepped != null && !collection.contains(prepped)) {
+ list.add(prepped);
+ collection.add(prepped);
+ }
}
+
+ return list.size > 0 ? list : null;
+ } catch (Error error) {
+ return null;
}
-
- return list.size > 0 ? list : null;
}
// Returns a List that has been filtered through a Set, so no duplicates will be found.
@@ -496,7 +520,7 @@ public class PhotoMetadata : MediaMetadata {
// NOTE: get_tag_multiple() in gexiv2 currently does not work with EXIF tags (as EXIF can
// never return a list of strings). It will quietly return NULL if attempted. Until fixed
// (there or here), don't use this function to access EXIF. See:
- // http://trac.yorba.org/ticket/2966
+ // https://gitlab.gnome.org/GNOME/gexiv2/issues/10
public Gee.List<string>? get_first_string_multiple(string[] tags) {
foreach (string tag in tags) {
Gee.List<string>? values = get_string_multiple(tag);
@@ -507,16 +531,20 @@ public class PhotoMetadata : MediaMetadata {
return null;
}
- public void set_string(string tag, string value, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS) {
- string? prepped = prepare_input_text(value, options, DEFAULT_USER_TEXT_INPUT_LENGTH);
+ public void set_string(string tag, string value, PrepareInputTextOptions options = PREPARE_STRING_OPTIONS,
+ int length = DEFAULT_USER_TEXT_INPUT_LENGTH) {
+ string? prepped = prepare_input_text(value, options, length);
if (prepped == null) {
warning("Not setting tag %s to string %s: invalid UTF-8", tag, value);
return;
}
- if (!exiv2.set_tag_string(tag, prepped))
- warning("Unable to set tag %s to string %s from source %s", tag, value, source_name);
+ try {
+ exiv2.try_set_tag_string(tag, prepped);
+ } catch (Error error) {
+ warning("Unable to set tag %s to string %s from source %s: %s", tag, value, source_name, error.message);
+ }
}
private delegate void SetGenericValue(string tag);
@@ -562,13 +590,16 @@ public class PhotoMetadata : MediaMetadata {
return;
// append a null pointer to the end of the string array -- this is a necessary
- // workaround for http://trac.yorba.org/ticket/3264. See also
- // http://trac.yorba.org/ticket/3257, which describes the user-visible behavior
- // seen in the Flickr Connector as a result of the former bug.
+ // workaround for https://bugzilla.gnome.org/show_bug.cgi?id=712479. See also
+ // https://bugzilla.gnome.org/show_bug.cgi?id=717438, which describes the
+ // user-visible behavior seen in the Flickr Connector as a result of the former bug.
values += null;
- if (!exiv2.set_tag_multiple(tag, values))
- warning("Unable to set %d strings to tag %s from source %s", values.length, tag, source_name);
+ try {
+ exiv2.try_set_tag_multiple(tag, values);
+ } catch (Error err) {
+ warning("Unable to set %d strings to tag %s from source %s: %s", values.length, tag, source_name, err.message);
+ }
}
public void set_all_string_multiple(string[] tags, Gee.Collection<string> values, SetOption option) {
@@ -576,13 +607,16 @@ public class PhotoMetadata : MediaMetadata {
}
public bool get_long(string tag, out long value) {
+ value = 0;
if (!has_tag(tag)) {
- value = 0;
-
return false;
}
- value = exiv2.get_tag_long(tag);
+ try {
+ value = exiv2.try_get_tag_long(tag);
+ } catch (Error error) {
+ return false;
+ }
return true;
}
@@ -599,8 +633,11 @@ public class PhotoMetadata : MediaMetadata {
}
public void set_long(string tag, long value) {
- if (!exiv2.set_tag_long(tag, value))
- warning("Unable to set tag %s to long %ld from source %s", tag, value, source_name);
+ try {
+ exiv2.try_set_tag_long(tag, value);
+ } catch (Error err) {
+ warning("Unable to set tag %s to long %ld from source %s: %s", tag, value, source_name, err.message);
+ }
}
public void set_all_long(string[] tags, long value, SetOption option) {
@@ -609,11 +646,19 @@ public class PhotoMetadata : MediaMetadata {
public bool get_rational(string tag, out MetadataRational rational) {
int numerator, denominator;
- bool result = exiv2.get_exif_tag_rational(tag, out numerator, out denominator);
-
- rational = MetadataRational(numerator, denominator);
-
- return result;
+ try {
+ if (exiv2.try_get_exif_tag_rational(tag, out numerator, out denominator)) {
+ rational = MetadataRational(numerator, denominator);
+ } else {
+ rational = MetadataRational.invalid();
+ return false;
+ }
+ } catch (Error error) {
+ rational = MetadataRational.invalid();
+ return false;
+ }
+
+ return true;
}
public bool get_first_rational(string[] tags, out MetadataRational rational) {
@@ -628,9 +673,11 @@ public class PhotoMetadata : MediaMetadata {
}
public void set_rational(string tag, MetadataRational rational) {
- if (!exiv2.set_exif_tag_rational(tag, rational.numerator, rational.denominator)) {
- warning("Unable to set tag %s to rational %s from source %s", tag, rational.to_string(),
- source_name);
+ try {
+ exiv2.try_set_exif_tag_rational(tag, rational.numerator, rational.denominator);
+ } catch (Error err) {
+ warning("Unable to set tag %s to rational %s from source %s: %s", tag, rational.to_string(),
+ source_name, err.message);
}
}
@@ -769,7 +816,10 @@ public class PhotoMetadata : MediaMetadata {
}
public void remove_exif_thumbnail() {
- exiv2.erase_exif_thumbnail();
+ try {
+ exiv2.try_erase_exif_thumbnail();
+ } catch (Error err) { }
+
if (exif != null) {
Exif.Mem.new_default().free(exif.data);
exif.data = null;
@@ -778,7 +828,9 @@ public class PhotoMetadata : MediaMetadata {
}
public void remove_tag(string tag) {
- exiv2.clear_tag(tag);
+ try {
+ exiv2.try_clear_tag(tag);
+ } catch (Error err){}
}
public void remove_tags(string[] tags) {
@@ -799,6 +851,9 @@ public class PhotoMetadata : MediaMetadata {
case MetadataDomain.IPTC:
exiv2.clear_iptc();
break;
+ default:
+ // Just ignore any unknown tags
+ break;
}
}
@@ -881,7 +936,7 @@ public class PhotoMetadata : MediaMetadata {
public static string[] HEIGHT_TAGS = {
"Exif.Photo.PixelYDimension",
"Xmp.exif.PixelYDimension",
- "Xmp.tiff.ImageHeight",
+ "Xmp.tiff.ImageLength",
"Xmp.exif.PixelYDimension"
};
@@ -923,7 +978,7 @@ public class PhotoMetadata : MediaMetadata {
// (sometimes) appropriate tag for the description. And there's general confusion about
// whether Exif.Image.ImageDescription is a description (which is what the tag name
// suggests) or a title (which is what the specification states).
- // See: http://trac.yorba.org/wiki/PhotoTags
+ // See: https://wiki.gnome.org/Apps/Shotwell/PhotoTags
//
// Hence, the following logic tries to do the right thing in most of these cases. If
// the iPhoto title tag is detected, it and the iPhoto description tag are used. Otherwise,
@@ -997,8 +1052,9 @@ public class PhotoMetadata : MediaMetadata {
* newlines from comments */
if (!is_string_empty(comment))
set_all_generic(COMMENT_TAGS, option, (tag) => {
+ // 4095 is coming from acdsee.notes which is limited to that
set_string(tag, comment, PREPARE_STRING_OPTIONS &
- ~PrepareInputTextOptions.STRIP_CRLF);
+ ~PrepareInputTextOptions.STRIP_CRLF, 4095);
});
else
remove_tags(COMMENT_TAGS);
@@ -1139,24 +1195,37 @@ public class PhotoMetadata : MediaMetadata {
}
public bool has_orientation() {
- return exiv2.get_orientation() == GExiv2.Orientation.UNSPECIFIED;
+ try {
+ return exiv2.try_get_orientation() == GExiv2.Orientation.UNSPECIFIED;
+ } catch (Error err) {
+ debug("Failed to get orientation: %s", err.message);
+ return false;
+ }
}
// If not present, returns Orientation.TOP_LEFT.
public Orientation get_orientation() {
// GExiv2.Orientation is the same value-wise as Orientation, with one exception:
// GExiv2.Orientation.UNSPECIFIED must be handled
- GExiv2.Orientation orientation = exiv2.get_orientation();
- if (orientation == GExiv2.Orientation.UNSPECIFIED || orientation < Orientation.MIN ||
- orientation > Orientation.MAX)
+ try {
+ GExiv2.Orientation orientation = exiv2.try_get_orientation();
+ if (orientation == GExiv2.Orientation.UNSPECIFIED || orientation < Orientation.MIN ||
+ orientation > Orientation.MAX)
+ return Orientation.TOP_LEFT;
+ else
+ return (Orientation) orientation;
+ } catch (Error error) {
return Orientation.TOP_LEFT;
- else
- return (Orientation) orientation;
+ }
}
public void set_orientation(Orientation orientation) {
// GExiv2.Orientation is the same value-wise as Orientation
- exiv2.set_orientation((GExiv2.Orientation) orientation);
+ try {
+ exiv2.try_set_orientation((GExiv2.Orientation) orientation);
+ } catch (Error err) {
+ debug("Failed to set the orientation: %s", err.message);
+ }
}
public bool get_gps(out double longitude, out string long_ref, out double latitude, out string lat_ref,
@@ -1164,14 +1233,22 @@ public class PhotoMetadata : MediaMetadata {
longitude = 0.0;
latitude = 0.0;
altitude = 0.0;
- if (!exiv2.get_gps_longitude(out longitude) || !exiv2.get_gps_latitude(out latitude)) {
- long_ref = null;
- lat_ref = null;
-
- return false;
+ try {
+ if (!exiv2.try_get_gps_longitude(out longitude) || !exiv2.try_get_gps_latitude(out latitude)) {
+ long_ref = null;
+ lat_ref = null;
+
+ return false;
+ }
+ } catch (Error err) {
+ debug("Failed to get GPS lon/lat: %s", err.message);
}
- exiv2.get_gps_altitude(out altitude);
+ try {
+ exiv2.try_get_gps_altitude(out altitude);
+ } catch (Error err) {
+ debug("Failed to get GPS altitude: %s", err.message);
+ }
long_ref = get_string("Exif.GPSInfo.GPSLongitudeRef");
lat_ref = get_string("Exif.GPSInfo.GPSLatitudeRef");
@@ -1179,6 +1256,37 @@ public class PhotoMetadata : MediaMetadata {
return true;
}
+ public GpsCoords get_gps_coords() {
+ GpsCoords gps_coords = GpsCoords();
+ try {
+ double altitude;
+ gps_coords.has_gps = exiv2.try_get_gps_info(out gps_coords.longitude, out gps_coords.latitude, out altitude) ? 1 : 0;
+ if (gps_coords.has_gps > 0) {
+ if (get_string("Exif.GPSInfo.GPSLongitudeRef") == "W" && gps_coords.longitude > 0)
+ gps_coords.longitude = -gps_coords.longitude;
+ if (get_string("Exif.GPSInfo.GPSLatitudeRef") == "S" && gps_coords.latitude > 0)
+ gps_coords.latitude = -gps_coords.latitude;
+ }
+ } catch (Error err) {
+ gps_coords.has_gps = 0;
+ }
+
+ return gps_coords;
+ }
+
+ public void set_gps_coords(GpsCoords gps_coords) {
+ try {
+ if (gps_coords.has_gps > 0) {
+ var altitude = 0.0;
+ exiv2.try_get_gps_altitude(out altitude);
+ exiv2.try_set_gps_info(gps_coords.longitude, gps_coords.latitude, altitude);
+ } else
+ exiv2.try_delete_gps_info();
+ } catch (Error err) {
+ debug("Failed to set or remove GPS info: %s", err.message);
+ }
+ }
+
public bool get_exposure(out MetadataRational exposure) {
return get_rational("Exif.Photo.ExposureTime", out exposure);
}
@@ -1326,7 +1434,7 @@ public class PhotoMetadata : MediaMetadata {
// Other photo managers, notably F-Spot, take hints from Urgency fields about what the rating
// of an imported photo should be, and we have decided to do as well. Xmp.xmp.Rating is the only
// field we've seen photo manages export ratings to, while Urgency fields seem to have a fundamentally
- // different meaning. See http://trac.yorba.org/wiki/PhotoTags#Rating for more information.
+ // different meaning. See https://wiki.gnome.org/Apps/Shotwell/PhotoTags#Rating for more information.
public void set_rating(Rating rating) {
int int_rating = rating.serialize();
set_string("Xmp.xmp.Rating", int_rating.to_string());
diff --git a/src/photos/PngSupport.vala b/src/photos/PngSupport.vala
index c891136..e154fc4 100644
--- a/src/photos/PngSupport.vala
+++ b/src/photos/PngSupport.vala
@@ -88,33 +88,6 @@ public class PngReader : GdkReader {
public PngReader(string filepath) {
base (filepath, PhotoFileFormat.PNG);
}
-
- public override Gdk.Pixbuf scaled_read(Dimensions full, Dimensions scaled) throws Error {
- Gdk.Pixbuf result = null;
- /* if we encounter a situation where there are two orders of magnitude or more of
- difference between the full image size and the scaled size, and if the full image
- size has five or more decimal digits of precision, Gdk.Pixbuf.from_file_at_scale( ) can
- fail due to what appear to be floating-point round-off issues. This isn't surprising,
- since 32-bit floats only have 6-7 decimal digits of precision in their mantissa. In
- this case, we prefetch the image at a larger scale and then downsample it to the
- desired scale as a post-process step. This short-circuits Gdk.Pixbuf's buggy
- scaling code. */
- if (((full.width > 9999) || (full.height > 9999)) && ((scaled.width < 100) ||
- (scaled.height < 100))) {
- Dimensions prefetch_dimensions = full.get_scaled_by_constraint(1000,
- ScaleConstraint.DIMENSIONS);
-
- result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), prefetch_dimensions.width,
- prefetch_dimensions.height, false);
-
- result = result.scale_simple(scaled.width, scaled.height, Gdk.InterpType.HYPER);
- } else {
- result = new Gdk.Pixbuf.from_file_at_scale(get_filepath(), scaled.width,
- scaled.height, false);
- }
-
- return result;
- }
}
public class PngWriter : PhotoFileWriter {
diff --git a/src/photos/RawSupport.vala b/src/photos/RawSupport.vala
index 8c23826..538c949 100644
--- a/src/photos/RawSupport.vala
+++ b/src/photos/RawSupport.vala
@@ -51,7 +51,7 @@ public class RawFileFormatDriver : PhotoFileFormatDriver {
public class RawFileFormatProperties : PhotoFileFormatProperties {
private static string[] KNOWN_EXTENSIONS = {
- "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cap", "iiq", "eip", "dcs", "dcr", "drf",
+ "3fr", "arw", "srf", "sr2", "bay", "crw", "cr2", "cr3", "cap", "iiq", "eip", "dcs", "dcr", "drf",
"k25", "kdc", "dng", "erf", "fff", "mef", "mos", "mrw", "nef", "nrw", "orf", "ptx", "pef",
"pxn", "r3d", "raf", "raw", "rw2", "raw", "rwl", "rwz", "x3f", "srw"
};
@@ -63,6 +63,7 @@ public class RawFileFormatProperties : PhotoFileFormatProperties {
/* manufacturer blessed MIME types */
"image/x-canon-cr2",
+ "image/x-canon-cr3",
"image/x-canon-crw",
"image/x-fuji-raf",
"image/x-adobe-dng",
@@ -85,6 +86,7 @@ public class RawFileFormatProperties : PhotoFileFormatProperties {
"image/x-bay",
"image/x-crw",
"image/x-cr2",
+ "image/x-cr3",
"image/x-cap",
"image/x-iiq",
"image/x-eip",
@@ -174,7 +176,6 @@ public class RawSniffer : PhotoFileSniffer {
try {
processor.open_file(file.get_path());
- processor.unpack();
processor.adjust_sizes_info_only();
} catch (GRaw.Exception exception) {
if (exception is GRaw.Exception.UNSUPPORTED_FILE)
@@ -195,7 +196,7 @@ public class RawSniffer : PhotoFileSniffer {
// ignored
}
- if (detected.metadata != null) {
+ if (calc_md5 && detected.metadata != null) {
detected.exif_md5 = detected.metadata.exif_hash();
detected.thumbnail_md5 = detected.metadata.thumbnail_hash();
}
@@ -211,15 +212,19 @@ public class RawSniffer : PhotoFileSniffer {
}
public class RawReader : PhotoFileReader {
+ private PhotoMetadata? cached_metadata = null;
+
public RawReader(string filepath) {
base (filepath, PhotoFileFormat.RAW);
}
public override PhotoMetadata read_metadata() throws Error {
- PhotoMetadata metadata = new PhotoMetadata();
- metadata.read_from_file(get_file());
-
- return metadata;
+ if (cached_metadata == null) {
+ PhotoMetadata metadata = new PhotoMetadata();
+ metadata.read_from_file(get_file());
+ cached_metadata = metadata;
+ }
+ return cached_metadata;
}
public override Gdk.Pixbuf unscaled_read() throws Error {
diff --git a/src/photos/TiffSupport.vala b/src/photos/TiffSupport.vala
index 7ed8b98..cadcd0e 100644
--- a/src/photos/TiffSupport.vala
+++ b/src/photos/TiffSupport.vala
@@ -151,6 +151,9 @@ private class TiffMetadataWriter : PhotoFileMetadataWriter {
}
}
+private const uint16 FILE_MARKER_TIFF = 42;
+private const uint16 FILE_MARKER_BIGTIFF = 43;
+
public bool is_tiff(File file, Cancellable? cancellable = null) throws Error {
DataInputStream dins = new DataInputStream(file.read());
@@ -173,8 +176,9 @@ public bool is_tiff(File file, Cancellable? cancellable = null) throws Error {
// second two bytes: some random number
uint16 lue = dins.read_uint16(cancellable);
- if (lue != 42)
+ if (lue != FILE_MARKER_TIFF && lue != FILE_MARKER_BIGTIFF) {
return false;
+ }
// remaining bytes are offset of first IFD, which doesn't matter for our purposes
return true;
diff --git a/src/photos/WebPSupport.vala b/src/photos/WebPSupport.vala
new file mode 100644
index 0000000..2f4723c
--- /dev/null
+++ b/src/photos/WebPSupport.vala
@@ -0,0 +1,240 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+namespace Photos {
+
+public class WebpFileFormatDriver : PhotoFileFormatDriver {
+ private static WebpFileFormatDriver instance = null;
+
+ public static void init() {
+ instance = new WebpFileFormatDriver();
+ WebpFileFormatProperties.init();
+ }
+
+ public static WebpFileFormatDriver get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormatProperties get_properties() {
+ return WebpFileFormatProperties.get_instance();
+ }
+
+ public override PhotoFileReader create_reader(string filepath) {
+ return new WebpReader(filepath);
+ }
+
+ public override PhotoMetadata create_metadata() {
+ return new PhotoMetadata();
+ }
+
+ public override bool can_write_image() {
+ return false;
+ }
+
+ public override bool can_write_metadata() {
+ return true;
+ }
+
+ public override PhotoFileWriter? create_writer(string filepath) {
+ return null;
+ }
+
+ public override PhotoFileMetadataWriter? create_metadata_writer(string filepath) {
+ return new WebpMetadataWriter(filepath);
+ }
+
+ public override PhotoFileSniffer create_sniffer(File file, PhotoFileSniffer.Options options) {
+ return new WebpSniffer(file, options);
+ }
+}
+
+private class WebpFileFormatProperties : PhotoFileFormatProperties {
+ private static string[] KNOWN_EXTENSIONS = {
+ "webp"
+ };
+
+ private static string[] KNOWN_MIME_TYPES = {
+ "image/webp"
+ };
+
+ private static WebpFileFormatProperties instance = null;
+
+ public static void init() {
+ instance = new WebpFileFormatProperties();
+ }
+
+ public static WebpFileFormatProperties get_instance() {
+ return instance;
+ }
+
+ public override PhotoFileFormat get_file_format() {
+ return PhotoFileFormat.WEBP;
+ }
+
+ public override PhotoFileFormatFlags get_flags() {
+ return PhotoFileFormatFlags.NONE;
+ }
+
+ public override string get_default_extension() {
+ return "webp";
+ }
+
+ public override string get_user_visible_name() {
+ return _("WebP");
+ }
+
+ public override string[] get_known_extensions() {
+ return KNOWN_EXTENSIONS;
+ }
+
+ public override string get_default_mime_type() {
+ return KNOWN_MIME_TYPES[0];
+ }
+
+ public override string[] get_mime_types() {
+ return KNOWN_MIME_TYPES;
+ }
+}
+
+private class WebpSniffer : PhotoFileSniffer {
+ private DetectedPhotoInformation detected = null;
+
+ public WebpSniffer(File file, PhotoFileSniffer.Options options) {
+ base (file, options);
+ detected = new DetectedPhotoInformation();
+ }
+
+ public override DetectedPhotoInformation? sniff(out bool is_corrupted) throws Error {
+ is_corrupted = false;
+
+ if (!is_webp(file))
+ return null;
+
+ // valac chokes on the ternary operator here
+ Checksum? md5_checksum = null;
+ if (calc_md5)
+ md5_checksum = new Checksum(ChecksumType.MD5);
+
+ detected.metadata = new PhotoMetadata();
+ try {
+ detected.metadata.read_from_file(file);
+ } catch (Error err) {
+ debug("Failed to load meta-data from file: %s", err.message);
+ // no metadata detected
+ detected.metadata = null;
+ }
+
+ if (calc_md5 && detected.metadata != null) {
+ detected.exif_md5 = detected.metadata.exif_hash();
+ detected.thumbnail_md5 = detected.metadata.thumbnail_hash();
+ }
+
+ // if no MD5, don't read as much, as the needed info will probably be gleaned
+ // in the first 8K to 16K
+ uint8[] buffer = calc_md5 ? new uint8[64 * 1024] : new uint8[8 * 1024];
+ size_t count = 0;
+
+ // loop through until all conditions we're searching for are met
+ FileInputStream fins = file.read(null);
+ var ba = new ByteArray();
+ for (;;) {
+ size_t bytes_read = fins.read(buffer, null);
+ if (bytes_read <= 0)
+ break;
+
+ ba.append(buffer[0:bytes_read]);
+
+ count += bytes_read;
+
+ if (calc_md5)
+ md5_checksum.update(buffer, bytes_read);
+
+ WebP.Data d = WebP.Data();
+ d.bytes = ba.data;
+
+ WebP.ParsingState state;
+ var demux = new WebP.Demuxer.partial(d, out state);
+
+ if (state == WebP.ParsingState.PARSE_ERROR) {
+ is_corrupted = true;
+ break;
+ }
+
+ if (state > WebP.ParsingState.PARSED_HEADER) {
+ detected.file_format = PhotoFileFormat.WEBP;
+ detected.format_name = "WebP";
+ detected.channels = 4;
+ detected.bits_per_channel = 8;
+ detected.image_dim.width = (int) demux.get(WebP.FormatFeature.CANVAS_WIDTH);
+ detected.image_dim.height = (int) demux.get(WebP.FormatFeature.CANVAS_HEIGHT);
+
+ // if not searching for anything else, exit
+ if (!calc_md5)
+ break;
+ }
+ }
+
+ if (fins != null)
+ fins.close(null);
+
+ if (calc_md5)
+ detected.md5 = md5_checksum.get_string();
+
+ return detected;
+ }
+}
+
+private class WebpReader : PhotoFileReader {
+ public WebpReader(string filepath) {
+ base (filepath, PhotoFileFormat.WEBP);
+ }
+
+ public override PhotoMetadata read_metadata() throws Error {
+ PhotoMetadata metadata = new PhotoMetadata();
+ metadata.read_from_file(get_file());
+
+ return metadata;
+ }
+
+ public override Gdk.Pixbuf unscaled_read() throws Error {
+ uint8[] buffer;
+
+ FileUtils.get_data(this.get_filepath(), out buffer);
+ int width, height;
+ var pixdata = WebP.DecodeRGBA(buffer, out width, out height);
+ pixdata.length = width * height * 4;
+
+ return new Gdk.Pixbuf.from_data(pixdata, Gdk.Colorspace.RGB, true, 8, width, height, width * 4);
+ }
+}
+
+private class WebpMetadataWriter : PhotoFileMetadataWriter {
+ public WebpMetadataWriter(string filepath) {
+ base (filepath, PhotoFileFormat.WEBP);
+ }
+
+ public override void write_metadata(PhotoMetadata metadata) throws Error {
+ metadata.write_to_file(get_file());
+ }
+}
+
+public bool is_webp(File file, Cancellable? cancellable = null) throws Error {
+ var ins = file.read();
+
+ uint8 buffer[12];
+ try {
+ ins.read(buffer, null);
+ if (buffer[0] == 'R' && buffer[1] == 'I' && buffer[2] == 'F' && buffer[3] == 'F' &&
+ buffer[8] == 'W' && buffer[9] == 'E' && buffer[10] == 'B' && buffer[11] == 'P')
+ return true;
+ } catch (Error error) {
+ debug ("Failed to read from file %s: %s", file.get_path (), error.message);
+ }
+
+ return false;
+}
+
+}
diff --git a/src/plugins/DataImportsInterfaces.vala b/src/plugins/DataImportsInterfaces.vala
index f2c8a53..518f8d0 100644
--- a/src/plugins/DataImportsInterfaces.vala
+++ b/src/plugins/DataImportsInterfaces.vala
@@ -120,7 +120,7 @@ public interface ImportableMediaItem : GLib.Object {
public abstract string get_filename();
- public abstract time_t? get_exposure_time();
+ public abstract DateTime? get_exposure_time();
}
/**
@@ -416,7 +416,7 @@ public interface PluginHost : GLib.Object, Spit.HostInterface {
* @param host_progress_delta the amount of progress the host should update
* the progress bar during import preparation. Plugins should ensure that
* a proportion of progress for each media item is set aside for the host
- * in oder to ensure a smoother update to the progress bar.
+ * in order to ensure a smoother update to the progress bar.
*
* @param progress_message the text to be displayed below the progress bar. If that
* parameter is null, the message will be left unchanged.
diff --git a/src/plugins/ManifestWidget.vala b/src/plugins/ManifestWidget.vala
index 8fb0ba2..55ccdc3 100644
--- a/src/plugins/ManifestWidget.vala
+++ b/src/plugins/ManifestWidget.vala
@@ -10,10 +10,7 @@ namespace Plugins {
[GtkTemplate (ui = "/org/gnome/Shotwell/ui/manifest_widget.ui")]
public class ManifestWidgetMediator : Gtk.Box {
[GtkChild]
- private Gtk.Button about_button;
-
- [GtkChild]
- private Gtk.ScrolledWindow list_bin;
+ private unowned Gtk.ScrolledWindow list_bin;
private ManifestListView list = new ManifestListView();
@@ -21,247 +18,212 @@ public class ManifestWidgetMediator : Gtk.Box {
Object();
list_bin.add(list);
-
- about_button.clicked.connect(on_about);
- list.get_selection().changed.connect(on_selection_changed);
-
- set_about_button_sensitivity();
+ }
+}
+
+private class CollectionModel<G> : GLib.ListModel, Object {
+ private Gee.Collection<G> target;
+ private unowned Gee.List<G>? as_list = null;
+
+ public CollectionModel(Gee.Collection<G> target) {
+ Object();
+ this.target = target.read_only_view;
+ if (this.target is Gee.List) {
+ this.as_list = (Gee.List<G>)this.target;
+ }
}
-
- private void on_about() {
- string[] ids = list.get_selected_ids();
- if (ids.length == 0)
- return;
-
- string id = ids[0];
-
- Spit.PluggableInfo info = Spit.PluggableInfo();
- if (!get_pluggable_info(id, ref info)) {
- warning("Unable to retrieve information for plugin %s", id);
-
- return;
+
+ GLib.Object? get_item(uint position) {
+ if (position >= this.target.size) {
+ return null;
}
-
- // prepare authors names (which are comma-delimited by the plugin) for the about box
- // (which wants an array of names)
- string[]? authors = null;
- if (info.authors != null) {
- string[] split = info.authors.split(",");
- for (int ctr = 0; ctr < split.length; ctr++) {
- string stripped = split[ctr].strip();
- if (!is_string_empty(stripped)) {
- if (authors == null)
- authors = new string[0];
-
- authors += stripped;
- }
+
+ if (this.as_list != null) {
+ return (GLib.Object) this.as_list.@get((int) position);
+ }
+
+ var count = 0U;
+ foreach (var g in this.target) {
+ if (count == position) {
+ return (GLib.Object)g;
}
+ count++;
}
-
- Gtk.AboutDialog about_dialog = new Gtk.AboutDialog();
- about_dialog.authors = authors;
- about_dialog.comments = info.brief_description;
- about_dialog.copyright = info.copyright;
- about_dialog.license = info.license;
- about_dialog.wrap_license = info.is_license_wordwrapped;
- about_dialog.logo = (info.icons != null && info.icons.length > 0) ? info.icons[0] :
- Resources.get_icon(Resources.ICON_GENERIC_PLUGIN);
- about_dialog.program_name = get_pluggable_name(id);
- about_dialog.translator_credits = info.translators;
- about_dialog.version = info.version;
- about_dialog.website = info.website_url;
- about_dialog.website_label = info.website_name;
-
- about_dialog.run();
-
- about_dialog.destroy();
+
+ return null;
}
-
- private void on_selection_changed() {
- set_about_button_sensitivity();
+
+ GLib.Type get_item_type() {
+ return typeof(G);
}
-
- private void set_about_button_sensitivity() {
- // have to get the array and then get its length rather than do so in one call due to a
- // bug in Vala 0.10:
- // list.get_selected_ids().length -> uninitialized value
- // this appears to be fixed in Vala 0.11
- string[] ids = list.get_selected_ids();
- about_button.sensitive = (ids.length == 1);
+
+ uint get_n_items() {
+ return this.target.size;
}
+
}
-private class ManifestListView : Gtk.TreeView {
- private const int ICON_SIZE = 24;
- private const int ICON_X_PADDING = 6;
- private const int ICON_Y_PADDING = 2;
-
- private enum Column {
- ENABLED,
- CAN_ENABLE,
- ICON,
- NAME,
- ID,
- N_COLUMNS
+private class Selection : Object {
+ public signal void changed();
+}
+
+private class PluggableRow : Gtk.Box {
+ public Spit.Pluggable pluggable { get; construct; }
+ public bool enabled {get; construct; }
+
+ public PluggableRow(Spit.Pluggable pluggable_, bool enable_) {
+ Object(orientation: Gtk.Orientation.VERTICAL, pluggable: pluggable_,
+ enabled: enable_, margin_top: 6, margin_bottom:6, margin_start:6, margin_end:6);
}
-
- private Gtk.TreeStore store = new Gtk.TreeStore(Column.N_COLUMNS,
- typeof(bool), // ENABLED
- typeof(bool), // CAN_ENABLE
- typeof(Gdk.Pixbuf), // ICON
- typeof(string), // NAME
- typeof(string) // ID
- );
-
- public ManifestListView() {
- set_model(store);
-
- Gtk.CellRendererToggle checkbox_renderer = new Gtk.CellRendererToggle();
- checkbox_renderer.radio = false;
- checkbox_renderer.activatable = true;
-
- Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
- icon_renderer.stock_size = Gtk.IconSize.MENU;
- icon_renderer.xpad = ICON_X_PADDING;
- icon_renderer.ypad = ICON_Y_PADDING;
-
- Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
-
- Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
- column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE);
- column.pack_start(checkbox_renderer, false);
- column.pack_start(icon_renderer, false);
- column.pack_end(text_renderer, true);
-
- column.add_attribute(checkbox_renderer, "active", Column.ENABLED);
- column.add_attribute(checkbox_renderer, "visible", Column.CAN_ENABLE);
- column.add_attribute(icon_renderer, "pixbuf", Column.ICON);
- column.add_attribute(text_renderer, "text", Column.NAME);
-
- append_column(column);
+
+ public override void constructed() {
+ base.constructed();
+ var content = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6);
+ pack_start(content, true);
+
+ var revealer = new Gtk.Revealer();
+ revealer.margin_top = 6;
+ pack_end(revealer, true);
- set_headers_visible(false);
- set_enable_search(false);
- set_show_expanders(true);
- set_reorderable(false);
- set_enable_tree_lines(false);
- set_grid_lines(Gtk.TreeViewGridLines.NONE);
- get_selection().set_mode(Gtk.SelectionMode.BROWSE);
+ var info = pluggable.get_info();
- Gtk.IconTheme icon_theme = Resources.get_icon_theme_engine();
+ var image = new Gtk.Image.from_icon_name(info.icon_name, Gtk.IconSize.BUTTON);
+ content.pack_start(image, false, false, 6);
+ image.hexpand = false;
+
+ var label = new Gtk.Label(pluggable.get_pluggable_name());
+ label.halign = Gtk.Align.START;
+ content.pack_start(label, true, true, 6);
+
+ var button = new Gtk.ToggleButton();
+ button.get_style_context().add_class("flat");
+ content.pack_end(button, false, false, 6);
+ button.bind_property("active", revealer, "reveal-child", BindingFlags.DEFAULT);
+ image = new Gtk.Image.from_icon_name("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
+ button.add(image);
+
+ var plugin_enabled = new Gtk.Switch();
+ plugin_enabled.hexpand = false;
+ plugin_enabled.vexpand = false;
+ plugin_enabled.valign = Gtk.Align.CENTER;
+ plugin_enabled.set_active(enabled);
+
+ content.pack_end(plugin_enabled, false, false, 6);
+ plugin_enabled.notify["active"].connect(() => {
+ var id = pluggable.get_id();
+ set_pluggable_enabled(id, plugin_enabled.active);
+ });
+
+ if (pluggable is Spit.Publishing.Service) {
+#if 0
+ var manage = new Gtk.Button.from_icon_name("avatar-default-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
+ manage.get_style_context().add_class("flat");
+ // TRANSLATORS: %s is the name of an online service such as YouTube, Mastodon, ...
+ manage.set_tooltip_text(_("Manage accounts for %s").printf(pluggable.get_pluggable_name()));
+ content.pack_start(manage, false, false, 6);
+#endif
+ }
+
+ var grid = new Gtk.Grid();
+ grid.get_style_context().add_class("content");
+ grid.set_row_spacing(12);
+ grid.set_column_spacing(6);
+ revealer.add(grid);
+ label = new Gtk.Label(info.copyright);
+ label.hexpand = true;
+ label.halign = Gtk.Align.START;
+ grid.attach(label, 0, 0, 2, 1);
+ label = new Gtk.Label(_("Authors"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ label.margin_start = 12;
+ grid.attach(label, 0, 1, 1, 1);
+ label = new Gtk.Label(info.authors);
+ label.halign = Gtk.Align.START;
+ label.hexpand = true;
+ grid.attach(label, 1, 1, 1, 1);
+
+ label = new Gtk.Label(_("Version"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ label.margin_start = 12;
+ grid.attach(label, 0, 2, 1, 1);
+ label = new Gtk.Label(info.version);
+ label.halign = Gtk.Align.START;
+ label.hexpand = true;
+ grid.attach(label, 1, 2, 1, 1);
+
+ label = new Gtk.Label(_("License"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ label.margin_start = 12;
+ grid.attach(label, 0, 3, 1, 1);
+ var link = new Gtk.LinkButton.with_label(info.license_url, info.license_blurp);
+ link.halign = Gtk.Align.START;
+ // remove the annoying padding around the link
+ link.get_style_context().remove_class("text-button");
+ link.get_style_context().add_class("shotwell-plain-link");
+ grid.attach(link, 1, 3, 1, 1);
+
+ label = new Gtk.Label(_("Website"));
+ label.get_style_context().add_class("dim-label");
+ label.halign = Gtk.Align.END;
+ label.margin_start = 12;
+ grid.attach(label, 0, 4, 1, 1);
+ link = new Gtk.LinkButton.with_label(info.website_url, info.website_name);
+ link.halign = Gtk.Align.START;
+ // remove the annoying padding around the link
+ link.get_style_context().remove_class("text-button");
+ link.get_style_context().add_class("shotwell-plain-link");
+ grid.attach(link, 1, 4, 1, 1);
- // create a list of plugins (sorted by name) that are separated by extension points (sorted
- // by name)
- foreach (ExtensionPoint extension_point in get_extension_points(compare_extension_point_names)) {
- Gtk.TreeIter category_iter;
- store.append(out category_iter, null);
-
- Gdk.Pixbuf? icon = null;
- if (extension_point.icon_name != null) {
- Gtk.IconInfo? icon_info = icon_theme.lookup_by_gicon(
- new ThemedIcon(extension_point.icon_name), ICON_SIZE, 0);
- if (icon_info != null) {
- try {
- icon = icon_info.load_icon();
- } catch (Error err) {
- warning("Unable to load icon %s: %s", extension_point.icon_name, err.message);
- }
- }
- }
-
- store.set(category_iter, Column.NAME, extension_point.name, Column.CAN_ENABLE, false,
- Column.ICON, icon);
-
- Gee.Collection<Spit.Pluggable> pluggables = get_pluggables_for_type(
- extension_point.pluggable_type, compare_pluggable_names, true);
- foreach (Spit.Pluggable pluggable in pluggables) {
+ }
+}
+
+private class ManifestListView : Gtk.Box {
+ public ManifestListView() {
+ Object(orientation: Gtk.Orientation.VERTICAL, spacing: 6);
+ }
+
+ public signal void row_selected(Spit.Pluggable? pluggable);
+
+ public override void constructed() {
+ base.constructed();
+
+ foreach (var extension_point in get_extension_points(compare_extension_point_names)) {
+ var label = new Gtk.Label(null);
+ label.set_markup("<span weight=\"bold\">%s</span>".printf(extension_point.name));
+ label.halign = Gtk.Align.START;
+ label.hexpand = true;
+ add(label);
+
+ var pluggables = get_pluggables_for_type(extension_point.pluggable_type, compare_pluggable_names, true);
+ var box = new Gtk.ListBox();
+ box.set_selection_mode(Gtk.SelectionMode.NONE);
+ box.hexpand = true;
+ box.margin_start = 12;
+ box.margin_end = 12;
+
+ var added = 0;
+ foreach (var pluggable in pluggables) {
bool enabled;
+
if (!get_pluggable_enabled(pluggable.get_id(), out enabled))
continue;
-
- Spit.PluggableInfo info = Spit.PluggableInfo();
- pluggable.get_info(ref info);
-
- icon = (info.icons != null && info.icons.length > 0)
- ? info.icons[0]
- : Resources.get_icon(Resources.ICON_GENERIC_PLUGIN, ICON_SIZE);
-
- Gtk.TreeIter plugin_iter;
- store.append(out plugin_iter, category_iter);
-
- store.set(plugin_iter, Column.ENABLED, enabled, Column.NAME, pluggable.get_pluggable_name(),
- Column.ID, pluggable.get_id(), Column.CAN_ENABLE, true, Column.ICON, icon);
+
+ var pluggable_row = new PluggableRow(pluggable, enabled);
+
+ added++;
+ box.insert(pluggable_row, -1);
+ }
+ if (added > 0) {
+ add(box);
}
}
-
- expand_all();
- }
-
- public string[] get_selected_ids() {
- string[] ids = new string[0];
-
- List<Gtk.TreePath> selected = get_selection().get_selected_rows(null);
- foreach (Gtk.TreePath path in selected) {
- Gtk.TreeIter iter;
- string? id = get_id_at_path(path, out iter);
- if (id != null)
- ids += id;
- }
-
- return ids;
- }
-
- private string? get_id_at_path(Gtk.TreePath path, out Gtk.TreeIter iter) {
- if (!store.get_iter(out iter, path))
- return null;
-
- unowned string id;
- store.get(iter, Column.ID, out id);
-
- return id;
- }
- // Because we want each row to left-align and not for each column to line up in a grid
- // (otherwise the checkboxes -- hidden or not -- would cause the rest of the row to line up
- // along the icon's left edge), we put all the renderers into a single column. However, the
- // checkbox renderer then triggers its "toggle" signal any time the row is single-clicked,
- // whether or not the actual checkbox hit-tests.
- //
- // The only way found to work around this is to capture the button-down event and do our own
- // hit-testing.
- public override bool button_press_event(Gdk.EventButton event) {
- Gtk.TreePath path;
- Gtk.TreeViewColumn col;
- int cellx;
- int celly;
- if (!get_path_at_pos((int) event.x, (int) event.y, out path, out col, out cellx,
- out celly))
- return base.button_press_event(event);
-
- // Perform custom hit testing as described above. The first cell in the column is offset
- // from the left edge by whatever size the group description icon is allocated (including
- // padding).
- if (cellx < (ICON_SIZE + ICON_X_PADDING) || cellx > (2 * (ICON_X_PADDING + ICON_SIZE)))
- return base.button_press_event(event);
-
- Gtk.TreeIter iter;
- string? id = get_id_at_path(path, out iter);
- if (id == null)
- return base.button_press_event(event);
-
- bool enabled;
- if (!get_pluggable_enabled(id, out enabled))
- return base.button_press_event(event);
-
- // toggle and set
- enabled = !enabled;
- set_pluggable_enabled(id, enabled);
-
- store.set(iter, Column.ENABLED, enabled);
-
- return true;
+ show_all();
}
-}
+}
}
diff --git a/src/plugins/Plugins.vala b/src/plugins/Plugins.vala
index 6aff461..cfab7e8 100644
--- a/src/plugins/Plugins.vala
+++ b/src/plugins/Plugins.vala
@@ -6,10 +6,6 @@
namespace Plugins {
-// GModule doesn't have a truly generic way to determine if a file is a shared library by extension,
-// so these are hard-coded
-private const string[] SHARED_LIB_EXTS = { "so", "la" };
-
// Although not expecting this system to last very long, these ranges declare what versions of this
// interface are supported by the current implementation.
private const int MIN_SPIT_INTERFACE = 0;
@@ -39,8 +35,12 @@ private class ModuleRep {
private ModuleRep(File file) {
this.file = file;
-
+
+#if VALA_0_46
+ module = Module.open(file.get_path(), ModuleFlags.LAZY);
+#else
module = Module.open(file.get_path(), ModuleFlags.BIND_LAZY);
+#endif
}
~ModuleRep() {
@@ -221,7 +221,7 @@ public string? get_pluggable_module_id(Spit.Pluggable needle) {
return (module_rep != null) ? module_rep.spit_module.get_id() : null;
}
-public Gee.Collection<ExtensionPoint> get_extension_points(owned CompareDataFunc? compare_func = null) {
+public Gee.Collection<ExtensionPoint> get_extension_points(owned CompareDataFunc<ExtensionPoint>? compare_func = null) {
Gee.Collection<ExtensionPoint> sorted = new Gee.TreeSet<ExtensionPoint>((owned) compare_func);
sorted.add_all(extension_points.values);
@@ -229,7 +229,7 @@ public Gee.Collection<ExtensionPoint> get_extension_points(owned CompareDataFunc
}
public Gee.Collection<Spit.Pluggable> get_pluggables_for_type(Type type,
- owned CompareDataFunc? compare_func = null, bool include_disabled = false) {
+ owned CompareDataFunc<Spit.Pluggable>? compare_func = null, bool include_disabled = false) {
// if this triggers it means the extension point didn't register itself at init() time
assert(extension_points.has_key(type));
@@ -252,12 +252,14 @@ public string? get_pluggable_name(string id) {
? pluggable_rep.pluggable.get_pluggable_name() : null;
}
-public bool get_pluggable_info(string id, ref Spit.PluggableInfo info) {
+public bool get_pluggable_info(string id, out Spit.PluggableInfo info) {
PluggableRep? pluggable_rep = pluggable_table.get(id);
- if (pluggable_rep == null || !pluggable_rep.activated)
+ if (pluggable_rep == null || !pluggable_rep.activated) {
+ info = null;
return false;
+ }
- pluggable_rep.pluggable.get_info(ref info);
+ info = pluggable_rep.pluggable.get_info();
return true;
}
@@ -290,30 +292,19 @@ public File get_pluggable_module_file(Spit.Pluggable pluggable) {
return (module_rep != null) ? module_rep.file : null;
}
-public int compare_pluggable_names(void *a, void *b) {
- Spit.Pluggable *apluggable = (Spit.Pluggable *) a;
- Spit.Pluggable *bpluggable = (Spit.Pluggable *) b;
-
- return apluggable->get_pluggable_name().collate(bpluggable->get_pluggable_name());
+public int compare_pluggable_names(Spit.Pluggable a, Spit.Pluggable b) {
+ return a.get_pluggable_name().collate(b.get_pluggable_name());
}
-public int compare_extension_point_names(void *a, void *b) {
- ExtensionPoint *apoint = (ExtensionPoint *) a;
- ExtensionPoint *bpoint = (ExtensionPoint *) b;
-
- return apoint->name.collate(bpoint->name);
+public int compare_extension_point_names(ExtensionPoint a, ExtensionPoint b) {
+ return a.name.collate(b.name);
}
private bool is_shared_library(File file) {
string name, ext;
disassemble_filename(file.get_basename(), out name, out ext);
-
- foreach (string shared_ext in SHARED_LIB_EXTS) {
- if (ext == shared_ext)
- return true;
- }
-
- return false;
+
+ return ext == Module.SUFFIX;
}
private void search_for_plugins(File dir) throws Error {
diff --git a/src/plugins/PublishingInterfaces.vala b/src/plugins/PublishingInterfaces.vala
index 6518142..05b161f 100644
--- a/src/plugins/PublishingInterfaces.vala
+++ b/src/plugins/PublishingInterfaces.vala
@@ -9,7 +9,7 @@
*
* The Shotwell Pluggable Publishing API allows you to write plugins that upload
* photos and videos to web services. The Shotwell distribution includes publishing
- * support for four core services: Facebook, Flickr, Picasa Web Albums, and YouTube.
+ * support for three core services: Flickr, Google Photos, and YouTube.
* To enable Shotwell to connect to additional services, developers like you write
* publishing plugins, dynamically-loadable shared objects that are linked into the
* Shotwell process at runtime. Publishing plugins are just one of several kinds of
@@ -87,7 +87,7 @@ public errordomain PublishingError {
/**
* Indicates that a secure connection to the remote host cannot be
* established. This might have various reasons such as expired
- * certificats, invalid certificates, self-signed certificates...
+ * certificates, invalid certificates, self-signed certificates...
*/
SSL_FAILED
}
@@ -268,6 +268,8 @@ public interface PluginHost : GLib.Object, Spit.HostInterface {
CANCEL = 1
}
+ public abstract string get_current_profile_id();
+
/**
* Notifies the user that an unrecoverable publishing error has occurred and halts
* the publishing process.
@@ -367,7 +369,7 @@ public interface PluginHost : GLib.Object, Spit.HostInterface {
* The text displayed depends on the type of media the current publishing service
* supports. To provide visual consistency across publishing services and to allow
* Shotwell to handle internationalization, always use this convenience method; don’t
- * contruct and install success panes manually.
+ * construct and install success panes manually.
*
* If an error has posted, the {@link PluginHost} will not honor
* this request.
@@ -413,7 +415,7 @@ public interface PluginHost : GLib.Object, Spit.HostInterface {
* the callback 'on_login_clicked'. Every Publisher should provide a welcome pane to
* introduce the service and explain service-specific features or restrictions. To provide
* visual consistency across publishing services and to allow Shotwell to handle
- * internationalization, always use this convenience method; don’t contruct and install
+ * internationalization, always use this convenience method; don’t construct and install
* welcome panes manually.
*
* If an error has posted, the {@link PluginHost} will not honor this request.
@@ -565,6 +567,11 @@ public interface Publishable : GLib.Object {
*/
public abstract GLib.DateTime get_exposure_date_time();
+ /**
+ * Returns the rating on the file.
+ */
+ public abstract uint get_rating();
+
//
// For future expansion.
//
@@ -578,6 +585,17 @@ public interface Publishable : GLib.Object {
protected virtual void reserved7() {}
}
+public interface Account : Object {
+ public abstract string display_name();
+}
+
+public class DefaultAccount : Spit.Publishing.Account, Object {
+ public string display_name() {
+ return "";
+ }
+}
+
+
/**
* Describes the features and capabilities of a remote publishing service.
*
@@ -590,10 +608,26 @@ public interface Service : Object, Spit.Pluggable {
*/
public abstract Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host);
+ public virtual Spit.Publishing.Publisher create_publisher_with_account(Spit.Publishing.PluginHost host,
+ Spit.Publishing.Account? account) {
+ return this.create_publisher(host);
+ }
+
/**
* Returns the kinds of media that this service can work with.
*/
public abstract Spit.Publishing.Publisher.MediaType get_supported_media();
+
+ /**
+ * Returns a list of accounts associated with the service
+ * Returns: null if there are no accounts, identifier
+ */
+ public virtual Gee.List<Account>? get_accounts(string profile_id) {
+ var list = new Gee.ArrayList<Account>();
+ list.add(new DefaultAccount());
+
+ return list;
+ }
//
// For future expansion.
@@ -617,6 +651,8 @@ public interface Authenticator : Object {
public abstract void logout();
public abstract void refresh();
+ public abstract void set_accountname(string name);
+
public abstract GLib.HashTable<string, Variant> get_authentication_parameter();
}
diff --git a/src/plugins/SpitInterfaces.vala b/src/plugins/SpitInterfaces.vala
index 3e2c70e..94e6f95 100644
--- a/src/plugins/SpitInterfaces.vala
+++ b/src/plugins/SpitInterfaces.vala
@@ -4,6 +4,8 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
+private extern const string _VERSION;
+
/**
* Shotwell Pluggable Interface Technology (SPIT)
*
@@ -156,27 +158,26 @@ public interface Module : Object {
protected virtual void reserved7() {}
}
+
/**
* A structure holding an assortment of information about a {@link Pluggable}.
*/
-public struct PluggableInfo {
- public string? version;
- public string? brief_description;
+public class PluggableInfo : Object {
+ public string? version {get; set; default = _VERSION; }
+ public string? brief_description {get; set; }
/**
* A comma-delimited list of the authors of this {@link Pluggable}.
*/
- public string? authors;
- public string? copyright;
- public string? license;
- public bool is_license_wordwrapped;
- public string? website_url;
- public string? website_name;
- public string? translators;
- /**
- * An icon representing this plugin at one or more sizes. Shotwell may select an icon
- * according to the size that closest fits the control its being drawn in.
- */
- public Gdk.Pixbuf[]? icons;
+ public string? authors { get; set; }
+ public string? copyright {get; set; }
+ public string? license_blurp { get; set; default = _("LGPL v2.1 or later"); }
+ public string? license_url { get; set; default = "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html"; }
+ public string? website_url {get; set; default = "https://wiki.gnome.org/Apps/Shotwell";}
+ public string? website_name { get; set; default = _("Visit the Shotwell home page");}
+ public string? translators {get; set; default = _("translator-credits"); }
+
+ // Name of an icon in the theme, to be set in the Pluggable implementation
+ public string icon_name {get; set; default = "application-x-addon-symbolic"; }
}
/**
@@ -225,7 +226,7 @@ public interface Pluggable : Object {
/**
* Returns extra information about the Pluggable that is used to identify it to the user.
*/
- public abstract void get_info(ref PluggableInfo info);
+ public abstract PluggableInfo get_info();
/**
* Called when the Pluggable is enabled (activated) or disabled (deactivated).
diff --git a/src/plugins/StandardHostInterface.vala b/src/plugins/StandardHostInterface.vala
index d0f3ed4..aa012ef 100644
--- a/src/plugins/StandardHostInterface.vala
+++ b/src/plugins/StandardHostInterface.vala
@@ -16,20 +16,17 @@ public class StandardHostInterface : Object, Spit.HostInterface {
this.config_domain = config_domain;
config_id = parse_key(pluggable.get_id());
module_file = get_pluggable_module_file(pluggable);
- pluggable.get_info(ref info);
+ info = pluggable.get_info();
}
private static string parse_key(string id) {
// special case: legacy plugins (Web publishers moved into SPIT) have special names
// new plugins will use their full ID
switch (id) {
- case "org.yorba.shotwell.publishing.facebook":
- return "facebook";
-
- case "org.yorba.shotwell.publishing.flickr":
+ case "org.gnome.shotwell.publishing.flickr":
return "flickr";
- case "org.yorba.shotwell.publishing.youtube":
+ case "org.gnome.shotwell.publishing.youtube":
return "youtube";
default:
diff --git a/src/publishing/APIGlue.vala b/src/publishing/APIGlue.vala
index 23c4e8c..56013a2 100644
--- a/src/publishing/APIGlue.vala
+++ b/src/publishing/APIGlue.vala
@@ -126,7 +126,11 @@ public class MediaSourcePublishableWrapper : Spit.Publishing.Publishable, GLib.O
}
public GLib.DateTime get_exposure_date_time() {
- return new GLib.DateTime.from_unix_local(wrapped.get_exposure_time());
+ return wrapped.get_exposure_time().to_local();
+ }
+
+ public uint get_rating() {
+ return wrapped.get_rating();
}
}
diff --git a/src/publishing/LoginWelcomePaneWidget.vala b/src/publishing/LoginWelcomePaneWidget.vala
new file mode 100644
index 0000000..3e9847b
--- /dev/null
+++ b/src/publishing/LoginWelcomePaneWidget.vala
@@ -0,0 +1,45 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Jens Georg <mail@jensge.org>
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+namespace PublishingUI {
+
+[GtkTemplate (ui = "/org/gnome/Shotwell/ui/login_welcome_pane_widget.ui")]
+public class LoginWelcomePane : Spit.Publishing.DialogPane, Gtk.Box {
+ [GtkChild]
+ private unowned Gtk.Button login_button;
+ [GtkChild]
+ private unowned Gtk.Label not_logged_in_label;
+
+ public Gtk.Widget get_widget() {
+ return this;
+ }
+
+ public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
+ return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+ }
+
+ public void on_pane_installed() {
+ }
+
+ public void on_pane_uninstalled() {
+ }
+
+ public signal void login_requested();
+
+ public LoginWelcomePane(string service_welcome_message) {
+ Object();
+
+ login_button.clicked.connect(on_login_clicked);
+ not_logged_in_label.set_use_markup(true);
+ not_logged_in_label.set_markup(service_welcome_message);
+ }
+
+ private void on_login_clicked() {
+ login_requested();
+ }
+}
+} // namespace PublishingUI
diff --git a/src/publishing/ProgressPaneWidget.vala b/src/publishing/ProgressPaneWidget.vala
new file mode 100644
index 0000000..0c89d77
--- /dev/null
+++ b/src/publishing/ProgressPaneWidget.vala
@@ -0,0 +1,44 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Jens Georg <mail@jensge.org>
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+namespace PublishingUI {
+
+[GtkTemplate (ui = "/org/gnome/Shotwell/ui/progress_pane_widget.ui")]
+public class ProgressPane : Spit.Publishing.DialogPane, Gtk.Box {
+ [GtkChild]
+ private unowned Gtk.ProgressBar progress_bar;
+
+ public Gtk.Widget get_widget() {
+ return this;
+ }
+
+ public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
+ return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+ }
+
+ public void on_pane_installed() {
+ }
+
+ public void on_pane_uninstalled() {
+ }
+
+ public void set_text(string text) {
+ progress_bar.set_text(text);
+ }
+
+ public void set_progress(double progress) {
+ progress_bar.set_fraction(progress);
+ }
+
+ public void set_status(string status_text, double progress) {
+ if (status_text != progress_bar.get_text())
+ progress_bar.set_text(status_text);
+
+ set_progress(progress);
+ }
+}
+} // namespace PublishingUI
diff --git a/src/publishing/Publishing.vala b/src/publishing/Publishing.vala
index 455013c..c41e121 100644
--- a/src/publishing/Publishing.vala
+++ b/src/publishing/Publishing.vala
@@ -8,10 +8,9 @@ namespace Publishing {
public void init() throws Error {
string[] core_ids = new string[0];
- core_ids += "org.yorba.shotwell.publishing.facebook";
- core_ids += "org.yorba.shotwell.publishing.flickr";
- core_ids += "org.yorba.shotwell.publishing.youtube";
- core_ids += "org.yorba.shotwell.publishing.gnome-photos";
+ core_ids += "org.gnome.shotwell.publishing.flickr";
+ core_ids += "org.gnome.shotwell.publishing.youtube";
+ core_ids += "org.gnome.shotwell.publishing.gnome-photos";
Plugins.register_extension_point(typeof(Spit.Publishing.Service), _("Publishing"),
Resources.PUBLISH, core_ids);
diff --git a/src/publishing/PublishingPluginHost.vala b/src/publishing/PublishingPluginHost.vala
index ca935ab..7804924 100644
--- a/src/publishing/PublishingPluginHost.vala
+++ b/src/publishing/PublishingPluginHost.vala
@@ -22,7 +22,7 @@ public class ConcretePublishingHost : Plugins.StandardHostInterface,
Spit.Publishing.Publisher.MediaType.NONE;
public ConcretePublishingHost(Service service, PublishingUI.PublishingDialog dialog,
- Publishable[] publishables) {
+ Publishable[] publishables, Account account) {
base(service, "sharing");
this.dialog = dialog;
this.publishables = publishables;
@@ -30,7 +30,11 @@ public class ConcretePublishingHost : Plugins.StandardHostInterface,
foreach (Publishable curr_publishable in publishables)
this.media_type |= curr_publishable.get_media_type();
- this.active_publisher = service.create_publisher(this);
+ this.active_publisher = service.create_publisher_with_account(this, account);
+ }
+
+ public string get_current_profile_id() {
+ return Shotwell.ProfileManager.get_instance().id();
}
private void on_login_clicked() {
diff --git a/src/publishing/PublishingUI.vala b/src/publishing/PublishingUI.vala
index d3d4a69..de642a4 100644
--- a/src/publishing/PublishingUI.vala
+++ b/src/publishing/PublishingUI.vala
@@ -6,134 +6,6 @@
namespace PublishingUI {
-public class ConcreteDialogPane : Spit.Publishing.DialogPane, GLib.Object {
- protected Gtk.Box pane_widget = null;
- protected Gtk.Builder builder = null;
-
- public ConcreteDialogPane() {
- builder = AppWindow.create_builder();
- }
-
- public Gtk.Widget get_widget() {
- return pane_widget;
- }
-
- public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
- return Spit.Publishing.DialogPane.GeometryOptions.NONE;
- }
-
- public void on_pane_installed() {
- }
-
- public void on_pane_uninstalled() {
- }
-}
-
-public class StaticMessagePane : ConcreteDialogPane {
- private Gtk.Label msg_label = null;
-
- public StaticMessagePane(string message_string, bool enable_markup = false) {
- base();
- msg_label = builder.get_object("static_msg_label") as Gtk.Label;
- pane_widget = builder.get_object("static_msg_pane_widget") as Gtk.Box;
-
- if (enable_markup) {
- msg_label.set_markup(message_string);
- msg_label.set_line_wrap(true);
- msg_label.set_use_markup(true);
- } else {
- msg_label.set_label(message_string);
- }
- }
-}
-
-public class LoginWelcomePane : ConcreteDialogPane {
- private Gtk.Button login_button = null;
- private Gtk.Label not_logged_in_label = null;
-
- public signal void login_requested();
-
- public LoginWelcomePane(string service_welcome_message) {
- base();
- pane_widget = builder.get_object("welcome_pane_widget") as Gtk.Box;
- login_button = builder.get_object("login_button") as Gtk.Button;
- not_logged_in_label = builder.get_object("not_logged_in_label") as Gtk.Label;
-
- login_button.clicked.connect(on_login_clicked);
- not_logged_in_label.set_use_markup(true);
- not_logged_in_label.set_markup(service_welcome_message);
- }
-
- private void on_login_clicked() {
- login_requested();
- }
-}
-
-public class ProgressPane : ConcreteDialogPane {
- private Gtk.ProgressBar progress_bar = null;
-
- public ProgressPane() {
- base();
- pane_widget = (Gtk.Box) builder.get_object("progress_pane_widget");
- progress_bar = (Gtk.ProgressBar) builder.get_object("publishing_progress_bar");
- }
-
- public void set_text(string text) {
- progress_bar.set_text(text);
- }
-
- public void set_progress(double progress) {
- progress_bar.set_fraction(progress);
- }
-
- public void set_status(string status_text, double progress) {
- if (status_text != progress_bar.get_text())
- progress_bar.set_text(status_text);
-
- set_progress(progress);
- }
-}
-
-public class SuccessPane : StaticMessagePane {
- public SuccessPane(Spit.Publishing.Publisher.MediaType published_media, int num_uploaded = 1) {
- string? message_string = null;
-
- // Here, we check whether more than one item is being uploaded, and if so, display
- // an alternate message.
- if (published_media == Spit.Publishing.Publisher.MediaType.VIDEO) {
- message_string = ngettext ("The selected video was successfully published.",
- "The selected videos were successfully published.",
- num_uploaded);
- }
- else if (published_media == Spit.Publishing.Publisher.MediaType.PHOTO) {
- message_string = ngettext ("The selected photo was successfully published.",
- "The selected photos were successfully published.",
- num_uploaded);
- }
- else if (published_media == (Spit.Publishing.Publisher.MediaType.PHOTO
- | Spit.Publishing.Publisher.MediaType.VIDEO)) {
- message_string = _("The selected photos/videos were successfully published.");
- }
- else {
- assert_not_reached ();
- }
-
- base(message_string);
- }
-}
-
-public class AccountFetchWaitPane : StaticMessagePane {
- public AccountFetchWaitPane() {
- base(_("Fetching account information…"));
- }
-}
-
-public class LoginWaitPane : StaticMessagePane {
- public LoginWaitPane() {
- base(_("Logging in…"));
- }
-}
-
public class PublishingDialog : Gtk.Dialog {
private const int LARGE_WINDOW_WIDTH = 860;
private const int LARGE_WINDOW_HEIGHT = 688;
@@ -205,12 +77,13 @@ public class PublishingDialog : Gtk.Dialog {
}
set_title(title);
- service_selector_box_model = new Gtk.ListStore(2, typeof(Gdk.Pixbuf), typeof(string));
+ service_selector_box_model = new Gtk.ListStore(3, typeof(string), typeof(string),
+ typeof(Spit.Publishing.Account));
service_selector_box = new Gtk.ComboBox.with_model(service_selector_box_model);
Gtk.CellRendererPixbuf renderer_pix = new Gtk.CellRendererPixbuf();
service_selector_box.pack_start(renderer_pix,true);
- service_selector_box.add_attribute(renderer_pix, "pixbuf", 0);
+ service_selector_box.add_attribute(renderer_pix, "icon-name", 0);
Gtk.CellRendererText renderer_text = new Gtk.CellRendererText();
service_selector_box.pack_start(renderer_text,true);
@@ -226,30 +99,26 @@ public class PublishingDialog : Gtk.Dialog {
Gtk.TreeIter iter;
foreach (Spit.Publishing.Service service in loaded_services) {
- service_selector_box_model.append(out iter);
-
string curr_service_id = service.get_id();
- service.get_info(ref info);
+ info = service.get_info();
- if (null != info.icons && 0 < info.icons.length) {
- // check if the icons object is set -- if set use that icon
- service_selector_box_model.set(iter, 0, info.icons[0], 1,
- service.get_pluggable_name());
-
- // in case the icons object is not set on the next iteration
- info.icons[0] = Resources.get_icon(Resources.ICON_GENERIC_PLUGIN);
- } else {
- // if icons object is null or zero length use a generic icon
- service_selector_box_model.set(iter, 0, Resources.get_icon(
- Resources.ICON_GENERIC_PLUGIN), 1, service.get_pluggable_name());
- }
-
- if (last_used_service == null) {
- service_selector_box.set_active_iter(iter);
- last_used_service = service.get_id();
- } else if (last_used_service == curr_service_id) {
- service_selector_box.set_active_iter(iter);
+ var accounts = service.get_accounts(Shotwell.ProfileManager.get_instance().id());
+
+ foreach (var account in accounts) {
+ service_selector_box_model.append(out iter);
+
+ var account_name = account.display_name();
+ var display_name = service.get_pluggable_name() + (account_name == "" ? "" : "/" + account_name);
+
+ service_selector_box_model.set(iter, 0, info.icon_name, 1, display_name, 2, account);
+
+ if (last_used_service == null) {
+ service_selector_box.set_active_iter(iter);
+ last_used_service = service.get_id();
+ } else if (last_used_service == curr_service_id) {
+ service_selector_box.set_active_iter(iter);
+ }
}
}
@@ -373,15 +242,17 @@ public class PublishingDialog : Gtk.Dialog {
return filtered_services;
}
- // Because of this bug: http://trac.yorba.org/ticket/3623, we use some extreme measures. The
- // bug occurs because, in some cases, when publishing is started asynchronous network
- // transactions are performed. The mechanism inside libsoup that we use to perform asynchronous
- // network transactions isn't based on threads but is instead based on the GLib event loop. So
- // whenever we run a network transaction, the GLib event loop gets spun. One consequence of
- // this is that PublishingDialog.go( ) can be called multiple times. Note that since events
- // are processed sequentially, PublishingDialog.go( ) is never called re-entrantly. It just
- // gets called twice back-to-back in quick succession. So use a timer to do a short circuit
- // return if this call to go( ) follows immediately on the heels of another call to go( ).
+ // Because of this bug: https://bugzilla.gnome.org/show_bug.cgi?id=717505, we use some
+ // extreme measures. The bug occurs because, in some cases, when publishing is started
+ // asynchronous network transactions are performed. The mechanism inside libsoup that we
+ // use to perform asynchronous network transactions isn't based on threads but is instead
+ // based on the GLib event loop. So whenever we run a network transaction, the GLib event
+ // loop gets spun. One consequence of this is that PublishingDialog.go( ) can be called
+ // multiple times. Note that since events are processed sequentially, PublishingDialog.go()
+ // is never called re-entrantly. It just gets called twice back-to-back in quick
+ // succession. So use a timer to do a short circuit return if this call to go( ) follows
+ // immediately on the heels of another call to go( )
+ // FIXME: Port publising to async libsoup, then there is no nested main loop anymore.
private static Timer since_last_start = null;
private static bool elapsed_is_valid = false;
public static void go(Gee.Collection<MediaSource> to_publish) {
@@ -412,7 +283,7 @@ public class PublishingDialog : Gtk.Dialog {
// There are no enabled publishing services that accept this media type,
// warn the user.
AppWindow.error_message_with_title(_("Unable to publish"),
- _("Shotwell cannot publish the selected items because you do not have a compatible publishing plugin enabled. To correct this, choose <b>Edit %s Preferences</b> and enable one or more of the publishing plugins on the <b>Plugins</b> tab.").printf("▸"),
+ _("Shotwell cannot publish the selected items because you do not have a compatible publishing plugin enabled. To correct this, choose Edit %s Preferences and enable one or more of the publishing plugins on the <b>Plugins</b> tab.").printf("▸"),
null, false);
return;
@@ -458,14 +329,17 @@ public class PublishingDialog : Gtk.Dialog {
}
Value service_name_val;
+ Value account_val;
service_selector_box_model.get_value(iter, 1, out service_name_val);
+ service_selector_box_model.get_value(iter, 2, out account_val);
string service_name = (string) service_name_val;
-
+ var service_account = (Spit.Publishing.Account) account_val;
+
Spit.Publishing.Service? selected_service = null;
Spit.Publishing.Service[] services = load_all_services();
foreach (Spit.Publishing.Service service in services) {
- if (service.get_pluggable_name() == service_name) {
+ if (service_name.has_prefix(service.get_pluggable_name())) {
selected_service = service;
break;
}
@@ -474,7 +348,7 @@ public class PublishingDialog : Gtk.Dialog {
Config.Facade.get_instance().set_last_used_service(selected_service.get_id());
- host = new Spit.Publishing.ConcretePublishingHost(selected_service, this, publishables);
+ host = new Spit.Publishing.ConcretePublishingHost(selected_service, this, publishables, service_account);
host.start_publishing();
}
diff --git a/src/publishing/StaticMessagePaneWidget.vala b/src/publishing/StaticMessagePaneWidget.vala
new file mode 100644
index 0000000..5f8de66
--- /dev/null
+++ b/src/publishing/StaticMessagePaneWidget.vala
@@ -0,0 +1,62 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Jens Georg <mail@jensge.org>
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+namespace PublishingUI {
+
+[GtkTemplate (ui = "/org/gnome/Shotwell/ui/static_message_pane_widget.ui")]
+public class StaticMessagePane : Spit.Publishing.DialogPane, Gtk.Box {
+ public bool show_spinner{get; construct; default=false; }
+
+ [GtkChild]
+ private unowned Gtk.Label static_msg_label;
+
+ [GtkChild]
+ private unowned Gtk.Spinner spinner;
+
+ public Gtk.Widget get_widget() {
+ return this;
+ }
+
+ public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
+ return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+ }
+
+ public void on_pane_installed() {
+ }
+
+ public void on_pane_uninstalled() {
+ }
+
+ public StaticMessagePane(string message_string, bool enable_markup = false, bool show_spinner = false) {
+ Object(show_spinner: false);
+
+ spinner.active = show_spinner;
+
+ if (enable_markup) {
+ static_msg_label.set_markup(message_string);
+ static_msg_label.set_line_wrap(true);
+ static_msg_label.set_use_markup(true);
+ } else {
+ static_msg_label.set_label(message_string);
+ }
+ }
+}
+
+public class AccountFetchWaitPane : StaticMessagePane {
+ public AccountFetchWaitPane() {
+ base(_("Fetching account information…"), false, true);
+ }
+}
+
+public class LoginWaitPane : StaticMessagePane {
+ public LoginWaitPane() {
+ base(_("Logging in…"), false, true);
+ }
+}
+
+
+} // namespace PublishingUI
diff --git a/src/publishing/SuccessPaneWidget.vala b/src/publishing/SuccessPaneWidget.vala
new file mode 100644
index 0000000..05b0c16
--- /dev/null
+++ b/src/publishing/SuccessPaneWidget.vala
@@ -0,0 +1,39 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Jens Georg <mail@jensge.org>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace PublishingUI {
+
+public class SuccessPane : StaticMessagePane {
+ public SuccessPane(Spit.Publishing.Publisher.MediaType published_media, int num_uploaded = 1) {
+ string? message_string = null;
+
+ // Here, we check whether more than one item is being uploaded, and if so, display
+ // an alternate message.
+ if (published_media == Spit.Publishing.Publisher.MediaType.VIDEO) {
+ message_string = ngettext ("The selected video was successfully published.",
+ "The selected videos were successfully published.",
+ num_uploaded);
+ }
+ else if (published_media == Spit.Publishing.Publisher.MediaType.PHOTO) {
+ message_string = ngettext ("The selected photo was successfully published.",
+ "The selected photos were successfully published.",
+ num_uploaded);
+ }
+ else if (published_media == (Spit.Publishing.Publisher.MediaType.PHOTO
+ | Spit.Publishing.Publisher.MediaType.VIDEO)) {
+ message_string = _("The selected photos/videos were successfully published.");
+ }
+ else {
+ assert_not_reached ();
+ }
+
+ base(message_string);
+ }
+}
+}
+
+
diff --git a/src/publishing/meson.build b/src/publishing/meson.build
new file mode 100644
index 0000000..38178d6
--- /dev/null
+++ b/src/publishing/meson.build
@@ -0,0 +1,27 @@
+libsw_publishing_gui = static_library(
+ 'publishing_gui',
+ [
+ 'StaticMessagePaneWidget.vala',
+ 'ProgressPaneWidget.vala',
+ 'SuccessPaneWidget.vala',
+ 'LoginWelcomePaneWidget.vala',
+ ],
+ vala_header : 'shotwell-internal-publishing-gui.h',
+ vala_vapi : 'shotwell-internal-publishing-gui.vapi',
+ include_directories : config_incdir,
+ dependencies: [
+ gtk,
+ gee,
+ sw_plugin
+ ],
+ vala_args : [
+ '--gresources',
+ join_paths(meson.project_source_root(), 'data',
+ 'org.gnome.Shotwell.gresource.xml')
+ ]
+)
+
+sw_publishing_gui = declare_dependency(
+ include_directories : include_directories('.'),
+ link_with : libsw_publishing_gui
+)
diff --git a/src/searches/SavedSearchDialog.vala b/src/searches/SavedSearchDialog.vala
index 526da35..b08c8a8 100644
--- a/src/searches/SavedSearchDialog.vala
+++ b/src/searches/SavedSearchDialog.vala
@@ -641,18 +641,19 @@ public class SavedSearchDialog : Gtk.Dialog {
}
[GtkChild]
- private Gtk.Button add_criteria;
+ private unowned Gtk.Button add_criteria;
[GtkChild]
- private Gtk.ComboBoxText operator;
+ private unowned Gtk.ComboBoxText operator;
[GtkChild]
- private Gtk.Entry search_title;
+ private unowned Gtk.Entry search_title;
[GtkChild]
- private Gtk.ListBox row_listbox;
+ private unowned Gtk.ListBox row_listbox;
private Gee.ArrayList<SearchRowContainer> row_list = new Gee.ArrayList<SearchRowContainer>();
private bool edit_mode = false;
private SavedSearch? previous_search = null;
private bool valid = false;
+ private ulong notify_id = 0;
public SavedSearchDialog() {
Object (use_header_bar : Resources.use_header_bar());
@@ -701,6 +702,12 @@ public class SavedSearchDialog : Gtk.Dialog {
add_criteria.clicked.connect(on_add_criteria);
search_title.changed.connect(on_title_changed);
+ if (Resources.use_header_bar() == 1) {
+ var box = search_title.get_parent();
+ box.remove(search_title);
+ box.get_parent().remove(box);
+ ((Gtk.HeaderBar) get_header_bar()).set_custom_title(search_title);
+ }
}
// Displays the dialog.
@@ -709,9 +716,14 @@ public class SavedSearchDialog : Gtk.Dialog {
destroy();
}
+ double upper;
// Adds a row of search criteria.
private void on_add_criteria() {
+ this.upper = row_listbox.get_adjustment().upper;
+ this.notify_id = row_listbox.get_adjustment().notify["upper"].connect(on_scroll);
add_text_search();
+ // Wait for upper to change. Then scroll to it, disconnect afterwards
+ // Otherwise the ListBox will randomly scroll to the bottom
}
private void add_text_search() {
@@ -723,11 +735,26 @@ public class SavedSearchDialog : Gtk.Dialog {
private void add_row(SearchRowContainer row) {
if (row_list.size == 1)
row_list.get(0).allow_removal(true);
- row_listbox.add(row.get_widget());
+ row_listbox.insert(row.get_widget(), row_list.size);
row_list.add(row);
row.remove.connect(on_remove_row);
row.changed.connect(on_row_changed);
set_valid(row.is_complete());
+
+ }
+
+ private void on_scroll() {
+ var adj = row_listbox.get_adjustment();
+ if (adj.upper < this.upper) {
+ return;
+ }
+
+ if (this.notify_id != 0) {
+ adj.disconnect(this.notify_id);
+ this.notify_id = 0;
+ }
+
+ adj.value = adj.upper;
}
// Removes a row of search criteria.
diff --git a/src/searches/SearchBoolean.vala b/src/searches/SearchBoolean.vala
index 5e69e57..fc83e04 100644
--- a/src/searches/SearchBoolean.vala
+++ b/src/searches/SearchBoolean.vala
@@ -776,11 +776,11 @@ public class SearchConditionDate : SearchCondition {
// Determines whether the source is included.
public override bool predicate(MediaSource source) {
- time_t exposure_time = source.get_exposure_time();
- if (exposure_time == 0)
+ var exposure_time = source.get_exposure_time();
+ if (exposure_time == null)
return context == Context.IS_NOT_SET;
- DateTime dt = new DateTime.from_unix_local(exposure_time);
+ var dt = exposure_time.to_local();
switch (context) {
case Context.EXACT:
DateTime second = date_one.add_days(1);
diff --git a/src/sidebar/Tree.vala b/src/sidebar/Tree.vala
index ea039ea..aae81a0 100644
--- a/src/sidebar/Tree.vala
+++ b/src/sidebar/Tree.vala
@@ -97,7 +97,6 @@ public class Sidebar.Tree : Gtk.TreeView {
Gtk.TreeViewColumn text_column = new Gtk.TreeViewColumn();
text_column.set_expand(true);
Gtk.CellRendererPixbuf icon_renderer = new Gtk.CellRendererPixbuf();
- icon_renderer.follow_state = true;
text_column.pack_start (icon_renderer, false);
text_column.add_attribute(icon_renderer, "gicon", Columns.ICON);
text_column.set_cell_data_func(icon_renderer, icon_renderer_function);
@@ -790,17 +789,6 @@ public class Sidebar.Tree : Gtk.TreeView {
store.set(iter, Columns.ICON, icon);
}
- private void load_branch_icons(Gtk.TreeIter iter) {
- load_entry_icons(iter);
-
- Gtk.TreeIter child_iter;
- if (store.iter_children(out child_iter, iter)) {
- do {
- load_branch_icons(child_iter);
- } while (store.iter_next(ref child_iter));
- }
- }
-
private bool on_selection(Gtk.TreeSelection selection, Gtk.TreeModel model, Gtk.TreePath path,
bool path_currently_selected) {
// only allow selection if a page is selectable
diff --git a/src/slideshow/Slideshow.vala b/src/slideshow/Slideshow.vala
index 5d14b64..0ee9392 100644
--- a/src/slideshow/Slideshow.vala
+++ b/src/slideshow/Slideshow.vala
@@ -8,16 +8,16 @@ namespace Slideshow {
public void init() throws Error {
string[] core_ids = new string[0];
- core_ids += "org.yorba.shotwell.transitions.crumble";
- core_ids += "org.yorba.shotwell.transitions.fade";
- core_ids += "org.yorba.shotwell.transitions.slide";
- core_ids += "org.yorba.shotwell.transitions.blinds";
- core_ids += "org.yorba.shotwell.transitions.circle";
- core_ids += "org.yorba.shotwell.transitions.circles";
- core_ids += "org.yorba.shotwell.transitions.clock";
- core_ids += "org.yorba.shotwell.transitions.stripes";
- core_ids += "org.yorba.shotwell.transitions.squares";
- core_ids += "org.yorba.shotwell.transitions.chess";
+ core_ids += "org.gnome.shotwell.transitions.crumble";
+ core_ids += "org.gnome.shotwell.transitions.fade";
+ core_ids += "org.gnome.shotwell.transitions.slide";
+ core_ids += "org.gnome.shotwell.transitions.blinds";
+ core_ids += "org.gnome.shotwell.transitions.circle";
+ core_ids += "org.gnome.shotwell.transitions.circles";
+ core_ids += "org.gnome.shotwell.transitions.clock";
+ core_ids += "org.gnome.shotwell.transitions.stripes";
+ core_ids += "org.gnome.shotwell.transitions.squares";
+ core_ids += "org.gnome.shotwell.transitions.chess";
Plugins.register_extension_point(typeof(Spit.Transitions.Descriptor), _("Slideshow Transitions"),
Resources.ICON_SLIDESHOW_EXTENSION_POINT, core_ids);
diff --git a/src/slideshow/TransitionEffects.vala b/src/slideshow/TransitionEffects.vala
index 5f7dc88..23c666a 100644
--- a/src/slideshow/TransitionEffects.vala
+++ b/src/slideshow/TransitionEffects.vala
@@ -71,7 +71,7 @@ public class TransitionEffectsManager {
return effects.keys;
}
- public Gee.Collection<string> get_effect_names(owned CompareDataFunc? comparator = null) {
+ public Gee.Collection<string> get_effect_names(owned CompareDataFunc<string>? comparator = null) {
Gee.Collection<string> effect_names = new Gee.TreeSet<string>((owned) comparator);
foreach (Spit.Transitions.Descriptor desc in effects.values)
effect_names.add(desc.get_pluggable_name());
@@ -273,7 +273,7 @@ public class TransitionClock {
}
public class NullTransitionDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor {
- public const string EFFECT_ID = "org.yorba.shotwell.transitions.null";
+ public const string EFFECT_ID = "org.gnome.shotwell.transitions.null";
public int get_pluggable_interface(int min_host_version, int max_host_version) {
return Spit.Transitions.CURRENT_INTERFACE;
@@ -287,7 +287,8 @@ public class NullTransitionDescriptor : Object, Spit.Pluggable, Spit.Transitions
return _("None");
}
- public void get_info(ref Spit.PluggableInfo info) {
+ public Spit.PluggableInfo get_info() {
+ return new Spit.PluggableInfo();
}
public void activation(bool enabled) {
@@ -325,7 +326,7 @@ public class NullEffect : Object, Spit.Transitions.Effect {
}
}
public class RandomEffectDescriptor : Object, Spit.Pluggable, Spit.Transitions.Descriptor {
- public const string EFFECT_ID = "org.yorba.shotwell.transitions.random";
+ public const string EFFECT_ID = "org.gnome.shotwell.transitions.random";
public int get_pluggable_interface(int min_host_version, int max_host_version) {
return Spit.Transitions.CURRENT_INTERFACE;
@@ -339,7 +340,8 @@ public class RandomEffectDescriptor : Object, Spit.Pluggable, Spit.Transitions.D
return _("Random");
}
- public void get_info(ref Spit.PluggableInfo info) {
+ public Spit.PluggableInfo get_info() {
+ return new Spit.PluggableInfo();
}
public void activation(bool enabled) {
diff --git a/src/unit/rc/Unit.m4 b/src/unit/rc/Unit.m4
deleted file mode 100644
index 2665dd6..0000000
--- a/src/unit/rc/Unit.m4
+++ /dev/null
@@ -1,29 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
- */
-
-/* This file is the master unit file for the _UNIT_NAME_ unit. It should be edited to include
- * whatever code is deemed necessary.
- *
- * The init() and terminate() methods are mandatory.
- *
- * If the unit needs to be configured prior to initialization, add the proper parameters to
- * the preconfigure() method, implement it, and ensure in init() that it's been called.
- */
-
-namespace _UNIT_NAME_ {
-
-// preconfigure may be deleted if not used.
-public void preconfigure() {
-}
-
-public void init() throws Error {
-}
-
-public void terminate() {
-}
-
-}
-
diff --git a/src/unit/rc/UnitInternals.m4 b/src/unit/rc/UnitInternals.m4
deleted file mode 100644
index 71614d4..0000000
--- a/src/unit/rc/UnitInternals.m4
+++ /dev/null
@@ -1,32 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
- *
- * Auto-generated file. Do not modify!
- */
-
-namespace _UNIT_NAME_ {
-
-private int _unit_init_count = 0;
-
-public void init_entry() throws Error {
- if (_unit_init_count++ != 0)
- return;
-
- _UNIT_USES_INITS_
-
- _UNIT_NAME_.init();
-}
-
-public void terminate_entry() {
- if (_unit_init_count == 0 || --_unit_init_count != 0)
- return;
-
- _UNIT_NAME_.terminate();
-
- _UNIT_USES_TERMINATORS_
-}
-
-}
-
diff --git a/src/unit/rc/template.vala b/src/unit/rc/template.vala
deleted file mode 100644
index 31fc93d..0000000
--- a/src/unit/rc/template.vala
+++ /dev/null
@@ -1,7 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
- */
-
-
diff --git a/src/unit/rc/unitize_entry.m4 b/src/unit/rc/unitize_entry.m4
deleted file mode 100644
index 31602b2..0000000
--- a/src/unit/rc/unitize_entry.m4
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later). See the COPYING file in this distribution.
- *
- * Auto-generated file. Do not modify!
- */
-
-namespace _APP_UNIT_ {
-
-public void app_init() throws Error {
- _APP_UNIT_.init_entry();
-}
-
-public void app_terminate() {
- _APP_UNIT_.terminate_entry();
-}
-
-}
diff --git a/src/util/Util.vala b/src/util/Util.vala
index b87ea3a..45943d7 100644
--- a/src/util/Util.vala
+++ b/src/util/Util.vala
@@ -8,6 +8,8 @@ namespace Util {
// Use these file attributes when loading file information for a complete FileInfo objects
public const string FILE_ATTRIBUTES = "standard::*,time::*,id::file,id::filesystem,etag::value";
+ public const int64 USEC_PER_SEC = 1000000;
+
public void init() throws Error {
}
diff --git a/src/util/file.vala b/src/util/file.vala
index c1ee06d..652a141 100644
--- a/src/util/file.vala
+++ b/src/util/file.vala
@@ -31,9 +31,11 @@ public bool claim_file(File file) throws Error {
// same or similar as what has been requested (adds numerals to the end of the name until a unique
// one has been found). The file may exist when this function returns, and it should be
// overwritten. It does *not* attempt to create the parent directory, however.
+// The used parameter allows you to pass in a collection of names which should be deemed to be
+// already claimed but which may not yet exist in the file system.
//
// This function is thread-safe.
-public File? generate_unique_file(File dir, string basename, out bool collision) throws Error {
+public File? generate_unique_file(File dir, string basename, out bool collision, Gee.Collection<string>? used = null) throws Error {
// create the file to atomically "claim" it
File file = dir.get_child(basename);
if (claim_file(file)) {
@@ -51,7 +53,9 @@ public File? generate_unique_file(File dir, string basename, out bool collision)
// generate a unique filename
for (int ctr = 1; ctr < int.MAX; ctr++) {
string new_name = (ext != null) ? "%s_%d.%s".printf(name, ctr, ext) : "%s_%d".printf(name, ctr);
-
+ if (used != null && used.contains(new_name)) {
+ continue;
+ }
file = dir.get_child(new_name);
if (claim_file(file))
return file;
@@ -151,11 +155,11 @@ public void delete_all_files(File dir, Gee.Set<string>? exceptions = null, Progr
}
}
-public time_t query_file_modified(File file) throws Error {
+public DateTime query_file_modified(File file) throws Error {
FileInfo info = file.query_info(FileAttribute.TIME_MODIFIED, FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
null);
- return info.get_modification_time().tv_sec;
+ return info.get_modification_date_time();
}
public bool query_is_directory(File file) {
@@ -199,20 +203,6 @@ public string? get_file_info_id(FileInfo info) {
return info.get_attribute_string(FileAttribute.ID_FILE);
}
-// Breaks a uint64 skip amount into several smaller skips.
-public void skip_uint64(InputStream input, uint64 skip_amount) throws GLib.Error {
- while (skip_amount > 0) {
- // skip() throws an error if the amount is too large, so check against ssize_t.MAX
- if (skip_amount >= ssize_t.MAX) {
- input.skip(ssize_t.MAX);
- skip_amount -= ssize_t.MAX;
- } else {
- input.skip((size_t) skip_amount);
- skip_amount = 0;
- }
- }
-}
-
// Returns the number of files (and/or directories) within a directory.
public uint64 count_files_in_directory(File dir) throws GLib.Error {
if (!query_is_directory(dir))
diff --git a/src/util/image.vala b/src/util/image.vala
index 0a30339..95ac998 100644
--- a/src/util/image.vala
+++ b/src/util/image.vala
@@ -343,7 +343,7 @@ private Cairo.Surface get_background_surface() {
string color_b;
var config = Config.Facade.get_instance();
- var type = config.get_transparent_background_type();
+ var type = "checkered"; //config.get_transparent_background_type();
switch (type) {
case "checkered":
color_a = "#808080";
@@ -386,7 +386,8 @@ public void paint_pixmap_with_background (Cairo.Context ctx, Gdk.Pixbuf pixbuf,
}
Gdk.cairo_set_source_pixbuf(ctx, pixbuf, x, y);
- ctx.paint();
+ ctx.rectangle(x, y, pixbuf.width , pixbuf.height);
+ ctx.fill();
}
// Force an axially-aligned box to be inside a rotated rectangle.
@@ -422,3 +423,6 @@ Box clamp_inside_rotated_image(Box src, int img_w, int img_h, double angle_deg,
src.right + right_offset, src.bottom + bottom_offset);
}
+double degrees_to_radians(double theta) {
+ return (theta * (GLib.Math.PI / 180.0));
+}
diff --git a/src/util/misc.vala b/src/util/misc.vala
index 6111ea3..2106621 100644
--- a/src/util/misc.vala
+++ b/src/util/misc.vala
@@ -54,22 +54,12 @@ public bool int_value_equals(Value a, Value b) {
return (int) a == (int) b;
}
-public ulong timeval_to_ms(TimeVal time_val) {
- return (((ulong) time_val.tv_sec) * 1000) + (((ulong) time_val.tv_usec) / 1000);
-}
-
public ulong now_ms() {
- return timeval_to_ms(TimeVal());
-}
-
-public ulong now_sec() {
- TimeVal time_val = TimeVal();
-
- return time_val.tv_sec;
+ return (ulong) (GLib.get_real_time() / 1000);
}
-public inline time_t now_time_t() {
- return (time_t) now_sec();
+public int64 now_sec() {
+ return (ulong) (GLib.get_real_time() / Util.USEC_PER_SEC);
}
public string md5_file(File file) throws Error {
@@ -216,14 +206,16 @@ public Gee.List<MediaSource>? unserialize_media_sources(uchar* serialized, int s
return list;
}
-public string format_local_datespan(Time from_date, Time to_date) {
+public string format_local_datespan(DateTime from_date, DateTime to_date) {
string from_format, to_format;
// Ticket #3240 - Change the way date ranges are pretty-
// printed if the start and end date occur on consecutive days.
- if (from_date.year == to_date.year) {
+ if (from_date.get_year() == to_date.get_year()) {
// are these consecutive dates?
- if ((from_date.month == to_date.month) && (from_date.day == (to_date.day - 1))) {
+ // get_day_of_year() looks like it saves a bit of code, but then we would
+ // not recognize the change of months
+ if ((from_date.get_month() == to_date.get_month()) && (from_date.get_day_of_month() == (to_date.get_day_of_month() - 1))) {
// Yes; display like so: Sat, July 4 - 5, 20X6
from_format = Resources.get_start_multiday_span_format_string();
to_format = Resources.get_end_multiday_span_format_string();
@@ -244,7 +236,7 @@ public string format_local_datespan(Time from_date, Time to_date) {
to_date.format(to_format)));
}
-public string format_local_date(Time date) {
+public string format_local_date(DateTime date) {
return String.strip_leading_zeroes(date.format(Resources.get_long_date_format_string()));
}
@@ -273,7 +265,9 @@ public class OneShotScheduler {
}
public void at_idle() {
- at_priority_idle(Priority.DEFAULT_IDLE);
+ // needs to be lower (higher priority) than Clutter.PRIORITY_REDRAW which is
+ // set at Priority.HIGH_IDLE + 50
+ at_priority_idle(Priority.HIGH_IDLE + 40);
}
public void at_priority_idle(int priority) {
diff --git a/src/util/string.vala b/src/util/string.vala
index bf7e605..89424d0 100644
--- a/src/util/string.vala
+++ b/src/util/string.vala
@@ -13,13 +13,13 @@ public inline bool is_string_empty(string? s) {
}
// utf8 case sensitive compare
-public int utf8_cs_compare(void *a, void *b) {
- return ((string) a).collate((string) b);
+public int utf8_cs_compare(string a, string b) {
+ return a.collate(b);
}
// utf8 case insensitive compare
-public int utf8_ci_compare(void *a, void *b) {
- return ((string) a).down().collate(((string) b).down());
+public int utf8_ci_compare(string a, string b) {
+ return a.down().collate(b.down());
}
// utf8 array to string
@@ -145,7 +145,7 @@ public string? prepare_input_text(string? text, PrepareInputTextOptions options,
// Using composed form rather than GLib's default (decomposed) as NFC is the preferred form in
// Linux and WWW. More importantly, Pango seems to have serious problems displaying decomposed
// forms of Korean language glyphs (and perhaps others). See:
- // http://trac.yorba.org/ticket/2952
+ // https://bugzilla.gnome.org/show_bug.cgi?id=716914
if ((options & PrepareInputTextOptions.NORMALIZE) != 0)
prepped = prepped.normalize(-1, NormalizeMode.NFC);
@@ -237,6 +237,8 @@ public string remove_diacritics(string istring) {
case UnicodeType.ENCLOSING_MARK:
// Ignore those
continue;
+ default:
+ break;
}
builder.append_unichar(ch);
}
@@ -255,7 +257,7 @@ public string to_hex_string(string str) {
// A note on the collated_* and precollated_* methods:
//
-// A bug report (http://trac.yorba.org/ticket/3152) indicated that two different Hirigana characters
+// A bug report (https://bugzilla.gnome.org/show_bug.cgi?id=717135) indicated that two different Hirigana characters
// as Tag names would trigger an assertion. Investigation showed that the characters' collation
// keys computed as equal when the locale was set to anything but the default locale (C) or
// Japanese. A related bug was that another hash table was using str_equal, which does not use
diff --git a/src/util/system.vala b/src/util/system.vala
index 1e69304..48e2cc9 100644
--- a/src/util/system.vala
+++ b/src/util/system.vala
@@ -6,7 +6,7 @@
// Return the directory in which Shotwell is installed, or null if uninstalled.
File? get_sys_install_dir(File exec_dir) {
- // Assume that if the ui folder lives next to the binary, we runn in-tree
+ // Assume that if the ui folder lives next to the binary, we run in-tree
File child = exec_dir.get_child("ui");
if (!FileUtils.test(child.get_path(), FileTest.IS_DIR | FileTest.EXISTS)) {
@@ -39,7 +39,8 @@ async void show_file_in_filemanager(File file) throws Error {
DBusProxyFlags.DO_NOT_LOAD_PROPERTIES |
DBusProxyFlags.DO_NOT_CONNECT_SIGNALS);
var id = "%s_%s_%d_%s".printf(Environment.get_prgname(), Environment.get_host_name(),
- Posix.getpid(), TimeVal().to_iso8601());
+ Posix.getpid(),
+ GLib.get_monotonic_time().to_string());
yield manager.show_items({file.get_uri()}, id);
} catch (Error e) {
warning("Failed to launch file manager using DBus, using fall-back: %s", e.message);
diff --git a/src/util/ui.vala b/src/util/ui.vala
index 6d32738..bdc7157 100644
--- a/src/util/ui.vala
+++ b/src/util/ui.vala
@@ -60,7 +60,7 @@ public Gdk.Rectangle get_adjustment_page(Gtk.Adjustment hadj, Gtk.Adjustment vad
}
// Verifies that only the mask bits are set in the modifier field, disregarding mouse and
-// key modifers that are not normally of concern (i.e. Num Lock, Caps Lock, etc.). Mask can be
+// key modifiers that are not normally of concern (i.e. Num Lock, Caps Lock, etc.). Mask can be
// one or more bits set, but should only consist of these values:
// * Gdk.ModifierType.SHIFT_MASK
// * Gdk.ModifierType.CONTROL_MASK
@@ -87,16 +87,15 @@ public bool has_only_key_modifier(Gdk.ModifierType field, Gdk.ModifierType mask)
}
bool is_pointer_over(Gdk.Window window) {
- Gdk.DeviceManager? devmgr = window.get_display().get_device_manager();
- if (devmgr == null) {
- debug("No device for display");
+ var seat = window.get_display().get_default_seat();
+ if (seat == null) {
+ debug("No seat for display");
return false;
}
int x, y;
- devmgr.get_client_pointer().get_position(null, out x, out y);
- //gdk_device_get_position(devmgr.get_client_pointer(), null, out x, out y);
+ seat.get_pointer().get_position(null, out x, out y);
return x >= 0 && y >= 0 && x < window.get_width() && y < window.get_height();
}
diff --git a/src/video-support/AVIChunk.vala b/src/video-support/AVIChunk.vala
new file mode 100644
index 0000000..970f443
--- /dev/null
+++ b/src/video-support/AVIChunk.vala
@@ -0,0 +1,121 @@
+private class AVIChunk {
+ private GLib.File file = null;
+ private string section_name = "";
+ private uint64 section_size = 0;
+ private uint64 section_offset = 0;
+ private GLib.DataInputStream input = null;
+ private AVIChunk? parent = null;
+ private const int MAX_STRING_TO_SECTION_LENGTH = 1024;
+
+ public AVIChunk(GLib.File file) {
+ this.file = file;
+ }
+
+ private AVIChunk.with_input_stream(GLib.DataInputStream input, AVIChunk parent) {
+ this.input = input;
+ this.parent = parent;
+ }
+
+ public void open_file() throws GLib.Error {
+ close_file();
+ input = new GLib.DataInputStream(file.read());
+ input.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN);
+ section_size = 0;
+ section_offset = 0;
+ section_name = "";
+ }
+
+ public void close_file() throws GLib.Error {
+ if (null != input) {
+ input.close();
+ input = null;
+ }
+ }
+
+ public void nonsection_skip(uint64 skip_amount) throws GLib.Error {
+ skip_uint64(input, skip_amount);
+ }
+
+ public void skip(uint64 skip_amount) throws GLib.Error {
+ advance_section_offset(skip_amount);
+ skip_uint64(input, skip_amount);
+ }
+
+ public AVIChunk get_first_child_chunk() {
+ return new AVIChunk.with_input_stream(input, this);
+ }
+
+ private void advance_section_offset(uint64 amount) {
+ if ((section_offset + amount) > section_size)
+ amount = section_size - section_offset;
+
+ section_offset += amount;
+ if (null != parent) {
+ parent.advance_section_offset(amount);
+ }
+ }
+
+ public uchar read_byte() throws GLib.Error {
+ advance_section_offset(1);
+ return input.read_byte();
+ }
+
+ public uint16 read_uint16() throws GLib.Error {
+ advance_section_offset(2);
+ return input.read_uint16();
+ }
+
+ public void read_chunk() throws GLib.Error {
+ // don't use checked reads here because they advance the section offset, which we're trying
+ // to determine here
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ sb.append_c((char) input.read_byte());
+ sb.append_c((char) input.read_byte());
+ sb.append_c((char) input.read_byte());
+ sb.append_c((char) input.read_byte());
+ section_name = sb.str;
+ section_size = input.read_uint32();
+ section_offset = 0;
+ }
+
+ public string read_name() throws GLib.Error {
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ return sb.str;
+ }
+
+ public void next_chunk() throws GLib.Error {
+ skip(section_size_remaining());
+ section_size = 0;
+ section_offset = 0;
+ }
+
+ public string get_current_chunk_name() {
+ return section_name;
+ }
+
+ public bool is_last_chunk() {
+ return section_size == 0;
+ }
+
+ public uint64 section_size_remaining() {
+ assert(section_size >= section_offset);
+ return section_size - section_offset;
+ }
+
+ // Reads section contents into a string.
+ public string section_to_string() throws GLib.Error {
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ while (section_offset < section_size) {
+ sb.append_c((char) read_byte());
+ if (sb.len > MAX_STRING_TO_SECTION_LENGTH) {
+ return sb.str;
+ }
+ }
+ return sb.str;
+ }
+
+}
diff --git a/src/video-support/AVIMetadataLoader.vala b/src/video-support/AVIMetadataLoader.vala
new file mode 100644
index 0000000..2b507e2
--- /dev/null
+++ b/src/video-support/AVIMetadataLoader.vala
@@ -0,0 +1,227 @@
+public class AVIMetadataLoader {
+
+ private File file = null;
+
+ // A numerical date string, i.e 2010:01:28 14:54:25
+ private const int NUMERICAL_DATE_LENGTH = 19;
+
+ // Marker for timestamp section in a Nikon nctg blob.
+ private const uint16 NIKON_NCTG_TIMESTAMP_MARKER = 0x13;
+
+ // Size limit to ensure we don't parse forever on a bad file.
+ private const int MAX_STRD_LENGTH = 100;
+
+ public AVIMetadataLoader(File file) {
+ this.file = file;
+ }
+
+ public MetadataDateTime? get_creation_date_time() {
+ return new MetadataDateTime(get_creation_date_time_for_avi());
+ }
+
+ public string? get_title() {
+ // Not supported.
+ return null;
+ }
+
+ // Checks if the given file is an AVI file.
+ public bool is_supported() {
+ AVIChunk chunk = new AVIChunk(file);
+ bool ret = false;
+ try {
+ chunk.open_file();
+ chunk.read_chunk();
+ // Look for the header and identifier.
+ if ("RIFF" == chunk.get_current_chunk_name() &&
+ "AVI " == chunk.read_name()) {
+ ret = true;
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for AVI file: %s", e.message);
+ }
+
+ try {
+ chunk.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing AVI file: %s", e.message);
+ }
+ return ret;
+ }
+
+ // Parses a Nikon nctg tag. Based losely on avi_read_nikon() in FFmpeg.
+ private string read_nikon_nctg_tag(AVIChunk chunk) throws GLib.Error {
+ bool found_date = false;
+ while (chunk.section_size_remaining() > sizeof(uint16)*2) {
+ uint16 tag = chunk.read_uint16();
+ uint16 size = chunk.read_uint16();
+ if (NIKON_NCTG_TIMESTAMP_MARKER == tag) {
+ found_date = true;
+ break;
+ }
+ chunk.skip(size);
+ }
+
+ if (found_date) {
+ // Read numerical date string, example: 2010:01:28 14:54:25
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ for (int i = 0; i < NUMERICAL_DATE_LENGTH; i++) {
+ sb.append_c((char) chunk.read_byte());
+ }
+ return sb.str;
+ }
+ return "";
+ }
+
+ // Parses a Fujifilm strd tag. Based on information from:
+ // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
+ private string read_fuji_strd_tag(AVIChunk chunk) throws GLib.Error {
+ chunk.skip(98); // Ignore 98-byte binary blob.
+ chunk.skip(8); // Ignore the string "FUJIFILM"
+ // Read until we find four colons, then two more chars.
+ int colons = 0;
+ int post_colons = 0;
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ // End of date is two chars past the fourth colon.
+ while (colons <= 4 && post_colons < 2) {
+ char c = (char) chunk.read_byte();
+ if (4 == colons) {
+ post_colons++;
+ }
+ if (':' == c) {
+ colons++;
+ }
+ if (c.isprint()) {
+ sb.append_c(c);
+ }
+ if (sb.len > MAX_STRD_LENGTH) {
+ return ""; // Give up searching.
+ }
+ }
+
+ if (sb.str.length < NUMERICAL_DATE_LENGTH) {
+ return "";
+ }
+ // Date is now at the end of the string.
+ return sb.str.substring(sb.str.length - NUMERICAL_DATE_LENGTH);
+ }
+
+ // Recursively read file until the section is found.
+ private string? read_section(AVIChunk chunk) throws GLib.Error {
+ while (true) {
+ chunk.read_chunk();
+ string name = chunk.get_current_chunk_name();
+ if ("IDIT" == name) {
+ return chunk.section_to_string();
+ } else if ("nctg" == name) {
+ return read_nikon_nctg_tag(chunk);
+ } else if ("strd" == name) {
+ return read_fuji_strd_tag(chunk);
+ }
+
+ if ("LIST" == name) {
+ chunk.read_name(); // Read past list name.
+ string result = read_section(chunk.get_first_child_chunk());
+ if (null != result) {
+ return result;
+ }
+ }
+
+ if (chunk.is_last_chunk()) {
+ break;
+ }
+ chunk.next_chunk();
+ }
+ return null;
+ }
+
+ // Parses a date from a string.
+ // Largely based on GStreamer's avi/gstavidemux.c
+ // and the information here:
+ // http://www.eden-foundation.org/products/code/film_date_stamp/index.html
+ private DateTime? parse_date(string sdate) {
+ if (sdate.length == 0) {
+ return null;
+ }
+
+ int year, month, day, hour, min, sec;
+ char weekday[4];
+ char monthstr[4];
+ DateTime parsed_date;
+
+ if (sdate[0].isdigit()) {
+ // Format is: 2005:08:17 11:42:43
+ // Format is: 2010/11/30/ 19:42
+ // Format is: 2010/11/30 19:42
+ string tmp = sdate.dup();
+ tmp.canon("0123456789 ", ' '); // strip everything but numbers and spaces
+ sec = 0;
+ int result = tmp.scanf("%d %d %d %d %d %d", out year, out month, out day, out hour, out min, out sec);
+ if(result < 5) {
+ return null;
+ }
+
+ parsed_date = new DateTime.utc(year, month, day, hour, min, sec);
+ } else {
+ // Format is: Mon Mar 3 09:44:56 2008
+ if(7 != sdate.scanf("%3s %3s %d %d:%d:%d %d", weekday, monthstr, out day, out hour,
+ out min, out sec, out year)) {
+ return null; // Error
+ }
+ parsed_date = new DateTime.local(year, month_from_string((string)monthstr), day, hour, min, sec);
+ }
+
+ return parsed_date;
+ }
+
+ private DateMonth month_from_string(string s) {
+ switch (s.down()) {
+ case "jan":
+ return DateMonth.JANUARY;
+ case "feb":
+ return DateMonth.FEBRUARY;
+ case "mar":
+ return DateMonth.MARCH;
+ case "apr":
+ return DateMonth.APRIL;
+ case "may":
+ return DateMonth.MAY;
+ case "jun":
+ return DateMonth.JUNE;
+ case "jul":
+ return DateMonth.JULY;
+ case "aug":
+ return DateMonth.AUGUST;
+ case "sep":
+ return DateMonth.SEPTEMBER;
+ case "oct":
+ return DateMonth.OCTOBER;
+ case "nov":
+ return DateMonth.NOVEMBER;
+ case "dec":
+ return DateMonth.DECEMBER;
+ }
+ return DateMonth.BAD_MONTH;
+ }
+
+ private DateTime? get_creation_date_time_for_avi() {
+ AVIChunk chunk = new AVIChunk(file);
+ DateTime? timestamp = null;
+ try {
+ chunk.open_file();
+ chunk.nonsection_skip(12); // Advance past 12 byte header.
+ string sdate = read_section(chunk);
+ if (null != sdate) {
+ timestamp = parse_date(sdate.strip());
+ }
+ } catch (GLib.Error e) {
+ debug("Error while reading AVI file: %s", e.message);
+ }
+
+ try {
+ chunk.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing AVI file: %s", e.message);
+ }
+ return timestamp;
+ }
+}
diff --git a/src/video-support/QuickTimeAtom.vala b/src/video-support/QuickTimeAtom.vala
new file mode 100644
index 0000000..996046a
--- /dev/null
+++ b/src/video-support/QuickTimeAtom.vala
@@ -0,0 +1,118 @@
+private class QuickTimeAtom {
+ private GLib.File file = null;
+ private string section_name = "";
+ private uint64 section_size = 0;
+ private uint64 section_offset = 0;
+ private GLib.DataInputStream input = null;
+ private QuickTimeAtom? parent = null;
+
+ public QuickTimeAtom(GLib.File file) {
+ this.file = file;
+ }
+
+ private QuickTimeAtom.with_input_stream(GLib.DataInputStream input, QuickTimeAtom parent) {
+ this.input = input;
+ this.parent = parent;
+ }
+
+ public void open_file() throws GLib.Error {
+ close_file();
+ input = new GLib.DataInputStream(file.read());
+ input.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
+ section_size = 0;
+ section_offset = 0;
+ section_name = "";
+ }
+
+ public void close_file() throws GLib.Error {
+ if (null != input) {
+ input.close();
+ input = null;
+ }
+ }
+
+ private void advance_section_offset(uint64 amount) {
+ section_offset += amount;
+ if (null != parent) {
+ parent.advance_section_offset(amount);
+ }
+ }
+
+ public QuickTimeAtom get_first_child_atom() {
+ // Child will simply have the input stream
+ // but not the size/offset. This works because
+ // child atoms follow immediately after a header,
+ // so no skipping is required to access the child
+ // from the current position.
+ return new QuickTimeAtom.with_input_stream(input, this);
+ }
+
+ public uchar read_byte() throws GLib.Error {
+ advance_section_offset(1);
+ return input.read_byte();
+ }
+
+ public uint32 read_uint32() throws GLib.Error {
+ advance_section_offset(4);
+ return input.read_uint32();
+ }
+
+ public uint64 read_uint64() throws GLib.Error {
+ advance_section_offset(8);
+ return input.read_uint64();
+ }
+
+ public void read_atom() throws GLib.Error {
+ // Read atom size.
+ section_size = read_uint32();
+
+ // Read atom name.
+ GLib.StringBuilder sb = new GLib.StringBuilder();
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ sb.append_c((char) read_byte());
+ section_name = sb.str;
+
+ // Check string.
+ if (section_name.length != 4) {
+ throw new IOError.NOT_SUPPORTED("QuickTime atom name length is invalid for %s",
+ file.get_path());
+ }
+ for (int i = 0; i < section_name.length; i++) {
+ if (!section_name[i].isprint()) {
+ throw new IOError.NOT_SUPPORTED("Bad QuickTime atom in file %s", file.get_path());
+ }
+ }
+
+ if (1 == section_size) {
+ // This indicates the section size is a 64-bit
+ // value, specified below the atom name.
+ section_size = read_uint64();
+ }
+ }
+
+ private void skip(uint64 skip_amount) throws GLib.Error {
+ skip_uint64(input, skip_amount);
+ }
+
+ public uint64 section_size_remaining() {
+ assert(section_size >= section_offset);
+ return section_size - section_offset;
+ }
+
+ public void next_atom() throws GLib.Error {
+ skip(section_size_remaining());
+ section_size = 0;
+ section_offset = 0;
+ }
+
+ public string get_current_atom_name() {
+ return section_name;
+ }
+
+ public bool is_last_atom() {
+ return 0 == section_size;
+ }
+
+}
diff --git a/src/video-support/QuicktimeMetdataLoader.vala b/src/video-support/QuicktimeMetdataLoader.vala
new file mode 100644
index 0000000..0a831d2
--- /dev/null
+++ b/src/video-support/QuicktimeMetdataLoader.vala
@@ -0,0 +1,127 @@
+public class QuickTimeMetadataLoader {
+
+ // Quicktime calendar date/time format is number of seconds since January 1, 1904.
+ // This converts to UNIX time (66 years + 17 leap days).
+ public const int64 QUICKTIME_EPOCH_ADJUSTMENT = 2082844800;
+
+ private File file = null;
+
+ public QuickTimeMetadataLoader(File file) {
+ this.file = file;
+ }
+
+ public MetadataDateTime? get_creation_date_time() {
+ var dt = get_creation_date_time_for_quicktime();
+ if (dt == null) {
+ return null;
+ } else {
+ return new MetadataDateTime(dt);
+ }
+ }
+
+ public string? get_title() {
+ // Not supported.
+ return null;
+ }
+
+ // Checks if the given file is a QuickTime file.
+ public bool is_supported() {
+ QuickTimeAtom test = new QuickTimeAtom(file);
+
+ bool ret = false;
+ try {
+ test.open_file();
+ test.read_atom();
+
+ // Look for the header.
+ if ("ftyp" == test.get_current_atom_name()) {
+ ret = true;
+ } else {
+ // Some versions of QuickTime don't have
+ // an ftyp section, so we'll just look
+ // for the mandatory moov section.
+ while(true) {
+ if ("moov" == test.get_current_atom_name()) {
+ ret = true;
+ break;
+ }
+ test.next_atom();
+ test.read_atom();
+ if (test.is_last_atom()) {
+ break;
+ }
+ }
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for QuickTime file for %s: %s", file.get_path(), e.message);
+ }
+
+ try {
+ test.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing Quicktime file: %s", e.message);
+ }
+ return ret;
+ }
+
+ private DateTime? get_creation_date_time_for_quicktime() {
+ QuickTimeAtom test = new QuickTimeAtom(file);
+ DateTime? timestamp = null;
+
+ try {
+ test.open_file();
+ bool done = false;
+ while(!done) {
+ // Look for "moov" section.
+ test.read_atom();
+ if (test.is_last_atom()) break;
+ if ("moov" == test.get_current_atom_name()) {
+ QuickTimeAtom child = test.get_first_child_atom();
+ while (!done) {
+ // Look for "mvhd" section, or break if none is found.
+ child.read_atom();
+ if (child.is_last_atom() || 0 == child.section_size_remaining()) {
+ done = true;
+ break;
+ }
+
+ if ("mvhd" == child.get_current_atom_name()) {
+ // Skip 4 bytes (version + flags)
+ child.read_uint32();
+ // Grab the timestamp.
+
+ // Some Android phones package videos recorded with their internal cameras in a 3GP
+ // container that looks suspiciously like a QuickTime container but really isn't -- for
+ // the timestamps of these Android 3GP videos are relative to the UNIX epoch
+ // (January 1, 1970) instead of the QuickTime epoch (January 1, 1904). So, if we detect a
+ // QuickTime movie with a negative timestamp, we can be pretty sure it isn't a valid
+ // QuickTime movie that was shot before 1904 but is instead a non-compliant 3GP video
+ // file. If we detect such a video, we correct its time. See this Redmine ticket
+ // (https://bugzilla.gnome.org/show_bug.cgi?id=717384) for more information.
+
+ if ((child.read_uint32() - QUICKTIME_EPOCH_ADJUSTMENT) < 0) {
+ timestamp = new DateTime.from_unix_utc(child.read_uint32());
+ } else {
+ timestamp = new DateTime.from_unix_utc(child.read_uint32() - QUICKTIME_EPOCH_ADJUSTMENT);
+ }
+ done = true;
+ break;
+ }
+ child.next_atom();
+ }
+ }
+ test.next_atom();
+ }
+ } catch (GLib.Error e) {
+ debug("Error while testing for QuickTime file: %s", e.message);
+ }
+
+ try {
+ test.close_file();
+ } catch (GLib.Error e) {
+ debug("Error while closing Quicktime file: %s", e.message);
+ }
+
+ return timestamp;
+ }
+}
diff --git a/src/VideoSupport.vala b/src/video-support/Video.vala
index ec827ea..0238d7f 100644
--- a/src/VideoSupport.vala
+++ b/src/video-support/Video.vala
@@ -4,352 +4,32 @@
* See the COPYING file in this distribution.
*/
-public errordomain VideoError {
- FILE, // there's a problem reading the video container file (doesn't exist, no read
- // permission, etc.)
-
- CONTENTS, // we can read the container file but its contents are indecipherable (no codec,
- // malformed data, etc.)
-}
-
-public class VideoImportParams {
- // IN:
- public File file;
- public ImportID import_id = ImportID();
- public string? md5;
- public time_t exposure_time_override;
-
- // IN/OUT:
- public Thumbnails? thumbnails;
-
- // OUT:
- public VideoRow row = new VideoRow();
-
- public VideoImportParams(File file, ImportID import_id, string? md5,
- Thumbnails? thumbnails = null, time_t exposure_time_override = 0) {
- this.file = file;
- this.import_id = import_id;
- this.md5 = md5;
- this.thumbnails = thumbnails;
- this.exposure_time_override = exposure_time_override;
- }
-}
-
-public class VideoReader {
- private const double UNKNOWN_CLIP_DURATION = -1.0;
- private const uint THUMBNAILER_TIMEOUT = 10000; // In milliseconds.
-
- // File extensions for video containers that pack only metadata as per the AVCHD spec
- private const string[] METADATA_ONLY_FILE_EXTENSIONS = { "bdm", "bdmv", "cpi", "mpl" };
-
- private double clip_duration = UNKNOWN_CLIP_DURATION;
- private Gdk.Pixbuf preview_frame = null;
- private File file = null;
- private GLib.Pid thumbnailer_pid = 0;
- public DateTime? timestamp { get; private set; default = null; }
-
- public VideoReader(File file) {
- this.file = file;
- }
-
- public static bool is_supported_video_file(File file) {
- var mime_type = ContentType.guess(file.get_basename(), new uchar[0], null);
- // special case: deep-check content-type of files ending with .ogg
- if (mime_type == "audio/ogg" && file.has_uri_scheme("file")) {
- try {
- var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE,
- FileQueryInfoFlags.NONE);
- var content_type = info.get_content_type();
- if (content_type != null && content_type.has_prefix ("video/")) {
- return true;
- }
- } catch (Error error) {
- debug("Failed to query content type: %s", error.message);
- }
- }
-
- return is_supported_video_filename(file.get_basename());
- }
-
- public static bool is_supported_video_filename(string filename) {
- string mime_type;
- mime_type = ContentType.guess(filename, new uchar[0], null);
- // Guessed mp4 from filename has application/ as prefix, so check for mp4 in the end
- if (mime_type.has_prefix ("video/") || mime_type.has_suffix("mp4")) {
- string? extension = null;
- string? name = null;
- disassemble_filename(filename, out name, out extension);
-
- if (extension == null)
- return true;
-
- foreach (string s in METADATA_ONLY_FILE_EXTENSIONS) {
- if (utf8_ci_compare(s, extension) == 0)
- return false;
- }
-
- return true;
- } else {
- debug("Skipping %s, unsupported mime type %s", filename, mime_type);
- return false;
- }
- }
-
- public static ImportResult prepare_for_import(VideoImportParams params) {
-#if MEASURE_IMPORT
- Timer total_time = new Timer();
-#endif
- File file = params.file;
-
- FileInfo info = null;
- try {
- info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
- FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
- } catch (Error err) {
- return ImportResult.FILE_ERROR;
- }
-
- if (info.get_file_type() != FileType.REGULAR)
- return ImportResult.NOT_A_FILE;
-
- if (!is_supported_video_file(file)) {
- message("Not importing %s: file is marked as a video file but doesn't have a" +
- "supported extension", file.get_path());
-
- return ImportResult.UNSUPPORTED_FORMAT;
- }
-
- TimeVal timestamp = info.get_modification_time();
-
- // make sure params has a valid md5
- assert(params.md5 != null);
-
- time_t exposure_time = params.exposure_time_override;
- string title = "";
- string comment = "";
-
- VideoReader reader = new VideoReader(file);
- bool is_interpretable = true;
- double clip_duration = 0.0;
- Gdk.Pixbuf preview_frame = reader.read_preview_frame();
- try {
- clip_duration = reader.read_clip_duration();
- } catch (VideoError err) {
- if (err is VideoError.FILE) {
- return ImportResult.FILE_ERROR;
- } else if (err is VideoError.CONTENTS) {
- is_interpretable = false;
- clip_duration = 0.0;
- } else {
- error("can't prepare video for import: an unknown kind of video error occurred");
- }
- }
-
- try {
- VideoMetadata metadata = reader.read_metadata();
- MetadataDateTime? creation_date_time = metadata.get_creation_date_time();
-
- if (creation_date_time != null && creation_date_time.get_timestamp() != 0)
- exposure_time = creation_date_time.get_timestamp();
-
- string? video_title = metadata.get_title();
- string? video_comment = metadata.get_comment();
- if (video_title != null)
- title = video_title;
- if (video_comment != null)
- comment = video_comment;
- } catch (Error err) {
- warning("Unable to read video metadata: %s", err.message);
- }
-
- if (exposure_time == 0) {
- // Use time reported by Gstreamer, if available.
- exposure_time = (time_t) (reader.timestamp != null ?
- reader.timestamp.to_unix() : 0);
- }
-
- params.row.video_id = VideoID();
- params.row.filepath = file.get_path();
- params.row.filesize = info.get_size();
- params.row.timestamp = timestamp.tv_sec;
- params.row.width = preview_frame.width;
- params.row.height = preview_frame.height;
- params.row.clip_duration = clip_duration;
- params.row.is_interpretable = is_interpretable;
- params.row.exposure_time = exposure_time;
- params.row.import_id = params.import_id;
- params.row.event_id = EventID();
- params.row.md5 = params.md5;
- params.row.time_created = 0;
- params.row.title = title;
- params.row.comment = comment;
- params.row.backlinks = "";
- params.row.time_reimported = 0;
- params.row.flags = 0;
-
- if (params.thumbnails != null) {
- params.thumbnails = new Thumbnails();
- ThumbnailCache.generate_for_video_frame(params.thumbnails, preview_frame);
- }
-
-#if MEASURE_IMPORT
- debug("IMPORT: total time to import video = %lf", total_time.elapsed());
-#endif
- return ImportResult.SUCCESS;
- }
-
- private void read_internal() throws VideoError {
- if (!does_file_exist())
- throw new VideoError.FILE("video file '%s' does not exist or is inaccessible".printf(
- file.get_path()));
-
- try {
- Gst.PbUtils.Discoverer d = new Gst.PbUtils.Discoverer((Gst.ClockTime) (Gst.SECOND * 5));
- Gst.PbUtils.DiscovererInfo info = d.discover_uri(file.get_uri());
-
- clip_duration = ((double) info.get_duration()) / 1000000000.0;
-
- // Get creation time.
- // TODO: Note that TAG_DATE can be changed to TAG_DATE_TIME in the future
- // (and the corresponding output struct) in order to implement #2836.
- Date? video_date = null;
- if (info.get_tags() != null && info.get_tags().get_date(Gst.Tags.DATE, out video_date)) {
- // possible for get_date() to return true and a null Date
- if (video_date != null) {
- timestamp = new DateTime.local(video_date.get_year(), video_date.get_month(),
- video_date.get_day(), 0, 0, 0);
- }
- }
- } catch (Error e) {
- debug("Video read error: %s", e.message);
- throw new VideoError.CONTENTS("GStreamer couldn't extract clip information: %s"
- .printf(e.message));
- }
- }
-
- // Used by thumbnailer() to kill the external process if need be.
- private bool on_thumbnailer_timer() {
- debug("Thumbnailer timer called");
- if (thumbnailer_pid != 0) {
- debug("Killing thumbnailer process: %d", thumbnailer_pid);
-#if VALA_0_40
- Posix.kill(thumbnailer_pid, Posix.Signal.KILL);
-#else
- Posix.kill(thumbnailer_pid, Posix.SIGKILL);
-#endif
- }
- return false; // Don't call again.
- }
-
- // Performs video thumbnailing.
- // Note: not thread-safe if called from the same instance of the class.
- private Gdk.Pixbuf? thumbnailer(string video_file) {
- // Use Shotwell's thumbnailer, redirect output to stdout.
- debug("Launching thumbnailer process: %s", AppDirs.get_thumbnailer_bin().get_path());
- string[] argv = {AppDirs.get_thumbnailer_bin().get_path(), video_file};
- int child_stdout;
- try {
- GLib.Process.spawn_async_with_pipes(null, argv, null, GLib.SpawnFlags.SEARCH_PATH |
- GLib.SpawnFlags.DO_NOT_REAP_CHILD, null, out thumbnailer_pid, null, out child_stdout,
- null);
- debug("Spawned thumbnailer, child pid: %d", (int) thumbnailer_pid);
- } catch (Error e) {
- debug("Error spawning process: %s", e.message);
- if (thumbnailer_pid != 0)
- GLib.Process.close_pid(thumbnailer_pid);
- return null;
- }
-
- // Start timer.
- Timeout.add(THUMBNAILER_TIMEOUT, on_thumbnailer_timer);
-
- // Read pixbuf from stream.
- Gdk.Pixbuf? buf = null;
- try {
- GLib.UnixInputStream unix_input = new GLib.UnixInputStream(child_stdout, true);
- buf = new Gdk.Pixbuf.from_stream(unix_input, null);
- } catch (Error e) {
- debug("Error creating pixbuf: %s", e.message);
- buf = null;
- }
-
- // Make sure process exited properly.
- int child_status = 0;
- int ret_waitpid = Posix.waitpid(thumbnailer_pid, out child_status, 0);
- if (ret_waitpid < 0) {
- debug("waitpid returned error code: %d", ret_waitpid);
- buf = null;
- } else if (0 != Process.exit_status(child_status)) {
- debug("Thumbnailer exited with error code: %d",
- Process.exit_status(child_status));
- buf = null;
- }
-
- GLib.Process.close_pid(thumbnailer_pid);
- thumbnailer_pid = 0;
- return buf;
- }
-
- private bool does_file_exist() {
- return FileUtils.test(file.get_path(), FileTest.EXISTS | FileTest.IS_REGULAR);
- }
-
- public Gdk.Pixbuf? read_preview_frame() {
- if (preview_frame != null)
- return preview_frame;
-
- if (!does_file_exist())
- return null;
-
- // Get preview frame from thumbnailer.
- preview_frame = thumbnailer(file.get_path());
- if (null == preview_frame)
- preview_frame = Resources.get_noninterpretable_badge_pixbuf();
-
- return preview_frame;
- }
-
- public double read_clip_duration() throws VideoError {
- if (clip_duration == UNKNOWN_CLIP_DURATION)
- read_internal();
-
- return clip_duration;
- }
-
- public VideoMetadata read_metadata() throws Error {
- VideoMetadata metadata = new VideoMetadata();
- metadata.read_from_file(File.new_for_path(file.get_path()));
-
- return metadata;
- }
-}
-
public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public const string TYPENAME = "video";
-
+
public const uint64 FLAG_TRASH = 0x0000000000000001;
public const uint64 FLAG_OFFLINE = 0x0000000000000002;
public const uint64 FLAG_FLAGGED = 0x0000000000000004;
-
+
public class InterpretableResults {
internal Video video;
internal bool update_interpretable = false;
internal bool is_interpretable = false;
internal Gdk.Pixbuf? new_thumbnail = null;
-
+
public InterpretableResults(Video video) {
this.video = video;
}
-
+
public void foreground_finish() {
if (update_interpretable)
video.set_is_interpretable(is_interpretable);
-
+
if (new_thumbnail != null) {
try {
ThumbnailCache.replace(video, ThumbnailCache.Size.BIG, new_thumbnail);
ThumbnailCache.replace(video, ThumbnailCache.Size.MEDIUM, new_thumbnail);
-
+
video.notify_thumbnail_altered();
} catch (Error err) {
message("Unable to update video thumbnails for %s: %s", video.to_string(),
@@ -358,19 +38,19 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
}
}
}
-
+
private static bool normal_regen_complete;
private static bool offline_regen_complete;
public static VideoSourceCollection global;
private VideoRow backing_row;
-
+
public Video(VideoRow row) {
this.backing_row = row;
-
+
// normalize user text
this.backing_row.title = prep_title(this.backing_row.title);
-
+
if (((row.flags & FLAG_TRASH) != 0) || ((row.flags & FLAG_OFFLINE) != 0))
rehydrate_backlinks(global, row.backlinks);
}
@@ -381,7 +61,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
// https://bugzilla.gnome.org/show_bug.cgi?id=655594
normal_regen_complete = false;
offline_regen_complete = false;
-
+
// initialize GStreamer, but don't pass it our actual command line arguments -- we don't
// want our end users to be able to parameterize the GStreamer configuration
unowned string[] args = null;
@@ -404,7 +84,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
}
global = new VideoSourceCollection();
-
+
Gee.ArrayList<VideoRow?> all = VideoTable.get_instance().get_all();
Gee.ArrayList<Video> all_videos = new Gee.ArrayList<Video>();
Gee.ArrayList<Video> trashed_videos = new Gee.ArrayList<Video>();
@@ -412,14 +92,14 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
int count = all.size;
for (int ctr = 0; ctr < count; ctr++) {
Video video = new Video(all.get(ctr));
-
+
if (video.is_trashed())
trashed_videos.add(video);
else if (video.is_offline())
offline_videos.add(video);
else
all_videos.add(video);
-
+
if (monitor != null)
monitor(ctr, count);
}
@@ -428,7 +108,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
global.add_many_to_offline(offline_videos);
global.add_many(all_videos);
}
-
+
public static void notify_normal_thumbs_regenerated() {
if (normal_regen_complete)
return;
@@ -449,12 +129,12 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public static void terminate() {
}
-
+
public static ExporterUI? export_many(Gee.Collection<Video> videos, Exporter.CompletionCallback done,
- bool export_in_place = false) {
+ bool export_in_place = false) {
if (videos.size == 0)
return null;
-
+
// in place export is relatively easy -- provide a fast, separate code path for it
if (export_in_place) {
ExporterUI temp_exporter = new ExporterUI(new Exporter.for_temp_file(videos,
@@ -470,11 +150,11 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
video = v;
break;
}
-
+
File save_as = ExportUI.choose_file(video.get_basename());
if (save_as == null)
return null;
-
+
try {
AppWindow.get_instance().set_busy_cursor();
video.export(save_as);
@@ -483,7 +163,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
AppWindow.get_instance().set_normal_cursor();
export_error_dialog(save_as, false);
}
-
+
return null;
}
@@ -491,7 +171,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
File export_dir = ExportUI.choose_dir(_("Export Videos"));
if (export_dir == null)
return null;
-
+
ExporterUI exporter = new ExporterUI(new Exporter(videos, export_dir,
Scaling.for_original(), ExportFormatParameters.unmodified()));
exporter.export(done);
@@ -499,7 +179,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
return exporter;
}
- protected override void commit_backlinks(SourceCollection? sources, string? backlinks) {
+ protected override void commit_backlinks(SourceCollection? sources, string? backlinks) {
try {
VideoTable.get_instance().update_backlinks(get_video_id(), backlinks);
lock (backing_row) {
@@ -529,10 +209,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
return false;
#endif
}
-
+
public static ImportResult import_create(VideoImportParams params, out Video video) {
video = null;
-
+
// add to the database
try {
if (VideoTable.get_instance().add(params.row).is_invalid())
@@ -540,13 +220,13 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
} catch (DatabaseError err) {
return ImportResult.DATABASE_ERROR;
}
-
+
// create local object but don't add to global until thumbnails generated
video = new Video(params.row);
return ImportResult.SUCCESS;
}
-
+
public static void import_failed(Video video) {
try {
VideoTable.get_instance().remove(video.get_video_id());
@@ -554,17 +234,17 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
AppWindow.database_error(err);
}
}
-
+
public override BackingFileState[] get_backing_files_state() {
BackingFileState[] backing = new BackingFileState[1];
lock (backing_row) {
- backing[0] = new BackingFileState(backing_row.filepath, backing_row.filesize,
+ backing[0] = new BackingFileState(backing_row.filepath, backing_row.filesize,
backing_row.timestamp, backing_row.md5);
}
-
+
return backing;
}
-
+
public override Gdk.Pixbuf? get_thumbnail(int scale) throws Error {
return ThumbnailCache.fetch(this, scale);
}
@@ -577,21 +257,21 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override Gdk.Pixbuf get_preview_pixbuf(Scaling scaling) throws Error {
Gdk.Pixbuf pixbuf = get_thumbnail(ThumbnailCache.Size.BIG);
-
+
return scaling.perform_on_pixbuf(pixbuf, Gdk.InterpType.NEAREST, true);
}
public override Gdk.Pixbuf? create_thumbnail(int scale) throws Error {
VideoReader reader = new VideoReader(get_file());
Gdk.Pixbuf? frame = reader.read_preview_frame();
-
+
return (frame != null) ? frame : Resources.get_noninterpretable_badge_pixbuf().copy();
}
-
+
public override string get_typename() {
return TYPENAME;
}
-
+
public override int64 get_instance_id() {
return get_video_id().id;
}
@@ -605,7 +285,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override PhotoFileFormat get_preferred_thumbnail_format() {
return PhotoFileFormat.get_system_default_format();
}
-
+
public override string? get_title() {
lock (backing_row) {
return backing_row.title;
@@ -614,7 +294,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override void set_title(string? title) {
string? new_title = prep_title(title);
-
+
lock (backing_row) {
if (backing_row.title == new_title)
return;
@@ -641,7 +321,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override bool set_comment(string? comment) {
string? new_comment = prep_title(comment);
-
+
lock (backing_row) {
if (backing_row.comment == new_comment)
return true;
@@ -656,7 +336,7 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
// successfully committed to the database, so update it in the in-memory row cache
backing_row.comment = new_comment;
}
-
+
notify_altered(new Alteration("metadata", "comment"));
return true;
@@ -711,10 +391,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override void mark_offline() {
add_flags(FLAG_OFFLINE);
}
-
+
public override void mark_online() {
remove_flags(FLAG_OFFLINE);
-
+
if ((!get_is_interpretable()))
check_is_interpretable().foreground_finish();
}
@@ -722,48 +402,48 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override void trash() {
add_flags(FLAG_TRASH);
}
-
+
public override void untrash() {
remove_flags(FLAG_TRASH);
}
-
+
public bool is_flagged() {
return is_flag_set(FLAG_FLAGGED);
}
-
+
public void mark_flagged() {
add_flags(FLAG_FLAGGED, new Alteration("metadata", "flagged"));
}
-
+
public void mark_unflagged() {
remove_flags(FLAG_FLAGGED, new Alteration("metadata", "flagged"));
}
-
+
public override EventID get_event_id() {
lock (backing_row) {
return backing_row.event_id;
}
}
-
+
public override string to_string() {
lock (backing_row) {
return "[%s] %s".printf(backing_row.video_id.id.to_string(), backing_row.filepath);
}
}
-
+
public VideoID get_video_id() {
lock (backing_row) {
return backing_row.video_id;
}
}
-
- public override time_t get_exposure_time() {
+
+ public override DateTime? get_exposure_time() {
lock (backing_row) {
return backing_row.exposure_time;
}
}
-
- public void set_exposure_time(time_t time) {
+
+ public void set_exposure_time(DateTime time) {
lock (backing_row) {
try {
VideoTable.get_instance().set_exposure_time(backing_row.video_id, time);
@@ -772,10 +452,10 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
}
backing_row.exposure_time = time;
}
-
+
notify_altered(new Alteration("metadata", "exposure-time"));
- }
-
+ }
+
public Dimensions get_frame_dimensions() {
lock (backing_row) {
return Dimensions(backing_row.width, backing_row.height);
@@ -785,99 +465,99 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
public override Dimensions get_dimensions(Photo.Exception disallowed_steps = Photo.Exception.NONE) {
return get_frame_dimensions();
}
-
+
public override uint64 get_filesize() {
return get_master_filesize();
}
-
+
public override uint64 get_master_filesize() {
lock (backing_row) {
return backing_row.filesize;
}
}
-
- public override time_t get_timestamp() {
+
+ public override DateTime? get_timestamp() {
lock (backing_row) {
return backing_row.timestamp;
}
}
-
+
public void set_master_timestamp(FileInfo info) {
- TimeVal time_val = info.get_modification_time();
-
+ var time_val = info.get_modification_date_time();
+
try {
lock (backing_row) {
- if (backing_row.timestamp == time_val.tv_sec)
+ if (backing_row.timestamp.equal(time_val))
return;
-
- VideoTable.get_instance().set_timestamp(backing_row.video_id, time_val.tv_sec);
- backing_row.timestamp = time_val.tv_sec;
+
+ VideoTable.get_instance().set_timestamp(backing_row.video_id, time_val);
+ backing_row.timestamp = time_val;
}
- } catch (DatabaseError err) {
+ } catch (Error err) {
AppWindow.database_error(err);
-
+
return;
}
-
+
notify_altered(new Alteration("metadata", "master-timestamp"));
}
-
+
public string get_filename() {
lock (backing_row) {
return backing_row.filepath;
}
}
-
+
public override File get_file() {
return File.new_for_path(get_filename());
}
-
+
public override File get_master_file() {
return get_file();
}
-
+
public void export(File dest_file) throws Error {
File source_file = File.new_for_path(get_filename());
source_file.copy(dest_file, FileCopyFlags.OVERWRITE | FileCopyFlags.TARGET_DEFAULT_PERMS,
null, null);
}
-
+
public double get_clip_duration() {
lock (backing_row) {
return backing_row.clip_duration;
}
}
-
+
public bool get_is_interpretable() {
lock (backing_row) {
return backing_row.is_interpretable;
}
}
-
+
private void set_is_interpretable(bool is_interpretable) {
lock (backing_row) {
if (backing_row.is_interpretable == is_interpretable)
return;
-
+
backing_row.is_interpretable = is_interpretable;
}
-
+
try {
VideoTable.get_instance().update_is_interpretable(get_video_id(), is_interpretable);
} catch (DatabaseError e) {
AppWindow.database_error(e);
}
}
-
+
// Intended to be called from a background thread but can be called from foreground as well.
// Caller should call InterpretableResults.foreground_process() only from foreground thread,
// however
public InterpretableResults check_is_interpretable() {
InterpretableResults results = new InterpretableResults(this);
-
+
double clip_duration = -1.0;
Gdk.Pixbuf? preview_frame = null;
-
+
VideoReader backing_file_reader = new VideoReader(get_file());
try {
clip_duration = backing_file_reader.read_clip_duration();
@@ -887,111 +567,111 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
// non-interpretable (e.g. its codec is not present on the users system).
results.update_interpretable = get_is_interpretable();
results.is_interpretable = false;
-
+
return results;
}
-
+
// if already marked interpretable, this is only confirming what we already knew
if (get_is_interpretable()) {
results.update_interpretable = false;
results.is_interpretable = true;
-
+
return results;
}
-
+
debug("video %s has become interpretable", get_file().get_basename());
-
+
// save this here, this can be done in background thread
lock (backing_row) {
backing_row.clip_duration = clip_duration;
}
-
+
results.update_interpretable = true;
results.is_interpretable = true;
results.new_thumbnail = preview_frame;
-
+
return results;
}
-
+
public override void destroy() {
VideoID video_id = get_video_id();
ThumbnailCache.remove(this);
-
+
try {
VideoTable.get_instance().remove(video_id);
} catch (DatabaseError err) {
error("failed to remove video %s from video table", to_string());
}
-
+
base.destroy();
}
protected override bool internal_delete_backing() throws Error {
bool ret = delete_original_file();
-
+
// Return false if parent method failed.
return base.internal_delete_backing() && ret;
}
-
+
private void notify_flags_altered(Alteration? additional_alteration) {
Alteration alteration = new Alteration("metadata", "flags");
if (additional_alteration != null)
alteration = alteration.compress(additional_alteration);
-
+
notify_altered(alteration);
}
-
+
public uint64 add_flags(uint64 flags_to_add, Alteration? additional_alteration = null) {
uint64 new_flags;
lock (backing_row) {
new_flags = internal_add_flags(backing_row.flags, flags_to_add);
if (backing_row.flags == new_flags)
return backing_row.flags;
-
+
try {
VideoTable.get_instance().set_flags(get_video_id(), new_flags);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return backing_row.flags;
}
-
+
backing_row.flags = new_flags;
}
-
+
notify_flags_altered(additional_alteration);
-
+
return new_flags;
}
-
+
public uint64 remove_flags(uint64 flags_to_remove, Alteration? additional_alteration = null) {
uint64 new_flags;
lock (backing_row) {
new_flags = internal_remove_flags(backing_row.flags, flags_to_remove);
if (backing_row.flags == new_flags)
return backing_row.flags;
-
+
try {
VideoTable.get_instance().set_flags(get_video_id(), new_flags);
} catch (DatabaseError e) {
AppWindow.database_error(e);
return backing_row.flags;
}
-
+
backing_row.flags = new_flags;
}
-
+
notify_flags_altered(additional_alteration);
-
+
return new_flags;
}
-
+
public bool is_flag_set(uint64 flag) {
lock (backing_row) {
return internal_is_flag_set(backing_row.flags, flag);
}
}
-
+
public void set_master_file(File file) {
string new_filepath = file.get_path();
string? old_filepath = null;
@@ -999,195 +679,25 @@ public class Video : VideoSource, Flaggable, Monitorable, Dateable {
lock (backing_row) {
if (backing_row.filepath == new_filepath)
return;
-
+
old_filepath = backing_row.filepath;
-
+
VideoTable.get_instance().set_filepath(backing_row.video_id, new_filepath);
backing_row.filepath = new_filepath;
}
- } catch (DatabaseError err) {
+ } catch (Error err) {
AppWindow.database_error(err);
-
+
return;
}
-
+
assert(old_filepath != null);
notify_master_replaced(File.new_for_path(old_filepath), file);
-
+
notify_altered(new Alteration.from_list("backing:master,metadata:name"));
}
-
+
public VideoMetadata read_metadata() throws Error {
return (new VideoReader(get_file())).read_metadata();
}
}
-
-public class VideoSourceCollection : MediaSourceCollection {
- public enum State {
- UNKNOWN,
- ONLINE,
- OFFLINE,
- TRASH
- }
-
- public override TransactionController transaction_controller {
- get {
- if (_transaction_controller == null)
- _transaction_controller = new MediaSourceTransactionController(this);
-
- return _transaction_controller;
- }
- }
-
- private TransactionController _transaction_controller = null;
- private Gee.MultiMap<uint64?, Video> filesize_to_video =
- new Gee.TreeMultiMap<uint64?, Video>(uint64_compare);
-
- public VideoSourceCollection() {
- base("VideoSourceCollection", get_video_key);
-
- get_trashcan().contents_altered.connect(on_trashcan_contents_altered);
- get_offline_bin().contents_altered.connect(on_offline_contents_altered);
- }
-
- protected override MediaSourceHoldingTank create_trashcan() {
- return new MediaSourceHoldingTank(this, is_video_trashed, get_video_key);
- }
-
- protected override MediaSourceHoldingTank create_offline_bin() {
- return new MediaSourceHoldingTank(this, is_video_offline, get_video_key);
- }
-
- public override MediaMonitor create_media_monitor(Workers workers, Cancellable cancellable) {
- return new VideoMonitor(cancellable);
- }
-
- public override bool holds_type_of_source(DataSource source) {
- return source is Video;
- }
-
- public override string get_typename() {
- return Video.TYPENAME;
- }
-
- public override bool is_file_recognized(File file) {
- return VideoReader.is_supported_video_file(file);
- }
-
- private void on_trashcan_contents_altered(Gee.Collection<DataSource>? added,
- Gee.Collection<DataSource>? removed) {
- trashcan_contents_altered((Gee.Collection<Video>?) added,
- (Gee.Collection<Video>?) removed);
- }
-
- private void on_offline_contents_altered(Gee.Collection<DataSource>? added,
- Gee.Collection<DataSource>? removed) {
- offline_contents_altered((Gee.Collection<Video>?) added,
- (Gee.Collection<Video>?) removed);
- }
-
- protected override MediaSource? fetch_by_numeric_id(int64 numeric_id) {
- return fetch(VideoID(numeric_id));
- }
-
- public static int64 get_video_key(DataSource source) {
- Video video = (Video) source;
- VideoID video_id = video.get_video_id();
-
- return video_id.id;
- }
-
- public static bool is_video_trashed(DataSource source) {
- return ((Video) source).is_trashed();
- }
-
- public static bool is_video_offline(DataSource source) {
- return ((Video) source).is_offline();
- }
-
- public Video fetch(VideoID video_id) {
- return (Video) fetch_by_key(video_id.id);
- }
-
- public override Gee.Collection<string> get_event_source_ids(EventID event_id){
- return VideoTable.get_instance().get_event_source_ids(event_id);
- }
-
- public Video? get_state_by_file(File file, out State state) {
- Video? video = (Video?) fetch_by_master_file(file);
- if (video != null) {
- state = State.ONLINE;
-
- return video;
- }
-
- video = (Video?) get_trashcan().fetch_by_master_file(file);
- if (video != null) {
- state = State.TRASH;
-
- return video;
- }
-
- video = (Video?) get_offline_bin().fetch_by_master_file(file);
- if (video != null) {
- state = State.OFFLINE;
-
- return video;
- }
-
- state = State.UNKNOWN;
-
- return null;
- }
-
- private void compare_backing(Video video, FileInfo info, Gee.Collection<Video> matching_master) {
- if (video.get_filesize() != info.get_size())
- return;
-
- if (video.get_timestamp() == info.get_modification_time().tv_sec)
- matching_master.add(video);
- }
-
- public void fetch_by_matching_backing(FileInfo info, Gee.Collection<Video> matching_master) {
- foreach (DataObject object in get_all())
- compare_backing((Video) object, info, matching_master);
-
- foreach (MediaSource media in get_offline_bin_contents())
- compare_backing((Video) media, info, matching_master);
- }
-
- protected override void notify_contents_altered(Gee.Iterable<DataObject>? added,
- Gee.Iterable<DataObject>? removed) {
- if (added != null) {
- foreach (DataObject object in added) {
- Video video = (Video) object;
-
- filesize_to_video.set(video.get_master_filesize(), video);
- }
- }
-
- if (removed != null) {
- foreach (DataObject object in removed) {
- Video video = (Video) object;
-
- filesize_to_video.remove(video.get_master_filesize(), video);
- }
- }
-
- base.notify_contents_altered(added, removed);
- }
-
- public VideoID get_basename_filesize_duplicate(string basename, uint64 filesize) {
- foreach (Video video in filesize_to_video.get(filesize)) {
- if (utf8_ci_compare(video.get_master_file().get_basename(), basename) == 0)
- return video.get_video_id();
- }
-
- return VideoID(); // the default constructor of the VideoID struct creates an invalid
- // video id, which is just what we want in this case
- }
-
- public bool has_basename_filesize_duplicate(string basename, uint64 filesize) {
- return get_basename_filesize_duplicate(basename, filesize).is_valid();
- }
-}
diff --git a/src/video-support/VideoImportParams.vala b/src/video-support/VideoImportParams.vala
new file mode 100644
index 0000000..6804c53
--- /dev/null
+++ b/src/video-support/VideoImportParams.vala
@@ -0,0 +1,28 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public class VideoImportParams {
+ // IN:
+ public File file;
+ public ImportID import_id = ImportID();
+ public string? md5;
+ public DateTime? exposure_time_override;
+
+ // IN/OUT:
+ public Thumbnails? thumbnails;
+
+ // OUT:
+ public VideoRow row = new VideoRow();
+
+ public VideoImportParams(File file, ImportID import_id, string? md5,
+ Thumbnails? thumbnails = null, DateTime? exposure_time_override = null) {
+ this.file = file;
+ this.import_id = import_id;
+ this.md5 = md5;
+ this.thumbnails = thumbnails;
+ this.exposure_time_override = exposure_time_override;
+ }
+}
diff --git a/src/video-support/VideoMetadata.vala b/src/video-support/VideoMetadata.vala
new file mode 100644
index 0000000..02580f8
--- /dev/null
+++ b/src/video-support/VideoMetadata.vala
@@ -0,0 +1,51 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+public class VideoMetadata : MediaMetadata {
+
+ private MetadataDateTime timestamp = null;
+ private string title = null;
+ private string comment = null;
+
+ public VideoMetadata() {
+ }
+
+ ~VideoMetadata() {
+ }
+
+ public override void read_from_file(File file) throws Error {
+ QuickTimeMetadataLoader quicktime = new QuickTimeMetadataLoader(file);
+ if (quicktime.is_supported()) {
+ timestamp = quicktime.get_creation_date_time();
+ title = quicktime.get_title();
+ // TODO: is there an quicktime.get_comment ??
+ comment = null;
+ return;
+ }
+ AVIMetadataLoader avi = new AVIMetadataLoader(file);
+ if (avi.is_supported()) {
+ timestamp = avi.get_creation_date_time();
+ title = avi.get_title();
+ comment = null;
+ return;
+ }
+
+ throw new IOError.NOT_SUPPORTED("File %s is not a supported video format", file.get_path());
+ }
+
+ public override MetadataDateTime? get_creation_date_time() {
+ return timestamp;
+ }
+
+ public override string? get_title() {
+ return title;
+ }
+
+ public override string? get_comment() {
+ return comment;
+ }
+
+}
diff --git a/src/video-support/VideoMetadataReaderProcess.vala b/src/video-support/VideoMetadataReaderProcess.vala
new file mode 100644
index 0000000..26d61a6
--- /dev/null
+++ b/src/video-support/VideoMetadataReaderProcess.vala
@@ -0,0 +1,66 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+using Gst;
+using Gst.PbUtils;
+
+int main(string[] args) {
+ Intl.setlocale(GLib.LocaleCategory.NUMERIC, "C");
+
+ var option_context = new OptionContext("- shotwell video metadata reader helper binary");
+ option_context.set_help_enabled(true);
+ option_context.add_group(Gst.init_get_option_group());
+
+ double clip_duration;
+ GLib.DateTime timestamp = null;
+
+ try {
+ option_context.parse(ref args);
+
+ if (args.length < 2)
+ throw new IOError.INVALID_ARGUMENT("Missing URI");
+
+ var f = File.new_for_commandline_arg (args[1]);
+
+ Gst.PbUtils.Discoverer d = new Gst.PbUtils.Discoverer((Gst.ClockTime) (Gst.SECOND * 5));
+ Gst.PbUtils.DiscovererInfo info = d.discover_uri(f.get_uri());
+
+ clip_duration = ((double) info.get_duration()) / 1000000000.0;
+
+ // Get creation time.
+ // TODO: Note that TAG_DATE can be changed to TAG_DATE_TIME in the future
+ // (and the corresponding output struct) in order to implement #2836.
+ Gst.DateTime? video_date = null;
+
+ Gst.TagList? tags = null;
+
+ var stream_info = info.get_stream_info();
+ if (stream_info is Gst.PbUtils.DiscovererContainerInfo) {
+ tags = ((Gst.PbUtils.DiscovererContainerInfo)stream_info).get_tags();
+ }
+ else if (stream_info is Gst.PbUtils.DiscovererStreamInfo) {
+ tags = ((Gst.PbUtils.DiscovererStreamInfo)stream_info).get_tags();
+ }
+
+ if (tags != null && tags.get_date_time(Gst.Tags.DATE_TIME, out video_date)) {
+ // possible for get_date() to return true and a null Date
+ if (video_date != null) {
+ timestamp = video_date.to_g_date_time().to_local();
+ }
+ }
+
+ print("%.3f\n", clip_duration);
+ if (timestamp != null) {
+ print("%s\n", timestamp.format_iso8601());
+ } else {
+ print("none\n");
+ }
+ } catch (Error error) {
+ critical("Failed to parse options: %s", error.message);
+
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/src/video-support/VideoReader.vala b/src/video-support/VideoReader.vala
new file mode 100644
index 0000000..11f11e1
--- /dev/null
+++ b/src/video-support/VideoReader.vala
@@ -0,0 +1,317 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public errordomain VideoError {
+ FILE, // there's a problem reading the video container file (doesn't exist, no read
+ // permission, etc.)
+
+ CONTENTS, // we can read the container file but its contents are indecipherable (no codec,
+ // malformed data, etc.)
+}
+
+public class VideoReader {
+ private const double UNKNOWN_CLIP_DURATION = -1.0;
+ private const uint THUMBNAILER_TIMEOUT = 10000; // In milliseconds.
+
+ // File extensions for video containers that pack only metadata as per the AVCHD spec
+ private const string[] METADATA_ONLY_FILE_EXTENSIONS = { "bdm", "bdmv", "cpi", "mpl" };
+
+ private double clip_duration = UNKNOWN_CLIP_DURATION;
+ private Gdk.Pixbuf preview_frame = null;
+ private File file = null;
+ private Subprocess thumbnailer_process = null;
+ public DateTime? timestamp { get; private set; default = null; }
+
+ public VideoReader(File file) {
+ this.file = file;
+ }
+
+ public static bool is_supported_video_file(File file) {
+ var mime_type = ContentType.guess(file.get_basename(), new uchar[0], null);
+ // special case: deep-check content-type of files ending with .ogg
+ if (mime_type == "audio/ogg" && file.has_uri_scheme("file")) {
+ try {
+ var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE,
+ FileQueryInfoFlags.NONE);
+ var content_type = info.get_content_type();
+ if (content_type != null && content_type.has_prefix ("video/")) {
+ return true;
+ }
+ } catch (Error error) {
+ debug("Failed to query content type: %s", error.message);
+ }
+ }
+
+ return is_supported_video_filename(file.get_basename());
+ }
+
+ public static bool is_supported_video_filename(string filename) {
+ string mime_type;
+ mime_type = ContentType.guess(filename, new uchar[0], null);
+ // Guessed mp4/mxf from filename has application/ as prefix, so check for mp4/mxf in the end
+ if (mime_type.has_prefix ("video/") ||
+ mime_type.has_suffix("mp4") ||
+ mime_type.has_suffix("mxf")) {
+ string? extension = null;
+ string? name = null;
+ disassemble_filename(filename, out name, out extension);
+
+ if (extension == null)
+ return true;
+
+ foreach (string s in METADATA_ONLY_FILE_EXTENSIONS) {
+ if (utf8_ci_compare(s, extension) == 0)
+ return false;
+ }
+
+ return true;
+ } else {
+ debug("Skipping %s, unsupported mime type %s", filename, mime_type);
+ return false;
+ }
+ }
+
+ public static ImportResult prepare_for_import(VideoImportParams params) {
+#if MEASURE_IMPORT
+ Timer total_time = new Timer();
+#endif
+ File file = params.file;
+
+ FileInfo info = null;
+ try {
+ info = file.query_info(DirectoryMonitor.SUPPLIED_ATTRIBUTES,
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ } catch (Error err) {
+ return ImportResult.FILE_ERROR;
+ }
+
+ if (info.get_file_type() != FileType.REGULAR)
+ return ImportResult.NOT_A_FILE;
+
+ if (!is_supported_video_file(file)) {
+ message("Not importing %s: file is marked as a video file but doesn't have a" +
+ "supported extension", file.get_path());
+
+ return ImportResult.UNSUPPORTED_FORMAT;
+ }
+
+ var timestamp = info.get_modification_date_time();
+
+ // make sure params has a valid md5
+ assert(params.md5 != null);
+
+ DateTime exposure_time = params.exposure_time_override;
+ string title = "";
+ string comment = "";
+
+ VideoReader reader = new VideoReader(file);
+ bool is_interpretable = true;
+ double clip_duration = 0.0;
+ Gdk.Pixbuf preview_frame = reader.read_preview_frame();
+ try {
+ clip_duration = reader.read_clip_duration();
+ } catch (VideoError err) {
+ if (err is VideoError.FILE) {
+ return ImportResult.FILE_ERROR;
+ } else if (err is VideoError.CONTENTS) {
+ is_interpretable = false;
+ clip_duration = 0.0;
+ } else {
+ error("can't prepare video for import: an unknown kind of video error occurred");
+ }
+ }
+
+ try {
+ VideoMetadata metadata = reader.read_metadata();
+ MetadataDateTime? creation_date_time = metadata.get_creation_date_time();
+
+ if (creation_date_time != null && creation_date_time.get_timestamp() != null)
+ exposure_time = creation_date_time.get_timestamp();
+
+ string? video_title = metadata.get_title();
+ string? video_comment = metadata.get_comment();
+ if (video_title != null)
+ title = video_title;
+ if (video_comment != null)
+ comment = video_comment;
+ } catch (Error err) {
+ warning("Unable to read video metadata: %s", err.message);
+ }
+
+ if (exposure_time == null) {
+ // Use time reported by Gstreamer, if available.
+ exposure_time = reader.timestamp;
+ }
+
+ params.row.video_id = VideoID();
+ params.row.filepath = file.get_path();
+ params.row.filesize = info.get_size();
+ params.row.timestamp = timestamp;
+ params.row.width = preview_frame.width;
+ params.row.height = preview_frame.height;
+ params.row.clip_duration = clip_duration;
+ params.row.is_interpretable = is_interpretable;
+ params.row.exposure_time = exposure_time;
+ params.row.import_id = params.import_id;
+ params.row.event_id = EventID();
+ params.row.md5 = params.md5;
+ params.row.time_created = 0;
+ params.row.title = title;
+ params.row.comment = comment;
+ params.row.backlinks = "";
+ params.row.time_reimported = 0;
+ params.row.flags = 0;
+
+ if (params.thumbnails != null) {
+ params.thumbnails = new Thumbnails();
+ ThumbnailCache.generate_for_video_frame(params.thumbnails, preview_frame);
+ }
+
+#if MEASURE_IMPORT
+ debug("IMPORT: total time to import video = %lf", total_time.elapsed());
+#endif
+ return ImportResult.SUCCESS;
+ }
+
+ private void read_internal() throws VideoError {
+ if (!does_file_exist())
+ throw new VideoError.FILE("video file '%s' does not exist or is inaccessible".printf(
+ file.get_path()));
+
+ uint id = 0;
+ try {
+ var cancellable = new Cancellable();
+
+ id = Timeout.add_seconds(10, () => {
+ cancellable.cancel();
+ id = 0;
+
+ return false;
+ });
+
+ Bytes stdout_buf = null;
+ Bytes stderr_buf = null;
+
+ var process = new GLib.Subprocess(GLib.SubprocessFlags.STDOUT_PIPE, AppDirs.get_metadata_helper().get_path(), file.get_uri());
+ var result = process.communicate(null, cancellable, out stdout_buf, out stderr_buf);
+ if (result && process.get_if_exited() && process.get_exit_status () == 0 && stdout_buf != null && stdout_buf.get_size() > 0) {
+ string[] lines = ((string) stdout_buf.get_data()).split("\n");
+
+ var old = Intl.setlocale(GLib.LocaleCategory.NUMERIC, "C");
+ clip_duration = double.parse(lines[0]);
+ Intl.setlocale(GLib.LocaleCategory.NUMERIC, old);
+ if (lines[1] != "none")
+ timestamp = new DateTime.from_iso8601(lines[1], null);
+ } else {
+ string message = "";
+ if (stderr_buf != null && stderr_buf.get_size() > 0) {
+ message = (string) stderr_buf.get_data();
+ }
+ warning ("External Metadata helper failed");
+ }
+ } catch (Error e) {
+ debug("Video read error: %s", e.message);
+ throw new VideoError.CONTENTS("GStreamer couldn't extract clip information: %s"
+ .printf(e.message));
+ }
+
+ if (id != 0) {
+ Source.remove(id);
+ }
+ }
+
+ // Used by thumbnailer() to kill the external process if need be.
+ private bool on_thumbnailer_timer() {
+ debug("Thumbnailer timer called");
+ if (thumbnailer_process != null) {
+ thumbnailer_process.force_exit();
+ }
+ return false; // Don't call again.
+ }
+
+ // Performs video thumbnailing.
+ // Note: not thread-safe if called from the same instance of the class.
+ private Gdk.Pixbuf? thumbnailer(string video_file) {
+ // Use Shotwell's thumbnailer, redirect output to stdout.
+ debug("Launching thumbnailer process: %s", AppDirs.get_thumbnailer_bin().get_path());
+ FileIOStream stream;
+ File output_file;
+ try {
+ output_file = File.new_tmp(null, out stream);
+ } catch (Error e) {
+ debug("Failed to create temporary file: %s", e.message);
+ return null;
+ }
+
+ try {
+ thumbnailer_process = new Subprocess(SubprocessFlags.NONE,
+ AppDirs.get_thumbnailer_bin().get_path(), video_file, output_file.get_path());
+ } catch (Error e) {
+ debug("Error spawning process: %s", e.message);
+ return null;
+ }
+
+ // Start timer.
+ Timeout.add(THUMBNAILER_TIMEOUT, on_thumbnailer_timer);
+
+ // Make sure process exited properly.
+ try {
+ thumbnailer_process.wait_check();
+
+ // Read pixbuf from stream.
+ Gdk.Pixbuf? buf = null;
+ try {
+ buf = new Gdk.Pixbuf.from_stream(stream.get_input_stream(), null);
+ return buf;
+ } catch (Error e) {
+ debug("Error creating pixbuf: %s", e.message);
+ }
+ } catch (Error err) {
+ debug("Thumbnailer process exited with error: %s", err.message);
+ }
+
+ try {
+ output_file.delete(null);
+ } catch (Error err) {
+ debug("Failed to remove temporary file: %s", err.message);
+ }
+
+ return null;
+ }
+
+ private bool does_file_exist() {
+ return FileUtils.test(file.get_path(), FileTest.EXISTS | FileTest.IS_REGULAR);
+ }
+
+ public Gdk.Pixbuf? read_preview_frame() {
+ if (preview_frame != null)
+ return preview_frame;
+
+ if (!does_file_exist())
+ return null;
+
+ // Get preview frame from thumbnailer.
+ preview_frame = thumbnailer(file.get_path());
+ if (null == preview_frame)
+ preview_frame = Resources.get_noninterpretable_badge_pixbuf();
+
+ return preview_frame;
+ }
+
+ public double read_clip_duration() throws VideoError {
+ if (clip_duration == UNKNOWN_CLIP_DURATION)
+ read_internal();
+
+ return clip_duration;
+ }
+
+ public VideoMetadata read_metadata() throws Error {
+ VideoMetadata metadata = new VideoMetadata();
+ metadata.read_from_file(File.new_for_path(file.get_path()));
+
+ return metadata;
+ }
+}
diff --git a/src/video-support/VideoSourceCollection.vala b/src/video-support/VideoSourceCollection.vala
new file mode 100644
index 0000000..89daad3
--- /dev/null
+++ b/src/video-support/VideoSourceCollection.vala
@@ -0,0 +1,175 @@
+/* Copyright 2016 Software Freedom Conservancy Inc.
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public class VideoSourceCollection : MediaSourceCollection {
+ public enum State {
+ UNKNOWN,
+ ONLINE,
+ OFFLINE,
+ TRASH
+ }
+
+ public override TransactionController transaction_controller {
+ get {
+ if (_transaction_controller == null)
+ _transaction_controller = new MediaSourceTransactionController(this);
+
+ return _transaction_controller;
+ }
+ }
+
+ private TransactionController _transaction_controller = null;
+ private Gee.MultiMap<uint64?, Video> filesize_to_video =
+ new Gee.TreeMultiMap<uint64?, Video>(uint64_compare);
+
+ public VideoSourceCollection() {
+ base("VideoSourceCollection", get_video_key);
+
+ get_trashcan().contents_altered.connect(on_trashcan_contents_altered);
+ get_offline_bin().contents_altered.connect(on_offline_contents_altered);
+ }
+
+ protected override MediaSourceHoldingTank create_trashcan() {
+ return new MediaSourceHoldingTank(this, is_video_trashed, get_video_key);
+ }
+
+ protected override MediaSourceHoldingTank create_offline_bin() {
+ return new MediaSourceHoldingTank(this, is_video_offline, get_video_key);
+ }
+
+ public override MediaMonitor create_media_monitor(Workers workers, Cancellable cancellable) {
+ return new VideoMonitor(cancellable);
+ }
+
+ public override bool holds_type_of_source(DataSource source) {
+ return source is Video;
+ }
+
+ public override string get_typename() {
+ return Video.TYPENAME;
+ }
+
+ public override bool is_file_recognized(File file) {
+ return VideoReader.is_supported_video_file(file);
+ }
+
+ private void on_trashcan_contents_altered(Gee.Collection<DataSource>? added,
+ Gee.Collection<DataSource>? removed) {
+ trashcan_contents_altered((Gee.Collection<Video>?) added,
+ (Gee.Collection<Video>?) removed);
+ }
+
+ private void on_offline_contents_altered(Gee.Collection<DataSource>? added,
+ Gee.Collection<DataSource>? removed) {
+ offline_contents_altered((Gee.Collection<Video>?) added,
+ (Gee.Collection<Video>?) removed);
+ }
+
+ protected override MediaSource? fetch_by_numeric_id(int64 numeric_id) {
+ return fetch(VideoID(numeric_id));
+ }
+
+ public static int64 get_video_key(DataSource source) {
+ Video video = (Video) source;
+ VideoID video_id = video.get_video_id();
+
+ return video_id.id;
+ }
+
+ public static bool is_video_trashed(DataSource source) {
+ return ((Video) source).is_trashed();
+ }
+
+ public static bool is_video_offline(DataSource source) {
+ return ((Video) source).is_offline();
+ }
+
+ public Video fetch(VideoID video_id) {
+ return (Video) fetch_by_key(video_id.id);
+ }
+
+ public override Gee.Collection<string> get_event_source_ids(EventID event_id){
+ return VideoTable.get_instance().get_event_source_ids(event_id);
+ }
+
+ public Video? get_state_by_file(File file, out State state) {
+ Video? video = (Video?) fetch_by_master_file(file);
+ if (video != null) {
+ state = State.ONLINE;
+
+ return video;
+ }
+
+ video = (Video?) get_trashcan().fetch_by_master_file(file);
+ if (video != null) {
+ state = State.TRASH;
+
+ return video;
+ }
+
+ video = (Video?) get_offline_bin().fetch_by_master_file(file);
+ if (video != null) {
+ state = State.OFFLINE;
+
+ return video;
+ }
+
+ state = State.UNKNOWN;
+
+ return null;
+ }
+
+ private void compare_backing(Video video, FileInfo info, Gee.Collection<Video> matching_master) {
+ if (video.get_filesize() != info.get_size())
+ return;
+
+ if (video.get_timestamp().equal(info.get_modification_date_time()))
+ matching_master.add(video);
+ }
+
+ public void fetch_by_matching_backing(FileInfo info, Gee.Collection<Video> matching_master) {
+ foreach (DataObject object in get_all())
+ compare_backing((Video) object, info, matching_master);
+
+ foreach (MediaSource media in get_offline_bin_contents())
+ compare_backing((Video) media, info, matching_master);
+ }
+
+ protected override void notify_contents_altered(Gee.Iterable<DataObject>? added,
+ Gee.Iterable<DataObject>? removed) {
+ if (added != null) {
+ foreach (DataObject object in added) {
+ Video video = (Video) object;
+
+ filesize_to_video.set(video.get_master_filesize(), video);
+ }
+ }
+
+ if (removed != null) {
+ foreach (DataObject object in removed) {
+ Video video = (Video) object;
+
+ filesize_to_video.remove(video.get_master_filesize(), video);
+ }
+ }
+
+ base.notify_contents_altered(added, removed);
+ }
+
+ public VideoID get_basename_filesize_duplicate(string basename, uint64 filesize) {
+ foreach (Video video in filesize_to_video.get(filesize)) {
+ if (utf8_ci_compare(video.get_master_file().get_basename(), basename) == 0)
+ return video.get_video_id();
+ }
+
+ return VideoID(); // the default constructor of the VideoID struct creates an invalid
+ // video id, which is just what we want in this case
+ }
+
+ public bool has_basename_filesize_duplicate(string basename, uint64 filesize) {
+ return get_basename_filesize_duplicate(basename, filesize).is_valid();
+ }
+}
diff --git a/src/video-support/meson.build b/src/video-support/meson.build
new file mode 100644
index 0000000..da3f9d7
--- /dev/null
+++ b/src/video-support/meson.build
@@ -0,0 +1,36 @@
+executable(
+ 'shotwell-video-metadata-handler',
+ [
+ 'VideoMetadataReaderProcess.vala'
+ ],
+ dependencies : [
+ gio,
+ 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
+)
+
+libvideometadata_handling = static_library(
+ 'video_metadata_handling',
+ [
+ 'AVIChunk.vala',
+ 'AVIMetadataLoader.vala',
+ 'QuickTimeAtom.vala',
+ 'QuicktimeMetdataLoader.vala',
+ 'util.vala'
+ ],
+ vala_header : 'shotwell-internal-video-metadata-handling.h',
+ vala_vapi : 'shotwell-internal-video-metadata-handling.vapi',
+ include_directories : config_incdir,
+ dependencies : [
+ gio,
+ metadata
+ ]
+)
+
+metadata_handling = declare_dependency(
+ include_directories : include_directories('.'),
+ link_with : libvideometadata_handling
+)
diff --git a/src/video-support/util.vala b/src/video-support/util.vala
new file mode 100644
index 0000000..ad06680
--- /dev/null
+++ b/src/video-support/util.vala
@@ -0,0 +1,13 @@
+// Breaks a uint64 skip amount into several smaller skips.
+public void skip_uint64(InputStream input, uint64 skip_amount) throws GLib.Error {
+ while (skip_amount > 0) {
+ // skip() throws an error if the amount is too large, so check against ssize_t.MAX
+ if (skip_amount >= ssize_t.MAX) {
+ input.skip(ssize_t.MAX);
+ skip_amount -= ssize_t.MAX;
+ } else {
+ input.skip((size_t) skip_amount);
+ skip_amount = 0;
+ }
+ }
+}