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
7public class MediaSourceItem : CheckerboardItem {
8    private string? natural_collation_key = null;
9
10    // preserve the same constructor arguments and semantics as CheckerboardItem so that we're
11    // a drop-in replacement
12    public MediaSourceItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title,
13        string? comment, bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
14        base(source, initial_pixbuf_dim, title, comment, marked_up, alignment);
15    }
16
17    public new void set_title(string text, bool marked_up = false,
18        Pango.Alignment alignment = Pango.Alignment.LEFT) {
19        base.set_title(text, marked_up, alignment);
20        this.natural_collation_key = null;
21    }
22
23    public string get_natural_collation_key() {
24        if (this.natural_collation_key == null) {
25            this.natural_collation_key = NaturalCollate.collate_key(this.get_title());
26        }
27        return this.natural_collation_key;
28    }
29}
30
31public abstract class MediaPage : CheckerboardPage {
32    public const int SORT_ORDER_ASCENDING = 0;
33    public const int SORT_ORDER_DESCENDING = 1;
34
35    // steppings should divide evenly into (Thumbnail.MAX_SCALE - Thumbnail.MIN_SCALE)
36    public const int MANUAL_STEPPING = 16;
37    public const int SLIDER_STEPPING = 4;
38
39    public enum SortBy {
40        MIN = 1,
41        TITLE = 1,
42        EXPOSURE_DATE = 2,
43        RATING = 3,
44        FILENAME = 4,
45        MAX = 4
46    }
47
48    protected class ZoomSliderAssembly : Gtk.ToolItem {
49        private Gtk.Scale slider;
50        private Gtk.Adjustment adjustment;
51
52        public signal void zoom_changed();
53
54        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            Gtk.EventBox zoom_out_box = new Gtk.EventBox();
59            zoom_out_box.set_above_child(true);
60            zoom_out_box.set_visible_window(false);
61            zoom_out_box.add(zoom_out);
62            zoom_out_box.button_press_event.connect(on_zoom_out_pressed);
63
64            zoom_group.pack_start(zoom_out_box, false, false, 0);
65
66            // virgin ZoomSliderAssemblies are created such that they have whatever value is
67            // persisted in the configuration system for the photo thumbnail scale
68            int persisted_scale = Config.Facade.get_instance().get_photo_thumbnail_scale();
69            adjustment = new Gtk.Adjustment(ZoomSliderAssembly.scale_to_slider(persisted_scale), 0,
70                ZoomSliderAssembly.scale_to_slider(Thumbnail.MAX_SCALE), 1, 10, 0);
71
72            slider = new Gtk.Scale(Gtk.Orientation.HORIZONTAL, adjustment);
73            slider.value_changed.connect(on_slider_changed);
74            slider.set_draw_value(false);
75            slider.set_size_request(200, -1);
76            slider.set_tooltip_text(_("Adjust the size of the thumbnails"));
77
78            zoom_group.pack_start(slider, false, false, 0);
79
80            Gtk.Image zoom_in = new Gtk.Image.from_icon_name("image-zoom-in-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
81            Gtk.EventBox zoom_in_box = new Gtk.EventBox();
82            zoom_in_box.set_above_child(true);
83            zoom_in_box.set_visible_window(false);
84            zoom_in_box.add(zoom_in);
85            zoom_in_box.button_press_event.connect(on_zoom_in_pressed);
86
87            zoom_group.pack_start(zoom_in_box, false, false, 0);
88
89            add(zoom_group);
90        }
91
92        public static double scale_to_slider(int value) {
93            assert(value >= Thumbnail.MIN_SCALE);
94            assert(value <= Thumbnail.MAX_SCALE);
95
96            return (double) ((value - Thumbnail.MIN_SCALE) / SLIDER_STEPPING);
97        }
98
99        public static int slider_to_scale(double value) {
100            int res = ((int) (value * SLIDER_STEPPING)) + Thumbnail.MIN_SCALE;
101
102            assert(res >= Thumbnail.MIN_SCALE);
103            assert(res <= Thumbnail.MAX_SCALE);
104
105            return res;
106        }
107
108        private bool on_zoom_out_pressed(Gdk.EventButton event) {
109            snap_to_min();
110            return true;
111        }
112
113        private bool on_zoom_in_pressed(Gdk.EventButton event) {
114            snap_to_max();
115            return true;
116        }
117
118        private void on_slider_changed() {
119            zoom_changed();
120        }
121
122        public void snap_to_min() {
123            slider.set_value(scale_to_slider(Thumbnail.MIN_SCALE));
124        }
125
126        public void snap_to_max() {
127            slider.set_value(scale_to_slider(Thumbnail.MAX_SCALE));
128        }
129
130        public void increase_step() {
131            int new_scale = compute_zoom_scale_increase(get_scale());
132
133            if (get_scale() == new_scale)
134                return;
135
136            slider.set_value(scale_to_slider(new_scale));
137        }
138
139        public void decrease_step() {
140            int new_scale = compute_zoom_scale_decrease(get_scale());
141
142            if (get_scale() == new_scale)
143                return;
144
145            slider.set_value(scale_to_slider(new_scale));
146        }
147
148        public int get_scale() {
149            return slider_to_scale(slider.get_value());
150        }
151
152        public void set_scale(int scale) {
153            if (get_scale() == scale)
154                return;
155
156            slider.set_value(scale_to_slider(scale));
157        }
158    }
159
160    private ZoomSliderAssembly? connected_slider = null;
161    private DragAndDropHandler dnd_handler = null;
162    private MediaViewTracker tracker;
163
164    protected MediaPage(string page_name) {
165        base (page_name);
166
167        tracker = new MediaViewTracker(get_view());
168
169        get_view().items_altered.connect(on_media_altered);
170
171        get_view().freeze_notifications();
172        get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES,
173            Config.Facade.get_instance().get_display_photo_titles());
174        get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS,
175            Config.Facade.get_instance().get_display_photo_comments());
176        get_view().set_property(Thumbnail.PROP_SHOW_TAGS,
177            Config.Facade.get_instance().get_display_photo_tags());
178        get_view().set_property(Thumbnail.PROP_SIZE, get_thumb_size());
179        get_view().set_property(Thumbnail.PROP_SHOW_RATINGS,
180            Config.Facade.get_instance().get_display_photo_ratings());
181        get_view().thaw_notifications();
182
183        // enable drag-and-drop export of media
184        dnd_handler = new DragAndDropHandler(this);
185    }
186
187    private static int compute_zoom_scale_increase(int current_scale) {
188        int new_scale = current_scale + MANUAL_STEPPING;
189        return new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
190    }
191
192    private static int compute_zoom_scale_decrease(int current_scale) {
193        int new_scale = current_scale - MANUAL_STEPPING;
194        return new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
195    }
196
197    protected override void init_collect_ui_filenames(Gee.List<string> ui_filenames) {
198        base.init_collect_ui_filenames(ui_filenames);
199
200        ui_filenames.add("media.ui");
201    }
202
203    private const GLib.ActionEntry[] entries = {
204        { "Export", on_export },
205        { "SendTo", on_send_to },
206        { "SendToContextMenu", on_send_to },
207        { "RemoveFromLibrary", on_remove_from_library },
208        { "MoveToTrash", on_move_to_trash },
209        { "NewEvent", on_new_event },
210        { "AddTags", on_add_tags },
211        { "ModifyTags", on_modify_tags },
212        { "IncreaseSize", on_increase_size },
213        { "DecreaseSize", on_decrease_size },
214        { "Flag", on_flag_unflag },
215        { "IncreaseRating", on_increase_rating },
216        { "DecreaseRating", on_decrease_rating },
217        { "RateRejected", on_rate_rejected },
218        { "RateUnrated", on_rate_unrated },
219        { "RateOne", on_rate_one },
220        { "RateTwo", on_rate_two },
221        { "RateThree", on_rate_three },
222        { "RateFour", on_rate_four },
223        { "RateFive", on_rate_five },
224        { "EditTitle", on_edit_title },
225        { "EditComment", on_edit_comment },
226        { "PlayVideo", on_play_video },
227
228        // Toggle actions
229        { "ViewTitle", on_action_toggle, null, "false", on_display_titles },
230        { "ViewComment", on_action_toggle, null, "false", on_display_comments },
231        { "ViewRatings", on_action_toggle, null, "false", on_display_ratings },
232        { "ViewTags", on_action_toggle, null, "false", on_display_tags },
233
234        // Radio actions
235        { "SortBy", on_action_radio, "s", "'1'", on_sort_changed },
236        { "Sort", on_action_radio, "s", "'ascending'", on_sort_changed },
237    };
238
239    protected override void add_actions (GLib.ActionMap map) {
240        base.add_actions (map);
241
242        bool sort_order;
243        int sort_by;
244        get_config_photos_sort(out sort_order, out sort_by);
245
246        map.add_action_entries (entries, this);
247        get_action ("ViewTitle").change_state (Config.Facade.get_instance ().get_display_photo_titles ());
248        get_action ("ViewComment").change_state (Config.Facade.get_instance ().get_display_photo_comments ());
249        get_action ("ViewRatings").change_state (Config.Facade.get_instance ().get_display_photo_ratings ());
250        get_action ("ViewTags").change_state (Config.Facade.get_instance ().get_display_photo_tags ());
251        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        var action = new GLib.SimpleAction.stateful("RawDeveloper",
256                GLib.VariantType.STRING, d == RawDeveloper.SHOTWELL ? "Shotwell" : "Camera");
257        action.change_state.connect(on_raw_developer_changed);
258        action.set_enabled(true);
259        map.add_action(action);
260    }
261
262    protected override void remove_actions(GLib.ActionMap map) {
263        base.remove_actions(map);
264        foreach (var entry in entries) {
265            map.remove_action(entry.name);
266        }
267    }
268
269    protected override void update_actions(int selected_count, int count) {
270        set_action_sensitive("Export", selected_count > 0);
271        set_action_sensitive("EditTitle", selected_count > 0);
272        set_action_sensitive("EditComment", selected_count > 0);
273        set_action_sensitive("IncreaseSize", get_thumb_size() < Thumbnail.MAX_SCALE);
274        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        if (DesktopIntegration.is_send_to_installed())
279            set_action_sensitive("SendTo", selected_count > 0);
280        else
281            set_action_sensitive("SendTo", false);
282
283        set_action_sensitive("Rate", selected_count > 0);
284        update_rating_sensitivities();
285
286        update_development_menu_item_sensitivity();
287
288        set_action_sensitive("PlayVideo", selected_count == 1
289            && get_view().get_selected_source_at(0) is Video);
290
291        update_flag_action(selected_count);
292
293        base.update_actions(selected_count, count);
294    }
295
296    private void on_media_altered(Gee.Map<DataObject, Alteration> altered) {
297        foreach (DataObject object in altered.keys) {
298            if (altered.get(object).has_detail("metadata", "flagged")) {
299                update_flag_action(get_view().get_selected_count());
300
301                break;
302            }
303        }
304    }
305
306    private void update_rating_sensitivities() {
307        set_action_sensitive("RateRejected", can_rate_selected(Rating.REJECTED));
308        set_action_sensitive("RateUnrated", can_rate_selected(Rating.UNRATED));
309        set_action_sensitive("RateOne", can_rate_selected(Rating.ONE));
310        set_action_sensitive("RateTwo", can_rate_selected(Rating.TWO));
311        set_action_sensitive("RateThree", can_rate_selected(Rating.THREE));
312        set_action_sensitive("RateFour", can_rate_selected(Rating.FOUR));
313        set_action_sensitive("RateFive", can_rate_selected(Rating.FIVE));
314        set_action_sensitive("IncreaseRating", can_increase_selected_rating());
315        set_action_sensitive("DecreaseRating", can_decrease_selected_rating());
316    }
317
318    private void update_development_menu_item_sensitivity() {
319        if (get_view().get_selected().size == 0) {
320            set_action_sensitive("RawDeveloper", false);
321            return;
322        }
323
324        // Collect some stats about what's selected.
325        bool is_raw = false;    // True if any RAW photos are selected
326        foreach (DataView view in get_view().get_selected()) {
327            Photo? photo = ((Thumbnail) view).get_media_source() as Photo;
328            if (photo != null && photo.get_master_file_format() == PhotoFileFormat.RAW) {
329                is_raw = true;
330
331                break;
332            }
333        }
334
335        // Enable/disable menu.
336        set_action_sensitive("RawDeveloper", is_raw);
337    }
338
339    private void update_flag_action(int selected_count) {
340        set_action_sensitive("Flag", selected_count > 0);
341    }
342
343    public override Core.ViewTracker? get_view_tracker() {
344        return tracker;
345    }
346
347    public void set_display_ratings(bool display) {
348        get_view().freeze_notifications();
349        get_view().set_property(Thumbnail.PROP_SHOW_RATINGS, display);
350        get_view().thaw_notifications();
351
352        this.set_action_active ("ViewRatings", display);
353    }
354
355    private bool can_rate_selected(Rating rating) {
356        foreach (DataView view in get_view().get_selected()) {
357            if(((Thumbnail) view).get_media_source().get_rating() != rating)
358                return true;
359        }
360
361        return false;
362    }
363
364    private bool can_increase_selected_rating() {
365        foreach (DataView view in get_view().get_selected()) {
366            if(((Thumbnail) view).get_media_source().get_rating().can_increase())
367                return true;
368        }
369
370        return false;
371    }
372
373    private bool can_decrease_selected_rating() {
374        foreach (DataView view in get_view().get_selected()) {
375            if(((Thumbnail) view).get_media_source().get_rating().can_decrease())
376                return true;
377        }
378
379        return false;
380    }
381
382    public ZoomSliderAssembly create_zoom_slider_assembly() {
383        return new ZoomSliderAssembly();
384    }
385
386    protected override bool on_mousewheel_up(Gdk.EventScroll event) {
387        if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
388            increase_zoom_level();
389            return true;
390        } else {
391            return base.on_mousewheel_up(event);
392        }
393    }
394
395    protected override bool on_mousewheel_down(Gdk.EventScroll event) {
396        if ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0) {
397            decrease_zoom_level();
398            return true;
399        } else {
400            return base.on_mousewheel_down(event);
401        }
402    }
403
404    private void on_send_to() {
405        DesktopIntegration.send_to((Gee.Collection<MediaSource>) get_view().get_selected_sources());
406    }
407
408    protected void on_play_video() {
409        if (get_view().get_selected_count() != 1)
410            return;
411
412        Video? video = get_view().get_selected_at(0).get_source() as Video;
413        if (video == null)
414            return;
415
416        try {
417            AppInfo.launch_default_for_uri(video.get_file().get_uri(), null);
418        } catch (Error e) {
419            AppWindow.error_message(_("Shotwell was unable to play the selected video:\n%s").printf(
420                e.message));
421        }
422    }
423
424    protected override bool on_app_key_pressed(Gdk.EventKey event) {
425        bool handled = true;
426        switch (Gdk.keyval_name(event.keyval)) {
427            case "equal":
428            case "plus":
429            case "KP_Add":
430                activate_action("IncreaseSize");
431            break;
432
433            case "minus":
434            case "underscore":
435            case "KP_Subtract":
436                activate_action("DecreaseSize");
437            break;
438
439            case "period":
440                activate_action("IncreaseRating");
441            break;
442
443            case "comma":
444                activate_action("DecreaseRating");
445            break;
446
447            case "KP_1":
448                activate_action("RateOne");
449            break;
450
451            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            break;
474
475            case "slash":
476                activate_action("Flag");
477            break;
478
479            default:
480                handled = false;
481            break;
482        }
483
484        return handled ? true : base.on_app_key_pressed(event);
485    }
486
487    public override void switched_to() {
488        base.switched_to();
489
490        // set display options to match Configuration toggles (which can change while switched away)
491        get_view().freeze_notifications();
492        set_display_titles(Config.Facade.get_instance().get_display_photo_titles());
493        set_display_comments(Config.Facade.get_instance().get_display_photo_comments());
494        set_display_ratings(Config.Facade.get_instance().get_display_photo_ratings());
495        set_display_tags(Config.Facade.get_instance().get_display_photo_tags());
496        get_view().thaw_notifications();
497
498        // Update cursor position to match the selection that potentially moved while the user
499        // navigated in SinglePhotoPage
500        if (get_view().get_selected_count() > 0) {
501            CheckerboardItem? selected = (CheckerboardItem?) get_view().get_selected_at(0);
502            if (selected != null)
503                cursor_to_item(selected);
504        }
505
506        sync_sort();
507    }
508
509    public override void switching_from() {
510        disconnect_slider();
511
512        base.switching_from();
513    }
514
515    protected void connect_slider(ZoomSliderAssembly slider) {
516        connected_slider = slider;
517        connected_slider.zoom_changed.connect(on_zoom_changed);
518        load_persistent_thumbnail_scale();
519    }
520
521    private void save_persistent_thumbnail_scale() {
522        if (connected_slider == null)
523            return;
524
525        Config.Facade.get_instance().set_photo_thumbnail_scale(connected_slider.get_scale());
526    }
527
528    private void load_persistent_thumbnail_scale() {
529        if (connected_slider == null)
530            return;
531
532        int persistent_scale = Config.Facade.get_instance().get_photo_thumbnail_scale();
533
534        connected_slider.set_scale(persistent_scale);
535        set_thumb_size(persistent_scale);
536    }
537
538    protected void disconnect_slider() {
539        if (connected_slider == null)
540            return;
541
542        connected_slider.zoom_changed.disconnect(on_zoom_changed);
543        connected_slider = null;
544    }
545
546    protected virtual void on_zoom_changed() {
547        if (connected_slider != null)
548            set_thumb_size(connected_slider.get_scale());
549
550        save_persistent_thumbnail_scale();
551    }
552
553    protected abstract void on_export();
554
555    protected virtual void on_increase_size() {
556        increase_zoom_level();
557    }
558
559    protected virtual void on_decrease_size() {
560        decrease_zoom_level();
561    }
562
563    private void on_add_tags() {
564        if (get_view().get_selected_count() == 0)
565            return;
566
567        AddTagsDialog dialog = new AddTagsDialog();
568        string[]? names = dialog.execute();
569
570        if (names != null) {
571            get_command_manager().execute(new AddTagsCommand(
572                HierarchicalTagIndex.get_global_index().get_paths_for_names_array(names),
573                (Gee.Collection<MediaSource>) get_view().get_selected_sources()));
574        }
575    }
576
577    private void on_modify_tags() {
578        if (get_view().get_selected_count() != 1)
579            return;
580
581        MediaSource media = (MediaSource) get_view().get_selected_at(0).get_source();
582
583        ModifyTagsDialog dialog = new ModifyTagsDialog(media);
584        Gee.ArrayList<Tag>? new_tags = dialog.execute();
585
586        if (new_tags == null)
587            return;
588
589        get_command_manager().execute(new ModifyTagsCommand(media, new_tags));
590    }
591
592    private void set_display_tags(bool display) {
593        get_view().freeze_notifications();
594        get_view().set_property(Thumbnail.PROP_SHOW_TAGS, display);
595        get_view().thaw_notifications();
596
597        this.set_action_active ("ViewTags", display);
598    }
599
600    private void on_new_event() {
601        if (get_view().get_selected_count() > 0)
602            get_command_manager().execute(new NewEventCommand(get_view().get_selected()));
603    }
604
605    private void on_flag_unflag() {
606        if (get_view().get_selected_count() == 0)
607            return;
608
609        Gee.Collection<MediaSource> sources =
610            (Gee.Collection<MediaSource>) get_view().get_selected_sources_of_type(typeof(MediaSource));
611
612        // If all are flagged, then unflag, otherwise flag
613        bool flag = false;
614        foreach (MediaSource source in sources) {
615            Flaggable? flaggable = source as Flaggable;
616            if (flaggable != null && !flaggable.is_flagged()) {
617                flag = true;
618
619                break;
620            }
621        }
622
623        get_command_manager().execute(new FlagUnflagCommand(sources, flag));
624    }
625
626    protected virtual void on_increase_rating() {
627        if (get_view().get_selected_count() == 0)
628            return;
629
630        SetRatingCommand command = new SetRatingCommand.inc_dec(get_view().get_selected(), true);
631        get_command_manager().execute(command);
632
633        update_rating_sensitivities();
634    }
635
636    protected virtual void on_decrease_rating() {
637        if (get_view().get_selected_count() == 0)
638            return;
639
640        SetRatingCommand command = new SetRatingCommand.inc_dec(get_view().get_selected(), false);
641        get_command_manager().execute(command);
642
643        update_rating_sensitivities();
644    }
645
646    protected virtual void on_set_rating(Rating rating) {
647        if (get_view().get_selected_count() == 0)
648            return;
649
650        SetRatingCommand command = new SetRatingCommand(get_view().get_selected(), rating);
651        get_command_manager().execute(command);
652
653        update_rating_sensitivities();
654    }
655
656    protected virtual void on_rate_rejected() {
657        on_set_rating(Rating.REJECTED);
658    }
659
660    protected virtual void on_rate_unrated() {
661        on_set_rating(Rating.UNRATED);
662    }
663
664    protected virtual void on_rate_one() {
665        on_set_rating(Rating.ONE);
666    }
667
668    protected virtual void on_rate_two() {
669        on_set_rating(Rating.TWO);
670    }
671
672    protected virtual void on_rate_three() {
673        on_set_rating(Rating.THREE);
674    }
675
676    protected virtual void on_rate_four() {
677        on_set_rating(Rating.FOUR);
678    }
679
680    protected virtual void on_rate_five() {
681        on_set_rating(Rating.FIVE);
682    }
683
684    private void on_remove_from_library() {
685        remove_photos_from_library((Gee.Collection<LibraryPhoto>) get_view().get_selected_sources());
686    }
687
688    protected virtual void on_move_to_trash() {
689        CheckerboardItem? restore_point = null;
690
691        if (cursor != null) {
692            restore_point = get_view().get_next(cursor) as CheckerboardItem;
693        }
694
695        var sources = get_view().get_selected_sources();
696
697        if ((restore_point != null) && (get_view().contains(restore_point))) {
698            set_cursor(restore_point);
699        }
700
701        if (get_view().get_selected_count() > 0) {
702            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            return;
711
712        Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources();
713
714        EditTitleDialog edit_title_dialog = new EditTitleDialog(media_sources[0].get_title());
715        string? new_title = edit_title_dialog.execute();
716        if (new_title != null)
717            get_command_manager().execute(new EditMultipleTitlesCommand(media_sources, new_title));
718    }
719
720    protected virtual void on_edit_comment() {
721        if (get_view().get_selected_count() == 0)
722            return;
723
724        Gee.List<MediaSource> media_sources = (Gee.List<MediaSource>) get_view().get_selected_sources();
725
726        EditCommentDialog edit_comment_dialog = new EditCommentDialog(media_sources[0].get_comment());
727        string? new_comment = edit_comment_dialog.execute();
728        if (new_comment != null)
729            get_command_manager().execute(new EditMultipleCommentsCommand(media_sources, new_comment));
730    }
731
732    protected virtual void on_display_titles(GLib.SimpleAction action, Variant? value) {
733        bool display = value.get_boolean ();
734
735        set_display_titles(display);
736
737        Config.Facade.get_instance().set_display_photo_titles(display);
738        action.set_state (value);
739    }
740
741    protected virtual void on_display_comments(GLib.SimpleAction action, Variant? value) {
742        bool display = value.get_boolean ();
743
744        set_display_comments(display);
745
746        Config.Facade.get_instance().set_display_photo_comments(display);
747        action.set_state (value);
748    }
749
750    protected virtual void on_display_ratings(GLib.SimpleAction action, Variant? value) {
751        bool display = value.get_boolean ();
752
753        set_display_ratings(display);
754
755        Config.Facade.get_instance().set_display_photo_ratings(display);
756        action.set_state (value);
757    }
758
759    protected virtual void on_display_tags(GLib.SimpleAction action, Variant? value) {
760        bool display = value.get_boolean ();
761
762        set_display_tags(display);
763
764        Config.Facade.get_instance().set_display_photo_tags(display);
765        action.set_state (value);
766    }
767
768    protected abstract void get_config_photos_sort(out bool sort_order, out int sort_by);
769
770    protected abstract void set_config_photos_sort(bool sort_order, int sort_by);
771
772    public virtual void on_sort_changed(GLib.SimpleAction action, Variant? value) {
773        action.set_state (value);
774
775        int sort_by = get_menu_sort_by();
776        bool sort_order = get_menu_sort_order();
777
778        set_view_comparator(sort_by, sort_order);
779        set_config_photos_sort(sort_order, sort_by);
780    }
781
782    private void on_raw_developer_changed(GLib.SimpleAction action,
783                                          Variant? value) {
784        RawDeveloper developer = RawDeveloper.SHOTWELL;
785
786        switch (value.get_string ()) {
787            case "Shotwell":
788                developer = RawDeveloper.SHOTWELL;
789                break;
790            case "Camera":
791                developer = RawDeveloper.CAMERA;
792                break;
793            default:
794                break;
795        }
796
797        developer_changed(developer);
798
799        action.set_state (value);
800    }
801
802    protected virtual void developer_changed(RawDeveloper rd) {
803        if (get_view().get_selected_count() == 0)
804            return;
805
806        // Check if any photo has edits
807
808        // Display warning only when edits could be destroyed
809        bool need_warn = false;
810
811        // Make a list of all photos that need their developer changed.
812        Gee.ArrayList<DataView> to_set = new Gee.ArrayList<DataView>();
813        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                if (p.has_transformations()) {
819                    need_warn = true;
820                }
821            }
822        }
823
824        if (!need_warn || Dialogs.confirm_warn_developer_changed(to_set.size)) {
825            SetRawDeveloperCommand command = new SetRawDeveloperCommand(to_set, rd);
826            get_command_manager().execute(command);
827
828            update_development_menu_item_sensitivity();
829        }
830    }
831
832    protected override void set_display_titles(bool display) {
833        base.set_display_titles(display);
834
835        this.set_action_active ("ViewTitle", display);
836    }
837
838    protected override void set_display_comments(bool display) {
839        base.set_display_comments(display);
840
841        this.set_action_active ("ViewComment", display);
842    }
843
844    private GLib.Action sort_by_title_action() {
845        var action = get_action ("SortBy");
846        assert(action != null);
847        return action;
848    }
849
850    private GLib.Action sort_ascending_action() {
851        var action = get_action ("Sort");
852        assert(action != null);
853        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    protected bool get_menu_sort_order() {
867        // 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    void set_view_comparator(int sort_by, bool ascending) {
876        Comparator comparator;
877        ComparatorPredicate predicate;
878
879        switch (sort_by) {
880            case SortBy.TITLE:
881                if (ascending)
882                    comparator = Thumbnail.title_ascending_comparator;
883                else comparator = Thumbnail.title_descending_comparator;
884                predicate = Thumbnail.title_comparator_predicate;
885                break;
886
887            case SortBy.EXPOSURE_DATE:
888                if (ascending)
889                    comparator = Thumbnail.exposure_time_ascending_comparator;
890                else comparator = Thumbnail.exposure_time_desending_comparator;
891                predicate = Thumbnail.exposure_time_comparator_predicate;
892                break;
893
894            case SortBy.RATING:
895                if (ascending)
896                    comparator = Thumbnail.rating_ascending_comparator;
897                else comparator = Thumbnail.rating_descending_comparator;
898                predicate = Thumbnail.rating_comparator_predicate;
899                break;
900
901            case SortBy.FILENAME:
902                if (ascending)
903                    comparator = Thumbnail.filename_ascending_comparator;
904                else comparator = Thumbnail.filename_descending_comparator;
905                predicate = Thumbnail.filename_comparator_predicate;
906                break;
907
908            default:
909                debug("Unknown sort criteria: %s", get_menu_sort_by().to_string());
910                comparator = Thumbnail.title_descending_comparator;
911                predicate = Thumbnail.title_comparator_predicate;
912                break;
913        }
914
915        get_view().set_comparator(comparator, predicate);
916    }
917
918    protected void sync_sort() {
919        // It used to be that the config and UI could both agree on what
920        // sort order and criteria were selected, but the sorting wouldn't
921        // match them, due to the current view's comparator not actually
922        // being set to match, and since there was a check to see if the
923        // config and UI matched that would frequently succeed in this case,
924        // the sorting was often wrong until the user went in and changed
925        // it.  Because there is no tidy way to query the current view's
926        // comparator, we now set it any time we even think the sorting
927        // might have changed to force them to always stay in sync.
928        //
929        // Although this means we pay for a re-sort every time, in practice,
930        // this isn't terribly expensive - it _might_ take as long as .5 sec.
931        // with a media page containing over 15000 items on a modern CPU.
932
933        bool sort_ascending;
934        int sort_by;
935        get_config_photos_sort(out sort_ascending, out sort_by);
936
937        set_menu_sort_by(sort_by);
938        set_menu_sort_order(sort_ascending);
939
940        set_view_comparator(sort_by, sort_ascending);
941    }
942
943    public override void destroy() {
944        disconnect_slider();
945
946        base.destroy();
947    }
948
949    public void increase_zoom_level() {
950        if (connected_slider != null) {
951            connected_slider.increase_step();
952        } else {
953            int new_scale = compute_zoom_scale_increase(get_thumb_size());
954            save_persistent_thumbnail_scale();
955            set_thumb_size(new_scale);
956        }
957    }
958
959    public void decrease_zoom_level() {
960        if (connected_slider != null) {
961            connected_slider.decrease_step();
962        } else {
963            int new_scale = compute_zoom_scale_decrease(get_thumb_size());
964            save_persistent_thumbnail_scale();
965            set_thumb_size(new_scale);
966        }
967    }
968
969    public virtual DataView create_thumbnail(DataSource source) {
970        return new Thumbnail((MediaSource) source, get_thumb_size());
971    }
972
973    // this is a view-level operation on this page only; it does not affect the persistent global
974    // thumbnail scale
975    public void set_thumb_size(int new_scale) {
976        if (get_thumb_size() == new_scale || !is_in_view())
977            return;
978
979        new_scale = new_scale.clamp(Thumbnail.MIN_SCALE, Thumbnail.MAX_SCALE);
980        get_checkerboard_layout().set_scale(new_scale);
981
982        // when doing mass operations on LayoutItems, freeze individual notifications
983        get_view().freeze_notifications();
984        get_view().set_property(Thumbnail.PROP_SIZE, new_scale);
985        get_view().thaw_notifications();
986
987        set_action_sensitive("IncreaseSize", new_scale < Thumbnail.MAX_SCALE);
988        set_action_sensitive("DecreaseSize", new_scale > Thumbnail.MIN_SCALE);
989    }
990
991    public int get_thumb_size() {
992        if (get_checkerboard_layout().get_scale() <= 0)
993            get_checkerboard_layout().set_scale(Config.Facade.get_instance().get_photo_thumbnail_scale());
994
995        return get_checkerboard_layout().get_scale();
996    }
997}
998
999