MediaPage.vala 34.7 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 6
 */

7
public class MediaSourceItem : CheckerboardItem {
8
    private string? natural_collation_key = null;
9 10 11

    // preserve the same constructor arguments and semantics as CheckerboardItem so that we're
    // a drop-in replacement
12 13 14
    public MediaSourceItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, 
        string? comment, bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
        base(source, initial_pixbuf_dim, title, comment, marked_up, alignment);
15 16
    }

17 18 19 20 21 22 23 24 25 26 27 28
    public new void set_title(string text, bool marked_up = false,
        Pango.Alignment alignment = Pango.Alignment.LEFT) {
        base.set_title(text, marked_up, alignment);
        this.natural_collation_key = null;
    }
    
    public string get_natural_collation_key() {
        if (this.natural_collation_key == null) {
            this.natural_collation_key = NaturalCollate.collate_key(this.get_title());
        }
        return this.natural_collation_key;
    }
29 30
}

31 32 33 34 35 36 37 38 39
public abstract class MediaPage : CheckerboardPage {
    public const int SORT_ORDER_ASCENDING = 0;
    public const int SORT_ORDER_DESCENDING = 1;

    // steppings should divide evenly into (Thumbnail.MAX_SCALE - Thumbnail.MIN_SCALE)
    public const int MANUAL_STEPPING = 16;
    public const int SLIDER_STEPPING = 4;

    public enum SortBy {
40
        MIN = 1,
41 42
        TITLE = 1,
        EXPOSURE_DATE = 2,
43
        RATING = 3,
44 45
        FILENAME = 4,
        MAX = 4
46 47 48
    }

    protected class ZoomSliderAssembly : Gtk.ToolItem {
49
        private Gtk.Scale slider;
50
        private Gtk.Adjustment adjustment;
51 52 53 54
        
        public signal void zoom_changed();

        public ZoomSliderAssembly() {
55
            Gtk.Box zoom_group = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
56

57
            Gtk.Image zoom_out = new Gtk.Image.from_icon_name("image-zoom-out-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
58 59 60 61 62 63 64 65
            Gtk.EventBox zoom_out_box = new Gtk.EventBox();
            zoom_out_box.set_above_child(true);
            zoom_out_box.set_visible_window(false);
            zoom_out_box.add(zoom_out);
            zoom_out_box.button_press_event.connect(on_zoom_out_pressed);
            
            zoom_group.pack_start(zoom_out_box, false, false, 0);

66 67 68 69 70
            // virgin ZoomSliderAssemblies are created such that they have whatever value is
            // persisted in the configuration system for the photo thumbnail scale
            int persisted_scale = Config.Facade.get_instance().get_photo_thumbnail_scale();
            adjustment = new Gtk.Adjustment(ZoomSliderAssembly.scale_to_slider(persisted_scale), 0,
                ZoomSliderAssembly.scale_to_slider(Thumbnail.MAX_SCALE), 1, 10, 0);
71

72
            slider = new Gtk.Scale(Gtk.Orientation.HORIZONTAL, adjustment);
73 74 75 76 77 78 79
            slider.value_changed.connect(on_slider_changed);
            slider.set_draw_value(false);
            slider.set_size_request(200, -1);
            slider.set_tooltip_text(_("Adjust the size of the thumbnails"));

            zoom_group.pack_start(slider, false, false, 0);

80
            Gtk.Image zoom_in = new Gtk.Image.from_icon_name("image-zoom-in-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
81 82 83 84 85 86 87 88
            Gtk.EventBox zoom_in_box = new Gtk.EventBox();
            zoom_in_box.set_above_child(true);
            zoom_in_box.set_visible_window(false);
            zoom_in_box.add(zoom_in);
            zoom_in_box.button_press_event.connect(on_zoom_in_pressed);

            zoom_group.pack_start(zoom_in_box, false, false, 0);

89
            add(zoom_group);
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
        }
        
        public static double scale_to_slider(int value) {
            assert(value >= Thumbnail.MIN_SCALE);
            assert(value <= Thumbnail.MAX_SCALE);
            
            return (double) ((value - Thumbnail.MIN_SCALE) / SLIDER_STEPPING);
        }

        public static int slider_to_scale(double value) {
            int res = ((int) (value * SLIDER_STEPPING)) + Thumbnail.MIN_SCALE;

            assert(res >= Thumbnail.MIN_SCALE);
            assert(res <= Thumbnail.MAX_SCALE);
            
            return res;
        }

        private bool on_zoom_out_pressed(Gdk.EventButton event) {
            snap_to_min();
            return true;
        }
        
        private bool on_zoom_in_pressed(Gdk.EventButton event) {
            snap_to_max();
            return true;
        }
        
        private void on_slider_changed() {
            zoom_changed();
        }
        
        public void snap_to_min() {
            slider.set_value(scale_to_slider(Thumbnail.MIN_SCALE));
        }

        public void snap_to_max() {
            slider.set_value(scale_to_slider(Thumbnail.MAX_SCALE));
        }
        
        public void increase_step() {
            int new_scale = compute_zoom_scale_increase(get_scale());

            if (get_scale() == new_scale)
                return;

            slider.set_value(scale_to_slider(new_scale));
        }
        
        public void decrease_step() {
            int new_scale = compute_zoom_scale_decrease(get_scale());

            if (get_scale() == new_scale)
                return;
            
            slider.set_value(scale_to_slider(new_scale));
        }
        
        public int get_scale() {
            return slider_to_scale(slider.get_value());
        }
151 152 153 154 155 156 157
        
        public void set_scale(int scale) {
            if (get_scale() == scale)
                return;

            slider.set_value(scale_to_slider(scale));
        }
158 159 160
    }
    
    private ZoomSliderAssembly? connected_slider = null;
161
    private DragAndDropHandler dnd_handler = null;
162 163
    private MediaViewTracker tracker;
    
164
    public MediaPage(string page_name) {
165
        base (page_name);
166 167 168
        
        tracker = new MediaViewTracker(get_view());
        
Jim Nelson's avatar
Jim Nelson committed
169
        get_view().items_altered.connect(on_media_altered);
170 171 172

        get_view().freeze_notifications();
        get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, 
Lucas Beeler's avatar
Lucas Beeler committed
173
            Config.Facade.get_instance().get_display_photo_titles());
174 175
        get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, 
            Config.Facade.get_instance().get_display_photo_comments());
176
        get_view().set_property(Thumbnail.PROP_SHOW_TAGS, 
Lucas Beeler's avatar
Lucas Beeler committed
177
            Config.Facade.get_instance().get_display_photo_tags());
178 179
        get_view().set_property(Thumbnail.PROP_SIZE, get_thumb_size());
        get_view().set_property(Thumbnail.PROP_SHOW_RATINGS,
Lucas Beeler's avatar
Lucas Beeler committed
180
            Config.Facade.get_instance().get_display_photo_ratings());
181
        get_view().thaw_notifications();
182 183 184

        // enable drag-and-drop export of media
        dnd_handler = new DragAndDropHandler(this);
185 186 187 188 189 190 191 192 193 194 195
    }
   
    private static int compute_zoom_scale_increase(int current_scale) {
        int new_scale = current_scale + MANUAL_STEPPING;
        return new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
    }
    
    private static int compute_zoom_scale_decrease(int current_scale) {
        int new_scale = current_scale - MANUAL_STEPPING;
        return new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
    }
196 197 198 199 200 201
    
    protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
        base.init_collect_ui_filenames(ui_filenames);
        
        ui_filenames.add("media.ui");
    }
202

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
    private const GLib.ActionEntry[] entries = {
        { "Export", on_export },
        { "SendTo", on_send_to },
        { "SendToContextMenu", on_send_to },
        { "RemoveFromLibrary", on_remove_from_library },
        { "MoveToTrash", on_move_to_trash },
        { "NewEvent", on_new_event },
        { "AddTags", on_add_tags },
        { "ModifyTags", on_modify_tags },
        { "IncreaseSize", on_increase_size },
        { "DecreaseSize", on_decrease_size },
        { "Flag", on_flag_unflag },
        { "IncreaseRating", on_increase_rating },
        { "DecreaseRating", on_decrease_rating },
        { "RateRejected", on_rate_rejected },
        { "RateUnrated", on_rate_unrated },
        { "RateOne", on_rate_one },
        { "RateTwo", on_rate_two },
        { "RateThree", on_rate_three },
        { "RateFour", on_rate_four },
        { "RateFive", on_rate_five },
        { "EditTitle", on_edit_title },
        { "EditComment", on_edit_comment },
        { "PlayVideo", on_play_video },

        // Toggle actions
        { "ViewTitle", on_action_toggle, null, "false", on_display_titles },
        { "ViewComment", on_action_toggle, null, "false", on_display_comments },
        { "ViewRatings", on_action_toggle, null, "false", on_display_ratings },
        { "ViewTags", on_action_toggle, null, "false", on_display_tags },

        // Radio actions
        { "SortBy", on_action_radio, "s", "'1'", on_sort_changed },
        { "Sort", on_action_radio, "s", "'ascending'", on_sort_changed },
    };

Jens Georg's avatar
Jens Georg committed
239 240
    protected override void add_actions (GLib.ActionMap map) {
        base.add_actions (map);
241

242 243 244
        bool sort_order;
        int sort_by;
        get_config_photos_sort(out sort_order, out sort_by);
245

Jens Georg's avatar
Jens Georg committed
246
        map.add_action_entries (entries, this);
247 248 249 250 251
        get_action ("ViewTitle").change_state (Config.Facade.get_instance ().get_display_photo_titles ());
        get_action ("ViewComment").change_state (Config.Facade.get_instance ().get_display_photo_comments ());
        get_action ("ViewRatings").change_state (Config.Facade.get_instance ().get_display_photo_ratings ());
        get_action ("ViewTags").change_state (Config.Facade.get_instance ().get_display_photo_tags ());
        get_action ("SortBy").change_state ("%d".printf (sort_by));
252
        get_action ("Sort").change_state (sort_order ? "ascending" : "descending");
253

254
        var d = Config.Facade.get_instance().get_default_raw_developer();
255 256 257 258
        var action = new GLib.SimpleAction.stateful("RawDeveloper",
                GLib.VariantType.STRING, d == RawDeveloper.SHOTWELL ? "Shotwell" : "Camera");
        action.change_state.connect(on_raw_developer_changed);
        action.set_enabled(true);
Jens Georg's avatar
Jens Georg committed
259
        map.add_action(action);
260
    }
261

Jens Georg's avatar
Jens Georg committed
262 263
    protected override void remove_actions(GLib.ActionMap map) {
        base.remove_actions(map);
264
        foreach (var entry in entries) {
Jens Georg's avatar
Jens Georg committed
265
            map.remove_action(entry.name);
266 267 268
        }
    }

269 270
    protected override void update_actions(int selected_count, int count) {
        set_action_sensitive("Export", selected_count > 0);
271
        set_action_sensitive("EditTitle", selected_count > 0);
272
        set_action_sensitive("EditComment", selected_count > 0);
273 274
        set_action_sensitive("IncreaseSize", get_thumb_size() < Thumbnail.MAX_SCALE);
        set_action_sensitive("DecreaseSize", get_thumb_size() > Thumbnail.MIN_SCALE);
275
        set_action_sensitive("RemoveFromLibrary", selected_count > 0);
276
        set_action_sensitive("MoveToTrash", selected_count > 0);
277 278 279 280
        
        if (DesktopIntegration.is_send_to_installed())
            set_action_sensitive("SendTo", selected_count > 0);
        else
Jens Georg's avatar
Jens Georg committed
281
            set_action_sensitive("SendTo", false);
282
        
283 284 285
        set_action_sensitive("Rate", selected_count > 0);
        update_rating_sensitivities();
        
286 287
        update_development_menu_item_sensitivity();
        
288 289 290
        set_action_sensitive("PlayVideo", selected_count == 1
            && get_view().get_selected_source_at(0) is Video);
        
Jim Nelson's avatar
Jim Nelson committed
291 292
        update_flag_action(selected_count);
        
293 294 295
        base.update_actions(selected_count, count);
    }
    
296 297 298 299 300 301 302 303
    private void on_media_altered(Gee.Map<DataObject, Alteration> altered) {
        foreach (DataObject object in altered.keys) {
            if (altered.get(object).has_detail("metadata", "flagged")) {
                update_flag_action(get_view().get_selected_count());
                
                break;
            }
        }
Jim Nelson's avatar
Jim Nelson committed
304 305
    }
    
306 307 308 309 310 311 312 313 314 315 316
    private void update_rating_sensitivities() {
        set_action_sensitive("RateRejected", can_rate_selected(Rating.REJECTED));
        set_action_sensitive("RateUnrated", can_rate_selected(Rating.UNRATED));
        set_action_sensitive("RateOne", can_rate_selected(Rating.ONE));
        set_action_sensitive("RateTwo", can_rate_selected(Rating.TWO));
        set_action_sensitive("RateThree", can_rate_selected(Rating.THREE));
        set_action_sensitive("RateFour", can_rate_selected(Rating.FOUR));
        set_action_sensitive("RateFive", can_rate_selected(Rating.FIVE));
        set_action_sensitive("IncreaseRating", can_increase_selected_rating());
        set_action_sensitive("DecreaseRating", can_decrease_selected_rating());
    }
317
    
318 319 320 321 322 323 324 325 326 327 328 329
    private void update_development_menu_item_sensitivity() {
        if (get_view().get_selected().size == 0) {
            set_action_sensitive("RawDeveloper", false);
            return;
        }
        
        // Collect some stats about what's selected.
        bool is_raw = false;    // True if any RAW photos are selected
        foreach (DataView view in get_view().get_selected()) {
            Photo? photo = ((Thumbnail) view).get_media_source() as Photo;
            if (photo != null && photo.get_master_file_format() == PhotoFileFormat.RAW) {
                is_raw = true;
330 331

                break;
332 333 334 335 336 337 338
            }
        }
        
        // Enable/disable menu.
        set_action_sensitive("RawDeveloper", is_raw);
    }
    
Jim Nelson's avatar
Jim Nelson committed
339 340 341 342
    private void update_flag_action(int selected_count) {
        set_action_sensitive("Flag", selected_count > 0);
    }
    
343 344 345
    public override Core.ViewTracker? get_view_tracker() {
        return tracker;
    }
346

347 348 349 350
    public void set_display_ratings(bool display) {
        get_view().freeze_notifications();
        get_view().set_property(Thumbnail.PROP_SHOW_RATINGS, display);
        get_view().thaw_notifications();
351 352

        this.set_action_active ("ViewRatings", display);
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
    }

    private bool can_rate_selected(Rating rating) {
        foreach (DataView view in get_view().get_selected()) {
            if(((Thumbnail) view).get_media_source().get_rating() != rating)
                return true;
        }

        return false;
    }

    private bool can_increase_selected_rating() {
        foreach (DataView view in get_view().get_selected()) {
            if(((Thumbnail) view).get_media_source().get_rating().can_increase())
                return true;
        }

        return false;
    }

    private bool can_decrease_selected_rating() {
        foreach (DataView view in get_view().get_selected()) {
            if(((Thumbnail) view).get_media_source().get_rating().can_decrease())
                return true;
        }
        
        return false;
    }
    
    public ZoomSliderAssembly create_zoom_slider_assembly() {
        return new ZoomSliderAssembly();
    }

    protected override bool on_mousewheel_up(Gdk.EventScroll event) {
        if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
            increase_zoom_level();
            return true;
        } else {
            return base.on_mousewheel_up(event);
        }
    }

    protected override bool on_mousewheel_down(Gdk.EventScroll event) {
        if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
            decrease_zoom_level();
            return true;
        } else {
            return base.on_mousewheel_down(event);
        }
    }
Eric Gregory's avatar
Eric Gregory committed
403
    
404 405 406 407
    private void on_send_to() {
        DesktopIntegration.send_to((Gee.Collection<MediaSource>) get_view().get_selected_sources());
    }
    
408 409 410 411
    protected void on_play_video() {
        if (get_view().get_selected_count() != 1)
            return;
        
412 413 414
        Video? video = get_view().get_selected_at(0).get_source() as Video;
        if (video == null)
            return;
415 416 417 418 419 420 421 422 423
        
        try {
            AppInfo.launch_default_for_uri(video.get_file().get_uri(), null);
        } catch (Error e) {
            AppWindow.error_message(_("Shotwell was unable to play the selected video:\n%s").printf(
                e.message));
        }
    }

424 425
    protected override bool on_app_key_pressed(Gdk.EventKey event) {
        bool handled = true;
426
        switch (Gdk.keyval_name(event.keyval)) {
427 428 429
            case "equal":
            case "plus":
            case "KP_Add":
430
                activate_action("IncreaseSize");
431 432 433 434 435
            break;
            
            case "minus":
            case "underscore":
            case "KP_Subtract":
436
                activate_action("DecreaseSize");
437
            break;
438
            
439
            case "period":
440
                activate_action("IncreaseRating");
441 442 443
            break;
            
            case "comma":
444
                activate_action("DecreaseRating");
445
            break;
446
            
447
            case "KP_1":
448
                activate_action("RateOne");
449 450 451
            break;
            
            case "KP_2":
452
                activate_action("RateTwo");
453
            break;
454
            
455
            case "KP_3":
456
                activate_action("RateThree");
457
            break;
458
            
459
            case "KP_4":
460
                activate_action("RateFour");
461
            break;
462
            
463
            case "KP_5":
464
                activate_action("RateFive");
465
            break;
466
            
467
            case "KP_0":
468
                activate_action("RateUnrated");
469
            break;
470
            
471
            case "KP_9":
472
                activate_action("RateRejected");
473 474
            break;
            
475
            case "slash":
476
                activate_action("Flag");
477 478
            break;
            
479 480 481 482 483 484 485 486 487 488 489 490 491
            default:
                handled = false;
            break;
        }
        
        return handled ? true : base.on_app_key_pressed(event);
    }

    public override void switched_to() {
        base.switched_to();
        
        // set display options to match Configuration toggles (which can change while switched away)
        get_view().freeze_notifications();
Lucas Beeler's avatar
Lucas Beeler committed
492
        set_display_titles(Config.Facade.get_instance().get_display_photo_titles());
493
        set_display_comments(Config.Facade.get_instance().get_display_photo_comments());
Lucas Beeler's avatar
Lucas Beeler committed
494 495
        set_display_ratings(Config.Facade.get_instance().get_display_photo_ratings());
        set_display_tags(Config.Facade.get_instance().get_display_photo_tags());
496 497
        get_view().thaw_notifications();

498 499 500 501 502 503 504 505
        // Update cursor position to match the selection that potentially moved while the user
        // navigated in SinglePhotoPage
        if (get_view().get_selected_count() > 0) {
            CheckerboardItem? selected = (CheckerboardItem?) get_view().get_selected_at(0);
            if (selected != null)
                cursor_to_item(selected);
        }

506 507
        sync_sort();
    }
508 509 510 511 512 513
    
    public override void switching_from() {
        disconnect_slider();

        base.switching_from();
    }
514 515 516 517

    protected void connect_slider(ZoomSliderAssembly slider) {
        connected_slider = slider;
        connected_slider.zoom_changed.connect(on_zoom_changed);
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
        load_persistent_thumbnail_scale();
    }
    
    private void save_persistent_thumbnail_scale() {
        if (connected_slider == null)
            return;
            
        Config.Facade.get_instance().set_photo_thumbnail_scale(connected_slider.get_scale());
    }
    
    private void load_persistent_thumbnail_scale() {
        if (connected_slider == null)
            return;

        int persistent_scale = Config.Facade.get_instance().get_photo_thumbnail_scale();

        connected_slider.set_scale(persistent_scale);
        set_thumb_size(persistent_scale);
536 537 538 539 540 541 542 543 544 545 546 547 548
    }
    
    protected void disconnect_slider() {
        if (connected_slider == null)
            return;
        
        connected_slider.zoom_changed.disconnect(on_zoom_changed);
        connected_slider = null;
    }

    protected virtual void on_zoom_changed() {
        if (connected_slider != null)
            set_thumb_size(connected_slider.get_scale());
549 550

        save_persistent_thumbnail_scale();
551 552
    }
    
553
    protected abstract void on_export();
554 555 556 557 558 559 560 561

    protected virtual void on_increase_size() {
        increase_zoom_level();
    }

    protected virtual void on_decrease_size() {
        decrease_zoom_level();
    }
562 563 564 565 566 567 568

    private void on_add_tags() {
        if (get_view().get_selected_count() == 0)
            return;
        
        AddTagsDialog dialog = new AddTagsDialog();
        string[]? names = dialog.execute();
569
        
570
        if (names != null) {
571 572
            get_command_manager().execute(new AddTagsCommand(
                HierarchicalTagIndex.get_global_index().get_paths_for_names_array(names),
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
                (Gee.Collection<MediaSource>) get_view().get_selected_sources()));
        }
    }

    private void on_modify_tags() {
        if (get_view().get_selected_count() != 1)
            return;
        
        MediaSource media = (MediaSource) get_view().get_selected_at(0).get_source();
        
        ModifyTagsDialog dialog = new ModifyTagsDialog(media);
        Gee.ArrayList<Tag>? new_tags = dialog.execute();
        
        if (new_tags == null)
            return;
        
        get_command_manager().execute(new ModifyTagsCommand(media, new_tags));
    }

    private void set_display_tags(bool display) {
        get_view().freeze_notifications();
        get_view().set_property(Thumbnail.PROP_SHOW_TAGS, display);
        get_view().thaw_notifications();
596 597

        this.set_action_active ("ViewTags", display);
598 599 600 601 602 603
    }

    private void on_new_event() {
        if (get_view().get_selected_count() > 0)
            get_command_manager().execute(new NewEventCommand(get_view().get_selected()));
    }
Jim Nelson's avatar
Jim Nelson committed
604 605 606 607 608
    
    private void on_flag_unflag() {
        if (get_view().get_selected_count() == 0)
            return;
        
609 610
        Gee.Collection<MediaSource> sources =
            (Gee.Collection<MediaSource>) get_view().get_selected_sources_of_type(typeof(MediaSource));
Jim Nelson's avatar
Jim Nelson committed
611 612 613
        
        // If all are flagged, then unflag, otherwise flag
        bool flag = false;
614
        foreach (MediaSource source in sources) {
Jim Nelson's avatar
Jim Nelson committed
615 616 617 618 619 620 621 622 623 624 625
            Flaggable? flaggable = source as Flaggable;
            if (flaggable != null && !flaggable.is_flagged()) {
                flag = true;
                
                break;
            }
        }
        
        get_command_manager().execute(new FlagUnflagCommand(sources, flag));
    }
    
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683
    protected virtual void on_increase_rating() {
        if (get_view().get_selected_count() == 0)
            return;
        
        SetRatingCommand command = new SetRatingCommand.inc_dec(get_view().get_selected(), true);
        get_command_manager().execute(command);

        update_rating_sensitivities();
    }

    protected virtual void on_decrease_rating() {
        if (get_view().get_selected_count() == 0)
            return;
        
        SetRatingCommand command = new SetRatingCommand.inc_dec(get_view().get_selected(), false);
        get_command_manager().execute(command);

        update_rating_sensitivities();
    }

    protected virtual void on_set_rating(Rating rating) {
        if (get_view().get_selected_count() == 0)
            return;
        
        SetRatingCommand command = new SetRatingCommand(get_view().get_selected(), rating);
        get_command_manager().execute(command);

        update_rating_sensitivities();
    }

    protected virtual void on_rate_rejected() {
        on_set_rating(Rating.REJECTED);
    }
    
    protected virtual void on_rate_unrated() {
        on_set_rating(Rating.UNRATED);
    }

    protected virtual void on_rate_one() {
        on_set_rating(Rating.ONE);
    }

    protected virtual void on_rate_two() {
        on_set_rating(Rating.TWO);
    }

    protected virtual void on_rate_three() {
        on_set_rating(Rating.THREE);
    }

    protected virtual void on_rate_four() {
        on_set_rating(Rating.FOUR);
    }

    protected virtual void on_rate_five() {
        on_set_rating(Rating.FIVE);
    }

684 685 686
    private void on_remove_from_library() {
        remove_photos_from_library((Gee.Collection<LibraryPhoto>) get_view().get_selected_sources());
    }
687

688
    protected virtual void on_move_to_trash() {
689 690 691 692 693 694
        CheckerboardItem? restore_point = null;

        if (cursor != null) {
            restore_point = get_view().get_next(cursor) as CheckerboardItem;
        }

695 696
        var sources = get_view().get_selected_sources();

697 698 699 700
        if ((restore_point != null) && (get_view().contains(restore_point))) {
            set_cursor(restore_point);
        }

701 702
        if (get_view().get_selected_count() > 0) {
            get_command_manager().execute(new TrashUntrashPhotosCommand(
703
                (Gee.Collection<MediaSource>) sources, true));
704
        }
705

706 707
    }

708
    protected virtual void on_edit_title() {
709
        if (get_view().get_selected_count() == 0)
710 711
            return;
        
712
        Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources();
713
        
714 715 716 717
        EditTitleDialog edit_title_dialog = new EditTitleDialog(media_sources[0].get_title());
        string? new_title = edit_title_dialog.execute();
        if (new_title != null)
            get_command_manager().execute(new EditMultipleTitlesCommand(media_sources, new_title));
718 719
    }

720 721 722 723 724 725 726 727 728 729 730 731
    protected virtual void on_edit_comment() {
        if (get_view().get_selected_count() == 0)
            return;
        
        Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources();
        
        EditCommentDialog edit_comment_dialog = new EditCommentDialog(media_sources[0].get_comment());
        string? new_comment = edit_comment_dialog.execute();
        if (new_comment != null)
            get_command_manager().execute(new EditMultipleCommentsCommand(media_sources, new_comment));
    }

732 733
    protected virtual void on_display_titles(GLib.SimpleAction action, Variant? value) {
        bool display = value.get_boolean ();
734 735 736
        
        set_display_titles(display);
        
Lucas Beeler's avatar
Lucas Beeler committed
737
        Config.Facade.get_instance().set_display_photo_titles(display);
738
        action.set_state (value);
739 740
    }

741 742
    protected virtual void on_display_comments(GLib.SimpleAction action, Variant? value) {
        bool display = value.get_boolean ();
743 744 745 746
        
        set_display_comments(display);
        
        Config.Facade.get_instance().set_display_photo_comments(display);
747
        action.set_state (value);
748 749
    }

750 751
    protected virtual void on_display_ratings(GLib.SimpleAction action, Variant? value) {
        bool display = value.get_boolean ();
752 753 754
        
        set_display_ratings(display);
        
Lucas Beeler's avatar
Lucas Beeler committed
755
        Config.Facade.get_instance().set_display_photo_ratings(display);
756
        action.set_state (value);
757 758
    }

759 760
    protected virtual void on_display_tags(GLib.SimpleAction action, Variant? value) {
        bool display = value.get_boolean ();
761 762 763
        
        set_display_tags(display);
        
Lucas Beeler's avatar
Lucas Beeler committed
764
        Config.Facade.get_instance().set_display_photo_tags(display);
765
        action.set_state (value);
766 767
    }

768 769 770 771
    protected abstract void get_config_photos_sort(out bool sort_order, out int sort_by);

    protected abstract void set_config_photos_sort(bool sort_order, int sort_by);

772
    public virtual void on_sort_changed(GLib.SimpleAction action, Variant? value) {
Jens Georg's avatar
Jens Georg committed
773 774
        action.set_state (value);

775 776 777 778 779
        int sort_by = get_menu_sort_by();
        bool sort_order = get_menu_sort_order();
        
        set_view_comparator(sort_by, sort_order);
        set_config_photos_sort(sort_order, sort_by);
780
    }
781
    
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
    private void on_raw_developer_changed(GLib.SimpleAction action,
                                          Variant? value) {
        RawDeveloper developer = RawDeveloper.SHOTWELL;

        switch (value.get_string ()) {
            case "Shotwell":
                developer = RawDeveloper.SHOTWELL;
                break;
            case "Camera":
                developer = RawDeveloper.CAMERA;
                break;
            default:
                break;
        }

        developer_changed(developer);

        action.set_state (value);
    }

802 803 804 805
    protected virtual void developer_changed(RawDeveloper rd) {
        if (get_view().get_selected_count() == 0)
            return;
        
806 807 808 809 810
        // Check if any photo has edits

        // Display warning only when edits could be destroyed
        bool need_warn = false;

811 812 813
        // Make a list of all photos that need their developer changed.
        Gee.ArrayList<DataView> to_set = new Gee.ArrayList<DataView>();
        foreach (DataView view in get_view().get_selected()) {
814
            Photo? p = view.get_source() as Photo;
815
            if (p != null && (!rd.is_equivalent(p.get_raw_developer()))) {
816
                to_set.add(view);
817 818 819 820 821
                
                if (p.has_transformations()) {
                    need_warn = true;
                }
            }
822 823
        }
        
824 825 826 827 828 829
        if (!need_warn || Dialogs.confirm_warn_developer_changed(to_set.size)) {
            SetRawDeveloperCommand command = new SetRawDeveloperCommand(to_set, rd);
            get_command_manager().execute(command);

            update_development_menu_item_sensitivity();
        }
830
    }
831 832 833

    protected override void set_display_titles(bool display) {
        base.set_display_titles(display);
834 835

        this.set_action_active ("ViewTitle", display);
836 837 838 839 840
    }

    protected override void set_display_comments(bool display) {
        base.set_display_comments(display);
    
841
        this.set_action_active ("ViewComment", display);
842 843
    }

844 845
    private GLib.Action sort_by_title_action() {
        var action = get_action ("SortBy");
846
        assert(action != null);
847 848
        return action;
    }
849

850 851
    private GLib.Action sort_ascending_action() {
        var action = get_action ("Sort");
852 853
        assert(action != null);
        return action;
854
    }
855 856

    protected int get_menu_sort_by() {
857
        // any member of the group knows the current value
858
        return int.parse (sort_by_title_action().get_state().get_string ());
859 860
    }
    
861
    protected void set_menu_sort_by(int val) {
862
        var sort = "%d".printf (val);
863
        sort_by_title_action().change_state (sort);
864
    }
865 866 867
    
    protected bool get_menu_sort_order() {
        // any member of the group knows the current value
868
        return sort_ascending_action().get_state ().get_string () == "ascending";
869 870 871
    }
    
    protected void set_menu_sort_order(bool ascending) {
872
        sort_ascending_action().change_state (ascending ? "ascending" : "descending");
873 874
    }
    
875 876 877 878 879
    void set_view_comparator(int sort_by, bool ascending) {
        Comparator comparator;
        ComparatorPredicate predicate;
        
        switch (sort_by) {
880
            case SortBy.TITLE:
881 882 883 884 885
                if (ascending)
                    comparator = Thumbnail.title_ascending_comparator;
                else comparator = Thumbnail.title_descending_comparator;
                predicate = Thumbnail.title_comparator_predicate;
                break;
886 887
            
            case SortBy.EXPOSURE_DATE:
888 889 890 891 892
                if (ascending)
                    comparator = Thumbnail.exposure_time_ascending_comparator;
                else comparator = Thumbnail.exposure_time_desending_comparator;
                predicate = Thumbnail.exposure_time_comparator_predicate;
                break;
893 894
            
            case SortBy.RATING:
895 896 897 898 899
                if (ascending)
                    comparator = Thumbnail.rating_ascending_comparator;
                else comparator = Thumbnail.rating_descending_comparator;
                predicate = Thumbnail.rating_comparator_predicate;
                break;
900
            
901 902 903 904 905 906 907
            case SortBy.FILENAME:
                if (ascending)
                    comparator = Thumbnail.filename_ascending_comparator;
                else comparator = Thumbnail.filename_descending_comparator;
                predicate = Thumbnail.filename_comparator_predicate;
                break;

908
            default:
909 910 911 912
                debug("Unknown sort criteria: %s", get_menu_sort_by().to_string());
                comparator = Thumbnail.title_descending_comparator;
                predicate = Thumbnail.title_comparator_predicate;
                break;
913
        }
914 915
        
        get_view().set_comparator(comparator, predicate);
916
    }
917

918
    protected void sync_sort() {
919 920 921 922 923 924 925 926 927 928 929 930 931
        // It used to be that the config and UI could both agree on what 
        // sort order and criteria were selected, but the sorting wouldn't
        // match them, due to the current view's comparator not actually 
        // being set to match, and since there was a check to see if the 
        // config and UI matched that would frequently succeed in this case,
        // the sorting was often wrong until the user went in and changed 
        // it.  Because there is no tidy way to query the current view's 
        // comparator, we now set it any time we even think the sorting 
        // might have changed to force them to always stay in sync.
        //
        // Although this means we pay for a re-sort every time, in practice,
        // this isn't terribly expensive - it _might_ take as long as .5 sec.
        // with a media page containing over 15000 items on a modern CPU.
932 933 934 935 936 937 938 939 940
        
        bool sort_ascending;
        int sort_by;
        get_config_photos_sort(out sort_ascending, out sort_by);
        
        set_menu_sort_by(sort_by);
        set_menu_sort_order(sort_ascending);
        
        set_view_comparator(sort_by, sort_ascending);
941 942 943 944
    }

    public override void destroy() {
        disconnect_slider();
945
        
946 947 948 949 950 951 952 953
        base.destroy();
    }

    public void increase_zoom_level() {
        if (connected_slider != null) {
            connected_slider.increase_step();
        } else {
            int new_scale = compute_zoom_scale_increase(get_thumb_size());
954
            save_persistent_thumbnail_scale();
955 956 957 958 959 960 961 962 963
            set_thumb_size(new_scale);
        }
    }

    public void decrease_zoom_level() {
        if (connected_slider != null) {
            connected_slider.decrease_step();
        } else {
            int new_scale = compute_zoom_scale_decrease(get_thumb_size());
964
            save_persistent_thumbnail_scale();
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985
            set_thumb_size(new_scale);
        }
    }

    public virtual DataView create_thumbnail(DataSource source) {
        return new Thumbnail((MediaSource) source, get_thumb_size());
    }

    // this is a view-level operation on this page only; it does not affect the persistent global
    // thumbnail scale
    public void set_thumb_size(int new_scale) {
        if (get_thumb_size() == new_scale || !is_in_view())
            return;
        
        new_scale = new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
        get_checkerboard_layout().set_scale(new_scale);
        
        // when doing mass operations on LayoutItems, freeze individual notifications
        get_view().freeze_notifications();
        get_view().set_property(Thumbnail.PROP_SIZE, new_scale);
        get_view().thaw_notifications();
986 987 988
        
        set_action_sensitive("IncreaseSize", new_scale < Thumbnail.MAX_SCALE);
        set_action_sensitive("DecreaseSize", new_scale > Thumbnail.MIN_SCALE);
989 990 991 992
    }

    public int get_thumb_size() {
        if (get_checkerboard_layout().get_scale() <= 0)
Lucas Beeler's avatar
Lucas Beeler committed
993
            get_checkerboard_layout().set_scale(Config.Facade.get_instance().get_photo_thumbnail_scale());
994 995 996 997 998
            
        return get_checkerboard_layout().get_scale();
    }
}