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

7
class SlideshowPage : SinglePhotoPage {
8
    private const int CHECK_ADVANCE_MSEC = 250;
9 10 11
    
    private CheckerboardPage controller;
    private Thumbnail thumbnail;
12
    private Gdk.Pixbuf next_pixbuf = null;
13 14
    private Gtk.Toolbar toolbar = new Gtk.Toolbar();
    private Gtk.ToolButton play_pause_button;
15
    private Gtk.ToolButton settings_button;
16 17 18
    private Timer timer = new Timer();
    private bool playing = true;
    private bool exiting = false;
19 20

    public signal void hide_toolbar();
21
    
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
    private class SettingsDialog : Gtk.Dialog {
        Gtk.Entry delay_entry;
        double delay;
        Gtk.HScale hscale;

        private bool update_entry(Gtk.ScrollType scroll, double new_value) {
            new_value = new_value.clamp(Config.SLIDESHOW_DELAY_MIN, Config.SLIDESHOW_DELAY_MAX);

            delay_entry.set_text("%.1f".printf(new_value));
            return false;
        }

        private void check_text() { //rename this function
            // parse through text, set delay
            string delay_text = delay_entry.get_text();
            delay_text.canon("0123456789.",'?');
            delay_text = delay_text.replace("?","");
         
            delay = delay_text.to_double();
            delay_entry.set_text(delay_text);

            delay = delay.clamp(Config.SLIDESHOW_DELAY_MIN, Config.SLIDESHOW_DELAY_MAX);
            hscale.set_value(delay);
        }        

        public SettingsDialog() {
            delay = Config.get_instance().get_slideshow_delay();

            set_modal(true);
            set_transient_for(AppWindow.get_fullscreen());

            add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 
                        Gtk.STOCK_OK, Gtk.ResponseType.OK);
            set_title("Settings");

            Gtk.Label delay_label = new Gtk.Label("Delay:");
            Gtk.Label units_label = new Gtk.Label("seconds");   
            delay_entry = new Gtk.Entry();
            delay_entry.set_max_length(5);
            delay_entry.set_text("%.1f".printf(delay));
            delay_entry.set_width_chars(4);
            delay_entry.set_activates_default(true);
            delay_entry.changed += check_text;

            Gtk.Adjustment adjustment = new Gtk.Adjustment(delay, Config.SLIDESHOW_DELAY_MIN, Config.SLIDESHOW_DELAY_MAX + 1, 0.1, 1, 1);
            hscale = new Gtk.HScale(adjustment);
            hscale.set_draw_value(false);
            hscale.set_size_request(150,-1);
            hscale.change_value += update_entry;

            Gtk.HBox query = new Gtk.HBox(false, 0);
            query.pack_start(delay_label, false, false, 3);
            query.pack_start(hscale, true, true, 3);
            query.pack_start(delay_entry, false, false, 3);
            query.pack_start(units_label, false, false, 3);

            set_default_response(Gtk.ResponseType.OK);

            vbox.pack_start(query, true, false, 6);
        }

        public double get_delay() {
84
            return delay;
85 86 87
        }
    }

88 89 90 91 92 93
    public SlideshowPage(CheckerboardPage controller, Thumbnail start) {
        base("Slideshow");
        
        this.controller = controller;
        this.thumbnail = start;
        
94 95
        set_default_interp(QUALITY_INTERP);
        
96 97 98 99
        // add toolbar buttons
        Gtk.ToolButton previous_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_GO_BACK);
        previous_button.set_label("Back");
        previous_button.set_tooltip_text("Go to the previous photo");
100
        previous_button.clicked += on_previous_manual;
101 102 103 104 105 106 107 108 109 110 111 112 113
        
        toolbar.insert(previous_button, -1);
        
        play_pause_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_MEDIA_PAUSE);
        play_pause_button.set_label("Pause");
        play_pause_button.set_tooltip_text("Pause the slideshow");
        play_pause_button.clicked += on_play_pause;
        
        toolbar.insert(play_pause_button, -1);
        
        Gtk.ToolButton next_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_GO_FORWARD);
        next_button.set_label("Next");
        next_button.set_tooltip_text("Go to the next photo");
114
        next_button.clicked += on_next_manual;
115 116
        
        toolbar.insert(next_button, -1);
117 118 119 120 121 122 123

        settings_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_PREFERENCES);
        settings_button.set_label("Settings");
        settings_button.set_tooltip_text("Change slideshow settings");
        settings_button.clicked += on_change_settings;
        
        toolbar.insert(settings_button, -1);
124 125 126 127 128 129 130
    }
    
    public override Gtk.Toolbar get_toolbar() {
        return toolbar;
    }
    
    public override void switched_to() {
131 132
        base.switched_to();

133
        // since the canvas might not be ready at this point, start with screen-sized photo
134
        set_pixbuf(thumbnail.get_photo().get_pixbuf(Scaling.for_screen()));
135

136
        // start the auto-advance timer
137 138
        Timeout.add(CHECK_ADVANCE_MSEC, auto_advance);
        timer.start();
139 140 141
        
        // prefetch the next pixbuf so it's ready when auto-advance fires
        schedule_prefetch();
142 143 144
    }
    
    public override void switching_from() {
145 146
        base.switching_from();

147 148 149
        exiting = true;
    }
    
150 151 152 153 154 155 156 157 158 159 160 161
    private void schedule_prefetch() {
        next_pixbuf = null;
        Idle.add(prefetch_next_pixbuf);
    }
    
    private bool prefetch_next_pixbuf() {
        // if multiple prefetches get lined up in the queue, this stops them from doing multiple
        // pipelines
        if (next_pixbuf != null)
            return false;
        
        Thumbnail next = (Thumbnail) controller.get_next_item(thumbnail);
162
        next_pixbuf = next.get_photo().get_pixbuf(get_canvas_scaling());
163 164 165 166
        
        return false;
    }
    
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    private void on_play_pause() {
        if (playing) {
            play_pause_button.set_stock_id(Gtk.STOCK_MEDIA_PLAY);
            play_pause_button.set_label("Play");
            play_pause_button.set_tooltip_text("Continue the slideshow");
        } else {
            play_pause_button.set_stock_id(Gtk.STOCK_MEDIA_PAUSE);
            play_pause_button.set_label("Pause");
            play_pause_button.set_tooltip_text("Pause the slideshow");
        }
        
        playing = !playing;
        
        // reset the timer
        timer.start();
    }
    
184 185
    private void on_previous_manual() {
        manual_advance((Thumbnail) controller.get_previous_item(thumbnail));
186 187
    }
    
188
    private void on_next_automatic() {
189
        thumbnail = (Thumbnail) controller.get_next_item(thumbnail);
190
        
191 192 193 194
        // if prefetch didn't happen in time, get pixbuf now
        Gdk.Pixbuf pixbuf = next_pixbuf;
        if (pixbuf == null) {
            warning("Slideshow prefetch was not ready");
195
            next_pixbuf = thumbnail.get_photo().get_pixbuf(get_canvas_scaling());
196 197 198
        }
        
        set_pixbuf(pixbuf);
199 200 201
        
        // reset the timer
        timer.start();
202 203 204
        
        // prefetch the next pixbuf
        schedule_prefetch();
205 206
    }
    
207 208 209 210
    private void on_next_manual() {
        manual_advance((Thumbnail) controller.get_next_item(thumbnail));
    }
    
211 212 213
    private void manual_advance(Thumbnail thumbnail) {
        this.thumbnail = thumbnail;
        
214
        // start with blown-up preview
215
        set_pixbuf(thumbnail.get_photo().get_preview_pixbuf(get_canvas_scaling()));
216 217 218 219
        
        // schedule improvement to real photo
        Idle.add(on_improvement);
        
220
        // reset the advance timer
221
        timer.start();
222 223 224
        
        // prefetch the next pixbuf
        schedule_prefetch();
225 226 227
    }
    
    private bool on_improvement() {
228
        set_pixbuf(thumbnail.get_photo().get_pixbuf(get_canvas_scaling()));
229 230 231 232
        
        return false;
    }
    
233 234 235 236 237 238 239
    private bool auto_advance() {
        if (exiting)
            return false;
        
        if (!playing)
            return true;
        
240
        if (timer.elapsed() < Config.get_instance().get_slideshow_delay())
241 242
            return true;
        
243
        on_next_automatic();
244 245 246 247 248 249 250 251 252 253 254 255 256
        
        return true;
    }
    
    private override bool key_press_event(Gdk.EventKey event) {
        bool handled = true;
        switch (Gdk.keyval_name(event.keyval)) {
            case "space":
                on_play_pause();
            break;
            
            case "Left":
            case "KP_Left":
257
                on_previous_manual();
258 259 260 261
            break;
            
            case "Right":
            case "KP_Right":
262
                on_next_manual();
263 264 265 266 267 268 269 270 271 272 273 274
            break;
            
            default:
                handled = false;
            break;
        }
        
        if (handled)
            return true;
        
        return (base.key_press_event != null) ? base.key_press_event(event) : true;
    }
275

276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    private void on_change_settings() {
        SettingsDialog settings_dialog = new SettingsDialog();
        settings_dialog.show_all();
        bool slideshow_playing = playing;
        playing = false;
        hide_toolbar();

        int response = settings_dialog.run();
        if (response == Gtk.ResponseType.OK) {
            // sync with the config setting so it will persist
            Config.get_instance().set_slideshow_delay(settings_dialog.get_delay());
        }

        settings_dialog.destroy();
        playing = slideshow_playing;
        timer.start();
    }

294 295 296 297 298 299 300 301
    public override int get_queryable_count() {
        return 1;
    }

    public override int get_selected_queryable_count() {
        return get_queryable_count();
    }

302
    public override Gee.Iterable<Queryable>? get_queryables() {
303
        Gee.ArrayList<LibraryPhoto> photo_array_list = new Gee.ArrayList<LibraryPhoto>();
304 305 306 307 308 309 310
        photo_array_list.add(thumbnail.get_photo());
        return photo_array_list;
    }

    public override Gee.Iterable<Queryable>? get_selected_queryables() {
        return get_queryables();
    }
311 312
}

313
public class CollectionPage : CheckerboardPage {
314 315 316 317 318 319 320 321 322 323 324 325
    public const int SORT_BY_MIN = 0;
    public const int SORT_BY_NAME = 0;
    public const int SORT_BY_EXPOSURE_DATE = 1;
    public const int SORT_BY_MAX = 1;
    
    public const int SORT_ORDER_MIN = 0;
    public const int SORT_ORDER_ASCENDING = 0;
    public const int SORT_ORDER_DESCENDING = 1;
    public const int SORT_ORDER_MAX = 1;
    
    public const int DEFAULT_SORT_BY = SORT_BY_EXPOSURE_DATE;
    public const int DEFAULT_SORT_ORDER = SORT_ORDER_DESCENDING;
326 327

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

331 332
    private const int IMPROVAL_PRIORITY = Priority.LOW;
    private const int IMPROVAL_DELAY_MS = 250;
333
    
Jim Nelson's avatar
Jim Nelson committed
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
    private class CompareName : Comparator<LayoutItem> {
        public override int64 compare(LayoutItem a, LayoutItem b) {
            string namea = ((Thumbnail) a).get_title();
            string nameb = ((Thumbnail) b).get_title();
            
            return strcmp(namea, nameb);
        }
    }
    
    private class ReverseCompareName : Comparator<LayoutItem> {
        public override int64 compare(LayoutItem a, LayoutItem b) {
            string namea = ((Thumbnail) a).get_title();
            string nameb = ((Thumbnail) b).get_title();
            
            return strcmp(nameb, namea);
        }
    }
    
    private class CompareDate : Comparator<LayoutItem> {
        public override int64 compare(LayoutItem a, LayoutItem b) {
            time_t timea = ((Thumbnail) a).get_photo().get_exposure_time();
            time_t timeb = ((Thumbnail) b).get_photo().get_exposure_time();
            
            return timea - timeb;
        }
    }
    
    private class ReverseCompareDate : Comparator<LayoutItem> {
        public override int64 compare(LayoutItem a, LayoutItem b) {
            time_t timea = ((Thumbnail) a).get_photo().get_exposure_time();
            time_t timeb = ((Thumbnail) b).get_photo().get_exposure_time();
            
            return timeb - timea;
        }
    }
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 403 404 405 406 407 408 409 410 411 412 413
    private class InternalPhotoCollection : Object, PhotoCollection {
        private CollectionPage page;
        
        public InternalPhotoCollection(CollectionPage page) {
            this.page = page;
        }
        
        public int get_count() {
            return page.get_count();
        }
        
        public PhotoBase? get_first_photo() {
            Thumbnail? thumbnail = (Thumbnail) page.get_first_item();
            
            return (thumbnail != null) ? thumbnail.get_photo() : null;
        }
        
        public PhotoBase? get_last_photo() {
            Thumbnail? thumbnail = (Thumbnail) page.get_last_item();
            
            return (thumbnail != null) ? thumbnail.get_photo() : null;
        }
        
        public PhotoBase? get_next_photo(PhotoBase current) {
            Thumbnail? thumbnail = page.get_thumbnail_for_photo((LibraryPhoto) current);
            if (thumbnail == null)
                return null;

            thumbnail = (Thumbnail) page.get_next_item(thumbnail);
            
            return (thumbnail != null) ? thumbnail.get_photo() : null;
        }
        
        public PhotoBase? get_previous_photo(PhotoBase current) {
            Thumbnail? thumbnail = page.get_thumbnail_for_photo((LibraryPhoto) current);
            if (thumbnail == null)
                return null;
            
            thumbnail = (Thumbnail) page.get_previous_item(thumbnail);
            
            return (thumbnail != null) ? thumbnail.get_photo() : null;
        }
    }
    
414 415
    private static Gtk.Adjustment slider_adjustment = null;
    
416 417
    private Gtk.Toolbar toolbar = new Gtk.Toolbar();
    private Gtk.HScale slider = null;
418
    private Gtk.ToolButton rotate_button = null;
419
    private Gtk.ToolButton slideshow_button = null;
420
    private int scale = Thumbnail.DEFAULT_SCALE;
421
    private bool improval_scheduled = false;
422
    private bool reschedule_improval = false;
423
    private Gee.ArrayList<File> drag_items = new Gee.ArrayList<File>();
424
    private bool thumbs_resized = false;
425 426
    private Gee.HashMap<LibraryPhoto, Thumbnail> thumbnail_map = 
        new Gee.HashMap<LibraryPhoto, Thumbnail>(direct_hash, direct_equal, direct_equal);
427

428 429
    // TODO: Mark fields for translation
    private const Gtk.ActionEntry[] ACTIONS = {
430
        { "FileMenu", null, "_File", null, null, on_file_menu },
431
        { "Export", Gtk.STOCK_SAVE_AS, "_Export Photos...", "<Ctrl>E", "Export selected photos to disk", on_export },
432 433

        { "EditMenu", null, "_Edit", null, null, on_edit_menu },
434 435 436
        { "SelectAll", Gtk.STOCK_SELECT_ALL, "Select _All", "<Ctrl>A", "Select all the photos in the library", on_select_all },
        { "Remove", Gtk.STOCK_DELETE, "_Remove", "Delete", "Remove the selected photos from the library", on_remove },
        
437
        { "PhotosMenu", null, "_Photos", null, null, on_photos_menu },
438 439
        { "IncreaseSize", Gtk.STOCK_ZOOM_IN, "Zoom _In", "bracketright", "Increase the magnification of the thumbnails", on_increase_size },
        { "DecreaseSize", Gtk.STOCK_ZOOM_OUT, "Zoom _Out", "bracketleft", "Decrease the magnification of the thumbnails", on_decrease_size },
440 441 442
        { "RotateClockwise", Resources.CLOCKWISE, "Rotate _Right", "<Ctrl>R", "Rotate the selected photos clockwise", on_rotate_clockwise },
        { "RotateCounterclockwise", Resources.COUNTERCLOCKWISE, "Rotate _Left", "<Ctrl><Shift>R", "Rotate the selected photos counterclockwise", on_rotate_counterclockwise },
        { "Mirror", Resources.MIRROR, "_Mirror", "<Ctrl>M", "Make mirror images of the selected photos", on_mirror },
443
        { "Revert", Gtk.STOCK_REVERT_TO_SAVED, "Re_vert to Original", null, "Revert to original photo", on_revert },
444
        { "Slideshow", Gtk.STOCK_MEDIA_PLAY, "_Slideshow", "F5", "Play a slideshow", on_slideshow },
445
        
446
        { "ViewMenu", null, "_View", null, null, on_view_menu },
447
        { "SortPhotos", null, "Sort _Photos", null, null, null },
448
        
449
        { "HelpMenu", null, "_Help", null, null, null }
450 451
    };
    
Jim Nelson's avatar
Jim Nelson committed
452 453 454 455
    private const Gtk.ToggleActionEntry[] TOGGLE_ACTIONS = {
        { "ViewTitle", null, "_Titles", "<Ctrl><Shift>T", "Display the title of each photo", on_display_titles, true }
    };
    
Jim Nelson's avatar
Jim Nelson committed
456 457 458 459 460 461
    private const Gtk.RadioActionEntry[] SORT_CRIT_ACTIONS = {
        { "SortByName", null, "By _Name", null, "Sort photos by name", SORT_BY_NAME },
        { "SortByExposureDate", null, "By Exposure _Date", null, "Sort photos by exposure date", SORT_BY_EXPOSURE_DATE }
    };
    
    private const Gtk.RadioActionEntry[] SORT_ORDER_ACTIONS = {
462 463
        { "SortAscending", Gtk.STOCK_SORT_ASCENDING, "_Ascending", null, "Sort photos in an ascending order", SORT_ORDER_ASCENDING },
        { "SortDescending", Gtk.STOCK_SORT_DESCENDING, "D_escending", null, "Sort photos in a descending order", SORT_ORDER_DESCENDING }
464 465
    };
    
466 467 468
    public CollectionPage(string? page_name = null, string? ui_filename = null, 
        Gtk.ActionEntry[]? child_actions = null) {
        base(page_name != null ? page_name : "Photos");
Jim Nelson's avatar
Jim Nelson committed
469
        
Jim Nelson's avatar
Jim Nelson committed
470
        init_ui_start("collection.ui", "CollectionActionGroup", ACTIONS, TOGGLE_ACTIONS);
471 472
        action_group.add_radio_actions(SORT_CRIT_ACTIONS, DEFAULT_SORT_BY, on_sort_changed);
        action_group.add_radio_actions(SORT_ORDER_ACTIONS, DEFAULT_SORT_ORDER, on_sort_changed);
473 474 475 476 477

        if (ui_filename != null)
            init_load_ui(ui_filename);
        
        if (child_actions != null)
478
            action_group.add_actions(child_actions, this);
479
        
Jim Nelson's avatar
Jim Nelson committed
480
        init_ui_bind("/CollectionMenuBar");
481
        init_context_menu("/CollectionContextMenu");
482
        
483
        set_layout_comparator(get_sort_comparator());
Jim Nelson's avatar
Jim Nelson committed
484
        
485 486 487 488 489
        // adjustment which is shared by all sliders in the application
        if (slider_adjustment == null)
            slider_adjustment = new Gtk.Adjustment(scale_to_slider(scale), 0, 
                scale_to_slider(Thumbnail.MAX_SCALE), 1, 10, 0);
        
490 491
        // set up page's toolbar (used by AppWindow for layout)
        //
492
        // rotate tool
493
        rotate_button = new Gtk.ToolButton.from_stock(Resources.CLOCKWISE);
494 495
        rotate_button.set_label(Resources.ROTATE_CLOCKWISE_LABEL);
        rotate_button.set_tooltip_text(Resources.ROTATE_CLOCKWISE_TOOLTIP);
496 497
        rotate_button.sensitive = false;
        rotate_button.clicked += on_rotate_clockwise;
498
        
499
        toolbar.insert(rotate_button, -1);
500
        
501 502 503 504 505 506 507 508 509
        // slideshow button
        slideshow_button = new Gtk.ToolButton.from_stock(Gtk.STOCK_MEDIA_PLAY);
        slideshow_button.set_label("Slideshow");
        slideshow_button.set_tooltip_text("Start a slideshow of these photos");
        slideshow_button.sensitive = false;
        slideshow_button.clicked += on_slideshow;
        
        toolbar.insert(slideshow_button, -1);
        
510 511 512 513 514 515 516
        // separator to force slider to right side of toolbar
        Gtk.SeparatorToolItem separator = new Gtk.SeparatorToolItem();
        separator.set_expand(true);
        separator.set_draw(false);
        
        toolbar.insert(separator, -1);
        
517
        // thumbnail size slider
518
        slider = new Gtk.HScale(slider_adjustment);
519 520 521 522 523 524 525
        slider.value_changed += on_slider_changed;
        slider.set_draw_value(false);

        Gtk.ToolItem toolitem = new Gtk.ToolItem();
        toolitem.add(slider);
        toolitem.set_expand(false);
        toolitem.set_size_request(200, -1);
526
        toolitem.set_tooltip_text("Adjust the size of the thumbnails");
527
        
528
        toolbar.insert(toolitem, -1);
529
        
530 531 532
        // initialize scale from slider (since the scale adjustment may be modified from default)
        scale = slider_to_scale(slider.get_value());

533
        // scrollbar policy
Jim Nelson's avatar
Jim Nelson committed
534 535
        set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
        
536 537
        // this schedules thumbnail improvement whenever the window is scrolled (and new
        // thumbnails may be exposed)
538 539 540
        get_hadjustment().value_changed += schedule_thumbnail_improval;
        get_vadjustment().value_changed += schedule_thumbnail_improval;
        
541 542
        show_all();

543
        schedule_thumbnail_improval();
544 545

        enable_drag_source(Gdk.DragAction.COPY);
Jim Nelson's avatar
Jim Nelson committed
546 547
    }
    
548
    public override Gtk.Toolbar get_toolbar() {
549 550 551
        return toolbar;
    }
    
552
    public override void switched_to() {
553 554
        base.switched_to();

555 556 557 558 559 560 561 562 563 564 565 566 567 568
        // if the thumbnails were resized while viewing another page, resize the ones on this page
        // now ... set_thumb_size does the refresh and thumbnail improval, so don't schedule if
        // going this route
        if (thumbs_resized) {
            set_thumb_size(slider_to_scale(slider.get_value()));
            thumbs_resized = false;
        } else {
            // need to refresh the layout in case any of the thumbnail dimensions were altered while we
            // were gone
            refresh();
            
            // schedule improvement in case any new photos were added
            schedule_thumbnail_improval();
        }
569 570
    }
    
571 572
    public override void returning_from_fullscreen() {
        refresh();
573 574
        
        base.returning_from_fullscreen();
575 576
    }
    
577
    protected override void selection_changed(int count) {
578
        rotate_button.sensitive = (count > 0);
579 580
    }
    
581 582
    protected override void on_item_activated(LayoutItem item) {
        Thumbnail thumbnail = (Thumbnail) item;
583
        
584
        // switch to full-page view
585
        debug("switching to %s", thumbnail.get_photo().to_string());
586

587
        LibraryWindow.get_app().switch_to_photo_page(this, thumbnail);
588 589
    }
    
590 591 592 593 594
    public override Gtk.Menu? get_context_menu() {
        // don't show a context menu if nothing is selected
        return (get_selected_count() != 0) ? base.get_context_menu() : null;
    }
    
595 596 597 598 599 600 601 602 603 604 605 606 607
    protected override bool on_context_invoked(Gtk.Menu context_menu) {
        bool selected = (get_selected_count() > 0);
        bool revert_possible = can_revert_selected();
        
        set_item_sensitive("/CollectionContextMenu/ContextRemove", selected);
        set_item_sensitive("/CollectionContextMenu/ContextRotateClockwise", selected);
        set_item_sensitive("/CollectionContextMenu/ContextRotateCounterclockwise", selected);
        set_item_sensitive("/CollectionContextMenu/ContextMirror", selected);
        set_item_sensitive("/CollectionContextMenu/ContextRevert", selected && revert_possible);
        
        return true;
    }
    
608 609 610 611 612 613 614 615 616 617 618
    public override LayoutItem? get_fullscreen_photo() {
        Gee.Iterable<LayoutItem> iter = null;
        
        // if no selection, use the first item
        if (get_selected_count() > 0) {
            iter = get_selected();
        } else {
            iter = get_items();
        }
        
        // use the first item of the selected collection to start things off
619
        foreach (LayoutItem item in iter)
620 621 622 623 624
            return item;
        
        return null;
    }
    
625 626 627 628
    public PhotoCollection get_photo_collection() {
        return new InternalPhotoCollection(this);
    }
    
629
    protected override void on_resize(Gdk.Rectangle rect) {
630 631
        // this schedules thumbnail improvement whenever the window size changes (and new thumbnails
        // may be exposed), therefore, uninterested in window position move
632 633 634 635
        schedule_thumbnail_improval();
    }
    
    private override void drag_begin(Gdk.DragContext context) {
636 637
        if (get_selected_count() == 0)
            return;
638 639
        
        drag_items.clear();
640

641 642 643 644
        // because drag_data_get may be called multiple times in a single drag, prepare all the exported
        // files first
        Gdk.Pixbuf icon = null;
        foreach (LayoutItem item in get_selected()) {
645
            LibraryPhoto photo = ((Thumbnail) item).get_photo();
646 647 648 649 650 651 652 653 654 655 656 657
            
            File file = null;
            try {
                file = photo.generate_exportable();
            } catch (Error err) {
                error("%s", err.message);
            }
            
            drag_items.add(file);
            
            // set up icon using the "first" photo, although Sets are not ordered
            if (icon == null)
658
                icon = photo.get_preview_pixbuf(Scaling.for_best_fit(AppWindow.DND_ICON_SCALE));
659 660
            
            debug("Prepared %s for export", file.get_path());
661
        }
662
        
663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
        assert(icon != null);
        Gtk.drag_source_set_icon_pixbuf(get_event_source(), icon);
    }
    
    private override void drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
        uint target_type, uint time) {
        assert(target_type == TargetType.URI_LIST);
        
        if (drag_items.size == 0)
            return;
        
        // prepare list of uris
        string[] uris = new string[drag_items.size];
        int ctr = 0;
        foreach (File file in drag_items)
            uris[ctr++] = file.get_uri();
        
        selection_data.set_uris(uris);
    }
    
    private override void drag_end(Gdk.DragContext context) {
        drag_items.clear();
    }
    
    private override bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
        debug("Drag failed: %d", (int) drag_result);
        
        drag_items.clear();
        
692 693 694 695
        foreach (LayoutItem item in get_selected()) {
            ((Thumbnail) item).get_photo().export_failed();
        }
        
696
        return false;
697
    }
698
    
699
    public void add_photo(LibraryPhoto photo) {
700 701 702 703
        // search for duplicates
        if (get_thumbnail_for_photo(photo) != null)
            return;
        
704
        photo.removed += on_photo_removed;
705
        photo.thumbnail_altered += on_thumbnail_altered;
706 707
        
        Thumbnail thumbnail = new Thumbnail(photo, scale);
Jim Nelson's avatar
Jim Nelson committed
708
        thumbnail.display_title(display_titles());
709 710
        
        add_item(thumbnail);
711
        thumbnail_map.set(photo, thumbnail);
712 713
        
        slideshow_button.sensitive = true;
714
    }
715
    
716 717
    // This method is called when the LibraryPhoto -- not the Thumbnail -- is removed from the
    // system, which can happen anywhere (whereas only this page can remove its thumbnails)
718
    private void on_photo_removed(LibraryPhoto photo) {
719
        Thumbnail found = get_thumbnail_for_photo(photo);
720
        if (found != null) {
721
            remove_item(found);
722 723
            thumbnail_map.remove(photo);
        }
724 725
        
        slideshow_button.sensitive = (get_count() > 0);
726 727
    }
    
728
    private void on_thumbnail_altered(LibraryPhoto photo) {
729
        queryable_altered(photo);
730

731 732 733 734
        // the thumbnail is only going to reload a low-quality interp, so schedule improval
        schedule_thumbnail_improval();
        
        // since the geometry might have changed, refresh the layout
735
        if (is_in_view())
736
            refresh();
737 738
    }
    
739
    private Thumbnail? get_thumbnail_for_photo(LibraryPhoto photo) {
740
        return thumbnail_map.get(photo);
741 742
    }
    
743
    public int increase_thumb_size() {
744
        if (scale == Thumbnail.MAX_SCALE)
745
            return scale;
746
        
747
        scale += MANUAL_STEPPING;
748
        if (scale > Thumbnail.MAX_SCALE)
749
            scale = Thumbnail.MAX_SCALE;
750
        
751
        set_thumb_size(scale);
752
        
753
        return scale;
754 755
    }
    
756
    public int decrease_thumb_size() {
757
        if (scale == Thumbnail.MIN_SCALE)
758
            return scale;
759
        
760
        scale -= MANUAL_STEPPING;
761
        if (scale < Thumbnail.MIN_SCALE)
762 763
            scale = Thumbnail.MIN_SCALE;
        
764 765
        set_thumb_size(scale);

766 767 768
        return scale;
    }
    
769 770 771
    public void set_thumb_size(int new_scale) {
        assert(new_scale >= Thumbnail.MIN_SCALE);
        assert(new_scale <= Thumbnail.MAX_SCALE);
772
        
773
        scale = new_scale;
774
        
775
        foreach (LayoutItem item in get_items())
776
            ((Thumbnail) item).resize(scale);
777
        
778 779 780 781
        if (is_in_view()) {
            refresh();
            schedule_thumbnail_improval();
        }
782
    }
783
    
784
    private void schedule_thumbnail_improval() {
785
        // don't bother if not in view
786
        if (!is_in_view())
787 788
            return;
            
789 790
        if (improval_scheduled == false) {
            improval_scheduled = true;
791
            Timeout.add_full(IMPROVAL_PRIORITY, IMPROVAL_DELAY_MS, improve_thumbnail_quality);
792 793
        } else {
            reschedule_improval = true;
794 795 796 797
        }
    }
    
    private bool improve_thumbnail_quality() {
798 799 800 801 802 803
        if (reschedule_improval) {
            reschedule_improval = false;
            
            return true;
        }

804 805
        foreach (LayoutItem item in get_items()) {
            Thumbnail thumbnail = (Thumbnail) item;
806
            if (thumbnail.is_exposed())
807 808 809 810 811 812 813 814
                thumbnail.paint_high_quality();
        }
        
        improval_scheduled = false;
        
        debug("improve_thumbnail_quality");
        
        return false;
815
    }
816 817 818 819 820 821
    
    private void on_file_menu() {
        set_item_sensitive("/CollectionMenuBar/FileMenu/Export", get_selected_count() > 0);
    }
    
    private void on_export() {
822
        Gee.ArrayList<LibraryPhoto> export_list = new Gee.ArrayList<LibraryPhoto>();
823 824 825 826 827
        foreach (LayoutItem item in get_selected())
            export_list.add(((Thumbnail) item).get_photo());

        if (export_list.size == 0)
            return;
828 829 830

        ExportDialog export_dialog = new ExportDialog(
            "Export Photo%s".printf(export_list.size > 1 ? "s" : ""));
831 832 833 834 835 836 837 838 839
        
        int scale;
        ScaleConstraint constraint;
        Jpeg.Quality quality;
        if (!export_dialog.execute(out scale, out constraint, out quality))
            return;

        // handle the single-photo case
        if (export_list.size == 1) {
840
            LibraryPhoto photo = export_list.get(0);
841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864
            
            File save_as = ExportUI.choose_file(photo.get_file());
            if (save_as == null)
                return;
                
            spin_event_loop();
            
            try {
                photo.export(save_as, scale, constraint, quality);
            } catch (Error err) {
                AppWindow.error_message("Unable to export photo %s: %s".printf(
                    photo.get_file().get_path(), err.message));
            }
            
            return;
        }

        // multiple photos
        File export_dir = ExportUI.choose_dir();
        if (export_dir == null)
            return;
        
        AppWindow.get_instance().set_busy_cursor();
        
865
        foreach (LibraryPhoto photo in export_list) {
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883
            File save_as = export_dir.get_child(photo.get_file().get_basename());
            if (save_as.query_exists(null)) {
                if (!ExportUI.query_overwrite(save_as))
                    continue;
            }
            
            spin_event_loop();

            try {
                photo.export(save_as, scale, constraint, quality);
            } catch (Error err) {
                AppWindow.error_message("Unable to export photo %s: %s".printf(save_as.get_path(),
                    err.message));
            }
        }
        
        AppWindow.get_instance().set_normal_cursor();
    }
884

885
    private void on_edit_menu() {
886 887
        set_item_sensitive("/CollectionMenuBar/EditMenu/SelectAll", get_count() > 0);
        set_item_sensitive("/CollectionMenuBar/EditMenu/Remove", get_selected_count() > 0);
888 889 890 891 892
    }
    
    private void on_select_all() {
        select_all();
    }
893
    
894 895
    private bool can_revert_selected() {
        foreach (LayoutItem item in get_selected()) {
896
            LibraryPhoto photo = ((Thumbnail) item).get_photo();
897 898 899 900 901 902 903
            if (photo.has_transformations())
                return true;
        }
        
        return false;
    }
    
904
    protected virtual void on_photos_menu() {
905
        bool selected = (get_selected_count() > 0);
906
        bool revert_possible = can_revert_selected();
907
        
908 909
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/IncreaseSize", scale < Thumbnail.MAX_SCALE);
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/DecreaseSize", scale > Thumbnail.MIN_SCALE);
910 911 912
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/RotateClockwise", selected);
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/RotateCounterclockwise", selected);
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/Mirror", selected);
913
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/Revert", selected && revert_possible);
914
        set_item_sensitive("/CollectionMenuBar/PhotosMenu/Slideshow", get_count() > 0);
915
    }
916
    
917
    private void on_increase_size() {
918
        increase_thumb_size();
919
        slider.set_value(scale_to_slider(scale));
920 921 922
    }

    private void on_decrease_size() {
923
        decrease_thumb_size();
924
        slider.set_value(scale_to_slider(scale));
925 926
    }

927
    private void on_remove() {
928 929 930
        if (get_selected_count() == 0)
            return;

931
        Gtk.MessageDialog dialog = new Gtk.MessageDialog(AppWindow.get_instance(), Gtk.DialogFlags.MODAL,
932 933 934 935 936 937
            Gtk.MessageType.WARNING, Gtk.ButtonsType.CANCEL,
            "If you remove these photos from your library you will lose all edits you've made to "
            + "them.  Shotwell can also delete the files from your drive.\n\nThis action cannot be undone.");
        dialog.add_button(Gtk.STOCK_DELETE, Gtk.ResponseType.NO);
        dialog.add_button("Keep files", Gtk.ResponseType.YES);
        dialog.title = "Remove photos?";
938

939 940 941 942
        Gtk.ResponseType result = (Gtk.ResponseType) dialog.run();
        
        dialog.destroy();
        
943
        if (result != Gtk.ResponseType.YES && result != Gtk.ResponseType.NO)
944 945
            return;
            
946
        // iterate over selected photos and remove them from entire system .. this will result
947 948 949
        // in on_photo_removed being called, which we don't want in this case is because it will
        // remove from the list while iterating, so disconnect the signals and do the work here
        foreach (LayoutItem item in get_selected()) {
950
            LibraryPhoto photo = ((Thumbnail) item).get_photo();
951
            photo.removed -= on_photo_removed;
952
            photo.thumbnail_altered -= on_thumbnail_altered;
953
            
954
            photo.remove(result == Gtk.ResponseType.NO);
955 956
            
            thumbnail_map.remove(photo);
957 958 959 960
        }
        
        // now remove from page, outside of iterator
        remove_selected();
961
        
962
        refresh();
963
    }
964
    
965
    private void do_rotations(Gee.Iterable<LayoutItem> c, Rotation rotation) {
966
        bool rotation_performed = false;
967
        foreach (LayoutItem item in c) {
968
            LibraryPhoto photo = ((Thumbnail) item).get_photo();
969
            photo.rotate(rotation);
970
            
971
            rotation_performed = true;
972 973
        }
        
974 975
        // geometry could've changed
        if (rotation_performed)
976
            refresh();
977 978 979
    }

    private void on_rotate_clockwise() {
980
        do_rotations(get_selected(), Rotation.CLOCKWISE);
981 982 983
    }
    
    private void on_rotate_counterclockwise() {
984
        do_rotations(get_selected(), Rotation.COUNTERCLOCKWISE);
985 986 987
    }
    
    private void on_mirror() {
988
        do_rotations(get_selected(), Rotation.MIRROR);
989 990
    }
    
991 992 993
    private void on_revert() {
        bool revert_performed = false;
        foreach (LayoutItem item in get_selected()) {
994
            LibraryPhoto photo = ((Thumbnail) item).get_photo();
995 996 997 998 999 1000 1001 1002 1003 1004
            photo.remove_all_transformations();
            
            revert_performed = true;
        }
        
        // geometry could change
        if (revert_performed)
            refresh();
    }
    
1005 1006 1007
    private void on_slideshow() {
        if (get_count() == 0)
            return;
1008 1009 1010 1011
        
        Thumbnail thumbnail = (Thumbnail) get_fullscreen_photo();
        if (thumbnail == null)
            return;
1012 1013
            
        AppWindow.get_instance().go_fullscreen(new FullscreenWindow(new SlideshowPage(this,
1014
            thumbnail)));
1015 1016
    }

1017 1018 1019 1020
    private void on_view_menu() {
        set_item_sensitive("/CollectionMenuBar/ViewMenu/Fullscreen", get_count() > 0);
    }
    
Jim Nelson's avatar
Jim Nelson committed
1021 1022 1023 1024 1025 1026 1027 1028
    private bool display_titles() {
        Gtk.ToggleAction action = (Gtk.ToggleAction) ui.get_action("/CollectionMenuBar/ViewMenu/ViewTitle");
        
        return action.get_active();
    }
    
    private void on_display_titles(Gtk.Action action) {
        bool display = ((Gtk.ToggleAction) action).get_active();
1029
        
1030
        foreach (LayoutItem item in get_items())
Jim Nelson's avatar
Jim Nelson committed
1031
            item.display_title(display);
1032
        
1033
        refresh();
1034 1035
    }
    
1036
    private static double scale_to_slider(int value) {
1037 1038 1039
        assert(value >= Thumbnail.MIN_SCALE);
        assert(value <= Thumbnail.MAX_SCALE);
        
1040
        return (double) ((value - Thumbnail.MIN_SCALE) / SLIDER_STEPPING);
1041 1042
    }
    
1043
    private static int slider_to_scale(double value) {
1044
        int res = ((int) (value * SLIDER_STEPPING)) + Thumbnail.MIN_SCALE;
1045 1046 1047 1048 1049 1050 1051 1052

        assert(res >= Thumbnail.MIN_SCALE);
        assert(res <= Thumbnail.MAX_SCALE);
        
        return res;
    }
    
    private void on_slider_changed() {
1053 1054 1055 1056 1057 1058
        if (!is_in_view()) {
            thumbs_resized = true;
            
            return;
        }
        
1059
        set_thumb_size(slider_to_scale(slider.get_value()));
1060
    }
1061
    
1062
    private override bool on_ctrl_pressed(Gdk.EventKey event) {
1063
        rotate_button.set_stock_id(Resources.COUNTERCLOCKWISE);
1064 1065
        rotate_button.set_label(Resources.ROTATE_COUNTERCLOCKWISE_LABEL);
        rotate_button.set_tooltip_text(Resources.ROTATE_COUNTERCLOCKWISE_TOOLTIP);
1066 1067
        rotate_button.clicked -= on_rotate_clockwise;
        rotate_button.clicked += on_rotate_counterclockwise;
1068 1069
        
        return false;
1070 1071
    }
    
1072
    private override bool on_ctrl_released(Gdk.EventKey event) {
1073
        rotate_button.set_stock_id(Resources.CLOCKWISE);
1074 1075
        rotate_button.set_label(Resources.ROTATE_CLOCKWISE_LABEL);
        rotate_button.set_tooltip_text(Resources.ROTATE_CLOCKWISE_TOOLTIP);
1076 1077
        rotate_button.clicked -= on_rotate_counterclockwise;
        rotate_button.clicked += on_rotate_clockwise;
1078 1079
        
        return false;
1080
    }
Jim Nelson's avatar
Jim Nelson committed
1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
    
    private int get_sort_criteria() {
        // any member of the group knows the current value
        Gtk.RadioAction action = (Gtk.RadioAction) ui.get_action("/CollectionMenuBar/ViewMenu/SortPhotos/SortByName");
        assert(action != null);
        
        int value = action.get_current_value();
        assert(value >= SORT_BY_MIN);
        assert(value <= SORT_BY_MAX);
        
        return value;
    }
    
    private int get_sort_order() {
        // any member of the group knows the current value
        Gtk.RadioAction action = (Gtk.RadioAction) ui.get_action("/CollectionMenuBar/ViewMenu/SortPhotos/SortAscending");
        assert(action != null);
        
        int value = action.get_current_value();
        assert(value >= SORT_ORDER_MIN);
        assert(value <= SORT_ORDER_MAX);
        
        return value;
    }
    
1106 1107 1108 1109
    private bool is_sort_ascending() {
        return get_sort_order() == SORT_ORDER_ASCENDING;
    }
    
Jim Nelson's avatar
Jim Nelson committed
1110
    private void on_sort_changed() {
1111 1112 1113 1114 1115
        set_layout_comparator(get_sort_comparator());
        refresh();
    }
    
    private Comparator<LayoutItem> get_sort_comparator() {
Jim Nelson's avatar
Jim Nelson committed
1116
        switch (get_sort_criteria()) {
Jim Nelson's avatar
Jim Nelson committed
1117
            case SORT_BY_NAME:
1118 1119
                if (is_sort_ascending())
                    return new CompareName();
Jim Nelson's avatar
Jim Nelson committed
1120
                else
1121
                    return new ReverseCompareName();
Jim Nelson's avatar
Jim Nelson committed
1122
            
Jim Nelson's avatar
Jim Nelson committed
1123
            case SORT_BY_EXPOSURE_DATE:
1124 1125
                if (is_sort_ascending())
                    return new CompareDate();
Jim Nelson's avatar
Jim Nelson committed
1126
                else
1127
                    return new ReverseCompareDate();
Jim Nelson's avatar
Jim Nelson committed
1128 1129 1130
            
            default:
                error("Unknown sort criteria: %d", get_sort_criteria());
1131 1132
                
                return new CompareName();
Jim Nelson's avatar
Jim Nelson committed
1133 1134
        }
    }
Jim Nelson's avatar
Jim Nelson committed
1135 1136
}