/* 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"); 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); Gtk.ToolButton? rotate_button = this.builder.get_object ("ToolRotate") as Gtk.ToolButton; 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); } return toolbar; } private static InjectionGroup create_file_menu_injectables() { InjectionGroup group = new InjectionGroup("FileExtrasPlaceholder"); group.add_menu_item(_("_Print"), "Print", "p"); group.add_separator(); group.add_menu_item(_("_Publish"), "Publish", "p"); group.add_menu_item(_("Send _To…"), "SendTo"); group.add_menu_item(_("Set as _Desktop Background"), "SetBackground", "b"); return group; } private static InjectionGroup create_edit_menu_injectables() { InjectionGroup group = new InjectionGroup("EditExtrasPlaceholder"); group.add_menu_item(_("_Duplicate"), "Duplicate", "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", "r"); group.add_menu_item(_("Rotate _Left"), "RotateCounterclockwise", "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", "c"); group.add_menu_item(_("_Paste Color Adjustments"), "PasteColorAdjustments", "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", "Return"); group.add_menu_item(_("Open With RA_W Editor"), "ExternalEditRAW", "Return"); group.add_menu_item(_("_Play"), "PlayVideo", "Y"); 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"); } 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); foreach (var entry in entries) { map.remove_action(entry.name); } } 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_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 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 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(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