/* Copyright 2009-2014 Yorba Foundation
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution.
 */

public class 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;
        }
    }
    
    private ExporterUI exporter = null;
    private CollectionSearchViewFilter search_filter = new CollectionSearchViewFilter();
    
    public CollectionPage(string page_name) {
        base (page_name);
        
        get_view().items_altered.connect(on_photos_altered);
        
        init_item_context_menu("/CollectionContextMenu");
        init_toolbar("/CollectionToolbar");
        
        show_all();

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

    public override Gtk.Toolbar get_toolbar() {
        if (toolbar == null) {
            base.get_toolbar();
            
            // separator to force slider to right side of toolbar
            Gtk.SeparatorToolItem separator = new Gtk.SeparatorToolItem();
            separator.set_expand(true);
            separator.set_draw(false);
            get_toolbar().insert(separator, -1);

            Gtk.SeparatorToolItem drawn_separator = new Gtk.SeparatorToolItem();
            drawn_separator.set_expand(false);
            drawn_separator.set_draw(true);
            
            get_toolbar().insert(drawn_separator, -1);
            
            // zoom slider assembly
            MediaPage.ZoomSliderAssembly zoom_slider_assembly = create_zoom_slider_assembly();
            connect_slider(zoom_slider_assembly);
            get_toolbar().insert(zoom_slider_assembly, -1);
        }
        
        return toolbar;
    }
    
    private static InjectionGroup create_file_menu_injectables() {
        InjectionGroup group = new InjectionGroup("/MenuBar/FileMenu/FileExtrasPlaceholder");
        
        group.add_menu_item("Print");
        group.add_separator();
        group.add_menu_item("Publish");
        group.add_menu_item("SendTo");
        group.add_menu_item("SetBackground");
        
        return group;
    }
    
    private static InjectionGroup create_edit_menu_injectables() {
        InjectionGroup group = new InjectionGroup("/MenuBar/EditMenu/EditExtrasPlaceholder");
        
        group.add_menu_item("Duplicate");

        return group;
    }

    private static InjectionGroup create_view_menu_fullscreen_injectables() {
        InjectionGroup group = new InjectionGroup("/MenuBar/ViewMenu/ViewExtrasFullscreenSlideshowPlaceholder");
        
        group.add_menu_item("Fullscreen", "CommonFullscreen");
        group.add_separator();
        group.add_menu_item("Slideshow");
        
        return group;
    }

    private static InjectionGroup create_photos_menu_edits_injectables() {
        InjectionGroup group = new InjectionGroup("/MenuBar/PhotosMenu/PhotosExtrasEditsPlaceholder");
        
        group.add_menu_item("RotateClockwise");
        group.add_menu_item("RotateCounterclockwise");
        group.add_menu_item("FlipHorizontally");
        group.add_menu_item("FlipVertically");
        group.add_separator();
        group.add_menu_item("Enhance");
        group.add_menu_item("Revert");
        group.add_separator();
        group.add_menu_item("CopyColorAdjustments");
        group.add_menu_item("PasteColorAdjustments");
        
        return group;
    }
  
    private static InjectionGroup create_photos_menu_date_injectables() {
        InjectionGroup group = new InjectionGroup("/MenuBar/PhotosMenu/PhotosExtrasDateTimePlaceholder");
        
        group.add_menu_item("AdjustDateTime");
        
        return group;
    }

    private static InjectionGroup create_photos_menu_externals_injectables() {
        InjectionGroup group = new InjectionGroup("/MenuBar/PhotosMenu/PhotosExtrasExternalsPlaceholder");
        
        group.add_menu_item("ExternalEdit");
        group.add_menu_item("ExternalEditRAW");
        group.add_menu_item("PlayVideo");
        
        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");
    }
    
    protected override Gtk.ActionEntry[] init_collect_action_entries() {
        Gtk.ActionEntry[] actions = base.init_collect_action_entries();

        Gtk.ActionEntry print = { "Print", Gtk.Stock.PRINT, TRANSLATABLE, "<Ctrl>P",
            TRANSLATABLE, on_print };
        print.label = Resources.PRINT_MENU;
        actions += print;
        
        Gtk.ActionEntry publish = { "Publish", Resources.PUBLISH, TRANSLATABLE, "<Ctrl><Shift>P",
            TRANSLATABLE, on_publish };
        publish.label = Resources.PUBLISH_MENU;
        publish.tooltip = Resources.PUBLISH_TOOLTIP;
        actions += publish;
  
        Gtk.ActionEntry rotate_right = { "RotateClockwise", Resources.CLOCKWISE,
            TRANSLATABLE, "<Ctrl>R", TRANSLATABLE, on_rotate_clockwise };
        rotate_right.label = Resources.ROTATE_CW_MENU;
        rotate_right.tooltip = Resources.ROTATE_CW_TOOLTIP;
        actions += rotate_right;

        Gtk.ActionEntry rotate_left = { "RotateCounterclockwise", Resources.COUNTERCLOCKWISE,
            TRANSLATABLE, "<Ctrl><Shift>R", TRANSLATABLE, on_rotate_counterclockwise };
        rotate_left.label = Resources.ROTATE_CCW_MENU;
        rotate_left.tooltip = Resources.ROTATE_CCW_TOOLTIP;
        actions += rotate_left;

        Gtk.ActionEntry hflip = { "FlipHorizontally", Resources.HFLIP, TRANSLATABLE, null,
            TRANSLATABLE, on_flip_horizontally };
        hflip.label = Resources.HFLIP_MENU;
        actions += hflip;
        
        Gtk.ActionEntry vflip = { "FlipVertically", Resources.VFLIP, TRANSLATABLE, null,
            TRANSLATABLE, on_flip_vertically };
        vflip.label = Resources.VFLIP_MENU;
        actions += vflip;

        Gtk.ActionEntry enhance = { "Enhance", Resources.ENHANCE, TRANSLATABLE, "<Ctrl>E",
            TRANSLATABLE, on_enhance };
        enhance.label = Resources.ENHANCE_MENU;
        enhance.tooltip = Resources.ENHANCE_TOOLTIP;
        actions += enhance;

        Gtk.ActionEntry copy_adjustments = { "CopyColorAdjustments", null, TRANSLATABLE,
            "<Ctrl><Shift>C", TRANSLATABLE, on_copy_adjustments};
        copy_adjustments.label = Resources.COPY_ADJUSTMENTS_MENU;
        copy_adjustments.tooltip = Resources.COPY_ADJUSTMENTS_TOOLTIP;
        actions += copy_adjustments;

        Gtk.ActionEntry paste_adjustments = { "PasteColorAdjustments", null, TRANSLATABLE,
            "<Ctrl><Shift>V", TRANSLATABLE, on_paste_adjustments};
        paste_adjustments.label = Resources.PASTE_ADJUSTMENTS_MENU;
        paste_adjustments.tooltip = Resources.PASTE_ADJUSTMENTS_TOOLTIP;
        actions += paste_adjustments;

        Gtk.ActionEntry revert = { "Revert", Gtk.Stock.REVERT_TO_SAVED, TRANSLATABLE, null,
            TRANSLATABLE, on_revert };
        revert.label = Resources.REVERT_MENU;
        actions += revert;
        
        Gtk.ActionEntry set_background = { "SetBackground", null, TRANSLATABLE, "<Ctrl>B",
            TRANSLATABLE, on_set_background };
        set_background.label = Resources.SET_BACKGROUND_MENU;
        set_background.tooltip = Resources.SET_BACKGROUND_TOOLTIP;
        actions += set_background;

        Gtk.ActionEntry duplicate = { "Duplicate", null, TRANSLATABLE, "<Ctrl>D", TRANSLATABLE,
            on_duplicate_photo };
        duplicate.label = Resources.DUPLICATE_PHOTO_MENU;
        duplicate.tooltip = Resources.DUPLICATE_PHOTO_TOOLTIP;
        actions += duplicate;

        Gtk.ActionEntry adjust_date_time = { "AdjustDateTime", null, TRANSLATABLE, null,
            TRANSLATABLE, on_adjust_date_time };
        adjust_date_time.label = Resources.ADJUST_DATE_TIME_MENU;
        actions += adjust_date_time;
        
        Gtk.ActionEntry external_edit = { "ExternalEdit", Gtk.Stock.EDIT, TRANSLATABLE, "<Ctrl>Return",
            TRANSLATABLE, on_external_edit };
        external_edit.label = Resources.EXTERNAL_EDIT_MENU;
        actions += external_edit;
        
        Gtk.ActionEntry edit_raw = { "ExternalEditRAW", null, TRANSLATABLE, "<Ctrl><Shift>Return", 
            TRANSLATABLE, on_external_edit_raw };
        edit_raw.label = Resources.EXTERNAL_EDIT_RAW_MENU;
        actions += edit_raw;
        
        Gtk.ActionEntry slideshow = { "Slideshow", null, TRANSLATABLE, "F5", TRANSLATABLE,
            on_slideshow };
        slideshow.label = _("S_lideshow");
        slideshow.tooltip = _("Play a slideshow");
        actions += slideshow;
        
        return actions;
    }
    
    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_short_label("RotateClockwise", Resources.ROTATE_CW_LABEL);
        set_action_short_label("RotateCounterclockwise", Resources.ROTATE_CCW_LABEL);
        set_action_short_label("Publish", Resources.PUBLISH_LABEL);
        
        set_action_important("RotateClockwise", true);
        set_action_important("RotateCounterclockwise", true);
        set_action_important("Enhance", true);
        set_action_important("Publish", true);
    }
    
    protected override void update_actions(int selected_count, int count) {
        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 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_visible("ExternalEdit", (!primary_is_video));
        set_action_sensitive("ExternalEdit", 
            one_selected && !is_string_empty(Config.Facade.get_instance().get_external_photo_app()));
        set_action_visible("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);
        set_action_sensitive("Slideshow", page_has_photos && (!primary_is_video));
        set_action_sensitive("Print", (!selection_has_videos) && has_selected);
        set_action_sensitive("Publish", has_selected);
        
        set_action_sensitive("SetBackground", (!selection_has_videos) && has_selected );
        if (has_selected) {
            Gtk.Action? set_background = get_action("SetBackground");
            if (set_background != null) {
                set_background.label = one_selected
                    ? Resources.SET_BACKGROUND_MENU
                    : Resources.SET_BACKGROUND_SLIDESHOW_MENU;
            }
        }
    }

    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(
                (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 clcik = 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(Gdk.EventKey event) {
        bool handled = true;
        switch (Gdk.keyval_name(event.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":
                key_press_event(event);
            break;
            
            case "bracketright":
                activate_action("RotateClockwise");
            break;
            
            case "bracketleft":
                activate_action("RotateCounterclockwise");
            break;
            
            default:
                handled = false;
            break;
        }
        
        return handled ? true : base.on_app_key_pressed(event);
    }

    protected override void on_export() {
        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 = Video.export_many((Gee.Collection<Video>) export_list, on_export_completed);
            return;
        }

        string title = null;
        if (has_some_videos)
            title = (export_list.size == 1) ? _("Export Photo/Video") : _("Export Photos/Videos");
        else
            title = (export_list.size == 1) ?  _("Export Photo") : _("Export Photos");
        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();

        int scale;
        ScaleConstraint constraint;
        if (!export_dialog.execute(out scale, out constraint, ref export_params))
            return;
        
        Scaling scaling = Scaling.for_constraint(constraint, 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 =
                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();
                export_error_dialog(save_as, false);
            }
            
            return;
        }

        // multiple photos or videos
        File export_dir = ExportUI.choose_dir(title);
        if (export_dir == null)
            return;
        
        exporter = new ExporterUI(new Exporter(export_list, export_dir, scaling, export_params));
        exporter.export(on_export_completed);
    }
    
    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()) {
            if (!revert_editable_dialog(AppWindow.get_instance(),
                (Gee.Collection<Photo>) get_view().get_selected_sources())) {
                return;
            }
            
            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);
    }
    
    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);

        int64 time_shift;
        bool keep_relativity, modify_originals;
        if (dialog.execute(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);
        
        bool desktop, screensaver;
        if (photos.size == 1) {
            SetBackgroundPhotoDialog dialog = new SetBackgroundPhotoDialog();
            if (dialog.execute(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();
            int delay;
            if (dialog.execute(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;
        
        // use first selected photo, else use first photo
        Gee.List<DataSource>? sources = (get_view().get_selected_count() > 0)
            ? get_view().get_selected_sources_of_type(typeof(LibraryPhoto))
            : get_view().get_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));
    }
    
    protected override bool on_ctrl_pressed(Gdk.EventKey? event) {
        Gtk.ToolButton? rotate_button = ui.get_widget("/CollectionToolbar/ToolRotate")
            as Gtk.ToolButton;
        if (rotate_button != null)
            rotate_button.set_related_action(get_action("RotateCounterclockwise"));
        
        return base.on_ctrl_pressed(event);
    }
    
    protected override bool on_ctrl_released(Gdk.EventKey? event) {
        Gtk.ToolButton? rotate_button = ui.get_widget("/CollectionToolbar/ToolRotate")
            as Gtk.ToolButton;
        if (rotate_button != null)
            rotate_button.set_related_action(get_action("RotateClockwise"));
        
        return base.on_ctrl_released(event);
    }
    
    public override SearchViewFilter get_search_view_filter() {
        return search_filter;
    }
}