CollectionPage.vala 29.2 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
4
 * See the COPYING file in this distribution.
5
 */
Jim Nelson's avatar
Jim Nelson committed
6

7 8 9 10 11
public class CollectionViewManager : ViewManager {
    private CollectionPage page;
    
    public CollectionViewManager(CollectionPage page) {
        this.page = page;
12
    }
13 14
    
    public override DataView create_view(DataSource source) {
15
        return page.create_thumbnail(source);
16
    }
17 18
}

19
public abstract class CollectionPage : MediaPage {
20 21
    private const double DESKTOP_SLIDESHOW_TRANSITION_SEC = 2.0;
    
22 23 24
    protected class CollectionSearchViewFilter : DefaultSearchViewFilter {
        public override uint get_criteria() {
            return SearchFilterCriteria.TEXT | SearchFilterCriteria.FLAG | 
25
                SearchFilterCriteria.MEDIA | SearchFilterCriteria.RATING | SearchFilterCriteria.SAVEDSEARCH;
26 27 28
        }
    }
    
29
    private ExporterUI exporter = null;
30
    private CollectionSearchViewFilter search_filter = new CollectionSearchViewFilter();
31
    
32
    protected CollectionPage(string page_name) {
33
        base (page_name);
34
        
35
        get_view().items_altered.connect(on_photos_altered);
36
        
37 38
        init_item_context_menu("CollectionContextMenu");
        init_toolbar("CollectionToolbar");
39
        
40
        show_all();
41 42

        // watch for updates to the external app settings
Lucas Beeler's avatar
Lucas Beeler committed
43
        Config.Facade.get_instance().external_app_changed.connect(on_external_app_changed);
44
    }
45

46 47 48
    public override Gtk.Toolbar get_toolbar() {
        if (toolbar == null) {
            base.get_toolbar();
49

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
            // 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);
66 67 68 69 70 71

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

72 73 74 75 76
        }
        
        return toolbar;
    }
    
77
    private static InjectionGroup create_file_menu_injectables() {
78
        InjectionGroup group = new InjectionGroup("FileExtrasPlaceholder");
79
        
80
        group.add_menu_item(_("_Print"), "Print", "<Primary>p");
81
        group.add_separator();
82 83
        group.add_menu_item(_("_Publish"), "Publish", "<Primary><Shift>p");
        group.add_menu_item(_("Send _To…"), "SendTo");
84
        group.add_menu_item(_("Set as _Desktop Background"), "SetBackground", "<Primary>b");
85 86
        
        return group;
87
    }
88 89
    
    private static InjectionGroup create_edit_menu_injectables() {
90
        InjectionGroup group = new InjectionGroup("EditExtrasPlaceholder");
91
        
92
        group.add_menu_item(_("_Duplicate"), "Duplicate", "<Primary>D");
93

94
        return group;
95 96
    }

97
    private static InjectionGroup create_view_menu_fullscreen_injectables() {
98
        InjectionGroup group = new InjectionGroup("ViewExtrasFullscreenSlideshowPlaceholder");
99
        
100
        group.add_menu_item(_("Fullscreen"), "CommonFullscreen", "F11");
101
        group.add_separator();
102
        group.add_menu_item(_("S_lideshow"), "Slideshow", "F5");
103 104
        
        return group;
105 106
    }

107
    private static InjectionGroup create_photos_menu_edits_injectables() {
108 109 110 111 112 113 114 115 116 117
        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");
118
        group.add_separator();
119 120
        group.add_menu_item(_("_Enhance"), "Enhance");
        group.add_menu_item(_("Re_vert to Original"), "Revert");
121
        group.add_separator();
122 123 124 125 126 127
        group.add_menu_item(_("_Copy Color Adjustments"),
                            "CopyColorAdjustments",
                            "<Primary><Shift>c");
        group.add_menu_item(_("_Paste Color Adjustments"),
                            "PasteColorAdjustments",
                            "<Primary><Shift>v");
128 129
        
        return group;
130 131
    }
  
132
    private static InjectionGroup create_photos_menu_date_injectables() {
133
        InjectionGroup group = new InjectionGroup("PhotosExtrasDateTimePlaceholder");
134
        
135
        group.add_menu_item(_("Adjust Date and Time…"), "AdjustDateTime", "F4");
136 137
        
        return group;
138 139
    }

140
    private static InjectionGroup create_photos_menu_externals_injectables() {
141
        InjectionGroup group = new InjectionGroup("PhotosExtrasExternalsPlaceholder");
142
        
143 144 145 146 147 148 149
        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");
150 151
        
        return group;
152
    }
153 154 155 156 157 158
    
    protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
        base.init_collect_ui_filenames(ui_filenames);
        
        ui_filenames.add("collection.ui");
    }
159

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    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 }
    };

Jens Georg's avatar
Jens Georg committed
179 180
    protected override void add_actions (GLib.ActionMap map) {
        base.add_actions (map);
181

Jens Georg's avatar
Jens Georg committed
182
        map.add_action_entries (entries, this);
183
    }
184

Jens Georg's avatar
Jens Georg committed
185 186
    protected override void remove_actions(GLib.ActionMap map) {
        base.remove_actions(map);
187
        foreach (var entry in entries) {
Jens Georg's avatar
Jens Georg committed
188
            map.remove_action(entry.name);
189 190 191
        }
    }

192 193 194 195 196 197 198 199 200 201 202 203 204
    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;
    }
    
205
    private bool selection_has_video() {
206
        return MediaSourceCollection.has_video((Gee.Collection<MediaSource>) get_view().get_selected_sources());
207 208 209
    }
    
    private bool page_has_photo() {
210
        return MediaSourceCollection.has_photo((Gee.Collection<MediaSource>) get_view().get_sources());
211 212 213
    }
    
    private bool selection_has_photo() {
214
        return MediaSourceCollection.has_photo((Gee.Collection<MediaSource>) get_view().get_selected_sources());
215
    }
216 217 218 219
    
    protected override void init_actions(int selected_count, int count) {
        base.init_actions(selected_count, count);
        
Jens Georg's avatar
Jens Georg committed
220 221 222 223
        set_action_sensitive("RotateClockwise", true);
        set_action_sensitive("RotateCounterclockwise", true);
        set_action_sensitive("Enhance", true);
        set_action_sensitive("Publish", true);
224 225
    }
    
226
    protected override void update_actions(int selected_count, int count) {
227 228 229 230 231
        //FIXME: Hack. Otherwise it will disable actions that just have been enabled by photo page
        if (AppWindow.get_instance().get_current_page() != this) {
            return;
        }

232 233
        base.update_actions(selected_count, count);

234 235
        bool one_selected = selected_count == 1;
        bool has_selected = selected_count > 0;
236 237 238 239 240 241

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

242 243
        bool selection_has_videos = selection_has_video();
        bool page_has_photos = page_has_photo();
244
        
245 246
        // 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
247
        set_action_sensitive("Duplicate", has_selected && (!selection_has_videos));
248
        set_action_sensitive("ExternalEdit", 
Jens Georg's avatar
Jens Georg committed
249 250
            (!primary_is_video) && one_selected && !is_string_empty(Config.Facade.get_instance().get_external_photo_app()));
        set_action_sensitive("ExternalEditRAW",
251
            one_selected && (!primary_is_video)
252 253
            && ((Photo) get_view().get_selected_at(0).get_source()).get_master_file_format() == 
                PhotoFileFormat.RAW
Lucas Beeler's avatar
Lucas Beeler committed
254
            && !is_string_empty(Config.Facade.get_instance().get_external_raw_app()));
255 256
        set_action_sensitive("Revert", (!selection_has_videos) && can_revert_selected());
        set_action_sensitive("Enhance", (!selection_has_videos) && has_selected);
257 258 259 260
        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());
261 262 263 264
        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);
265 266 267 268 269
        
        // Allow changing of exposure time, even if there's a video in the current
        // selection.
        set_action_sensitive("AdjustDateTime", has_selected);
        
270 271 272
        set_action_sensitive("NewEvent", has_selected);
        set_action_sensitive("AddTags", has_selected);
        set_action_sensitive("ModifyTags", one_selected);
273
        set_action_sensitive("Slideshow", page_has_photos && (!primary_is_video));
274 275
        set_action_sensitive("Print", (!selection_has_videos) && has_selected);
        set_action_sensitive("Publish", has_selected);
276
        
277 278
        set_action_sensitive("SetBackground", (!selection_has_videos) && has_selected );
        if (has_selected) {
279
            debug ("Setting action label for SetBackground...");
280
            var label = one_selected
281 282
                    ? Resources.SET_BACKGROUND_MENU
                    : Resources.SET_BACKGROUND_SLIDESHOW_MENU;
283
            this.update_menu_item_label ("SetBackground", label);
284
        }
285 286
    }

287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
    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());
303
            set_action_sensitive("CopyColorAdjustments", photo.has_color_adjustments());
304 305 306
            
            break;
        }
307 308
    }
    
309
    private void on_print() {
310 311 312 313
        if (get_view().get_selected_count() > 0) {
            PrintManager.get_instance().spool_photo(
                (Gee.Collection<Photo>) get_view().get_selected_sources_of_type(typeof(Photo)));
        }
314
    }
315
    
316 317 318
    private void on_external_app_changed() {
        int selected_count = get_view().get_selected_count();
        
Lucas Beeler's avatar
Lucas Beeler committed
319
        set_action_sensitive("ExternalEdit", selected_count == 1 && Config.Facade.get_instance().get_external_photo_app() != "");
320 321
    }
    
322
    // see #2020
323
    // double click = switch to photo page
324 325
    // Super + double click = open in external editor
    // Enter = switch to PhotoPage
326 327
    // Ctrl + Enter = open in external editor (handled with accelerators)
    // Shift + Ctrl + Enter = open in external RAW editor (handled with accelerators)
328 329
    protected override void on_item_activated(CheckerboardItem item, CheckerboardPage.Activator 
        activator, CheckerboardPage.KeyboardModifiers modifiers) {
330 331
        Thumbnail thumbnail = (Thumbnail) item;

332 333 334
        // 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
Jim Nelson's avatar
Jim Nelson committed
335
        if (thumbnail.get_media_source() is Video) {
336 337 338
            on_play_video();
            return;
        }
Jim Nelson's avatar
Jim Nelson committed
339 340 341 342 343
        
        LibraryPhoto? photo = thumbnail.get_media_source() as LibraryPhoto;
        if (photo == null)
            return;
        
344
        // switch to full-page view or open in external editor
Jim Nelson's avatar
Jim Nelson committed
345
        debug("activating %s", photo.to_string());
346 347 348 349 350

        if (activator == CheckerboardPage.Activator.MOUSE) {
            if (modifiers.super_pressed)
                on_external_edit();
            else
Jim Nelson's avatar
Jim Nelson committed
351
                LibraryWindow.get_app().switch_to_photo_page(this, photo);
352
        } else if (activator == CheckerboardPage.Activator.KEYBOARD) {
353
            if (!modifiers.shift_pressed && !modifiers.ctrl_pressed)
Jim Nelson's avatar
Jim Nelson committed
354
                LibraryWindow.get_app().switch_to_photo_page(this, photo);
355
        }
356
    }
357
    
358 359 360
    protected override bool on_app_key_pressed(Gdk.EventKey event) {
        bool handled = true;
        switch (Gdk.keyval_name(event.keyval)) {
361
            case "Page_Up":
362
            case "KP_Page_Up":
363
            case "Page_Down":
364 365 366 367 368 369
            case "KP_Page_Down":
            case "Home":
            case "KP_Home":
            case "End":
            case "KP_End":
                key_press_event(event);
370
            break;
371
            
372
            case "bracketright":
373
                activate_action("RotateClockwise");
374
            break;
375
            
376
            case "bracketleft":
377
                activate_action("RotateCounterclockwise");
378
            break;
379
            
380 381 382 383 384 385 386
            default:
                handled = false;
            break;
        }
        
        return handled ? true : base.on_app_key_pressed(event);
    }
387

388
    protected override void on_export() {
389 390 391
        if (exporter != null)
            return;
        
392 393
        Gee.Collection<MediaSource> export_list =
            (Gee.Collection<MediaSource>) get_view().get_selected_sources();
394 395
        if (export_list.size == 0)
            return;
396

397 398 399 400 401 402 403 404 405 406 407
        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;
        }

Lucas Beeler's avatar
Lucas Beeler committed
408 409
        string title = null;
        if (has_some_videos)
410
            title = ngettext("Export Photo/Video", "Export Photos/Videos", export_list.size);
Lucas Beeler's avatar
Lucas Beeler committed
411
        else
Jens Georg's avatar
Jens Georg committed
412
            title = ngettext("Export Photo", "Export Photos", export_list.size);
413
        ExportDialog export_dialog = new ExportDialog(title);
414

415 416 417 418 419 420 421 422 423 424 425
        // 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();

426 427
        int scale;
        ScaleConstraint constraint;
428
        if (!export_dialog.execute(out scale, out constraint, ref export_params))
429
            return;
430 431 432 433
        
        Scaling scaling = Scaling.for_constraint(constraint, scale, false);
        
        // handle the single-photo case, which is treated like a Save As file operation
434
        if (export_list.size == 1) {
435
            LibraryPhoto photo = null;
436
            foreach (LibraryPhoto p in (Gee.Collection<LibraryPhoto>) export_list) {
437 438 439
                photo = p;
                break;
            }
440
            
441 442
            File save_as =
                ExportUI.choose_file(photo.get_export_basename_for_parameters(export_params));
443 444 445 446
            if (save_as == null)
                return;
            
            try {
447
                AppWindow.get_instance().set_busy_cursor();
448 449
                photo.export(save_as, scaling, export_params.quality,
                    photo.get_export_format_for_parameters(export_params), export_params.mode ==
Jonas Bushart's avatar
Jonas Bushart committed
450
                    ExportFormatMode.UNMODIFIED, export_params.export_metadata);
451
                AppWindow.get_instance().set_normal_cursor();
452
            } catch (Error err) {
453 454
                AppWindow.get_instance().set_normal_cursor();
                export_error_dialog(save_as, false);
455 456 457 458 459
            }
            
            return;
        }

460 461
        // multiple photos or videos
        File export_dir = ExportUI.choose_dir(title);
462 463 464
        if (export_dir == null)
            return;
        
465
        exporter = new ExporterUI(new Exporter(export_list, export_dir, scaling, export_params));
466
        exporter.export(on_export_completed);
467
    }
468 469 470 471 472
    
    private void on_export_completed() {
        exporter = null;
    }
    
473
    private bool can_revert_selected() {
Jim Nelson's avatar
Jim Nelson committed
474 475 476
        foreach (DataSource source in get_view().get_selected_sources()) {
            LibraryPhoto? photo = source as LibraryPhoto;
            if (photo != null && (photo.has_transformations() || photo.has_editable()))
477 478 479 480 481 482
                return true;
        }
        
        return false;
    }
    
483
    private bool can_revert_editable_selected() {
Jim Nelson's avatar
Jim Nelson committed
484 485 486
        foreach (DataSource source in get_view().get_selected_sources()) {
            LibraryPhoto? photo = source as LibraryPhoto;
            if (photo != null && photo.has_editable())
487 488 489 490 491
                return true;
        }
        
        return false;
    }
492
   
493
    private void on_rotate_clockwise() {
494 495 496 497 498
        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,
499
            _("Rotating"), _("Undoing Rotate"));
500
        get_command_manager().execute(command);
501
    }
502 503

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

509
    private void on_rotate_counterclockwise() {
510 511 512 513 514
        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,
515
            _("Rotating"), _("Undoing Rotate"));
516
        get_command_manager().execute(command);
517 518
    }
    
519
    private void on_flip_horizontally() {
520 521 522 523
        if (get_view().get_selected_count() == 0)
            return;
        
        RotateMultipleCommand command = new RotateMultipleCommand(get_view().get_selected(),
524
            Rotation.MIRROR, Resources.HFLIP_LABEL, "", _("Flipping Horizontally"),
525
            _("Undoing Flip Horizontally"));
526
        get_command_manager().execute(command);
527 528
    }
    
529
    private void on_flip_vertically() {
530 531 532 533
        if (get_view().get_selected_count() == 0)
            return;
        
        RotateMultipleCommand command = new RotateMultipleCommand(get_view().get_selected(),
534
            Rotation.UPSIDE_DOWN, Resources.VFLIP_LABEL, "", _("Flipping Vertically"),
535
            _("Undoing Flip Vertically"));
536 537 538
        get_command_manager().execute(command);
    }
    
539
    private void on_revert() {
540 541
        if (get_view().get_selected_count() == 0)
            return;
542
        
543 544 545 546 547 548 549 550
        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();
551 552
        }
        
553 554
        RevertMultipleCommand command = new RevertMultipleCommand(get_view().get_selected());
        get_command_manager().execute(command);
555 556
    }
    
557 558 559 560
    public void on_copy_adjustments() {
        if (get_view().get_selected_count() != 1)
            return;
        Photo photo = (Photo) get_view().get_selected_at(0).get_source();
561 562
        PixelTransformationBundle.set_copied_color_adjustments(photo.get_color_adjustments());
        set_action_sensitive("PasteColorAdjustments", true);
563 564 565 566 567 568 569 570 571 572 573 574
    }
    
    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);
    }
    
575 576 577
    private void on_enhance() {
        if (get_view().get_selected_count() == 0)
            return;
578 579 580
        
        EnhanceMultipleCommand command = new EnhanceMultipleCommand(get_view().get_selected());
        get_command_manager().execute(command);
581
    }
582 583 584 585 586 587 588 589 590
    
    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);
    }
591

592 593 594 595
    private void on_adjust_date_time() {
        if (get_view().get_selected_count() == 0)
            return;

596 597 598 599 600 601 602 603 604 605
        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;
        }

606
        Dateable photo_source = (Dateable) get_view().get_selected_at(0).get_source();
607 608

        AdjustDateTimeDialog dialog = new AdjustDateTimeDialog(photo_source,
609
            get_view().get_selected_count(), true, selected_has_videos, only_videos_selected);
610 611 612 613 614 615 616 617 618

        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);
        }
    }
619
    
620 621 622 623 624 625 626 627 628 629 630
    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();
631
            open_external_editor_error_dialog(err, photo);
632 633 634 635 636 637 638 639 640 641
        }
    }
    
    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;
642

643 644
        try {
            AppWindow.get_instance().set_busy_cursor();
645
            photo.open_with_raw_external_editor();
646 647 648 649 650 651 652
            AppWindow.get_instance().set_normal_cursor();
        } catch (Error err) {
            AppWindow.get_instance().set_normal_cursor();
            AppWindow.error_message(Resources.launch_editor_failed(err));
        }
    }
    
653
    public void on_set_background() {
654 655 656
        Gee.ArrayList<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
        MediaSourceCollection.filter_media((Gee.Collection<MediaSource>) get_view().get_selected_sources(),
            photos, null);
657
        
658
        bool desktop, screensaver;
659
        if (photos.size == 1) {
660 661 662 663 664 665
            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();
            }
666 667 668
        } else if (photos.size > 1) {
            SetBackgroundSlideshowDialog dialog = new SetBackgroundSlideshowDialog();
            int delay;
669
            if (dialog.execute(out delay, out desktop, out screensaver)) {
670 671
                AppWindow.get_instance().set_busy_cursor();
                DesktopIntegration.set_background_slideshow(photos, delay,
672
                    DESKTOP_SLIDESHOW_TRANSITION_SEC, desktop, screensaver);
673 674 675
                AppWindow.get_instance().set_normal_cursor();
            }
        }
676
    }
677
    
678
    private void on_slideshow() {
679
        if (get_view().get_count() == 0)
680
            return;
681
        
682 683 684 685 686 687 688 689
        // 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]);
690 691
        if (thumbnail == null)
            return;
692
        
Jim Nelson's avatar
Jim Nelson committed
693 694 695 696
        LibraryPhoto? photo = thumbnail.get_media_source() as LibraryPhoto;
        if (photo == null)
            return;
        
697
        AppWindow.get_instance().go_fullscreen(new SlideshowPage(LibraryPhoto.global, get_view(),
Jim Nelson's avatar
Jim Nelson committed
698
            photo));
699
    }
700
    
701
    protected override bool on_ctrl_pressed(Gdk.EventKey? event) {
702 703 704
        Gtk.ToolButton? rotate_button = this.builder.get_object ("ToolRotate") as Gtk.ToolButton;
        if (rotate_button != null) {
            rotate_button.set_action_name ("win.RotateCounterclockwise");
705
            rotate_button.set_icon_name (Resources.COUNTERCLOCKWISE);
706 707 708
            rotate_button.set_tooltip_text (Resources.ROTATE_CCW_TOOLTIP);
        }

709
        return base.on_ctrl_pressed(event);
710 711
    }
    
712
    protected override bool on_ctrl_released(Gdk.EventKey? event) {
713 714 715
        Gtk.ToolButton? rotate_button = this.builder.get_object ("ToolRotate") as Gtk.ToolButton;
        if (rotate_button != null) {
            rotate_button.set_action_name ("win.RotateClockwise");
716
            rotate_button.set_icon_name (Resources.CLOCKWISE);
717 718 719
            rotate_button.set_tooltip_text (Resources.ROTATE_CW_TOOLTIP);
        }

720
        return base.on_ctrl_released(event);
721
    }
722 723 724 725
    
    public override SearchViewFilter get_search_view_filter() {
        return search_filter;
    }
Jim Nelson's avatar
Jim Nelson committed
726 727
}