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

public class CollectionViewManager : ViewManager {
    private CollectionPage page;
    
    public CollectionViewManager(CollectionPage page) {
        this.page = page;
    }
    
    public override DataView create_view(DataSource source) {
        return page.create_thumbnail(source);
    }
}

public abstract class CollectionPage : MediaPage {
    private const double DESKTOP_SLIDESHOW_TRANSITION_SEC = 2.0;
    
    protected class CollectionSearchViewFilter : DefaultSearchViewFilter {
        public override uint get_criteria() {
            return SearchFilterCriteria.TEXT | SearchFilterCriteria.FLAG | 
                SearchFilterCriteria.MEDIA | SearchFilterCriteria.RATING | SearchFilterCriteria.SAVEDSEARCH;
        }
    }
    
    private ExporterUI exporter = null;
    private CollectionSearchViewFilter search_filter = new CollectionSearchViewFilter();
    
    protected CollectionPage(string page_name) {
        base (page_name);
        
        get_view().items_altered.connect(on_photos_altered);
        
        init_item_context_menu("CollectionContextMenu");
        init_toolbar("CollectionToolbar");
        
        set_visible(true);

        // watch for updates to the external app settings
        Config.Facade.get_instance().external_app_changed.connect(on_external_app_changed);
    }

    public override Gtk.Box get_toolbar() {
        if (toolbar == null) {
            base.get_toolbar();

            // zoom slider assembly
            MediaPage.ZoomSliderAssembly zoom_slider_assembly = create_zoom_slider_assembly();
            connect_slider(zoom_slider_assembly);
            get_toolbar().append(zoom_slider_assembly);

            #if 0
            Gtk.Button? rotate_button = this.builder.get_object ("ToolRotate") as Gtk.Button;
            unowned Gtk.BindingSet binding_set = Gtk.BindingSet.by_class(rotate_button.get_class());
            Gtk.BindingEntry.add_signal(binding_set, Gdk.Key.KP_Space, Gdk.ModifierType.CONTROL_MASK, "clicked", 0);
            Gtk.BindingEntry.add_signal(binding_set, Gdk.Key.space, Gdk.ModifierType.CONTROL_MASK, "clicked", 0);
#endif
        }
        
        return toolbar;
    }
    
    private static InjectionGroup create_file_menu_injectables() {
        InjectionGroup group = new InjectionGroup("FileExtrasPlaceholder");
        
        group.add_menu_item(_("_Print"), "Print", "<Primary>p");
        group.add_separator();
        group.add_menu_item(_("_Publish"), "Publish", "<Primary><Shift>p");
        group.add_menu_item(_("Send _To…"), "SendTo");
        group.add_menu_item(_("Set as _Desktop Background"), "SetBackground", "<Primary>b");
        
        return group;
    }
    
    private static InjectionGroup create_edit_menu_injectables() {
        InjectionGroup group = new InjectionGroup("EditExtrasPlaceholder");
        
        group.add_menu_item(_("_Duplicate"), "Duplicate", "<Primary>D");

        return group;
    }

    private static InjectionGroup create_view_menu_fullscreen_injectables() {
        InjectionGroup group = new InjectionGroup("ViewExtrasFullscreenSlideshowPlaceholder");
        
        group.add_menu_item(_("Fullscreen"), "CommonFullscreen", "F11");
        group.add_separator();
        group.add_menu_item(_("S_lideshow"), "Slideshow", "F5");
        
        return group;
    }

    private static InjectionGroup create_photos_menu_edits_injectables() {
        InjectionGroup group = new InjectionGroup("PhotosExtrasEditsPlaceholder");
        
        group.add_menu_item(_("Rotate _Right"),
                            "RotateClockwise",
                            "<Primary>r");
        group.add_menu_item(_("Rotate _Left"),
                            "RotateCounterclockwise",
                            "<Primary><Shift>r");
        group.add_menu_item(_("Flip Hori_zontally"), "FlipHorizontally");
        group.add_menu_item(_("Flip Verti_cally"), "FlipVertically");
        group.add_separator();
        group.add_menu_item(_("_Enhance"), "Enhance");
        group.add_menu_item(_("Re_vert to Original"), "Revert");
        group.add_separator();
        group.add_menu_item(_("_Copy Color Adjustments"),
                            "CopyColorAdjustments",
                            "<Primary><Shift>c");
        group.add_menu_item(_("_Paste Color Adjustments"),
                            "PasteColorAdjustments",
                            "<Primary><Shift>v");
        
        return group;
    }
  
    private static InjectionGroup create_photos_menu_date_injectables() {
        InjectionGroup group = new InjectionGroup("PhotosExtrasDateTimePlaceholder");
        
        group.add_menu_item(_("Adjust Date and Time…"), "AdjustDateTime", "F4");
        
        return group;
    }

    private static InjectionGroup create_photos_menu_externals_injectables() {
        InjectionGroup group = new InjectionGroup("PhotosExtrasExternalsPlaceholder");
        
        group.add_menu_item(_("Open With E_xternal Editor"),
                            "ExternalEdit",
                            "<Primary>Return");
        group.add_menu_item(_("Open With RA_W Editor"),
                            "ExternalEditRAW",
                            "<Primary><Shift>Return");
        group.add_menu_item(_("_Play"), "PlayVideo", "<Primary>Y");
        
        return group;
    }
    
    protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
        base.init_collect_ui_filenames(ui_filenames);
        
        ui_filenames.add("collection.ui");
    }

    private const GLib.ActionEntry[] entries = {
        { "Print", on_print },
        { "Publish", on_publish },
        { "RotateClockwise", on_rotate_clockwise },
        { "RotateCounterclockwise", on_rotate_counterclockwise },
        { "FlipHorizontally", on_flip_horizontally },
        { "FlipVertically", on_flip_vertically },
        { "Enhance", on_enhance },
        { "CopyColorAdjustments", on_copy_adjustments },
        { "PasteColorAdjustments", on_paste_adjustments },
        { "Revert", on_revert },
        { "SetBackground", on_set_background },
        { "Duplicate", on_duplicate_photo },
        { "AdjustDateTime", on_adjust_date_time },
        { "ExternalEdit", on_external_edit },
        { "ExternalEditRAW", on_external_edit_raw },
        { "Slideshow", on_slideshow }
    };

    protected override void add_actions (GLib.ActionMap map) {
        base.add_actions (map);
        map.add_action_entries (entries, this);
    }

    protected override void remove_actions(GLib.ActionMap map) {
        base.remove_actions(map);
        map.remove_action_entries(entries);
    }

    protected override InjectionGroup[] init_collect_injection_groups() {
        InjectionGroup[] groups = base.init_collect_injection_groups();
        
        groups += create_file_menu_injectables();
        groups += create_edit_menu_injectables();
        groups += create_view_menu_fullscreen_injectables();
        groups += create_photos_menu_edits_injectables();
        groups += create_photos_menu_date_injectables();
        groups += create_photos_menu_externals_injectables();
        
        return groups;
    }
    
    private bool selection_has_video() {
        return MediaSourceCollection.has_video((Gee.Collection<MediaSource>) get_view().get_selected_sources());
    }
    
    private bool page_has_photo() {
        return MediaSourceCollection.has_photo((Gee.Collection<MediaSource>) get_view().get_sources());
    }
    
    private bool selection_has_photo() {
        return MediaSourceCollection.has_photo((Gee.Collection<MediaSource>) get_view().get_selected_sources());
    }
    
    protected override void init_actions(int selected_count, int count) {
        base.init_actions(selected_count, count);
        
        set_action_sensitive("RotateClockwise", true);
        set_action_sensitive("RotateCounterclockwise", true);
        set_action_sensitive("Enhance", true);
        set_action_sensitive("Publish", true);
    }
    
    protected override void update_actions(int selected_count, int count) {
        //FIXME: Hack. Otherwise it will disable actions that just have been enabled by photo page
        if (AppWindow.get_instance().get_current_page() != this) {
            return;
        }

        base.update_actions(selected_count, count);

        bool one_selected = selected_count == 1;
        bool has_selected = selected_count > 0;

        bool primary_is_video = false;
        if (has_selected)
            if (get_view().get_selected_at(0).get_source() is Video)
                primary_is_video = true;

        bool selection_has_videos = selection_has_video();
        bool selection_has_photos = selection_has_photo();
        bool page_has_photos = page_has_photo();
        
        // don't allow duplication of the selection if it contains a video -- videos are huge and
        // and they're not editable anyway, so there seems to be no use case for duplicating them
        set_action_sensitive("Duplicate", has_selected && (!selection_has_videos));
        set_action_sensitive("ExternalEdit", 
            (!primary_is_video) && one_selected && !is_string_empty(Config.Facade.get_instance().get_external_photo_app()));
        set_action_sensitive("ExternalEditRAW",
            one_selected && (!primary_is_video)
            && ((Photo) get_view().get_selected_at(0).get_source()).get_master_file_format() == 
                PhotoFileFormat.RAW
            && !is_string_empty(Config.Facade.get_instance().get_external_raw_app()));
        set_action_sensitive("Revert", (!selection_has_videos) && can_revert_selected());
        set_action_sensitive("Enhance", (!selection_has_videos) && has_selected);
        set_action_sensitive("CopyColorAdjustments", (!selection_has_videos) && one_selected &&
            ((Photo) get_view().get_selected_at(0).get_source()).has_color_adjustments());
        set_action_sensitive("PasteColorAdjustments", (!selection_has_videos) && has_selected &&
            PixelTransformationBundle.has_copied_color_adjustments());
        set_action_sensitive("RotateClockwise", (!selection_has_videos) && has_selected);
        set_action_sensitive("RotateCounterclockwise", (!selection_has_videos) && has_selected);
        set_action_sensitive("FlipHorizontally", (!selection_has_videos) && has_selected);
        set_action_sensitive("FlipVertically", (!selection_has_videos) && has_selected);
        
        // Allow changing of exposure time, even if there's a video in the current
        // selection.
        set_action_sensitive("AdjustDateTime", has_selected);
        
        set_action_sensitive("NewEvent", has_selected);
        set_action_sensitive("AddTags", has_selected);
        set_action_sensitive("ModifyTags", one_selected);

        // Allow starting slideshow even if first selected item is a video. Otherwise the
        // behavior is quite confusing, it will start if you do not select anything and just skipt the video
        set_action_sensitive("Slideshow", (page_has_photos && !has_selected) || selection_has_photos);
        set_action_sensitive("Print", (!selection_has_videos) && has_selected);
        set_action_sensitive("Publish", has_selected);
        
        set_action_sensitive("SetBackground", has_selected && selection_has_photos);
        if (has_selected) {
            debug ("Setting action label for SetBackground...");
            var label = one_selected
                    ? Resources.SET_BACKGROUND_MENU
                    : Resources.SET_BACKGROUND_SLIDESHOW_MENU;
            this.update_menu_item_label ("SetBackground", label);
        }
    }

    private void on_photos_altered(Gee.Map<DataObject, Alteration> altered) {
        // only check for revert if the media object is a photo and its image has changed in some 
        // way and it's in the selection
        foreach (DataObject object in altered.keys) {
            DataView view = (DataView) object;
            
            if (!view.is_selected() || !altered.get(view).has_subject("image"))
            continue;
            
            LibraryPhoto? photo = view.get_source() as LibraryPhoto;
            if (photo == null)
                continue;
            
            // since the photo can be altered externally to Shotwell now, need to make the revert
            // command available appropriately, even if the selection doesn't change
            set_action_sensitive("Revert", can_revert_selected());
            set_action_sensitive("CopyColorAdjustments", photo.has_color_adjustments());
            
            break;
        }
    }
    
    private void on_print() {
        if (get_view().get_selected_count() > 0) {
            PrintManager.get_instance().spool_photo.begin(
                (Gee.Collection<Photo>) get_view().get_selected_sources_of_type(typeof(Photo)));
        }
    }
    
    private void on_external_app_changed() {
        int selected_count = get_view().get_selected_count();
        
        set_action_sensitive("ExternalEdit", selected_count == 1 && Config.Facade.get_instance().get_external_photo_app() != "");
    }
    
    // see #2020
    // double click = switch to photo page
    // Super + double click = open in external editor
    // Enter = switch to PhotoPage
    // Ctrl + Enter = open in external editor (handled with accelerators)
    // Shift + Ctrl + Enter = open in external RAW editor (handled with accelerators)
    protected override void on_item_activated(CheckerboardItem item, CheckerboardPage.Activator 
        activator, CheckerboardPage.KeyboardModifiers modifiers) {
        Thumbnail thumbnail = (Thumbnail) item;

        // none of the fancy Super, Ctrl, Shift, etc., keyboard accelerators apply to videos,
        // since they can't be RAW files or be opened in an external editor, etc., so if this is
        // a video, just play it and do a short-circuit return
        if (thumbnail.get_media_source() is Video) {
            on_play_video();
            return;
        }
        
        LibraryPhoto? photo = thumbnail.get_media_source() as LibraryPhoto;
        if (photo == null)
            return;
        
        // switch to full-page view or open in external editor
        debug("activating %s", photo.to_string());

        if (activator == CheckerboardPage.Activator.MOUSE) {
            if (modifiers.super_pressed)
                on_external_edit();
            else
                LibraryWindow.get_app().switch_to_photo_page(this, photo);
        } else if (activator == CheckerboardPage.Activator.KEYBOARD) {
            if (!modifiers.shift_pressed && !modifiers.ctrl_pressed)
                LibraryWindow.get_app().switch_to_photo_page(this, photo);
        }
    }
    
    protected override bool on_app_key_pressed(Gtk.EventControllerKey event, uint keyval, uint keycode, Gdk.ModifierType modifiers) {
        bool handled = true;
        string? format = null;
        switch (Gdk.keyval_name(keyval)) {
            case "Page_Up":
            case "KP_Page_Up":
            case "Page_Down":
            case "KP_Page_Down":
            case "Home":
            case "KP_Home":
            case "End":
            case "KP_End":
                event.forward(this);
            break;
            case "bracketright":
                activate_action("win.RotateClockwise", format);
            break;
            
            case "bracketleft":
                activate_action("win.RotateCounterclockwise", format);
            break;
            
            default:
                handled = false;
            break;
        }
        
        return handled ? true : base.on_app_key_pressed(event, keyval, keycode, modifiers);
    }

    protected override void on_export() {
        run_exporter.begin();
    }

    private async void run_exporter() {
        if (exporter != null)
            return;
        
        Gee.Collection<MediaSource> export_list =
            (Gee.Collection<MediaSource>) get_view().get_selected_sources();
        if (export_list.size == 0)
            return;

        bool has_some_photos = selection_has_photo();
        bool has_some_videos = selection_has_video();
        assert(has_some_photos || has_some_videos);
               
        // if we don't have any photos, then everything is a video, so skip displaying the Export
        // dialog and go right to the video export operation
        if (!has_some_photos) {
            exporter = yield Video.export_many((Gee.Collection<Video>) export_list);
            exporter.export_completed.connect_after(on_export_completed);
            exporter.export();
            return;
        }

        string title = null;
        if (has_some_videos)
            title = ngettext("Export Photo/Video", "Export Photos/Videos", export_list.size);
        else
            title = ngettext("Export Photo", "Export Photos", export_list.size);
        ExportDialog export_dialog = new ExportDialog(title);

        // Setting up the parameters object requires a bit of thinking about what the user wants.
        // If the selection contains only photos, then we do what we've done in previous versions
        // of Shotwell -- we use whatever settings the user selected on his last export operation
        // (the thinking here being that if you've been exporting small PNGs for your blog
        // for the last n export operations, then it's likely that for your (n + 1)-th export
        // operation you'll also be exporting a small PNG for your blog). However, if the selection
        // contains any videos, then we set the parameters to the "Current" operating mode, since
        // videos can't be saved as PNGs (or any other specific photo format).
        ExportFormatParameters? export_params = (has_some_videos) ? ExportFormatParameters.current() :
            ExportFormatParameters.last();

        export_params = yield export_dialog.execute(export_params);
        if (export_params == null) {
            return;
        }
        
        Scaling scaling = Scaling.for_constraint(export_params.constraint, export_params.scale, false);
        
        // handle the single-photo case, which is treated like a Save As file operation
        if (export_list.size == 1) {
            LibraryPhoto photo = null;
            foreach (LibraryPhoto p in (Gee.Collection<LibraryPhoto>) export_list) {
                photo = p;
                break;
            }
            
            File save_as = yield
                ExportUI.choose_file(photo.get_export_basename_for_parameters(export_params));
            if (save_as == null)
                return;
            
            try {
                AppWindow.get_instance().set_busy_cursor();
                photo.export(save_as, scaling, export_params.quality,
                    photo.get_export_format_for_parameters(export_params), export_params.mode ==
                    ExportFormatMode.UNMODIFIED, export_params.export_metadata);
                AppWindow.get_instance().set_normal_cursor();
            } catch (Error err) {
                AppWindow.get_instance().set_normal_cursor();
                var message = _("Unable to export the following photo due to a file error.\n\n") +
                            save_as.get_path();
                AppWindow.error_message(message);
            }
            
            return;
        }

        // multiple photos or videos
        File export_dir = yield ExportUI.choose_dir(title);
        if (export_dir == null)
            return;
        
        exporter = new ExporterUI(export_list, export_dir, scaling, export_params);
        exporter.export_completed.connect_after(on_export_completed);
        exporter.export();
    }
    
    private void on_export_completed() {
        exporter = null;
    }
    
    private bool can_revert_selected() {
        foreach (DataSource source in get_view().get_selected_sources()) {
            LibraryPhoto? photo = source as LibraryPhoto;
            if (photo != null && (photo.has_transformations() || photo.has_editable()))
                return true;
        }
        
        return false;
    }
    
    private bool can_revert_editable_selected() {
        foreach (DataSource source in get_view().get_selected_sources()) {
            LibraryPhoto? photo = source as LibraryPhoto;
            if (photo != null && photo.has_editable())
                return true;
        }
        
        return false;
    }
   
    private void on_rotate_clockwise() {
        if (get_view().get_selected_count() == 0)
            return;
        
        RotateMultipleCommand command = new RotateMultipleCommand(get_view().get_selected(), 
            Rotation.CLOCKWISE, Resources.ROTATE_CW_FULL_LABEL, Resources.ROTATE_CW_TOOLTIP,
            _("Rotating"), _("Undoing Rotate"));
        get_command_manager().execute(command);
    }

    private void on_publish() {
        if (get_view().get_selected_count() > 0)
            PublishingUI.PublishingDialog.go(
                (Gee.Collection<MediaSource>) get_view().get_selected_sources());
    }

    private void on_rotate_counterclockwise() {
        if (get_view().get_selected_count() == 0)
            return;
        
        RotateMultipleCommand command = new RotateMultipleCommand(get_view().get_selected(), 
            Rotation.COUNTERCLOCKWISE, Resources.ROTATE_CCW_FULL_LABEL, Resources.ROTATE_CCW_TOOLTIP,
            _("Rotating"), _("Undoing Rotate"));
        get_command_manager().execute(command);
    }
    
    private void on_flip_horizontally() {
        if (get_view().get_selected_count() == 0)
            return;
        
        RotateMultipleCommand command = new RotateMultipleCommand(get_view().get_selected(),
            Rotation.MIRROR, Resources.HFLIP_LABEL, "", _("Flipping Horizontally"),
            _("Undoing Flip Horizontally"));
        get_command_manager().execute(command);
    }
    
    private void on_flip_vertically() {
        if (get_view().get_selected_count() == 0)
            return;
        
        RotateMultipleCommand command = new RotateMultipleCommand(get_view().get_selected(),
            Rotation.UPSIDE_DOWN, Resources.VFLIP_LABEL, "", _("Flipping Vertically"),
            _("Undoing Flip Vertically"));
        get_command_manager().execute(command);
    }
    
    private void on_revert() {
        if (get_view().get_selected_count() == 0)
            return;
        
        if (can_revert_editable_selected()) {
            revert_editable_dialog.begin(AppWindow.get_instance(),
                (Gee.Collection<Photo>) get_view().get_selected_sources(), (source, res) => {
                    if (revert_editable_dialog.end(res)) {
                        foreach (DataObject object in get_view().get_selected_sources())
                            ((Photo) object).revert_to_master();
                        
                        RevertMultipleCommand command = new RevertMultipleCommand(get_view().get_selected());
                        get_command_manager().execute(command);
                    }
                });
        } else {
            RevertMultipleCommand command = new RevertMultipleCommand(get_view().get_selected());
            get_command_manager().execute(command);    
        }
    }
    
    public void on_copy_adjustments() {
        if (get_view().get_selected_count() != 1)
            return;
        Photo photo = (Photo) get_view().get_selected_at(0).get_source();
        PixelTransformationBundle.set_copied_color_adjustments(photo.get_color_adjustments());
        set_action_sensitive("PasteColorAdjustments", true);
    }
    
    public void on_paste_adjustments() {
        PixelTransformationBundle? copied_adjustments = PixelTransformationBundle.get_copied_color_adjustments();
        if (get_view().get_selected_count() == 0 || copied_adjustments == null)
            return;
        
        AdjustColorsMultipleCommand command = new AdjustColorsMultipleCommand(get_view().get_selected(),
            copied_adjustments, Resources.PASTE_ADJUSTMENTS_LABEL, Resources.PASTE_ADJUSTMENTS_TOOLTIP);
        get_command_manager().execute(command);
    }
    
    private void on_enhance() {
        if (get_view().get_selected_count() == 0)
            return;
        
        EnhanceMultipleCommand command = new EnhanceMultipleCommand(get_view().get_selected());
        get_command_manager().execute(command);
    }
    
    private void on_duplicate_photo() {
        if (get_view().get_selected_count() == 0)
            return;
        
        DuplicateMultiplePhotosCommand command = new DuplicateMultiplePhotosCommand(
            get_view().get_selected());
        get_command_manager().execute(command);
    }

    private void on_adjust_date_time() {
        if (get_view().get_selected_count() == 0)
            return;

        bool selected_has_videos = false;
        bool only_videos_selected = true;
        
        foreach (DataView dv in get_view().get_selected()) {
            if (dv.get_source() is Video)
                selected_has_videos = true;
            else
                only_videos_selected = false;
        }

        Dateable photo_source = (Dateable) get_view().get_selected_at(0).get_source();

        AdjustDateTimeDialog dialog = new AdjustDateTimeDialog(photo_source,
            get_view().get_selected_count(), true, selected_has_videos, only_videos_selected);

        dialog.execute.begin((source, res) => {
            int64 time_shift;
            bool keep_relativity, modify_originals;
            if (dialog.execute.end(res, out time_shift, out keep_relativity, out modify_originals)) {
                AdjustDateTimePhotosCommand command = new AdjustDateTimePhotosCommand(
                    get_view().get_selected(), time_shift, keep_relativity, modify_originals);
                get_command_manager().execute(command);
            }    
        });
    }
    
    private void on_external_edit() {
        if (get_view().get_selected_count() != 1)
            return;
        
        Photo photo = (Photo) get_view().get_selected_at(0).get_source();
        try {
            AppWindow.get_instance().set_busy_cursor();
            photo.open_with_external_editor();
            AppWindow.get_instance().set_normal_cursor();
        } catch (Error err) {
            AppWindow.get_instance().set_normal_cursor();
            open_external_editor_error_dialog(err, photo);
        }
    }
    
    private void on_external_edit_raw() {
        if (get_view().get_selected_count() != 1)
            return;
        
        Photo photo = (Photo) get_view().get_selected_at(0).get_source();
        if (photo.get_master_file_format() != PhotoFileFormat.RAW)
            return;

        try {
            AppWindow.get_instance().set_busy_cursor();
            photo.open_with_raw_external_editor();
            AppWindow.get_instance().set_normal_cursor();
        } catch (Error err) {
            AppWindow.get_instance().set_normal_cursor();
            AppWindow.error_message(Resources.launch_editor_failed(err));
        }
    }
    
    public void on_set_background() {
        Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
        MediaSourceCollection.filter_media((Gee.Collection<MediaSource>) get_view().get_selected_sources(),
            photos, null);
        
        if (photos.size == 1) {
            SetBackgroundPhotoDialog dialog = new SetBackgroundPhotoDialog();
            dialog.execute.begin((source, res) => {
                bool desktop, screensaver;
                if (dialog.execute.end(res, out desktop, out screensaver)) {
                    AppWindow.get_instance().set_busy_cursor();
                    DesktopIntegration.set_background(photos[0], desktop, screensaver);
                    AppWindow.get_instance().set_normal_cursor();
                }
            });
        } else if (photos.size > 1) {
            SetBackgroundSlideshowDialog dialog = new SetBackgroundSlideshowDialog();
            dialog.execute.begin((source, res) => {
                int delay;
                bool desktop, screensaver;
                if (dialog.execute.end(res, out delay, out desktop, out screensaver)) {
                    AppWindow.get_instance().set_busy_cursor();
                    DesktopIntegration.set_background_slideshow(photos, delay,
                        DESKTOP_SLIDESHOW_TRANSITION_SEC, desktop, screensaver);
                    AppWindow.get_instance().set_normal_cursor();
                }
            });
        }
    }
    
    private void on_slideshow() {
        if (get_view().get_count() == 0)
            return;
        
        // check selection for valid starting photo, otherwise start at beginning of collection
        if (get_view().get_selected_count() > 0) {
            Gee.List<DataSource>? sources = 
                get_view().get_selected_sources_of_type(typeof(LibraryPhoto));
            if (sources == null || sources.size == 0)
                return;
            
            Thumbnail? thumbnail = (Thumbnail?) get_view().get_view_for_source(sources[0]);
            if (thumbnail == null)
                return;
            
            LibraryPhoto? photo = thumbnail.get_media_source() as LibraryPhoto;
            if (photo == null)
                return;
            
            AppWindow.get_instance().go_fullscreen(new SlideshowPage(LibraryPhoto.global, 
                get_view(), photo));
        } else {
            AppWindow.get_instance().go_fullscreen(new SlideshowPage(LibraryPhoto.global,
                get_view()));
        }
    }
    
    protected override bool on_ctrl_pressed() {
        Gtk.Button? rotate_button = this.builder.get_object ("ToolRotate") as Gtk.Button;
        Gtk.Image? rotate_icon = this.builder.get_object ("ToolRotateIcon") as Gtk.Image;
        if (rotate_button != null) {
            rotate_button.set_action_name ("win.RotateCounterclockwise");
            rotate_icon.set_from_icon_name (Resources.COUNTERCLOCKWISE);
            rotate_button.set_tooltip_text (Resources.ROTATE_CCW_TOOLTIP);
        }

        return base.on_ctrl_pressed();
    }
    
    protected override bool on_ctrl_released() {
        Gtk.Button? rotate_button = this.builder.get_object ("ToolRotate") as Gtk.Button;
        Gtk.Image? rotate_icon = this.builder.get_object ("ToolRotateIcon") as Gtk.Image;
        if (rotate_button != null && rotate_icon != null) {
            rotate_button.set_action_name ("win.RotateClockwise");
            rotate_icon.set_from_icon_name (Resources.CLOCKWISE);
            rotate_button.set_tooltip_text (Resources.ROTATE_CW_TOOLTIP);
        }

        return base.on_ctrl_released();
    }
    
    public override SearchViewFilter get_search_view_filter() {
        return search_filter;
    }
}

