/* Copyright 2009-2015 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 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", Resources.PRINT_LABEL, TRANSLATABLE, "P", TRANSLATABLE, on_print }; print.label = Resources.PRINT_MENU; actions += print; Gtk.ActionEntry publish = { "Publish", Resources.PUBLISH, TRANSLATABLE, "P", TRANSLATABLE, on_publish }; publish.label = Resources.PUBLISH_MENU; publish.tooltip = Resources.PUBLISH_TOOLTIP; actions += publish; Gtk.ActionEntry rotate_right = { "RotateClockwise", Resources.CLOCKWISE, TRANSLATABLE, "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, "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, "E", TRANSLATABLE, on_enhance }; enhance.label = Resources.ENHANCE_MENU; enhance.tooltip = Resources.ENHANCE_TOOLTIP; actions += enhance; Gtk.ActionEntry copy_adjustments = { "CopyColorAdjustments", null, TRANSLATABLE, "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, "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", null, TRANSLATABLE, null, TRANSLATABLE, on_revert }; revert.label = Resources.REVERT_MENU; actions += revert; Gtk.ActionEntry set_background = { "SetBackground", null, TRANSLATABLE, "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, "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", Resources.EDIT_LABEL, TRANSLATABLE, "Return", TRANSLATABLE, on_external_edit }; external_edit.label = Resources.EXTERNAL_EDIT_MENU; actions += external_edit; Gtk.ActionEntry edit_raw = { "ExternalEditRAW", null, TRANSLATABLE, "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) get_view().get_selected_sources()); } private bool page_has_photo() { return MediaSourceCollection.has_photo((Gee.Collection) get_view().get_sources()); } private bool selection_has_photo() { return MediaSourceCollection.has_photo((Gee.Collection) 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 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) 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 export_list = (Gee.Collection) 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