summaryrefslogtreecommitdiff
path: root/src/library/LibraryWindow.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/library/LibraryWindow.vala')
-rw-r--r--src/library/LibraryWindow.vala1587
1 files changed, 1587 insertions, 0 deletions
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;
+ }
+}
+