/* Copyright 2016 Software Freedom Conservancy Inc.
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution.
 */

public class LibraryWindow : AppWindow {
    public const int SIDEBAR_MIN_WIDTH = 120;
    public const int EXTENDED_INFO_MIN_WIDTH = 360;
    
    public static int PAGE_MIN_WIDTH {
        get {
            return Thumbnail.MAX_SCALE + (CheckerboardLayout.COLUMN_GUTTER_PADDING * 2);
        }
    }
    
    public const string SORT_EVENTS_ORDER_ASCENDING = "ascending";
    public const string SORT_EVENTS_ORDER_DESCENDING = "descending";
    
    private const string[] SUPPORTED_MOUNT_SCHEMES = {
        "gphoto2:",
        "disk:",
        "file:",
        "mtp:"
    };
    

    // 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 = 1;
    
    // This lists the order of the toplevel items in the sidebar.  New toplevel items should be
    // added here in the position they should appear in the sidebar.  To re-order, simply move
    // the item in this list to a new position.  These numbers should *not* persist anywhere
    // outside the app.
    private enum SidebarRootPosition {
        LIBRARY,
        CAMERAS,
        SAVED_SEARCH,
        EVENTS,
        IMPORT_ROLL,
        FOLDERS,
        FACES,
        TAGS
    }
    
    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();
        }

        public override void switching_from() {
        }

        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 bool import_recursive = true;

    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 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 Faces.Branch faces_branch = new Faces.Branch();
    private Events.Branch events_branch = new Events.Branch();
    private Camera.Branch camera_branch = new Camera.Branch();
    private Searches.Branch saved_search_branch = new Searches.Branch();
    private ImportRoll.Branch import_roll_branch = new ImportRoll.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.Revealer background_progress_frame = new Gtk.Revealer();
    private BackgroundProgressBar background_progress_bar = new BackgroundProgressBar();
    
    // Instantiate later in constructor because the map support loads its icons in there and we need
    // to have the global app instance available for that
    private BasicProperties basic_properties;
    private ExtendedProperties extended_properties = new ExtendedProperties();
    private Gtk.Revealer extended_properties_revealer = new Gtk.Revealer();
    
    private Gtk.Stack stack = new Gtk.Stack();
    private Gtk.Box layout = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
    private Gtk.Box right_vbox;
    private Gtk.Revealer toolbar_revealer = new Gtk.Revealer ();
    
    public LibraryWindow(ProgressMonitor progress_monitor) {
        base();

        // 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(faces_branch, SidebarRootPosition.FACES);
        sidebar_tree.graft(events_branch, SidebarRootPosition.EVENTS);
        sidebar_tree.graft(camera_branch, SidebarRootPosition.CAMERAS);
        sidebar_tree.graft(saved_search_branch, SidebarRootPosition.SAVED_SEARCH);
        sidebar_tree.graft(import_roll_branch, SidebarRootPosition.IMPORT_ROLL);
        
        properties_scheduler = new OneShotScheduler("LibraryWindow properties",
            on_update_properties_now);

        // setup search bar and add its accelerators to the window
        search_toolbar = new SearchFilterToolbar(search_actions);

        // create the main layout & start at the Library page
        basic_properties = new BasicProperties();
        create_layout(library_branch.photos_entry.get_page());
        
        // settings that should persist between sessions
        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);
        
        // Need to re-install F8 here as it will overwrite the binding created
        // by the menu
        const string[] accels = { "<Primary>f", "F8", null };
        Application.set_accels_for_action("win.CommonDisplaySearchbar", accels);
    }

    ~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());

        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 const GLib.ActionEntry[] common_actions = {
        // Normal actions
        { "CommonFileImport", on_file_import },
        { "ExternalLibraryImport", on_external_library_import },
        { "CommonPreferences", on_preferences },
        { "CommonEmptyTrash", on_empty_trash },
        { "CommonJumpToEvent", on_jump_to_event },
        { "CommonFind", on_find },
        { "CommonNewSearch", on_new_search },

        // Toggle actions
        { "CommonDisplayBasicProperties", on_action_toggle, null, "false", on_display_basic_properties },
        { "CommonDisplayExtendedProperties", on_action_toggle, null, "false", on_display_extended_properties },

        { "CommonDisplaySearchbar", null, null, "false", on_display_searchbar },
        { "CommonDisplaySidebar", on_action_toggle, null, "true", on_display_sidebar },
        { "CommonDisplayToolbar", null, null, "true", on_display_toolbar },

        { "CommonSortEvents", on_action_radio, "s", "'ascending'", on_events_sort_changed }
    };

    protected override void add_actions () {
        base.add_actions ();
        this.add_action_entries (common_actions, this);
        this.add_action_entries (search_actions.get_actions (), search_actions);

        lookup_action ("CommonDisplaySearchbar").change_state (Config.Facade.get_instance().get_display_search_bar());
        lookup_action ("CommonDisplaySidebar").change_state (is_sidebar_visible ());
        lookup_action ("CommonDisplayToolbar").change_state (is_toolbar_visible ());
    }

    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
        var action = this.lookup_action ("CommonDisplaySearchbar");

        if (action != null)
            action.change_state (true);
    }

    // show_all() may make visible certain items we wish to keep programmatically hidden
    public override void show_all() {
        base.show_all();

        var basic_properties_action = get_current_page ().get_common_action
            ("CommonDisplayBasicProperties");
        assert(basic_properties_action != null);

        if (!basic_properties_action.get_state().get_boolean())
            bottom_frame.hide();

        // 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");
    }
    
    public void rename_face_in_sidebar(Face face) {
        Faces.SidebarEntry? entry = faces_branch.get_entry_for_face(face);
        if (entry != null)
            sidebar_tree.rename_entry_in_place(entry);
        else
            assert_not_reached();
    }
    
    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() {
        var import_dialog = new Gtk.FileChooserNative(_("Import From Folder"), null,
            Gtk.FileChooserAction.SELECT_FOLDER, Resources.OK_LABEL, Resources.CANCEL_LABEL);
        import_dialog.set_local_only(false);
        import_dialog.set_select_multiple(true);
        import_dialog.set_current_folder(import_dir);

        var recursive = new Gtk.CheckButton.with_label(_("Recurse Into Subfolders"));
        recursive.active = import_recursive;
        import_dialog.set_extra_widget(recursive);
        
        int response = import_dialog.run();
        
        if (response == Gtk.ResponseType.ACCEPT) {
            import_dialog.hide();
            // 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, recursive.active);
            }
        }
        
        import_dir = import_dialog.get_current_folder();
        import_recursive = recursive.active;
        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() {
        var action = this.lookup_action ("CommonDisplaySearchbar");
        action.change_state (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() {
        var action = this.lookup_action ("CommonSortEvents") as GLib.SimpleAction;
        
        return (action != null) ? (action.state.get_string () == SORT_EVENTS_ORDER_ASCENDING)
            ? 0 : 1
            : 1;
    }

    private void on_events_sort_changed(GLib.SimpleAction action, Variant? value) {
        
        Config.Facade.get_instance().set_events_sort_ascending(
            value.get_string () == SORT_EVENTS_ORDER_ASCENDING);

        action.set_state (value);
    }
    
    private void on_preferences() {
        PreferencesDialog.show_preferences();
    }
    
    private void on_display_basic_properties(GLib.SimpleAction action, Variant? value) {
        bool display = value.get_boolean ();

        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);
        action.set_state (value);
    }

    private void on_action_toggle (GLib.Action action, Variant? value) {
        Variant new_state = ! (bool) action.get_state ();
        action.change_state (new_state);
    }

    private void on_action_radio (GLib.Action action, Variant? value) {
        action.change_state (value);
    }

    private void on_display_extended_properties(GLib.SimpleAction action, Variant? value) {
        bool display = value.get_boolean ();

        if (display) {
            extended_properties.update_properties(get_current_page());
        }
        extended_properties_revealer.set_reveal_child(display);

        action.set_state (value);
        Config.Facade.get_instance().set_display_extended_properties(display);
    }
    
    private void on_display_searchbar(GLib.SimpleAction action, Variant? value) {
        bool is_shown = value.get_boolean ();

        Config.Facade.get_instance().set_display_search_bar(is_shown);
        show_search_bar(is_shown);
        action.set_state (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(GLib.SimpleAction action, Variant? variant) {
        set_sidebar_visible(variant.get_boolean ());

        action.set_state (variant);
    }

    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 on_display_toolbar (GLib.SimpleAction action, Variant? variant) {
        set_toolbar_visible (variant.get_boolean ());

        action.set_state (variant);
    }

    private void set_toolbar_visible (bool visible) {
        if (get_current_page() == null) {
            return;
        }

        var toolbar = get_current_page ().get_toolbar ();
        if (toolbar != null) {
            this.toolbar_revealer.set_reveal_child (visible);
        }
        Config.Facade.get_instance().set_display_toolbar (visible);
    }

    private bool is_toolbar_visible () {
        return Config.Facade.get_instance ().get_display_toolbar ();
    }

    public void enqueue_batch_import(BatchImport batch_import, bool allow_user_cancel) {
        library_branch.import_queue_entry.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, bool recurse) {
        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 Edit %s Preferences.\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, recurse));
        }
        
        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;

        var seat = Gdk.Display.get_default().get_default_seat();
        get_window().get_device_position(seat.get_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));
        if (target == Gdk.Atom.NONE) {
            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, true);
        
        Gtk.drag_finish(context, true, false, time);
    }
    
    public void switch_to_library_page() {
        switch_to_page(library_branch.photos_entry.get_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_stack(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(library_branch.import_queue_entry.get_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();
        
        // find the VFS mount point
        var mount = camera.get_mount();
        
        // don't unmount mass storage cameras, as they are then unavailable to gPhoto
        if (mount != null && !camera.uri.has_prefix("file://")) {
            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_stack(Page page) {
        // need to show all before handing over to stack
        page.show_all();
        
        stack.add(page);
        // need to show_all() after pages are added and removed
        stack.show_all();
    }
    
    private void remove_from_stack(Page page) {
        stack.remove(page);
        
        // need to show_all() after pages are added and removed
        stack.show_all();
    }
    
    // check for settings that should persist between instances
    private void load_configuration() {
        var basic_display_action = lookup_action("CommonDisplayBasicProperties");
        assert(basic_display_action != null);
        basic_display_action.change_state (Config.Facade.get_instance().get_display_basic_properties());

        var extended_display_action = lookup_action("CommonDisplayExtendedProperties");
        assert(extended_display_action != null);
        extended_display_action.change_state(Config.Facade.get_instance().get_display_extended_properties());
        
        var search_bar_display_action = lookup_action("CommonDisplaySearchbar");
        assert(search_bar_display_action != null);
        search_bar_display_action.change_state(Config.Facade.get_instance().get_display_search_bar());

        var sort_events_action = lookup_action("CommonSortEvents");
        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...
        string event_sort_val = Config.Facade.get_instance().get_events_sort_ascending() ? SORT_EVENTS_ORDER_ASCENDING :
            SORT_EVENTS_ORDER_DESCENDING;
        
        sort_events_action.change_state (event_sort_val);
    }

    private void on_library_monitor_discovery_started() {
        background_progress_bar.start(_("Updating library…"), BackgroundProgressBar.Priority.STARTUP_SCAN);
    }
    
    private void on_library_monitor_discovery_completed() {
        background_progress_bar.stop(BackgroundProgressBar.Priority.STARTUP_SCAN, true);
    }
    
    private void on_library_monitor_auto_update_progress(int completed_files, int total_files) {
        if (total_files < MIN_PROGRESS_BAR_FILES)
            background_progress_bar.clear(BackgroundProgressBar.Priority.REALTIME_UPDATE);
        else {
            background_progress_bar.update(_("Updating library…"), BackgroundProgressBar.Priority.REALTIME_UPDATE,
                completed_files, total_files);
        }
    }
    
    private void on_library_monitor_auto_import_preparing() {
        background_progress_bar.start(_("Preparing to auto-import photos…"),
            BackgroundProgressBar.Priority.REALTIME_UPDATE);
    }
    
    private void on_library_monitor_auto_import_progress(uint64 completed_bytes, uint64 total_bytes) {
        background_progress_bar.update(_("Auto-importing photos…"),
            BackgroundProgressBar.Priority.REALTIME_UPDATE, completed_bytes, total_bytes);
    }
    
    private void on_metadata_writer_progress(uint completed, uint total) {
        if (total < MIN_PROGRESS_BAR_FILES)
            background_progress_bar.clear(BackgroundProgressBar.Priority.METADATA_WRITER);
        else {
            background_progress_bar.update(_("Writing metadata to files…"),
                BackgroundProgressBar.Priority.METADATA_WRITER, completed, total);
        }
    }
    
    private void create_layout(Page start_page) {
        
        // 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);
        
        background_progress_frame.set_border_width(2);
        background_progress_frame.add(background_progress_bar);
        background_progress_frame.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP);
        background_progress_frame.halign = Gtk.Align.FILL;
        background_progress_frame.valign = Gtk.Align.END;
        background_progress_frame.vexpand = false;
        background_progress_frame.hexpand = true;
        background_progress_bar.bind_property("should-be-visible", background_progress_frame, "reveal-child", GLib.BindingFlags.DEFAULT);

        // pad the bottom frame (properties)
        basic_properties.halign = Gtk.Align.FILL;
        basic_properties.valign = Gtk.Align.CENTER;
        basic_properties.hexpand = true;
        basic_properties.vexpand = true;
        basic_properties.margin_top = 10;
        basic_properties.margin_bottom = 10;
        basic_properties.margin_start = 6;
        basic_properties.margin_end = 6;

        bottom_frame.add(basic_properties);
        bottom_frame.get_style_context().remove_class("frame");
        
        // "attach" the progress bar to the sidebar tree, so the movable ridge is to resize the
        // top two and the basic information pane
        top_section.pack_start(scrolled_sidebar, true, true, 0);
        top_section.pack_end(background_progress_frame, false, false, 0);

        sidebar_paned.pack1(top_section, true, false);
        sidebar_paned.pack2(bottom_frame, false, false);
        sidebar_paned.set_position(1000);
        
        right_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
        right_vbox.pack_start(search_toolbar, false, false, 0);
        var stack_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
        stack_box.pack_start(stack, true, true, 0);
        right_vbox.pack_start(stack_box, true, true, 0);
        right_vbox.add (toolbar_revealer);
        
        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_vbox, 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
        stack.set_size_request(PAGE_MIN_WIDTH, -1);
        var scrolled = new Gtk.ScrolledWindow(null, null);
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
        scrolled.add(extended_properties);
        extended_properties_revealer.add(scrolled);
        extended_properties_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT);
        extended_properties_revealer.halign = Gtk.Align.END;
        extended_properties_revealer.valign = Gtk.Align.FILL;

        extended_properties.vexpand = true;
        extended_properties.set_margin_top (9);
        extended_properties.set_margin_bottom (9);
        extended_properties.set_margin_start (9);
        extended_properties.set_margin_end (9);
        scrolled.set_size_request(EXTENDED_INFO_MIN_WIDTH, -1);

        stack_box.pack_end(extended_properties_revealer, false, false, 0);
        extended_properties_revealer.halign = Gtk.Align.END;
        extended_properties_revealer.hexpand = false;
        if (Config.Facade.get_instance().get_display_extended_properties()) {
            extended_properties_revealer.set_reveal_child(true);
        } else {
            extended_properties_revealer.set_reveal_child(false);
        }

        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) {
            set_show_menubar (false);
            Application.set_menubar (null);

            Gtk.Toolbar toolbar = current_page.get_toolbar();
            if (toolbar != null)
                toolbar_revealer.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);
        }
        
        stack.set_visible_child(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();

        Application.set_menubar (page.get_menubar ());
        set_show_menubar (true);
        var old = get_settings().gtk_shell_shows_menubar;
        get_settings().gtk_shell_shows_menubar = !old;
        get_settings().gtk_shell_shows_menubar = old;
        
        Gtk.Toolbar toolbar = page.get_toolbar();
        if (toolbar != null) {
            toolbar_revealer.add(toolbar);
            toolbar.show_all();
            toolbar_revealer.set_reveal_child (this.is_toolbar_visible ());
        }

        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.set_reveal_child(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_stack(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.photos_entry.get_page());
        
        remove_from_stack(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)
        
        Library.LastImportSidebarEntry last_import_entry = library_branch.last_imported_entry;
        
        // Importing... -> Last Import (if available)
        if (selectable is Library.ImportQueueSidebarEntry && last_import_entry.visible) {
            switch_to_page(last_import_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.photos_entry.get_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;
    }
}

