diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-07-23 09:06:59 +0200 |
commit | 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch) | |
tree | d2e54377d14d604356c86862a326f64ae64dadd6 /src/library |
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/library')
-rw-r--r-- | src/library/Branch.vala | 54 | ||||
-rw-r--r-- | src/library/FlaggedBranch.vala | 61 | ||||
-rw-r--r-- | src/library/FlaggedPage.vala | 54 | ||||
-rw-r--r-- | src/library/ImportQueueBranch.vala | 74 | ||||
-rw-r--r-- | src/library/ImportQueuePage.vala | 208 | ||||
-rw-r--r-- | src/library/LastImportBranch.vala | 47 | ||||
-rw-r--r-- | src/library/LastImportPage.vala | 79 | ||||
-rw-r--r-- | src/library/Library.vala | 19 | ||||
-rw-r--r-- | src/library/LibraryWindow.vala | 1587 | ||||
-rw-r--r-- | src/library/OfflineBranch.vala | 51 | ||||
-rw-r--r-- | src/library/OfflinePage.vala | 130 | ||||
-rw-r--r-- | src/library/TrashBranch.vala | 73 | ||||
-rw-r--r-- | src/library/TrashPage.vala | 120 | ||||
-rw-r--r-- | src/library/mk/library.mk | 54 |
14 files changed, 2611 insertions, 0 deletions
diff --git a/src/library/Branch.vala b/src/library/Branch.vala new file mode 100644 index 0000000..dc05d60 --- /dev/null +++ b/src/library/Branch.vala @@ -0,0 +1,54 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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 Library.Branch : Sidebar.RootOnlyBranch { + public Branch() { + base (new Library.SidebarEntry()); + } + + public Library.MainPage get_main_page() { + return (Library.MainPage) ((Library.SidebarEntry) get_root()).get_page(); + } +} + +public class Library.SidebarEntry : Sidebar.SimplePageEntry { + private Icon icon = new ThemedIcon(Resources.ICON_PHOTOS); + + public SidebarEntry() { + } + + public override string get_sidebar_name() { + return Library.MainPage.NAME; + } + + public override Icon? get_sidebar_icon() { + return icon; + } + + protected override Page create_page() { + return new Library.MainPage(); + } +} + +public class Library.MainPage : CollectionPage { + public const string NAME = _("Library"); + + public MainPage(ProgressMonitor? monitor = null) { + base (NAME); + + foreach (MediaSourceCollection sources in MediaCollectionRegistry.get_instance().get_all()) + get_view().monitor_source_collection(sources, new CollectionViewManager(this), null, null, monitor); + } + + protected override void get_config_photos_sort(out bool sort_order, out int sort_by) { + Config.Facade.get_instance().get_library_photos_sort(out sort_order, out sort_by); + } + + protected override void set_config_photos_sort(bool sort_order, int sort_by) { + Config.Facade.get_instance().set_library_photos_sort(sort_order, sort_by); + } +} + diff --git a/src/library/FlaggedBranch.vala b/src/library/FlaggedBranch.vala new file mode 100644 index 0000000..472d999 --- /dev/null +++ b/src/library/FlaggedBranch.vala @@ -0,0 +1,61 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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 Library.FlaggedBranch : Sidebar.RootOnlyBranch { + public FlaggedBranch() { + base (new Library.FlaggedSidebarEntry()); + + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.flagged_contents_altered.connect(on_flagged_contents_altered); + + set_show_branch(get_total_flagged() != 0); + } + + ~FlaggedBranch() { + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.flagged_contents_altered.disconnect(on_flagged_contents_altered); + } + + private void on_flagged_contents_altered() { + set_show_branch(get_total_flagged() != 0); + } + + private int get_total_flagged() { + int total = 0; + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + total += media_sources.get_flagged().size; + + return total; + } +} + +public class Library.FlaggedSidebarEntry : Sidebar.SimplePageEntry, Sidebar.InternalDropTargetEntry { + public FlaggedSidebarEntry() { + } + + public override string get_sidebar_name() { + return FlaggedPage.NAME; + } + + public override Icon? get_sidebar_icon() { + return new ThemedIcon(Resources.ICON_FLAGGED_PAGE); + } + + protected override Page create_page() { + return new FlaggedPage(); + } + + public bool internal_drop_received(Gee.List<MediaSource> media) { + AppWindow.get_command_manager().execute(new FlagUnflagCommand(media, true)); + + return true; + } + + public bool internal_drop_received_arbitrary(Gtk.SelectionData data) { + return false; + } +} + diff --git a/src/library/FlaggedPage.vala b/src/library/FlaggedPage.vala new file mode 100644 index 0000000..28bc57b --- /dev/null +++ b/src/library/FlaggedPage.vala @@ -0,0 +1,54 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public class FlaggedPage : CollectionPage { + public const string NAME = _("Flagged"); + + private class FlaggedViewManager : CollectionViewManager { + public FlaggedViewManager(FlaggedPage owner) { + base (owner); + } + + public override bool include_in_view(DataSource source) { + Flaggable? flaggable = source as Flaggable; + + return (flaggable != null) && flaggable.is_flagged(); + } + } + + private class FlaggedSearchViewFilter : CollectionPage.CollectionSearchViewFilter { + public override uint get_criteria() { + return SearchFilterCriteria.TEXT | SearchFilterCriteria.MEDIA | + SearchFilterCriteria.RATING; + } + } + + private ViewManager view_manager; + private Alteration prereq = new Alteration("metadata", "flagged"); + private FlaggedSearchViewFilter search_filter = new FlaggedSearchViewFilter(); + + public FlaggedPage() { + base (NAME); + + view_manager = new FlaggedViewManager(this); + + foreach (MediaSourceCollection sources in MediaCollectionRegistry.get_instance().get_all()) + get_view().monitor_source_collection(sources, view_manager, prereq); + } + + protected override void get_config_photos_sort(out bool sort_order, out int sort_by) { + Config.Facade.get_instance().get_library_photos_sort(out sort_order, out sort_by); + } + + protected override void set_config_photos_sort(bool sort_order, int sort_by) { + Config.Facade.get_instance().set_library_photos_sort(sort_order, sort_by); + } + + public override SearchViewFilter get_search_view_filter() { + return search_filter; + } +} + diff --git a/src/library/ImportQueueBranch.vala b/src/library/ImportQueueBranch.vala new file mode 100644 index 0000000..32a3e0d --- /dev/null +++ b/src/library/ImportQueueBranch.vala @@ -0,0 +1,74 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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 Library.ImportQueueBranch : Sidebar.RootOnlyBranch { + private Library.ImportQueueSidebarEntry entry; + + public ImportQueueBranch() { + // can't pass to base() an object that was allocated in declaration; see + // https://bugzilla.gnome.org/show_bug.cgi?id=646286 + base (new Library.ImportQueueSidebarEntry()); + + entry = (Library.ImportQueueSidebarEntry) get_root(); + + // only attach signals to the page when it's created + entry.page_created.connect(on_page_created); + entry.destroying_page.connect(on_destroying_page); + + // don't use entry.get_page() or get_queue_page() because (a) we don't want to + // create the page during initialization, and (b) we know there's no import activity + // at this moment + set_show_branch(false); + } + + ~ImportQueueBranch() { + entry.page_created.disconnect(on_page_created); + entry.destroying_page.disconnect(on_destroying_page); + } + + public ImportQueuePage get_queue_page() { + return (ImportQueuePage) entry.get_page(); + } + + private void on_page_created() { + get_queue_page().batch_added.connect(on_batch_added_or_removed); + get_queue_page().batch_removed.connect(on_batch_added_or_removed); + } + + private void on_destroying_page() { + get_queue_page().batch_added.disconnect(on_batch_added_or_removed); + get_queue_page().batch_removed.disconnect(on_batch_added_or_removed); + } + + private void on_batch_added_or_removed() { + set_show_branch(get_queue_page().get_batch_count() > 0); + } + + public void enqueue_and_schedule(BatchImport batch_import, bool allow_user_cancel) { + // want to display the branch before passing to the page because this might result in the + // page being created, and want it all hooked up in the tree prior to creating the page + set_show_branch(true); + get_queue_page().enqueue_and_schedule(batch_import, allow_user_cancel); + } +} + +public class Library.ImportQueueSidebarEntry : Sidebar.SimplePageEntry { + public ImportQueueSidebarEntry() { + } + + public override string get_sidebar_name() { + return ImportQueuePage.NAME; + } + + public override Icon? get_sidebar_icon() { + return new ThemedIcon(Resources.ICON_IMPORTING); + } + + protected override Page create_page() { + return new ImportQueuePage(); + } +} + diff --git a/src/library/ImportQueuePage.vala b/src/library/ImportQueuePage.vala new file mode 100644 index 0000000..5ace1d8 --- /dev/null +++ b/src/library/ImportQueuePage.vala @@ -0,0 +1,208 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * 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 ImportQueuePage : SinglePhotoPage { + public const string NAME = _("Importing..."); + + private Gee.ArrayList<BatchImport> queue = new Gee.ArrayList<BatchImport>(); + private Gee.HashSet<BatchImport> cancel_unallowed = new Gee.HashSet<BatchImport>(); + private BatchImport current_batch = null; + private Gtk.ProgressBar progress_bar = new Gtk.ProgressBar(); + private bool stopped = false; + +#if UNITY_SUPPORT + UnityProgressBar uniprobar = UnityProgressBar.get_instance(); +#endif + + public signal void batch_added(BatchImport batch_import); + + public signal void batch_removed(BatchImport batch_import); + + public ImportQueuePage() { + base (NAME, false); + + // Set up toolbar + Gtk.Toolbar toolbar = get_toolbar(); + + // Stop button + Gtk.ToolButton stop_button = new Gtk.ToolButton.from_stock(Gtk.Stock.STOP); + stop_button.set_related_action(get_action("Stop")); + + toolbar.insert(stop_button, -1); + + // separator to force progress bar to right side of toolbar + Gtk.SeparatorToolItem separator = new Gtk.SeparatorToolItem(); + separator.set_draw(false); + + toolbar.insert(separator, -1); + + // Progress bar + Gtk.ToolItem progress_item = new Gtk.ToolItem(); + progress_item.set_expand(true); + progress_item.add(progress_bar); + progress_bar.set_show_text(true); + + toolbar.insert(progress_item, -1); +#if UNITY_SUPPORT + //UnityProgressBar: try to draw progress bar + uniprobar.set_visible(true); +#endif + } + + protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) { + ui_filenames.add("import_queue.ui"); + + base.init_collect_ui_filenames(ui_filenames); + } + + protected override Gtk.ActionEntry[] init_collect_action_entries() { + Gtk.ActionEntry[] actions = base.init_collect_action_entries(); + + Gtk.ActionEntry stop = { "Stop", Gtk.Stock.STOP, TRANSLATABLE, null, TRANSLATABLE, + on_stop }; + stop.label = _("_Stop Import"); + stop.tooltip = _("Stop importing photos"); + actions += stop; + + return actions; + } + + public void enqueue_and_schedule(BatchImport batch_import, bool allow_user_cancel) { + assert(!queue.contains(batch_import)); + + batch_import.starting.connect(on_starting); + batch_import.preparing.connect(on_preparing); + batch_import.progress.connect(on_progress); + batch_import.imported.connect(on_imported); + batch_import.import_complete.connect(on_import_complete); + batch_import.fatal_error.connect(on_fatal_error); + + if (!allow_user_cancel) + cancel_unallowed.add(batch_import); + + queue.add(batch_import); + batch_added(batch_import); + + if (queue.size == 1) + batch_import.schedule(); + + update_stop_action(); + } + + public int get_batch_count() { + return queue.size; + } + + private void update_stop_action() { + set_action_sensitive("Stop", !cancel_unallowed.contains(current_batch) && queue.size > 0); + } + + private void on_stop() { + update_stop_action(); + + if (queue.size == 0) + return; + + AppWindow.get_instance().set_busy_cursor(); + stopped = true; + + // mark all as halted and let each signal failure + foreach (BatchImport batch_import in queue) + batch_import.user_halt(); + } + + private void on_starting(BatchImport batch_import) { + update_stop_action(); + current_batch = batch_import; + } + + private void on_preparing() { + progress_bar.set_text(_("Preparing to import...")); + progress_bar.pulse(); + } + + private void on_progress(uint64 completed_bytes, uint64 total_bytes) { + double pct = (completed_bytes <= total_bytes) ? (double) completed_bytes / (double) total_bytes + : 0.0; + progress_bar.set_fraction(pct); +#if UNITY_SUPPORT + //UnityProgressBar: set progress + uniprobar.set_progress(pct); +#endif + } + + private void on_imported(ThumbnailSource source, Gdk.Pixbuf pixbuf, int to_follow) { + // only interested in updating the display for the last of the bunch + if (to_follow > 0 || !is_in_view()) + return; + + set_pixbuf(pixbuf, Dimensions.for_pixbuf(pixbuf)); + + // set the singleton collection to this item + get_view().clear(); + (source is LibraryPhoto) ? get_view().add(new PhotoView(source as LibraryPhoto)) : + get_view().add(new VideoView(source as Video)); + + progress_bar.set_ellipsize(Pango.EllipsizeMode.MIDDLE); + progress_bar.set_text(_("Imported %s").printf(source.get_name())); + } + + private void on_import_complete(BatchImport batch_import, ImportManifest manifest, + BatchImportRoll import_roll) { + assert(batch_import == current_batch); + current_batch = null; + + assert(queue.size > 0); + assert(queue.get(0) == batch_import); + + bool removed = queue.remove(batch_import); + assert(removed); + + // fail quietly if cancel was allowed + cancel_unallowed.remove(batch_import); + + // strip signal handlers + batch_import.starting.disconnect(on_starting); + batch_import.preparing.disconnect(on_preparing); + batch_import.progress.disconnect(on_progress); + batch_import.imported.disconnect(on_imported); + batch_import.import_complete.disconnect(on_import_complete); + batch_import.fatal_error.disconnect(on_fatal_error); + + // schedule next if available + if (queue.size > 0) { + queue.get(0).schedule(); + } else { + // reset UI + progress_bar.set_ellipsize(Pango.EllipsizeMode.NONE); + progress_bar.set_text(""); + progress_bar.set_fraction(0.0); +#if UNITY_SUPPORT + //UnityProgressBar: reset + uniprobar.reset(); +#endif + + // blank the display + blank_display(); + + // reset cursor if cancelled + if (stopped) + AppWindow.get_instance().set_normal_cursor(); + + stopped = false; + } + + update_stop_action(); + + // report the batch has been removed from the queue after everything else is set + batch_removed(batch_import); + } + + private void on_fatal_error(ImportResult result, string message) { + AppWindow.error_message(message); + } +} + diff --git a/src/library/LastImportBranch.vala b/src/library/LastImportBranch.vala new file mode 100644 index 0000000..bc03ee5 --- /dev/null +++ b/src/library/LastImportBranch.vala @@ -0,0 +1,47 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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 Library.LastImportBranch : Sidebar.RootOnlyBranch { + public LastImportBranch() { + base (new Library.LastImportSidebarEntry()); + + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.import_roll_altered.connect(on_import_rolls_altered); + + set_show_branch(MediaCollectionRegistry.get_instance().get_last_import_id() != null); + } + + ~LastImportBranch() { + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.import_roll_altered.disconnect(on_import_rolls_altered); + } + + public Library.LastImportSidebarEntry get_main_entry() { + return (Library.LastImportSidebarEntry) get_root(); + } + + private void on_import_rolls_altered() { + set_show_branch(MediaCollectionRegistry.get_instance().get_last_import_id() != null); + } +} + +public class Library.LastImportSidebarEntry : Sidebar.SimplePageEntry { + public LastImportSidebarEntry() { + } + + public override string get_sidebar_name() { + return LastImportPage.NAME; + } + + public override Icon? get_sidebar_icon() { + return new ThemedIcon(Resources.ICON_LAST_IMPORT); + } + + protected override Page create_page() { + return new LastImportPage(); + } +} + diff --git a/src/library/LastImportPage.vala b/src/library/LastImportPage.vala new file mode 100644 index 0000000..877faa5 --- /dev/null +++ b/src/library/LastImportPage.vala @@ -0,0 +1,79 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * 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 LastImportPage : CollectionPage { + public const string NAME = _("Last Import"); + + private class LastImportViewManager : CollectionViewManager { + private ImportID import_id; + + public LastImportViewManager(LastImportPage owner, ImportID import_id) { + base (owner); + + this.import_id = import_id; + } + + public override bool include_in_view(DataSource source) { + return ((MediaSource) source).get_import_id().id == import_id.id; + } + } + + private ImportID last_import_id = ImportID(); + private Alteration last_import_alteration = new Alteration("metadata", "import-id"); + + public LastImportPage() { + base (NAME); + + // be notified when the import rolls change + foreach (MediaSourceCollection col in MediaCollectionRegistry.get_instance().get_all()) { + col.import_roll_altered.connect(on_import_rolls_altered); + } + + // set up view manager for the last import roll + on_import_rolls_altered(); + } + + ~LastImportPage() { + foreach (MediaSourceCollection col in MediaCollectionRegistry.get_instance().get_all()) { + col.import_roll_altered.disconnect(on_import_rolls_altered); + } + } + + private void on_import_rolls_altered() { + // see if there's a new last ImportID, or no last import at all + ImportID? current_last_import_id = + MediaCollectionRegistry.get_instance().get_last_import_id(); + + if (current_last_import_id == null) { + get_view().halt_all_monitoring(); + get_view().clear(); + + return; + } + + if (current_last_import_id.id == last_import_id.id) + return; + + last_import_id = current_last_import_id; + + get_view().halt_all_monitoring(); + get_view().clear(); + + foreach (MediaSourceCollection col in MediaCollectionRegistry.get_instance().get_all()) { + get_view().monitor_source_collection(col, new LastImportViewManager(this, + last_import_id), last_import_alteration); + } + } + + protected override void get_config_photos_sort(out bool sort_order, out int sort_by) { + Config.Facade.get_instance().get_library_photos_sort(out sort_order, out sort_by); + } + + protected override void set_config_photos_sort(bool sort_order, int sort_by) { + Config.Facade.get_instance().set_library_photos_sort(sort_order, sort_by); + } +} + diff --git a/src/library/Library.vala b/src/library/Library.vala new file mode 100644 index 0000000..79a4880 --- /dev/null +++ b/src/library/Library.vala @@ -0,0 +1,19 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Library { + +public void init() throws Error { + Library.TrashSidebarEntry.init(); + Photo.develop_raw_photos_to_files = true; +} + +public void terminate() { + Library.TrashSidebarEntry.terminate(); +} + +} + diff --git a/src/library/LibraryWindow.vala b/src/library/LibraryWindow.vala new file mode 100644 index 0000000..dab1f6f --- /dev/null +++ b/src/library/LibraryWindow.vala @@ -0,0 +1,1587 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * 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 LibraryWindow : AppWindow { + public const int SIDEBAR_MIN_WIDTH = 224; + public const int SIDEBAR_MAX_WIDTH = 320; + + public static int PAGE_MIN_WIDTH { + get { + return Thumbnail.MAX_SCALE + (CheckerboardLayout.COLUMN_GUTTER_PADDING * 2); + } + } + + public const int SORT_EVENTS_ORDER_ASCENDING = 0; + public const int SORT_EVENTS_ORDER_DESCENDING = 1; + + private const string[] SUPPORTED_MOUNT_SCHEMES = { + "gphoto2:", + "disk:", + "file:" + }; + + 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; + + // 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 + // the item in this list to a new position. These numbers should *not* persist anywhere + // outside the app. + private enum SidebarRootPosition { + LIBRARY, + FLAGGED, + LAST_IMPORTED, + CAMERAS, + IMPORT_QUEUE, + SAVED_SEARCH, + EVENTS, + FOLDERS, + TAGS, + TRASH, + OFFLINE + } + + public enum TargetType { + URI_LIST, + MEDIA_LIST, + TAG_PATH + } + + public const string TAG_PATH_MIME_TYPE = "shotwell/tag-path"; + public const string MEDIA_LIST_MIME_TYPE = "shotwell/media-id-atom"; + + public const Gtk.TargetEntry[] DND_TARGET_ENTRIES = { + { "text/uri-list", Gtk.TargetFlags.OTHER_APP, TargetType.URI_LIST }, + { MEDIA_LIST_MIME_TYPE, Gtk.TargetFlags.SAME_APP, TargetType.MEDIA_LIST }, + { TAG_PATH_MIME_TYPE, Gtk.TargetFlags.SAME_WIDGET, TargetType.TAG_PATH } + }; + + // In fullscreen mode, want to use LibraryPhotoPage, but fullscreen has different requirements, + // esp. regarding when the widget is realized and when it should first try and throw them image + // on the page. This handles this without introducing lots of special cases in + // LibraryPhotoPage. + private class FullscreenPhotoPage : LibraryPhotoPage { + private CollectionPage collection; + private Photo start; + private ViewCollection? view; + + public FullscreenPhotoPage(CollectionPage collection, Photo start, ViewCollection? view) { + this.collection = collection; + this.start = start; + this.view = view; + } + + public override void switched_to() { + display_for_collection(collection, start, view); + + base.switched_to(); + } + + protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) { + // We intentionally don't call the base class here since we don't want the + // top-level menu in photo.ui. + ui_filenames.add("photo_context.ui"); + } + + } + + private string import_dir = Environment.get_home_dir(); + + private Gtk.Paned sidebar_paned = new Gtk.Paned(Gtk.Orientation.VERTICAL); + private Gtk.Paned client_paned = new Gtk.Paned(Gtk.Orientation.HORIZONTAL); + private Gtk.Frame bottom_frame = new Gtk.Frame(null); + + private Gtk.ActionGroup common_action_group = new Gtk.ActionGroup("LibraryWindowGlobalActionGroup"); + + private OneShotScheduler properties_scheduler = null; + private bool notify_library_is_home_dir = true; + + // Sidebar tree and roots (ordered by SidebarRootPosition) + private Sidebar.Tree sidebar_tree; + private Library.Branch library_branch = new Library.Branch(); + private Tags.Branch tags_branch = new Tags.Branch(); + private Folders.Branch folders_branch = new Folders.Branch(); + private Library.TrashBranch trash_branch = new Library.TrashBranch(); + private Events.Branch events_branch = new Events.Branch(); + private Library.OfflineBranch offline_branch = new Library.OfflineBranch(); + private Library.FlaggedBranch flagged_branch = new Library.FlaggedBranch(); + private Library.LastImportBranch last_import_branch = new Library.LastImportBranch(); + private Library.ImportQueueBranch import_queue_branch = new Library.ImportQueueBranch(); + private Camera.Branch camera_branch = new Camera.Branch(); + private Searches.Branch saved_search_branch = new Searches.Branch(); + private bool page_switching_enabled = true; + + private Gee.HashMap<Page, Sidebar.Entry> page_map = new Gee.HashMap<Page, Sidebar.Entry>(); + + private LibraryPhotoPage photo_page = null; + + // this is to keep track of cameras which initiate the app + private static Gee.HashSet<string> initial_camera_uris = new Gee.HashSet<string>(); + + private bool is_search_toolbar_visible = false; + + // Want to instantiate this in the constructor rather than here because the search bar has its + // own UIManager which will suck up the accelerators, and we want them to be associated with + // AppWindows instead. + private SearchFilterActions search_actions = new SearchFilterActions(); + 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 BasicProperties basic_properties = new BasicProperties(); + private ExtendedPropertiesWindow extended_properties; + + private Gtk.Notebook notebook = new Gtk.Notebook(); + private Gtk.Box layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + private Gtk.Box right_vbox; + + 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) { + // prep sidebar and add roots + sidebar_tree = new Sidebar.Tree(DND_TARGET_ENTRIES, Gdk.DragAction.ASK, + external_drop_handler); + + sidebar_tree.page_created.connect(on_page_created); + sidebar_tree.destroying_page.connect(on_destroying_page); + sidebar_tree.entry_selected.connect(on_sidebar_entry_selected); + sidebar_tree.selected_entry_removed.connect(on_sidebar_selected_entry_removed); + + sidebar_tree.graft(library_branch, SidebarRootPosition.LIBRARY); + sidebar_tree.graft(tags_branch, SidebarRootPosition.TAGS); + sidebar_tree.graft(folders_branch, SidebarRootPosition.FOLDERS); + sidebar_tree.graft(trash_branch, SidebarRootPosition.TRASH); + sidebar_tree.graft(events_branch, SidebarRootPosition.EVENTS); + sidebar_tree.graft(offline_branch, SidebarRootPosition.OFFLINE); + sidebar_tree.graft(flagged_branch, SidebarRootPosition.FLAGGED); + sidebar_tree.graft(last_import_branch, SidebarRootPosition.LAST_IMPORTED); + sidebar_tree.graft(import_queue_branch, SidebarRootPosition.IMPORT_QUEUE); + sidebar_tree.graft(camera_branch, SidebarRootPosition.CAMERAS); + sidebar_tree.graft(saved_search_branch, SidebarRootPosition.SAVED_SEARCH); + + // create and connect extended properties window + extended_properties = new ExtendedPropertiesWindow(this); + extended_properties.hide.connect(hide_extended_properties); + extended_properties.show.connect(show_extended_properties); + + 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); + + try { + File ui_file = Resources.get_ui("top.ui"); + ui.add_ui_from_file(ui_file.get_path()); + } catch (Error e) { + error(e.message); + } + + Gtk.MenuBar? menubar = ui.get_widget("/MenuBar") as Gtk.MenuBar; + layout.add(menubar); + + // We never want to invoke show_all() on the menubar since that will show empty menus, + // which should be hidden. + menubar.no_show_all = true; + + // create the main layout & start at the Library page + create_layout(library_branch.get_main_page()); + + // settings that should persist between sessions + load_configuration(); + + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) { + media_sources.trashcan_contents_altered.connect(on_trashcan_contents_altered); + media_sources.items_altered.connect(on_media_altered); + } + + // set up main window as a drag-and-drop destination (rather than each page; assume + // a drag and drop is for general library import, which means it goes to library_page) + Gtk.TargetEntry[] main_window_dnd_targets = { + DND_TARGET_ENTRIES[TargetType.URI_LIST], + DND_TARGET_ENTRIES[TargetType.MEDIA_LIST] + /* the main window accepts URI lists and media lists but not tag paths -- yet; we + might wish to support dropping tags onto photos at some future point */ + }; + Gtk.drag_dest_set(this, Gtk.DestDefaults.ALL, main_window_dnd_targets, + Gdk.DragAction.COPY | Gdk.DragAction.LINK | Gdk.DragAction.ASK); + + MetadataWriter.get_instance().progress.connect(on_metadata_writer_progress); + + LibraryMonitor? monitor = LibraryMonitorPool.get_instance().get_monitor(); + if (monitor != null) + on_library_monitor_installed(monitor); + + LibraryMonitorPool.get_instance().monitor_installed.connect(on_library_monitor_installed); + LibraryMonitorPool.get_instance().monitor_destroyed.connect(on_library_monitor_destroyed); + + CameraTable.get_instance().camera_added.connect(on_camera_added); + + background_progress_bar.set_show_text(true); + + } + + ~LibraryWindow() { + sidebar_tree.page_created.disconnect(on_page_created); + sidebar_tree.destroying_page.disconnect(on_destroying_page); + sidebar_tree.entry_selected.disconnect(on_sidebar_entry_selected); + sidebar_tree.selected_entry_removed.disconnect(on_sidebar_selected_entry_removed); + + unsubscribe_from_basic_information(get_current_page()); + + extended_properties.hide.disconnect(hide_extended_properties); + extended_properties.show.disconnect(show_extended_properties); + + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) { + media_sources.trashcan_contents_altered.disconnect(on_trashcan_contents_altered); + media_sources.items_altered.disconnect(on_media_altered); + } + + MetadataWriter.get_instance().progress.disconnect(on_metadata_writer_progress); + + LibraryMonitor? monitor = LibraryMonitorPool.get_instance().get_monitor(); + if (monitor != null) + on_library_monitor_destroyed(monitor); + + LibraryMonitorPool.get_instance().monitor_installed.disconnect(on_library_monitor_installed); + LibraryMonitorPool.get_instance().monitor_destroyed.disconnect(on_library_monitor_destroyed); + + CameraTable.get_instance().camera_added.disconnect(on_camera_added); + } + + private void on_library_monitor_installed(LibraryMonitor monitor) { + debug("on_library_monitor_installed: %s", monitor.get_root().get_path()); + + monitor.discovery_started.connect(on_library_monitor_discovery_started); + monitor.discovery_completed.connect(on_library_monitor_discovery_completed); + monitor.closed.connect(on_library_monitor_discovery_completed); + monitor.auto_update_progress.connect(on_library_monitor_auto_update_progress); + monitor.auto_import_preparing.connect(on_library_monitor_auto_import_preparing); + monitor.auto_import_progress.connect(on_library_monitor_auto_import_progress); + } + + private void on_library_monitor_destroyed(LibraryMonitor monitor) { + debug("on_library_monitor_destroyed: %s", monitor.get_root().get_path()); + + monitor.discovery_started.disconnect(on_library_monitor_discovery_started); + monitor.discovery_completed.disconnect(on_library_monitor_discovery_completed); + monitor.closed.disconnect(on_library_monitor_discovery_completed); + monitor.auto_update_progress.disconnect(on_library_monitor_auto_update_progress); + monitor.auto_import_preparing.disconnect(on_library_monitor_auto_import_preparing); + monitor.auto_import_progress.disconnect(on_library_monitor_auto_import_progress); + } + + private Gtk.ActionEntry[] create_common_actions() { + Gtk.ActionEntry[] actions = new Gtk.ActionEntry[0]; + + Gtk.ActionEntry import = { "CommonFileImport", Resources.IMPORT, + TRANSLATABLE, "<Ctrl>I", TRANSLATABLE, on_file_import }; + import.label = _("_Import From Folder..."); + import.tooltip = _("Import photos from disk to library"); + actions += import; + + Gtk.ActionEntry import_from_external = { + "ExternalLibraryImport", Resources.IMPORT, TRANSLATABLE, + null, TRANSLATABLE, on_external_library_import + }; + import_from_external.label = _("Import From _Application..."); + actions += import_from_external; + + Gtk.ActionEntry sort = { "CommonSortEvents", null, TRANSLATABLE, null, null, null }; + sort.label = _("Sort _Events"); + actions += sort; + + Gtk.ActionEntry preferences = { "CommonPreferences", Gtk.Stock.PREFERENCES, TRANSLATABLE, + null, TRANSLATABLE, on_preferences }; + preferences.label = Resources.PREFERENCES_MENU; + actions += preferences; + + Gtk.ActionEntry empty = { "CommonEmptyTrash", Gtk.Stock.CLEAR, TRANSLATABLE, null, null, + on_empty_trash }; + empty.label = _("Empty T_rash"); + empty.tooltip = _("Delete all photos in the trash"); + actions += empty; + + Gtk.ActionEntry jump_to_event = { "CommonJumpToEvent", null, TRANSLATABLE, null, + TRANSLATABLE, on_jump_to_event }; + jump_to_event.label = _("View Eve_nt for Photo"); + actions += jump_to_event; + + Gtk.ActionEntry find = { "CommonFind", Gtk.Stock.FIND, TRANSLATABLE, null, null, + on_find }; + find.label = _("_Find"); + find.tooltip = _("Find photos and videos by search criteria"); + actions += find; + + // add the common action for the FilterPhotos submenu (the submenu contains items from + // SearchFilterActions) + Gtk.ActionEntry filter_photos = { "CommonFilterPhotos", null, TRANSLATABLE, null, null, null }; + filter_photos.label = Resources.FILTER_PHOTOS_MENU; + actions += filter_photos; + + Gtk.ActionEntry new_search = { "CommonNewSearch", null, TRANSLATABLE, "<Ctrl>S", null, + on_new_search }; + new_search.label = _("Ne_w Saved Search..."); + actions += new_search; + + // top-level menus + + Gtk.ActionEntry file = { "FileMenu", null, TRANSLATABLE, null, null, null }; + file.label = _("_File"); + actions += file; + + Gtk.ActionEntry edit = { "EditMenu", null, TRANSLATABLE, null, null, null }; + edit.label = _("_Edit"); + actions += edit; + + Gtk.ActionEntry view = { "ViewMenu", null, TRANSLATABLE, null, null, null }; + view.label = _("_View"); + actions += view; + + Gtk.ActionEntry photo = { "PhotoMenu", null, TRANSLATABLE, null, null, null }; + photo.label = _("_Photo"); + actions += photo; + + Gtk.ActionEntry photos = { "PhotosMenu", null, TRANSLATABLE, null, null, null }; + photos.label = _("_Photos"); + actions += photos; + + Gtk.ActionEntry event = { "EventsMenu", null, TRANSLATABLE, null, null, null }; + event.label = _("Even_ts"); + actions += event; + + Gtk.ActionEntry tags = { "TagsMenu", null, TRANSLATABLE, null, null, null }; + tags.label = _("Ta_gs"); + actions += tags; + + Gtk.ActionEntry help = { "HelpMenu", null, TRANSLATABLE, null, null, null }; + help.label = _("_Help"); + actions += help; + + return actions; + } + + private Gtk.ToggleActionEntry[] create_common_toggle_actions() { + Gtk.ToggleActionEntry[] actions = new Gtk.ToggleActionEntry[0]; + + Gtk.ToggleActionEntry basic_props = { "CommonDisplayBasicProperties", null, + TRANSLATABLE, "<Ctrl><Shift>I", TRANSLATABLE, on_display_basic_properties, false }; + basic_props.label = _("_Basic Information"); + basic_props.tooltip = _("Display basic information for the selection"); + actions += basic_props; + + Gtk.ToggleActionEntry extended_props = { "CommonDisplayExtendedProperties", null, + TRANSLATABLE, "<Ctrl><Shift>X", TRANSLATABLE, on_display_extended_properties, false }; + extended_props.label = _("E_xtended Information"); + extended_props.tooltip = _("Display extended information for the selection"); + actions += extended_props; + + Gtk.ToggleActionEntry searchbar = { "CommonDisplaySearchbar", Gtk.Stock.FIND, TRANSLATABLE, + "F8", TRANSLATABLE, on_display_searchbar, is_search_toolbar_visible }; + searchbar.label = _("_Search Bar"); + searchbar.tooltip = _("Display the search bar"); + actions += searchbar; + + Gtk.ToggleActionEntry sidebar = { "CommonDisplaySidebar", null, TRANSLATABLE, + "F9", TRANSLATABLE, on_display_sidebar, is_sidebar_visible() }; + sidebar.label = _("S_idebar"); + sidebar.tooltip = _("Display the sidebar"); + actions += sidebar; + + return actions; + } + + private void add_common_radio_actions(Gtk.ActionGroup group) { + Gtk.RadioActionEntry[] actions = new Gtk.RadioActionEntry[0]; + + Gtk.RadioActionEntry ascending = { "CommonSortEventsAscending", + Gtk.Stock.SORT_ASCENDING, TRANSLATABLE, null, TRANSLATABLE, + SORT_EVENTS_ORDER_ASCENDING }; + ascending.label = _("_Ascending"); + ascending.tooltip = _("Sort photos in an ascending order"); + actions += ascending; + + Gtk.RadioActionEntry descending = { "CommonSortEventsDescending", + Gtk.Stock.SORT_DESCENDING, TRANSLATABLE, null, TRANSLATABLE, + SORT_EVENTS_ORDER_DESCENDING }; + descending.label = _("D_escending"); + descending.tooltip = _("Sort photos in a descending order"); + actions += descending; + + group.add_radio_actions(actions, SORT_EVENTS_ORDER_ASCENDING, on_events_sort_changed); + } + + protected override Gtk.ActionGroup[] create_common_action_groups() { + Gtk.ActionGroup[] groups = base.create_common_action_groups(); + + common_action_group.add_actions(create_common_actions(), this); + common_action_group.add_toggle_actions(create_common_toggle_actions(), this); + add_common_radio_actions(common_action_group); + + Gtk.Action? action = common_action_group.get_action("CommonDisplaySearchbar"); + if (action != null) { + action.short_label = Resources.FIND_LABEL; + action.is_important = true; + } + + groups += common_action_group; + groups += search_actions.get_action_group(); + + return groups; + } + + public override void replace_common_placeholders(Gtk.UIManager ui) { + base.replace_common_placeholders(ui); + } + + protected override void switched_pages(Page? old_page, Page? new_page) { + base.switched_pages(old_page, new_page); + + // monitor when the ViewFilter is changed in any page + if (old_page != null) { + old_page.get_view().view_filter_installed.disconnect(on_view_filter_installed); + old_page.get_view().view_filter_removed.disconnect(on_view_filter_removed); + } + + if (new_page != null) { + new_page.get_view().view_filter_installed.connect(on_view_filter_installed); + new_page.get_view().view_filter_removed.connect(on_view_filter_removed); + } + + search_actions.monitor_page_contents(old_page, new_page); + } + + private void on_view_filter_installed(ViewFilter filter) { + filter.refresh.connect(on_view_filter_refreshed); + } + + private void on_view_filter_removed(ViewFilter filter) { + filter.refresh.disconnect(on_view_filter_refreshed); + } + + private void on_view_filter_refreshed() { + // if view filter is reset to show all items, do nothing (leave searchbar in current + // state) + if (!get_current_page().get_view().are_items_filtered_out()) + return; + + // always show the searchbar when items are filtered + Gtk.ToggleAction? display_searchbar = get_common_action("CommonDisplaySearchbar") + as Gtk.ToggleAction; + if (display_searchbar != null) + display_searchbar.active = true; + } + + // show_all() may make visible certain items we wish to keep programmatically hidden + public override void show_all() { + base.show_all(); + + Gtk.ToggleAction? basic_properties_action = get_current_page().get_common_action( + "CommonDisplayBasicProperties") as Gtk.ToggleAction; + assert(basic_properties_action != null); + + if (!basic_properties_action.get_active()) + bottom_frame.hide(); + + Gtk.ToggleAction? searchbar_action = get_current_page().get_common_action( + "CommonDisplaySearchbar") as Gtk.ToggleAction; + assert(searchbar_action != null); + + // Make sure rejected pictures are not being displayed on startup + CheckerboardPage? current_page = get_current_page() as CheckerboardPage; + if (current_page != null) + init_view_filter(current_page); + + toggle_search_bar(should_show_search_bar(), current_page); + + // Sidebar + set_sidebar_visible(is_sidebar_visible()); + } + + public static LibraryWindow get_app() { + assert(instance is LibraryWindow); + + return (LibraryWindow) instance; + } + + // This may be called before Debug.init(), so no error logging may be made + public static bool is_mount_uri_supported(string uri) { + foreach (string scheme in SUPPORTED_MOUNT_SCHEMES) { + if (uri.has_prefix(scheme)) + return true; + } + + return false; + } + + public override string get_app_role() { + return Resources.APP_LIBRARY_ROLE; + } + + public void rename_tag_in_sidebar(Tag tag) { + Tags.SidebarEntry? entry = tags_branch.get_entry_for_tag(tag); + if (entry != null) + sidebar_tree.rename_entry_in_place(entry); + else + debug("No tag entry found for rename"); + } + + public void rename_event_in_sidebar(Event event) { + Events.EventEntry? entry = events_branch.get_entry_for_event(event); + if (entry != null) + sidebar_tree.rename_entry_in_place(entry); + else + debug("No event entry found for rename"); + } + + public void rename_search_in_sidebar(SavedSearch search) { + Searches.SidebarEntry? entry = saved_search_branch.get_entry_for_saved_search(search); + if (entry != null) + sidebar_tree.rename_entry_in_place(entry); + else + debug("No search entry found for rename"); + } + + protected override void on_quit() { + Config.Facade.get_instance().set_library_window_state(maximized, dimensions); + + Config.Facade.get_instance().set_sidebar_position(client_paned.position); + + base.on_quit(); + } + + private Photo? get_start_fullscreen_photo(CollectionPage page) { + ViewCollection view = page.get_view(); + + // if a selection is present, use the first selected LibraryPhoto, otherwise do + // nothing; if no selection present, use the first LibraryPhoto + Gee.List<DataSource>? sources = (view.get_selected_count() > 0) + ? view.get_selected_sources_of_type(typeof(LibraryPhoto)) + : view.get_sources_of_type(typeof(LibraryPhoto)); + + return (sources != null && sources.size != 0) + ? (Photo) sources[0] : null; + } + + private bool get_fullscreen_photo(Page page, out CollectionPage collection, out Photo start, + out ViewCollection? view_collection = null) { + collection = null; + start = null; + view_collection = null; + + // fullscreen behavior depends on the type of page being looked at + if (page is CollectionPage) { + collection = (CollectionPage) page; + Photo? photo = get_start_fullscreen_photo(collection); + if (photo == null) + return false; + + start = photo; + view_collection = null; + + return true; + } + + if (page is EventsDirectoryPage) { + ViewCollection view = page.get_view(); + if (view.get_count() == 0) + return false; + + Event? event = (Event?) ((DataView) view.get_at(0)).get_source(); + if (event == null) + return false; + + Events.EventEntry? entry = events_branch.get_entry_for_event(event); + if (entry == null) + return false; + + collection = (EventPage) entry.get_page(); + Photo? photo = get_start_fullscreen_photo(collection); + if (photo == null) + return false; + + start = photo; + view_collection = null; + + return true; + } + + if (page is LibraryPhotoPage) { + LibraryPhotoPage photo_page = (LibraryPhotoPage) page; + + CollectionPage? controller = photo_page.get_controller_page(); + if (controller == null) + return false; + + if (!photo_page.has_photo()) + return false; + + collection = controller; + start = photo_page.get_photo(); + view_collection = photo_page.get_view(); + + return true; + } + + return false; + } + + protected override void on_fullscreen() { + Page? current_page = get_current_page(); + if (current_page == null) + return; + + CollectionPage collection; + Photo start; + ViewCollection? view = null; + if (!get_fullscreen_photo(current_page, out collection, out start, out view)) + return; + + FullscreenPhotoPage fs_photo = new FullscreenPhotoPage(collection, start, view); + + go_fullscreen(fs_photo); + } + + private void on_file_import() { + Gtk.FileChooserDialog import_dialog = new Gtk.FileChooserDialog(_("Import From Folder"), null, + Gtk.FileChooserAction.SELECT_FOLDER, Gtk.Stock.CANCEL, Gtk.ResponseType.CANCEL, + Gtk.Stock.OK, Gtk.ResponseType.OK); + import_dialog.set_local_only(false); + import_dialog.set_select_multiple(true); + import_dialog.set_current_folder(import_dir); + + int response = import_dialog.run(); + + if (response == Gtk.ResponseType.OK) { + // force file linking if directory is inside current library directory + Gtk.ResponseType copy_files_response = + AppDirs.is_in_import_dir(File.new_for_uri(import_dialog.get_uri())) + ? Gtk.ResponseType.REJECT : copy_files_dialog(); + + if (copy_files_response != Gtk.ResponseType.CANCEL) { + dispatch_import_jobs(import_dialog.get_uris(), "folders", + copy_files_response == Gtk.ResponseType.ACCEPT); + } + } + + import_dir = import_dialog.get_current_folder(); + import_dialog.destroy(); + } + + private void on_external_library_import() { + Gtk.Dialog import_dialog = DataImportsUI.DataImportsDialog.get_or_create_instance(); + + import_dialog.run(); + } + + protected override void update_common_action_availability(Page? old_page, Page? new_page) { + base.update_common_action_availability(old_page, new_page); + + bool is_checkerboard = new_page is CheckerboardPage; + + set_common_action_sensitive("CommonDisplaySearchbar", is_checkerboard); + set_common_action_sensitive("CommonFind", is_checkerboard); + } + + protected override void update_common_actions(Page page, int selected_count, int count) { + // see on_fullscreen for the logic here ... both CollectionPage and EventsDirectoryPage + // are CheckerboardPages (but in on_fullscreen have to be handled differently to locate + // the view controller) + CollectionPage collection; + Photo start; + bool can_fullscreen = get_fullscreen_photo(page, out collection, out start); + + set_common_action_sensitive("CommonEmptyTrash", can_empty_trash()); + set_common_action_visible("CommonJumpToEvent", true); + set_common_action_sensitive("CommonJumpToEvent", can_jump_to_event()); + set_common_action_sensitive("CommonFullscreen", can_fullscreen); + + base.update_common_actions(page, selected_count, count); + } + + private void on_trashcan_contents_altered() { + set_common_action_sensitive("CommonEmptyTrash", can_empty_trash()); + } + + private bool can_empty_trash() { + return (LibraryPhoto.global.get_trashcan_count() > 0) || (Video.global.get_trashcan_count() > 0); + } + + private void on_empty_trash() { + Gee.ArrayList<MediaSource> to_remove = new Gee.ArrayList<MediaSource>(); + to_remove.add_all(LibraryPhoto.global.get_trashcan_contents()); + to_remove.add_all(Video.global.get_trashcan_contents()); + + remove_from_app(to_remove, _("Empty Trash"), _("Emptying Trash...")); + + AppWindow.get_command_manager().reset(); + } + + private void on_new_search() { + (new SavedSearchDialog()).show(); + } + + private bool can_jump_to_event() { + ViewCollection view = get_current_page().get_view(); + if (view.get_selected_count() == 1) { + DataSource selected_source = view.get_selected_source_at(0); + if (selected_source is Event) + return true; + else if (selected_source is MediaSource) + return ((MediaSource) view.get_selected_source_at(0)).get_event() != null; + else + return false; + } else { + return false; + } + } + + private void on_jump_to_event() { + ViewCollection view = get_current_page().get_view(); + + if (view.get_selected_count() != 1) + return; + + MediaSource? media = view.get_selected_source_at(0) as MediaSource; + if (media == null) + return; + + if (media.get_event() != null) + switch_to_event(media.get_event()); + } + + private void on_find() { + Gtk.ToggleAction action = (Gtk.ToggleAction) get_current_page().get_common_action( + "CommonDisplaySearchbar"); + action.active = true; + + // give it focus (which should move cursor to the text entry control) + search_toolbar.take_focus(); + } + + private void on_media_altered() { + set_common_action_sensitive("CommonJumpToEvent", can_jump_to_event()); + } + + private void on_clear_search() { + if (is_search_toolbar_visible) + search_actions.reset(); + } + + public int get_events_sort() { + Gtk.RadioAction? action = get_common_action("CommonSortEventsAscending") as Gtk.RadioAction; + + return (action != null) ? action.current_value : SORT_EVENTS_ORDER_DESCENDING; + } + + private void on_events_sort_changed(Gtk.Action action, Gtk.Action c) { + Gtk.RadioAction current = (Gtk.RadioAction) c; + + Config.Facade.get_instance().set_events_sort_ascending( + current.current_value == SORT_EVENTS_ORDER_ASCENDING); + } + + private void on_preferences() { + PreferencesDialog.show(); + } + + private void on_display_basic_properties(Gtk.Action action) { + bool display = ((Gtk.ToggleAction) action).get_active(); + + if (display) { + basic_properties.update_properties(get_current_page()); + bottom_frame.show(); + } else { + if (sidebar_paned.get_child2() != null) { + bottom_frame.hide(); + } + } + + // sync the setting so it will persist + Config.Facade.get_instance().set_display_basic_properties(display); + } + + private void on_display_extended_properties(Gtk.Action action) { + bool display = ((Gtk.ToggleAction) action).get_active(); + + if (display) { + extended_properties.update_properties(get_current_page()); + extended_properties.show_all(); + } else { + extended_properties.hide(); + } + } + + private void on_display_searchbar(Gtk.Action action) { + bool is_shown = ((Gtk.ToggleAction) action).get_active(); + Config.Facade.get_instance().set_display_search_bar(is_shown); + show_search_bar(is_shown); + } + + public void show_search_bar(bool display) { + if (!(get_current_page() is CheckerboardPage)) + return; + + is_search_toolbar_visible = display; + toggle_search_bar(should_show_search_bar(), get_current_page() as CheckerboardPage); + if (!display) + search_actions.reset(); + } + + private void on_display_sidebar(Gtk.Action action) { + set_sidebar_visible(((Gtk.ToggleAction) action).get_active()); + + } + + private void set_sidebar_visible(bool visible) { + sidebar_paned.set_visible(visible); + Config.Facade.get_instance().set_display_sidebar(visible); + } + + private bool is_sidebar_visible() { + return Config.Facade.get_instance().get_display_sidebar(); + } + + private void show_extended_properties() { + sync_extended_properties(true); + } + + private void hide_extended_properties() { + sync_extended_properties(false); + } + + private void sync_extended_properties(bool show) { + Gtk.ToggleAction? extended_display_action = get_common_action("CommonDisplayExtendedProperties") + as Gtk.ToggleAction; + assert(extended_display_action != null); + extended_display_action.set_active(show); + + // sync the setting so it will persist + Config.Facade.get_instance().set_display_extended_properties(show); + } + + public void enqueue_batch_import(BatchImport batch_import, bool allow_user_cancel) { + import_queue_branch.enqueue_and_schedule(batch_import, allow_user_cancel); + } + + private void import_reporter(ImportManifest manifest) { + ImportUI.report_manifest(manifest, true); + } + + private void dispatch_import_jobs(GLib.SList<string> uris, string job_name, bool copy_to_library) { + 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" + + "Do you want to continue importing photos?").printf("▸"), + _("_Import"), _("Library Location"), AppWindow.get_instance()); + + if (response == Gtk.ResponseType.CANCEL) + return; + + notify_library_is_home_dir = false; + } + + Gee.ArrayList<FileImportJob> jobs = new Gee.ArrayList<FileImportJob>(); + foreach (string uri in uris) { + File file_or_dir = File.new_for_uri(uri); + if (file_or_dir.get_path() == null) { + // TODO: Specify which directory/file. + AppWindow.error_message(_("Photos cannot be imported from this directory.")); + + continue; + } + + jobs.add(new FileImportJob(file_or_dir, copy_to_library)); + } + + if (jobs.size > 0) { + BatchImport batch_import = new BatchImport(jobs, job_name, import_reporter); + enqueue_batch_import(batch_import, true); + switch_to_import_queue_page(); + } + } + + private Gdk.DragAction get_drag_action() { + Gdk.ModifierType mask; + + get_window().get_device_position(Gdk.Display.get_default().get_device_manager() + .get_client_pointer(), null, null, out mask); + + bool ctrl = (mask & Gdk.ModifierType.CONTROL_MASK) != 0; + bool alt = (mask & Gdk.ModifierType.MOD1_MASK) != 0; + bool shift = (mask & Gdk.ModifierType.SHIFT_MASK) != 0; + + if (ctrl && !alt && !shift) + return Gdk.DragAction.COPY; + else if (!ctrl && alt && !shift) + return Gdk.DragAction.ASK; + else if (ctrl && !alt && shift) + return Gdk.DragAction.LINK; + else + return Gdk.DragAction.DEFAULT; + } + + public override bool drag_motion(Gdk.DragContext context, int x, int y, uint time) { + Gdk.Atom target = Gtk.drag_dest_find_target(this, context, Gtk.drag_dest_get_target_list(this)); + // Want to use GDK_NONE (or, properly bound, Gdk.Atom.NONE) but GTK3 doesn't have it bound + // See: https://bugzilla.gnome.org/show_bug.cgi?id=655094 + if (((int) target) == 0) { + debug("drag target is GDK_NONE"); + Gdk.drag_status(context, 0, time); + + return true; + } + + // internal drag + if (Gtk.drag_get_source_widget(context) != null) { + Gdk.drag_status(context, Gdk.DragAction.PRIVATE, time); + + return true; + } + + // since we cannot set a default action, we must set it when we spy a drag motion + Gdk.DragAction drag_action = get_drag_action(); + + if (drag_action == Gdk.DragAction.DEFAULT) + drag_action = Gdk.DragAction.ASK; + + Gdk.drag_status(context, drag_action, time); + + return true; + } + + public override void drag_data_received(Gdk.DragContext context, int x, int y, + Gtk.SelectionData selection_data, uint info, uint time) { + if (selection_data.get_data().length < 0) + debug("failed to retrieve SelectionData"); + + // If an external drop, piggyback on the sidebar ExternalDropHandler, otherwise it's an + // internal drop, which isn't handled by the main window + if (Gtk.drag_get_source_widget(context) == null) + external_drop_handler(context, null, selection_data, info, time); + else + Gtk.drag_finish(context, false, false, time); + } + + private void external_drop_handler(Gdk.DragContext context, Sidebar.Entry? entry, + Gtk.SelectionData data, uint info, uint time) { + string[] uris_array = data.get_uris(); + + GLib.SList<string> uris = new GLib.SList<string>(); + foreach (string uri in uris_array) + uris.append(uri); + + Gdk.DragAction selected_action = context.get_selected_action(); + if (selected_action == Gdk.DragAction.ASK) { + // Default action is to link, unless one or more URIs are external to the library + Gtk.ResponseType result = Gtk.ResponseType.REJECT; + foreach (string uri in uris) { + if (!AppDirs.is_in_import_dir(File.new_for_uri(uri))) { + result = copy_files_dialog(); + + break; + } + } + + switch (result) { + case Gtk.ResponseType.ACCEPT: + selected_action = Gdk.DragAction.COPY; + break; + + case Gtk.ResponseType.REJECT: + selected_action = Gdk.DragAction.LINK; + break; + + default: + // cancelled + Gtk.drag_finish(context, false, false, time); + + return; + } + } + + dispatch_import_jobs(uris, "drag-and-drop", selected_action == Gdk.DragAction.COPY); + + Gtk.drag_finish(context, true, false, time); + } + + public void switch_to_library_page() { + switch_to_page(library_branch.get_main_page()); + } + + public void switch_to_event(Event event) { + Events.EventEntry? entry = events_branch.get_entry_for_event(event); + if (entry != null) + switch_to_page(entry.get_page()); + } + + public void switch_to_tag(Tag tag) { + Tags.SidebarEntry? entry = tags_branch.get_entry_for_tag(tag); + if (entry != null) + switch_to_page(entry.get_page()); + } + + public void switch_to_saved_search(SavedSearch search) { + Searches.SidebarEntry? entry = saved_search_branch.get_entry_for_saved_search(search); + if (entry != null) + switch_to_page(entry.get_page()); + } + + public void switch_to_photo_page(CollectionPage controller, Photo current) { + assert(controller.get_view().get_view_for_source(current) != null); + if (photo_page == null) { + photo_page = new LibraryPhotoPage(); + add_to_notebook(photo_page); + + // need to do this to allow the event loop a chance to map and realize the page + // before switching to it + spin_event_loop(); + } + + photo_page.display_for_collection(controller, current); + switch_to_page(photo_page); + } + + public void switch_to_import_queue_page() { + switch_to_page(import_queue_branch.get_queue_page()); + } + + private void on_camera_added(DiscoveredCamera camera) { + Camera.SidebarEntry? entry = camera_branch.get_entry_for_camera(camera); + if (entry == null) + 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 + } + + // don't unmount mass storage cameras, as they are then unavailable to gPhoto + if (mount != null && !camera.uri.has_prefix("file://")) { + if (page.unmount_camera(mount)) + switch_to_page(page); + else + error_message("Unable to unmount the camera at this time."); + } else { + switch_to_page(page); + } + } + + // This should only be called by LibraryWindow and PageStub. + public void add_to_notebook(Page page) { + // need to show all before handing over to notebook + page.show_all(); + + int pos = notebook.append_page(page, null); + assert(pos >= 0); + + // need to show_all() after pages are added and removed + notebook.show_all(); + } + + private void remove_from_notebook(Page page) { + notebook.remove(page); + + // need to show_all() after pages are added and removed + notebook.show_all(); + } + + // check for settings that should persist between instances + private void load_configuration() { + Gtk.ToggleAction? basic_display_action = get_common_action("CommonDisplayBasicProperties") + as Gtk.ToggleAction; + assert(basic_display_action != null); + basic_display_action.set_active(Config.Facade.get_instance().get_display_basic_properties()); + + Gtk.ToggleAction? extended_display_action = get_common_action("CommonDisplayExtendedProperties") + as Gtk.ToggleAction; + assert(extended_display_action != null); + extended_display_action.set_active(Config.Facade.get_instance().get_display_extended_properties()); + + Gtk.ToggleAction? search_bar_display_action = get_common_action("CommonDisplaySearchbar") + as Gtk.ToggleAction; + assert(search_bar_display_action != null); + search_bar_display_action.set_active(Config.Facade.get_instance().get_display_search_bar()); + + Gtk.RadioAction? sort_events_action = get_common_action("CommonSortEventsAscending") + as Gtk.RadioAction; + assert(sort_events_action != null); + + // Ticket #3321 - Event sorting order wasn't saving on exit. + // Instead of calling set_active against one of the toggles, call + // set_current_value against the entire radio group... + int event_sort_val = Config.Facade.get_instance().get_events_sort_ascending() ? SORT_EVENTS_ORDER_ASCENDING : + SORT_EVENTS_ORDER_DESCENDING; + + sort_events_action.set_current_value(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); + } + + private void on_library_monitor_discovery_completed() { + stop_pulse_background_progress_bar(STARTUP_SCAN_PROGRESS_PRIORITY, 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); + else { + update_background_progress_bar(_("Updating library..."), REALTIME_UPDATE_PROGRESS_PRIORITY, + 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); + } + + 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); + } + + private void on_metadata_writer_progress(uint completed, uint total) { + if (total < MIN_PROGRESS_BAR_FILES) + clear_background_progress_bar(METADATA_WRITER_PROGRESS_PRIORITY); + else { + update_background_progress_bar(_("Writing metadata to files..."), + METADATA_WRITER_PROGRESS_PRIORITY, completed, total); + } + } + + private void create_layout(Page start_page) { + // use a Notebook to hold all the pages, which are switched when a sidebar child is selected + notebook.set_show_tabs(false); + notebook.set_show_border(false); + + // put the sidebar in a scrolling window + Gtk.ScrolledWindow scrolled_sidebar = new Gtk.ScrolledWindow(null, null); + scrolled_sidebar.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + scrolled_sidebar.add(sidebar_tree); + scrolled_sidebar.get_style_context().add_class(Gtk.STYLE_CLASS_SIDEBAR); + scrolled_sidebar.set_shadow_type(Gtk.ShadowType.IN); + get_style_context().add_class("sidebar-pane-separator"); + + // divy the sidebar up into selection tree list, background progress bar, and properties + Gtk.Frame top_frame = new Gtk.Frame(null); + top_frame.add(scrolled_sidebar); + top_frame.set_shadow_type(Gtk.ShadowType.IN); + + background_progress_frame.add(background_progress_bar); + background_progress_frame.set_shadow_type(Gtk.ShadowType.IN); + + // pad the bottom frame (properties) + Gtk.Alignment bottom_alignment = new Gtk.Alignment(0, 0.5f, 1, 0); + + Resources.style_widget(scrolled_sidebar, Resources.SCROLL_FRAME_STYLESHEET); + Resources.style_widget(bottom_frame, Resources.INSET_FRAME_STYLESHEET); + + bottom_alignment.set_padding(10, 10, 6, 0); + bottom_alignment.add(basic_properties); + + bottom_frame.add(bottom_alignment); + bottom_frame.set_shadow_type(Gtk.ShadowType.IN); + + // "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(top_frame, true, true, 0); + + sidebar_paned.pack1(top_section, true, false); + sidebar_paned.pack2(bottom_frame, false, false); + sidebar_paned.set_position(1000); + + // layout the selection tree to the left of the collection/toolbar box with an adjustable + // gutter between them, framed for presentation + Gtk.Frame right_frame = new Gtk.Frame(null); + right_frame.set_shadow_type(Gtk.ShadowType.IN); + + right_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + right_frame.add(right_vbox); + right_vbox.pack_start(search_toolbar, false, false, 0); + right_vbox.pack_start(notebook, true, true, 0); + + client_paned = new Gtk.Paned(Gtk.Orientation.HORIZONTAL); + client_paned.pack1(sidebar_paned, false, false); + sidebar_tree.set_size_request(SIDEBAR_MIN_WIDTH, -1); + client_paned.pack2(right_frame, true, false); + client_paned.set_position(Config.Facade.get_instance().get_sidebar_position()); + // TODO: Calc according to layout's size, to give sidebar a maximum width + notebook.set_size_request(PAGE_MIN_WIDTH, -1); + + layout.pack_end(client_paned, true, true, 0); + + add(layout); + + switch_to_page(start_page); + start_page.grab_focus(); + } + + public override void set_current_page(Page page) { + // switch_to_page() will call base.set_current_page(), maintain the semantics of this call + switch_to_page(page); + } + + public void set_page_switching_enabled(bool should_enable) { + page_switching_enabled = should_enable; + } + + public void switch_to_page(Page page) { + if (!page_switching_enabled) + return; + + if (page == get_current_page()) + return; + + Page current_page = get_current_page(); + if (current_page != null) { + Gtk.Toolbar toolbar = current_page.get_toolbar(); + if (toolbar != null) + right_vbox.remove(toolbar); + + current_page.switching_from(); + + // see note below about why the sidebar is uneditable while the LibraryPhotoPage is + // visible + if (current_page is LibraryPhotoPage) + sidebar_tree.enable_editing(); + + // old page unsubscribes to these signals (new page subscribes below) + unsubscribe_from_basic_information(current_page); + } + + notebook.set_current_page(notebook.page_num(page)); + + // do this prior to changing selection, as the change will fire a cursor-changed event, + // which will then call this function again + base.set_current_page(page); + + // if the visible page is the LibraryPhotoPage, we need to prevent single-click inline + // renaming in the sidebar because a single click while in the LibraryPhotoPage indicates + // the user wants to return to the controlling page ... that is, in this special case, the + // sidebar cursor is set not to the 'current' page, but the page the user came from + if (page is LibraryPhotoPage) + sidebar_tree.disable_editing(); + + // Update search filter to new page. + toggle_search_bar(should_show_search_bar(), page as CheckerboardPage); + + // Not all pages have sidebar entries + Sidebar.Entry? entry = page_map.get(page); + if (entry != null) { + // if the corresponding sidebar entry is an expandable entry and wants to be + // expanded when it's selected, then expand it + Sidebar.ExpandableEntry expandable_entry = entry as Sidebar.ExpandableEntry; + if (expandable_entry != null && expandable_entry.expand_on_select()) + sidebar_tree.expand_to_entry(entry); + + sidebar_tree.place_cursor(entry, true); + } + + on_update_properties(); + + if (page is CheckerboardPage) + init_view_filter((CheckerboardPage)page); + + page.show_all(); + + // subscribe to these signals for each event page so basic properties display will update + subscribe_for_basic_information(get_current_page()); + + page.switched_to(); + + Gtk.Toolbar toolbar = page.get_toolbar(); + if (toolbar != null) { + right_vbox.add(toolbar); + toolbar.show_all(); + } + + page.ready(); + } + + private void init_view_filter(CheckerboardPage page) { + search_toolbar.set_view_filter(page.get_search_view_filter()); + page.get_view().install_view_filter(page.get_search_view_filter()); + } + + private bool should_show_search_bar() { + return (get_current_page() is CheckerboardPage) ? is_search_toolbar_visible : false; + } + + // Turns the search bar on or off. Note that if show is true, page must not be null. + private void toggle_search_bar(bool show, CheckerboardPage? page = null) { + search_toolbar.visible = show; + if (show) { + assert(null != page); + search_toolbar.set_view_filter(page.get_search_view_filter()); + page.get_view().install_view_filter(page.get_search_view_filter()); + } else { + if (page != null) + page.get_view().install_view_filter(new DisabledViewFilter()); + } + } + + private void on_page_created(Sidebar.PageRepresentative entry, Page page) { + assert(!page_map.has_key(page)); + page_map.set(page, entry); + + add_to_notebook(page); + } + + private void on_destroying_page(Sidebar.PageRepresentative entry, Page page) { + // if page is the current page, switch to fallback before destroying + if (page == get_current_page()) + switch_to_page(library_branch.get_main_page()); + + remove_from_notebook(page); + + bool removed = page_map.unset(page); + assert(removed); + } + + private void on_sidebar_entry_selected(Sidebar.SelectableEntry selectable) { + Sidebar.PageRepresentative? page_rep = selectable as Sidebar.PageRepresentative; + if (page_rep != null) + switch_to_page(page_rep.get_page()); + } + + private void on_sidebar_selected_entry_removed(Sidebar.SelectableEntry selectable) { + // if the currently selected item is removed, want to jump to fallback page (which + // depends on the item that was selected) + + // Importing... -> Last Import (if available) + if (selectable is Library.ImportQueueSidebarEntry && last_import_branch.get_show_branch()) { + switch_to_page(last_import_branch.get_main_entry().get_page()); + + return; + } + + // Event page -> Events (master event directory) + if (selectable is Events.EventEntry && events_branch.get_show_branch()) { + switch_to_page(events_branch.get_master_entry().get_page()); + + return; + } + + // Any event directory -> Events (master event directory) + if (selectable is Events.DirectoryEntry && events_branch.get_show_branch()) { + switch_to_page(events_branch.get_master_entry().get_page()); + + return; + } + + // basic all-around default: jump to the Library page + switch_to_page(library_branch.get_main_page()); + } + + private void subscribe_for_basic_information(Page page) { + ViewCollection view = page.get_view(); + + view.items_state_changed.connect(on_update_properties); + view.items_altered.connect(on_update_properties); + view.contents_altered.connect(on_update_properties); + view.items_visibility_changed.connect(on_update_properties); + } + + private void unsubscribe_from_basic_information(Page page) { + ViewCollection view = page.get_view(); + + view.items_state_changed.disconnect(on_update_properties); + view.items_altered.disconnect(on_update_properties); + view.contents_altered.disconnect(on_update_properties); + view.items_visibility_changed.disconnect(on_update_properties); + } + + private void on_update_properties() { + properties_scheduler.at_idle(); + } + + private void on_update_properties_now() { + if (bottom_frame.visible) + basic_properties.update_properties(get_current_page()); + + if (extended_properties.visible) + extended_properties.update_properties(get_current_page()); + } + + public void mounted_camera_shell_notification(string uri, bool at_startup) { + debug("mount point reported: %s", uri); + + // ignore unsupport mount URIs + if (!is_mount_uri_supported(uri)) { + debug("Unsupported mount scheme: %s", uri); + + return; + } + + File uri_file = File.new_for_uri(uri); + + // find the VFS mount point + Mount mount = null; + try { + mount = uri_file.find_enclosing_mount(null); + } catch (Error err) { + debug("%s", err.message); + + return; + } + + // convert file: URIs into gphoto disk: URIs + string alt_uri = null; + if (uri.has_prefix("file://")) + alt_uri = CameraTable.get_port_uri(uri.replace("file://", "disk:")); + + // we only add uris when the notification is called on startup + if (at_startup) { + if (!is_string_empty(uri)) + initial_camera_uris.add(uri); + + if (!is_string_empty(alt_uri)) + initial_camera_uris.add(alt_uri); + } + } + + public override bool key_press_event(Gdk.EventKey event) { + if (sidebar_tree.has_focus && sidebar_tree.is_keypress_interpreted(event) + && sidebar_tree.key_press_event(event)) { + return true; + } + + if (base.key_press_event(event)) + return true; + + if (Gdk.keyval_name(event.keyval) == "Escape") { + on_clear_search(); + return true; + } + + return false; + } +} + diff --git a/src/library/OfflineBranch.vala b/src/library/OfflineBranch.vala new file mode 100644 index 0000000..4ed2e49 --- /dev/null +++ b/src/library/OfflineBranch.vala @@ -0,0 +1,51 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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 Library.OfflineBranch : Sidebar.RootOnlyBranch { + public OfflineBranch() { + base (new Library.OfflineSidebarEntry()); + + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.offline_contents_altered.connect(on_offline_contents_altered); + + set_show_branch(get_total_offline() != 0); + } + + ~OfflineBranch() { + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.trashcan_contents_altered.disconnect(on_offline_contents_altered); + } + + private void on_offline_contents_altered() { + set_show_branch(get_total_offline() != 0); + } + + private int get_total_offline() { + int total = 0; + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + total += media_sources.get_offline_bin_contents().size; + + return total; + } +} + +public class Library.OfflineSidebarEntry : Sidebar.SimplePageEntry { + public OfflineSidebarEntry() { + } + + public override string get_sidebar_name() { + return OfflinePage.NAME; + } + + public override Icon? get_sidebar_icon() { + return new ThemedIcon(Resources.ICON_MISSING_FILES); + } + + protected override Page create_page() { + return new OfflinePage(); + } +} + diff --git a/src/library/OfflinePage.vala b/src/library/OfflinePage.vala new file mode 100644 index 0000000..cb6af2d --- /dev/null +++ b/src/library/OfflinePage.vala @@ -0,0 +1,130 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * 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 OfflinePage : CheckerboardPage { + public const string NAME = _("Missing Files"); + + private class OfflineView : Thumbnail { + public OfflineView(MediaSource source) { + base (source); + + assert(source.is_offline()); + } + } + + private class OfflineSearchViewFilter : DefaultSearchViewFilter { + public override uint get_criteria() { + return SearchFilterCriteria.TEXT | SearchFilterCriteria.FLAG | + SearchFilterCriteria.MEDIA | SearchFilterCriteria.RATING; + } + } + + private OfflineSearchViewFilter search_filter = new OfflineSearchViewFilter(); + private MediaViewTracker tracker; + + public OfflinePage() { + base (NAME); + + init_item_context_menu("/OfflineContextMenu"); + init_toolbar("/OfflineToolbar"); + + tracker = new MediaViewTracker(get_view()); + + // monitor offline and initialize view with all items in it + LibraryPhoto.global.offline_contents_altered.connect(on_offline_contents_altered); + Video.global.offline_contents_altered.connect(on_offline_contents_altered); + + on_offline_contents_altered(LibraryPhoto.global.get_offline_bin_contents(), null); + on_offline_contents_altered(Video.global.get_offline_bin_contents(), null); + } + + ~OfflinePage() { + LibraryPhoto.global.offline_contents_altered.disconnect(on_offline_contents_altered); + Video.global.offline_contents_altered.disconnect(on_offline_contents_altered); + } + + protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) { + base.init_collect_ui_filenames(ui_filenames); + + ui_filenames.add("offline.ui"); + } + + protected override Gtk.ActionEntry[] init_collect_action_entries() { + Gtk.ActionEntry[] actions = base.init_collect_action_entries(); + + Gtk.ActionEntry remove = { "RemoveFromLibrary", Gtk.Stock.REMOVE, TRANSLATABLE, "Delete", + TRANSLATABLE, on_remove_from_library }; + remove.label = Resources.REMOVE_FROM_LIBRARY_MENU; + remove.tooltip = Resources.DELETE_FROM_LIBRARY_TOOLTIP; + actions += remove; + + return actions; + } + + public override Core.ViewTracker? get_view_tracker() { + return tracker; + } + + protected override void update_actions(int selected_count, int count) { + set_action_sensitive("RemoveFromLibrary", selected_count > 0); + set_action_important("RemoveFromLibrary", true); + + base.update_actions(selected_count, count); + } + + private void on_offline_contents_altered(Gee.Collection<MediaSource>? added, + Gee.Collection<MediaSource>? removed) { + if (added != null) { + foreach (MediaSource source in added) + get_view().add(new OfflineView(source)); + } + + if (removed != null) { + Marker marker = get_view().start_marking(); + foreach (MediaSource source in removed) + marker.mark(get_view().get_view_for_source(source)); + get_view().remove_marked(marker); + } + } + + private void on_remove_from_library() { + Gee.Collection<MediaSource> sources = + (Gee.Collection<MediaSource>) get_view().get_selected_sources(); + if (sources.size == 0) + return; + + if (!remove_offline_dialog(AppWindow.get_instance(), sources.size)) + return; + + AppWindow.get_instance().set_busy_cursor(); + + ProgressDialog progress = null; + if (sources.size >= 20) + progress = new ProgressDialog(AppWindow.get_instance(), _("Deleting...")); + + Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>(); + Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>(); + MediaSourceCollection.filter_media(sources, photos, videos); + + if (progress != null) { + LibraryPhoto.global.remove_from_app(photos, false, progress.monitor); + Video.global.remove_from_app(videos, false, progress.monitor); + } else { + LibraryPhoto.global.remove_from_app(photos, false); + Video.global.remove_from_app(videos, false); + } + + if (progress != null) + progress.close(); + + AppWindow.get_instance().set_normal_cursor(); + } + + public override SearchViewFilter get_search_view_filter() { + return search_filter; + } +} + diff --git a/src/library/TrashBranch.vala b/src/library/TrashBranch.vala new file mode 100644 index 0000000..5ef8b3c --- /dev/null +++ b/src/library/TrashBranch.vala @@ -0,0 +1,73 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * 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 Library.TrashBranch : Sidebar.RootOnlyBranch { + public TrashBranch() { + base (new Library.TrashSidebarEntry()); + } +} + +public class Library.TrashSidebarEntry : Sidebar.SimplePageEntry, Sidebar.InternalDropTargetEntry { + private static Icon? full_icon = null; + private static Icon? empty_icon = null; + + public TrashSidebarEntry() { + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.trashcan_contents_altered.connect(on_trashcan_contents_altered); + } + + ~TrashSidebarEntry() { + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) + media_sources.trashcan_contents_altered.disconnect(on_trashcan_contents_altered); + } + + internal static void init() { + full_icon = new ThemedIcon(Resources.ICON_TRASH_FULL); + empty_icon = new ThemedIcon(Resources.ICON_TRASH_EMPTY); + } + + internal static void terminate() { + full_icon = null; + empty_icon = null; + } + + public override string get_sidebar_name() { + return TrashPage.NAME; + } + + public override Icon? get_sidebar_icon() { + return get_current_icon(); + } + + private static Icon get_current_icon() { + foreach (MediaSourceCollection media_sources in MediaCollectionRegistry.get_instance().get_all()) { + if (media_sources.get_trashcan_count() > 0) + return full_icon; + } + + return empty_icon; + } + + public bool internal_drop_received(Gee.List<MediaSource> media) { + AppWindow.get_command_manager().execute(new TrashUntrashPhotosCommand(media, true)); + + return true; + } + + public bool internal_drop_received_arbitrary(Gtk.SelectionData data) { + return false; + } + + protected override Page create_page() { + return new TrashPage(); + } + + private void on_trashcan_contents_altered() { + sidebar_icon_changed(get_current_icon()); + } +} + + diff --git a/src/library/TrashPage.vala b/src/library/TrashPage.vala new file mode 100644 index 0000000..2991727 --- /dev/null +++ b/src/library/TrashPage.vala @@ -0,0 +1,120 @@ +/* Copyright 2010-2014 Yorba Foundation + * + * 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 TrashPage : CheckerboardPage { + public const string NAME = _("Trash"); + + private class TrashView : Thumbnail { + public TrashView(MediaSource source) { + base (source); + + assert(source.is_trashed()); + } + } + + private class TrashSearchViewFilter : DefaultSearchViewFilter { + public override uint get_criteria() { + return SearchFilterCriteria.TEXT | SearchFilterCriteria.FLAG | + SearchFilterCriteria.MEDIA | SearchFilterCriteria.RATING; + } + } + + private TrashSearchViewFilter search_filter = new TrashSearchViewFilter(); + private MediaViewTracker tracker; + + public TrashPage() { + base (NAME); + + init_item_context_menu("/TrashContextMenu"); + init_page_context_menu("/TrashPageMenu"); + init_toolbar("/TrashToolbar"); + + tracker = new MediaViewTracker(get_view()); + + // monitor trashcans and initialize view with all items in them + LibraryPhoto.global.trashcan_contents_altered.connect(on_trashcan_contents_altered); + Video.global.trashcan_contents_altered.connect(on_trashcan_contents_altered); + on_trashcan_contents_altered(LibraryPhoto.global.get_trashcan_contents(), null); + on_trashcan_contents_altered(Video.global.get_trashcan_contents(), null); + } + + protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) { + base.init_collect_ui_filenames(ui_filenames); + + ui_filenames.add("trash.ui"); + } + + protected override Gtk.ActionEntry[] init_collect_action_entries() { + Gtk.ActionEntry[] actions = base.init_collect_action_entries(); + + Gtk.ActionEntry delete_action = { "Delete", Gtk.Stock.DELETE, TRANSLATABLE, "Delete", + TRANSLATABLE, on_delete }; + delete_action.label = Resources.DELETE_PHOTOS_MENU; + delete_action.tooltip = Resources.DELETE_FROM_TRASH_TOOLTIP; + actions += delete_action; + + Gtk.ActionEntry restore = { "Restore", Gtk.Stock.UNDELETE, TRANSLATABLE, null, TRANSLATABLE, + on_restore }; + restore.label = Resources.RESTORE_PHOTOS_MENU; + restore.tooltip = Resources.RESTORE_PHOTOS_TOOLTIP; + actions += restore; + + return actions; + } + + public override Core.ViewTracker? get_view_tracker() { + return tracker; + } + + protected override void update_actions(int selected_count, int count) { + bool has_selected = selected_count > 0; + + set_action_sensitive("Delete", has_selected); + set_action_important("Delete", true); + set_action_sensitive("Restore", has_selected); + set_action_important("Restore", true); + set_common_action_important("CommonEmptyTrash", true); + + base.update_actions(selected_count, count); + } + + private void on_trashcan_contents_altered(Gee.Collection<MediaSource>? added, + Gee.Collection<MediaSource>? removed) { + if (added != null) { + foreach (MediaSource source in added) + get_view().add(new TrashView(source)); + } + + if (removed != null) { + Marker marker = get_view().start_marking(); + foreach (MediaSource source in removed) + marker.mark(get_view().get_view_for_source(source)); + get_view().remove_marked(marker); + } + } + + private void on_restore() { + if (get_view().get_selected_count() == 0) + return; + + get_command_manager().execute(new TrashUntrashPhotosCommand( + (Gee.Collection<LibraryPhoto>) get_view().get_selected_sources(), false)); + } + + protected override string get_view_empty_message() { + return _("Trash is empty"); + } + + private void on_delete() { + remove_from_app((Gee.Collection<MediaSource>) get_view().get_selected_sources(), _("Delete"), + (get_view().get_selected_count() == 1) ? ("Deleting a Photo") : _("Deleting Photos")); + } + + public override SearchViewFilter get_search_view_filter() { + return search_filter; + } +} + diff --git a/src/library/mk/library.mk b/src/library/mk/library.mk new file mode 100644 index 0000000..b4ab790 --- /dev/null +++ b/src/library/mk/library.mk @@ -0,0 +1,54 @@ + +# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with +# a init() and terminate() function declared in the namespace. +UNIT_NAME := Library + +# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all +# lowercase. The name of this file should be UNIT_DIR.mk. +UNIT_DIR := library + +# All Vala files in the unit should be listed here with no subdirectory prefix. +# +# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala. +UNIT_FILES := \ + LibraryWindow.vala \ + Branch.vala \ + TrashBranch.vala \ + OfflineBranch.vala \ + FlaggedBranch.vala \ + LastImportBranch.vala \ + ImportQueueBranch.vala \ + FlaggedPage.vala \ + ImportQueuePage.vala \ + LastImportPage.vala \ + OfflinePage.vala \ + TrashPage.vala + +# Any unit this unit relies upon (and should be initialized before it's initialized) should +# be listed here using its Vala namespace. +# +# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here. +UNIT_USES := \ + Util \ + Threads \ + Db \ + Plugins \ + Slideshow \ + Photos \ + Publishing \ + Core \ + Sidebar \ + Events \ + Tags \ + Camera \ + Searches \ + DataImports \ + Folders + +# List any additional files that are used in the build process as a part of this unit that should +# be packaged in the tarball. File names should be relative to the unit's home directory. +UNIT_RC := + +# unitize.mk must be called at the end of each UNIT_DIR.mk file. +include unitize.mk + |