Page.vala 66.4 KB
Newer Older
1
/* Copyright 2009-2010 Yorba Foundation
2 3 4 5
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
 * See the COPYING file in this distribution. 
 */
6

7
public abstract class Page : Gtk.ScrolledWindow, SidebarPage {
8
    private const int CONSIDER_CONFIGURE_HALTED_MSEC = 400;
9
    
10
    public Gtk.UIManager ui = new Gtk.UIManager();
11
    public Gtk.ActionGroup action_group = null;
12
    public Gtk.ActionGroup common_action_group = null;
13
    
14
    private string page_name;
15
    private ViewCollection view = null;
16
    private Gtk.Window container = null;
17
    private Gtk.Toolbar toolbar = new Gtk.Toolbar();
18
    private string menubar_path = null;
19
    private SidebarMarker marker = null;
20 21 22
    private Gdk.Rectangle last_position = Gdk.Rectangle();
    private Gtk.Widget event_source = null;
    private bool dnd_enabled = false;
23
    private bool in_view = false;
24 25 26
    private ulong last_configure_ms = 0;
    private bool report_move_finished = false;
    private bool report_resize_finished = false;
27
    private Gdk.Point last_down = Gdk.Point();
28
    private bool is_destroyed = false;
29 30 31
    protected bool ctrl_pressed = false;
    protected bool alt_pressed = false;
    protected bool shift_pressed = false;
32
    
33 34
    public Page(string page_name) {
        this.page_name = page_name;
35
        this.view = new ViewCollection("ViewCollection for Page %s".printf(page_name));
36
        
37 38
        last_down = { -1, -1 };
        
39
        set_flags(Gtk.WidgetFlags.CAN_FOCUS);
40 41

        popup_menu += on_context_keypress;
42
    }
43 44
    
    ~Page() {
45
#if TRACE_DTORS
46
        debug("DTOR: Page %s", page_name);
47 48 49
#endif
    }
    
50
    private void destroy_ui_manager_widgets(Gtk.UIManagerItemType item_type) {
51
        SList<weak Gtk.Widget> toplevels = ui.get_toplevels(item_type);
52 53 54 55
        for (int ctr = 0; ctr < toplevels.length(); ctr++)
            toplevels.nth(ctr).data.destroy();
    }
    
56 57
    // This is called by the page controller when it has removed this page ... pages should override
    // this (or the signal) to clean up
58
    public override void destroy() {
59 60
        debug("Page %s Destroyed", get_page_name());

61
        // untie signals
62
        detach_event_source();
63
        view.close();
64 65 66 67
        
        // remove refs to external objects which may be pointing to the Page
        clear_marker();
        clear_container();
68 69 70 71 72 73 74 75 76 77 78 79 80 81
        
        // Without destroying the menubar, Gtk spits out an assertion related to
        // a Gtk.AccelLabel being unable to disconnect a signal from the UI Manager's
        // Gtk.AccelGroup because the AccelGroup has already been finalized.  This only
        // happens during a drag-and-drop operation where the Page is destroyed.
        // Destroying the menubar explicitly solves this problem.  These calls go through and
        // ensure *all* widgets created by the UI manager are destroyed.
        destroy_ui_manager_widgets(Gtk.UIManagerItemType.MENUBAR);
        destroy_ui_manager_widgets(Gtk.UIManagerItemType.TOOLBAR);
        destroy_ui_manager_widgets(Gtk.UIManagerItemType.POPUP);
        
        // toolbars (as of yet) are not created by the UI Manager and need to be destroyed
        // explicitly
        toolbar.destroy();
82 83

        is_destroyed = true;
84 85
        
        base.destroy();
86 87
    }
    
88 89 90 91
    public string get_page_name() {
        return page_name;
    }
    
92
    public virtual void set_page_name(string page_name) {
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
        this.page_name = page_name;
    }
    
    public ViewCollection get_view() {
        return view;
    }
    
    // Usually when a controller is needed to iterate through a page's ViewCollection, the page's
    // own ViewCollection is the right choice.  Some pages may keep their own ViewCollection but
    // are actually referring to another ViewController for input (such as PhotoPage, whose own
    // ViewCollection merely refers to what's currently on the page while it uses another 
    // ViewController to flip through a collection of thumbnails).
    public virtual ViewCollection get_controller() {
        return view;
    }
    
109 110 111 112 113 114 115 116 117 118 119 120 121 122
    public Gtk.Window? get_container() {
        return container;
    }
    
    public virtual void set_container(Gtk.Window container) {
        assert(this.container == null);
        
        this.container = container;
    }
    
    public virtual void clear_container() {
        container = null;
    }
    
123 124 125 126
    public void set_event_source(Gtk.Widget event_source) {
        assert(this.event_source == null);

        this.event_source = event_source;
127
        event_source.set_flags(Gtk.WidgetFlags.CAN_FOCUS);
128

129 130 131 132
        // interested in mouse button and motion events on the event source
        event_source.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK
            | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK
            | Gdk.EventMask.BUTTON_MOTION_MASK);
133 134
        event_source.button_press_event += on_button_pressed_internal;
        event_source.button_release_event += on_button_released_internal;
135
        event_source.motion_notify_event += on_motion_internal;
136
        event_source.scroll_event += on_mousewheel_internal;
137 138
    }
    
139 140 141 142 143 144 145
    private void detach_event_source() {
        if (event_source == null)
            return;
        
        event_source.button_press_event -= on_button_pressed_internal;
        event_source.button_release_event -= on_button_released_internal;
        event_source.motion_notify_event -= on_motion_internal;
146
        event_source.scroll_event -= on_mousewheel_internal;
147 148
        
        disable_drag_source();
149 150
        
        event_source = null;
151 152
    }
    
153 154
    public Gtk.Widget? get_event_source() {
        return event_source;
155 156
    }
    
157 158 159 160
    public string get_sidebar_text() {
        return page_name;
    }
    
161
    public void set_marker(SidebarMarker marker) {
162
        this.marker = marker;
163 164
    }
    
165
    public SidebarMarker? get_marker() {
166
        return marker;
167 168
    }
    
169
    public void clear_marker() {
170
        marker = null;
171 172
    }
    
173
    public virtual Gtk.MenuBar get_menubar() {
174 175 176
        assert(menubar_path != null);
        
        return (Gtk.MenuBar) ui.get_widget(menubar_path);
177
    }
178

179 180 181 182 183 184 185
    public virtual Gtk.Toolbar get_toolbar() {
        return toolbar;
    }
    
    public virtual Gtk.Menu? get_page_context_menu() {
        return null;
    }
186 187
    
    public virtual void switching_from() {
188
        in_view = false;
189 190 191
    }
    
    public virtual void switched_to() {
192
        in_view = true;
193
        update_modifiers();
194 195 196 197
    }
    
    public bool is_in_view() {
        return in_view;
198 199
    }
    
200 201 202 203 204 205
    public virtual void switching_to_fullscreen() {
    }
    
    public virtual void returning_from_fullscreen() {
    }
    
206
    public void set_item_sensitive(string path, bool sensitive) {
207 208 209 210 211 212 213 214
        Gtk.Widget widget = ui.get_widget(path);
        if (widget == null) {
            critical("No widget for UI element %s", path);
            
            return;
        }
        
        widget.sensitive = sensitive;
215
    }
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
    
    public void set_item_display(string path, string? label, string? tooltip, bool sensitive) {
        Gtk.Action? action = ui.get_action(path);
        if (action == null) {
            critical("No action for UI element %s", path);
            
            return;
        }
        
        if (label != null)
            action.label = label;
        
        if (tooltip != null)
            action.tooltip = tooltip;
        
        action.sensitive = sensitive;
    }
    
Jim Nelson's avatar
Jim Nelson committed
234 235 236 237 238 239 240 241 242 243 244
    public void set_action_sensitive(string name, bool sensitive) {
        Gtk.Action action = action_group.get_action(name);
        if (action == null) {
            warning("Page %s: Unable to locate action %s", get_page_name(), name);
            
            return;
        }
        
        action.sensitive = sensitive;
    }
    
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
    public void set_action_visible(string name, bool sensitive) {
        Gtk.Action action = action_group.get_action(name);
        if (action == null) {
            warning("Page %s: Unable to locate action %s", get_page_name(), name);
            
            return;
        }
        
        action.visible = true;
        action.sensitive = sensitive;
    }
    
    public void set_action_hidden(string name) {
        Gtk.Action action = action_group.get_action(name);
        if (action == null) {
            warning("Page %s: Unable to locate action %s", get_page_name(), name);
            
            return;
        }
        
        action.visible = false;
        action.sensitive = false;
    }
    
269 270
    private void get_modifiers(out bool ctrl, out bool alt, out bool shift) {
            int x, y;
271
        Gdk.ModifierType mask;
272
        AppWindow.get_instance().window.get_pointer(out x, out y, out mask);
273

274 275 276 277
        ctrl = (mask & Gdk.ModifierType.CONTROL_MASK) != 0;
        alt = (mask & Gdk.ModifierType.MOD1_MASK) != 0;
        shift = (mask & Gdk.ModifierType.SHIFT_MASK) != 0;
    }
278

279 280 281 282 283
    private virtual void update_modifiers() {
        bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed;
        get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed,
            out shift_currently_pressed);
        
284 285 286 287 288 289 290 291 292 293 294 295 296 297
        if (ctrl_pressed && !ctrl_currently_pressed)
            on_ctrl_released(null);
        else if (!ctrl_pressed && ctrl_currently_pressed)
            on_ctrl_pressed(null);

        if (alt_pressed && !alt_currently_pressed)
            on_alt_released(null);
        else if (!alt_pressed && alt_currently_pressed)
            on_alt_pressed(null);

        if (shift_pressed && !shift_currently_pressed)
            on_shift_released(null);
        else if (!shift_pressed && shift_currently_pressed)
            on_shift_pressed(null);
298 299 300 301
        
        ctrl_pressed = ctrl_currently_pressed;
        alt_pressed = alt_currently_pressed;
        shift_pressed = shift_currently_pressed;
302
    }
303
    
304 305 306 307 308 309 310 311 312 313 314 315
    public PageWindow? get_page_window() {
        Gtk.Widget p = parent;
        while (p != null) {
            if (p is PageWindow)
                return (PageWindow) p;
            
            p = p.parent;
        }
        
        return null;
    }
    
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
    public CommandManager get_command_manager() {
        return AppWindow.get_command_manager();
    }
    
    private void decorate_command_manager_item(string path, string prefix, string default_explanation,
        CommandDescription? desc) {
        set_item_sensitive(path, desc != null);
        
        Gtk.Action action = ui.get_action(path);
        if (desc != null) {
            action.label = "%s %s".printf(prefix, desc.get_name());
            action.tooltip = desc.get_explanation();
        } else {
            action.label = prefix;
            action.tooltip = default_explanation;
        }
    }
    
    public void decorate_undo_item(string path) {
        decorate_command_manager_item(path, Resources.UNDO_MENU, Resources.UNDO_TOOLTIP,
            AppWindow.get_command_manager().get_undo_description());
    }
    
    public void decorate_redo_item(string path) {
        decorate_command_manager_item(path, Resources.REDO_MENU, Resources.REDO_TOOLTIP,
            AppWindow.get_command_manager().get_redo_description());
    }
    
344 345 346 347
    protected void init_ui(string ui_filename, string? menubar_path, string action_group_name, 
        Gtk.ActionEntry[]? entries = null, Gtk.ToggleActionEntry[]? toggle_entries = null) {
        init_ui_start(ui_filename, action_group_name, entries, toggle_entries);
        init_ui_bind(menubar_path);
Jim Nelson's avatar
Jim Nelson committed
348 349
    }
    
350
    protected void init_load_ui(string ui_filename) {
351
        File ui_file = Resources.get_ui(ui_filename);
352 353

        try {
354
            ui.add_ui_from_file(ui_file.get_path());
355
        } catch (Error gle) {
356
            error("Error loading UI file %s: %s", ui_file.get_path(), gle.message);
357
        }
358 359 360 361 362 363
    }
    
    protected void init_ui_start(string ui_filename, string action_group_name,
        Gtk.ActionEntry[]? entries = null, Gtk.ToggleActionEntry[]? toggle_entries = null) {
        init_load_ui(ui_filename);

364
        action_group = new Gtk.ActionGroup(action_group_name);
Jim Nelson's avatar
Jim Nelson committed
365
        if (entries != null)
366
            action_group.add_actions(entries, this);
367
        if (toggle_entries != null)
368
            action_group.add_toggle_actions(toggle_entries, this);
Jim Nelson's avatar
Jim Nelson committed
369 370
    }
    
Jim Nelson's avatar
Jim Nelson committed
371 372 373
    protected virtual void init_actions(int selected_count, int count) {
    }
    
374
    protected void init_ui_bind(string? menubar_path) {
375 376
        this.menubar_path = menubar_path;
        
377
        ui.insert_action_group(action_group, 0);
378 379 380
        
        common_action_group = new Gtk.ActionGroup("CommonActionGroup");
        AppWindow.get_instance().add_common_actions(common_action_group);
381
        ui.insert_action_group(common_action_group, 0);
382 383
        
        ui.ensure_update();
Jim Nelson's avatar
Jim Nelson committed
384 385
        
        init_actions(get_view().get_selected_count(), get_view().get_count());
386 387
    }
    
388 389
    // This method enables drag-and-drop on the event source and routes its events through this
    // object
390
    public void enable_drag_source(Gdk.DragAction actions, Gtk.TargetEntry[] source_target_entries) {
391 392 393 394 395
        if (dnd_enabled)
            return;
            
        assert(event_source != null);
        
396 397 398 399
        Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, source_target_entries, actions);
        
        // hook up handlers which route the event_source's DnD signals to the Page's (necessary
        // because Page is a NO_WINDOW widget and cannot support DnD on its own).
400 401 402 403 404 405 406 407 408
        event_source.drag_begin += on_drag_begin;
        event_source.drag_data_get += on_drag_data_get;
        event_source.drag_data_delete += on_drag_data_delete;
        event_source.drag_end += on_drag_end;
        event_source.drag_failed += on_drag_failed;
        
        dnd_enabled = true;
    }
    
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
    public void disable_drag_source() {
        if (!dnd_enabled)
            return;

        assert(event_source != null);
        
        event_source.drag_begin -= on_drag_begin;
        event_source.drag_data_get -= on_drag_data_get;
        event_source.drag_data_delete -= on_drag_data_delete;
        event_source.drag_end -= on_drag_end;
        event_source.drag_failed -= on_drag_failed;
        Gtk.drag_source_unset(event_source);
        
        dnd_enabled = false;
    }
    
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
    public bool is_dnd_enabled() {
        return dnd_enabled;
    }
    
    private void on_drag_begin(Gdk.DragContext context) {
        drag_begin(context);
    }
    
    private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data,
        uint info, uint time) {
        drag_data_get(context, selection_data, info, time);
    }
    
    private void on_drag_data_delete(Gdk.DragContext context) {
        drag_data_delete(context);
    }
    
    private void on_drag_end(Gdk.DragContext context) {
        drag_end(context);
    }
    
    // wierdly, Gtk 2.16.1 doesn't supply a drag_failed virtual method in the GtkWidget impl ...
    // Vala binds to it, but it's not available in gtkwidget.h, and so gcc complains.  Have to
    // makeshift one for now.
449
    // https://bugzilla.gnome.org/show_bug.cgi?id=584247
450 451 452 453 454 455 456 457
    public virtual bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
        return false;
    }
    
    private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) {
        return source_drag_failed(context, drag_result);
    }
    
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
    // Use this function rather than GDK or GTK's get_pointer, especially if called during a 
    // button-down mouse drag (i.e. a window grab).
    //
    // For more information, see: https://bugzilla.gnome.org/show_bug.cgi?id=599937
    public bool get_event_source_pointer(out int x, out int y, out Gdk.ModifierType mask) {
        if (event_source == null)
            return false;
        
        event_source.window.get_pointer(out x, out y, out mask);
        
        if (last_down.x < 0 || last_down.y < 0)
            return true;
            
        // check for bogus values inside a drag which goes outside the window
        // caused by (most likely) X windows signed 16-bit int overflow and fixup
        // (https://bugzilla.gnome.org/show_bug.cgi?id=599937)
        
        if ((x - last_down.x).abs() >= 0x7FFF)
            x += 0xFFFF;
        
        if ((y - last_down.y).abs() >= 0x7FFF)
            y += 0xFFFF;
        
        return true;
    }
    
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508
    protected virtual bool on_left_click(Gdk.EventButton event) {
        return false;
    }
    
    protected virtual bool on_middle_click(Gdk.EventButton event) {
        return false;
    }
    
    protected virtual bool on_right_click(Gdk.EventButton event) {
        return false;
    }
    
    protected virtual bool on_left_released(Gdk.EventButton event) {
        return false;
    }
    
    protected virtual bool on_middle_released(Gdk.EventButton event) {
        return false;
    }
    
    protected virtual bool on_right_released(Gdk.EventButton event) {
        return false;
    }
    
    private bool on_button_pressed_internal(Gdk.EventButton event) {
509 510
        switch (event.button) {
            case 1:
511 512 513
                if (event_source != null)
                    event_source.grab_focus();
                
514 515 516 517
                // stash location of mouse down for drag fixups
                last_down.x = (int) event.x;
                last_down.y = (int) event.y;
                
518
                return on_left_click(event);
519

520 521 522 523 524 525 526 527 528 529
            case 2:
                return on_middle_click(event);
            
            case 3:
                return on_right_click(event);
            
            default:
                return false;
        }
    }
530 531 532 533
    
    private bool on_button_released_internal(Gdk.EventButton event) {
        switch (event.button) {
            case 1:
534 535 536
                // clear when button released, only for drag fixups
                last_down = { -1, -1 };
                
537 538 539 540 541 542 543 544 545 546 547 548
                return on_left_released(event);
            
            case 2:
                return on_middle_released(event);
            
            case 3:
                return on_right_released(event);
            
            default:
                return false;
        }
    }
549

550
    protected virtual bool on_ctrl_pressed(Gdk.EventKey? event) {
551
        return false;
552 553
    }
    
554
    protected virtual bool on_ctrl_released(Gdk.EventKey? event) {
555
        return false;
556 557
    }
    
558
    protected virtual bool on_alt_pressed(Gdk.EventKey? event) {
559
        return false;
560 561
    }
    
562
    protected virtual bool on_alt_released(Gdk.EventKey? event) {
563 564 565
        return false;
    }
    
566
    protected virtual bool on_shift_pressed(Gdk.EventKey? event) {
567
        return false;
568 569
    }
    
570
    protected virtual bool on_shift_released(Gdk.EventKey? event) {
571
        return false;
572 573
    }
    
574 575 576 577 578 579 580 581
    protected virtual bool on_app_key_pressed(Gdk.EventKey event) {
        return false;
    }
    
    protected virtual bool on_app_key_released(Gdk.EventKey event) {
        return false;
    }
    
582
    public bool notify_app_key_pressed(Gdk.EventKey event) {
583 584 585 586
        bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed;
        get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed,
            out shift_currently_pressed);

587 588 589
        switch (Gdk.keyval_name(event.keyval)) {
            case "Control_L":
            case "Control_R":
590 591 592
                if (!ctrl_currently_pressed || ctrl_pressed)
                    return false;

593 594 595
                ctrl_pressed = true;
                
                return on_ctrl_pressed(event);
596 597 598

            case "Meta_L":
            case "Meta_R":
599 600
            case "Alt_L":
            case "Alt_R":
601 602 603
                if (!alt_currently_pressed || alt_pressed)
                    return false;

604 605 606 607 608 609
                alt_pressed = true;
                
                return on_alt_pressed(event);
            
            case "Shift_L":
            case "Shift_R":
610 611 612
                if (!shift_currently_pressed || shift_pressed)
                    return false;

613 614 615
                shift_pressed = true;
                
                return on_shift_pressed(event);
616
        }
617
        
618
        return on_app_key_pressed(event);
619
    }
620
    
621
    public bool notify_app_key_released(Gdk.EventKey event) {
622 623 624 625
        bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed;
        get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed,
            out shift_currently_pressed);

626 627 628
        switch (Gdk.keyval_name(event.keyval)) {
            case "Control_L":
            case "Control_R":
629 630 631
                if (ctrl_currently_pressed || !ctrl_pressed)
                    return false;

632 633 634 635
                ctrl_pressed = false;
                
                return on_ctrl_released(event);
            
636 637
            case "Meta_L":
            case "Meta_R":
638 639
            case "Alt_L":
            case "Alt_R":
640 641 642
                if (alt_currently_pressed || !alt_pressed)
                    return false;

643 644 645 646 647 648
                alt_pressed = false;
                
                return on_alt_released(event);
            
            case "Shift_L":
            case "Shift_R":
649 650 651
                if (shift_currently_pressed || !shift_pressed)
                    return false;

652 653 654
                shift_pressed = false;
                
                return on_shift_released(event);
655
        }
656
        
657
        return on_app_key_released(event);
658
    }
659
    
660 661
    public bool notify_app_focus_in(Gdk.EventFocus event) {
        update_modifiers();
662
        
663 664 665 666 667 668
        return false;
    }

    public bool notify_app_focus_out(Gdk.EventFocus event) {
        return false;
    }
669 670 671 672
    
    protected virtual void on_move(Gdk.Rectangle rect) {
    }
    
673 674 675 676 677 678
    protected virtual void on_move_start(Gdk.Rectangle rect) {
    }
    
    protected virtual void on_move_finished(Gdk.Rectangle rect) {
    }
    
679
    protected virtual void on_resize(Gdk.Rectangle rect) {
680
    }
681
    
682 683 684 685 686 687
    protected virtual void on_resize_start(Gdk.Rectangle rect) {
    }
    
    protected virtual void on_resize_finished(Gdk.Rectangle rect) {
    }
    
688 689 690 691 692
    protected virtual bool on_configure(Gdk.EventConfigure event, Gdk.Rectangle rect) {
        return false;
    }
    
    public bool notify_configure_event(Gdk.EventConfigure event) {
693 694 695 696 697 698
        Gdk.Rectangle rect = Gdk.Rectangle();
        rect.x = event.x;
        rect.y = event.y;
        rect.width = event.width;
        rect.height = event.height;
        
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
        // special case events, to report when a configure first starts (and appears to end)
        if (last_configure_ms == 0) {
            if (last_position.x != rect.x || last_position.y != rect.y) {
                on_move_start(rect);
                report_move_finished = true;
            }
            
            if (last_position.width != rect.width || last_position.height != rect.height) {
                on_resize_start(rect);
                report_resize_finished = true;
            }

            // need to check more often then the timeout, otherwise it could be up to twice the
            // wait time before it's noticed
            Timeout.add(CONSIDER_CONFIGURE_HALTED_MSEC / 8, check_configure_halted);
        }
715 716 717 718 719 720 721
        
        if (last_position.x != rect.x || last_position.y != rect.y)
            on_move(rect);
        
        if (last_position.width != rect.width || last_position.height != rect.height)
            on_resize(rect);
        
722
        last_position = rect;
723 724
        last_configure_ms = now_ms();

725
        return on_configure(event, rect);
726
    }
727
    
728
    private bool check_configure_halted() {
729 730 731
        if (is_destroyed)
            return false;

732 733
        if ((now_ms() - last_configure_ms) < CONSIDER_CONFIGURE_HALTED_MSEC)
            return true;
734
        
735 736 737 738 739 740 741 742 743 744 745 746 747
        if (report_move_finished)
            on_move_finished((Gdk.Rectangle) allocation);
        
        if (report_resize_finished)
            on_resize_finished((Gdk.Rectangle) allocation);
        
        last_configure_ms = 0;
        report_move_finished = false;
        report_resize_finished = false;
        
        return false;
    }
    
748 749 750 751 752 753 754 755
    protected virtual bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
        return false;
    }
    
    private bool on_motion_internal(Gdk.EventMotion event) {
        int x, y;
        Gdk.ModifierType mask;
        if (event.is_hint) {
756
            get_event_source_pointer(out x, out y, out mask);
757 758 759 760 761 762 763 764
        } else {
            x = (int) event.x;
            y = (int) event.y;
            mask = event.state;
        }
        
        return on_motion(event, x, y, mask);
    }
765

766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
    private bool on_mousewheel_internal(Gdk.EventScroll event) {
        switch (event.direction) {
            case Gdk.ScrollDirection.UP:
                return on_mousewheel_up(event);

            case Gdk.ScrollDirection.DOWN:
                return on_mousewheel_down(event);
            
            case Gdk.ScrollDirection.LEFT:
                return on_mousewheel_left(event);

            case Gdk.ScrollDirection.RIGHT:
                return on_mousewheel_right(event);
           
            default:
                return false;
        }
    }
    
    protected virtual bool on_mousewheel_up(Gdk.EventScroll event) {
        return false;
    }
    
    protected virtual bool on_mousewheel_down(Gdk.EventScroll event) {
        return false;
    }
    
    protected virtual bool on_mousewheel_left(Gdk.EventScroll event) {
        return false;
    }
    
    protected virtual bool on_mousewheel_right(Gdk.EventScroll event) {
        return false;
    }
    
801 802 803 804 805 806 807 808 809 810 811 812 813 814 815
    protected virtual bool on_context_keypress() {
        return false;
    }
    
    protected virtual bool on_context_buttonpress(Gdk.EventButton event) {
        return false;
    }
    
    protected virtual bool on_context_invoked() {
        return true;
    }

    protected bool popup_context_menu(Gtk.Menu? context_menu,
        Gdk.EventButton? event = null) {

816
        if (context_menu == null || !on_context_invoked())
817 818 819 820 821 822 823 824 825
            return false;

        if (event == null)
            context_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
        else
            context_menu.popup(null, null, null, event.button, event.time);

        return true;
    }
826 827 828
}

public abstract class CheckerboardPage : Page {
829 830
    private const int AUTOSCROLL_PIXELS = 50;
    private const int AUTOSCROLL_TICKS_MSEC = 50;
831
    
832
    private CheckerboardLayout layout;
833 834
    private Gtk.Menu item_context_menu = null;
    private Gtk.Menu page_context_menu = null;
835
    private Gtk.Viewport viewport = new Gtk.Viewport(null, null);
836 837 838
    protected CheckerboardItem anchor = null;
    protected CheckerboardItem cursor = null;
    private CheckerboardItem highlighted = null;
839
    private bool autoscroll_scheduled = false;
840
    private CheckerboardItem activated_item = null;
841
    private Gee.ArrayList<CheckerboardItem> previously_selected = null;
842

Jim Nelson's avatar
Jim Nelson committed
843
    public CheckerboardPage(string page_name) {
844
        base(page_name);
Jim Nelson's avatar
Jim Nelson committed
845
        
846
        layout = new CheckerboardLayout(get_view());
847
        layout.set_name(page_name);
848
        
849
        set_event_source(layout);
850

851 852 853
        set_border_width(0);
        set_shadow_type(Gtk.ShadowType.NONE);
        
854 855 856 857 858
        viewport.set_border_width(0);
        viewport.set_shadow_type(Gtk.ShadowType.NONE);
        
        viewport.add(layout);
        
859 860 861
        // want to set_adjustments before adding to ScrolledWindow to let our signal handlers
        // run first ... otherwise, the thumbnails draw late
        layout.set_adjustments(get_hadjustment(), get_vadjustment());
862
        
863
        add(viewport);
864 865 866
        
        // need to monitor items going hidden when dealing with anchor/cursor/highlighted items
        get_view().items_hidden += on_items_hidden;
Jim Nelson's avatar
Jim Nelson committed
867 868 869
        
        // scrollbar policy
        set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
870 871
    }
    
872 873
    public void init_item_context_menu(string path) {
        item_context_menu = (Gtk.Menu) ui.get_widget(path);
874
    }
875 876 877 878 879 880 881

    public void init_page_context_menu(string path) {
        page_context_menu = (Gtk.Menu) ui.get_widget(path);
    }
   
    public Gtk.Menu? get_context_menu() {
        // show page context menu if nothing is selected
882 883
        return (get_view().get_selected_count() != 0) ? get_item_context_menu() :
            get_page_context_menu();
884 885 886 887
    }
    
    public virtual Gtk.Menu? get_item_context_menu() {
        return item_context_menu;
888
    }
889
    
890 891
    public override Gtk.Menu? get_page_context_menu() {
        return page_context_menu;
892 893
    }
    
894 895
    protected override bool on_context_keypress() {
        return popup_context_menu(get_context_menu());
896 897
    }
    
898
    protected virtual void on_item_activated(CheckerboardItem item) {
899 900
    }
    
901 902 903 904
    public CheckerboardLayout get_checkerboard_layout() {
        return layout;
    }
    
905 906
    public override void switching_from() {
        layout.set_in_view(false);
907 908 909

        // unselect everything so selection won't persist after page loses focus 
        get_view().unselect_all();
910 911 912 913 914
        
        base.switching_from();
    }
    
    public override void switched_to() {
915
        layout.set_in_view(true);       
916

917 918 919
        base.switched_to();
    }
    
920
    public abstract CheckerboardItem? get_fullscreen_photo();
921
    
922 923
    public void set_page_message(string message) {
        layout.set_message(message);
924 925
        if (is_in_view())
            layout.queue_draw();
Jim Nelson's avatar
Jim Nelson committed
926 927
    }
    
928 929 930 931 932 933
    public override void set_page_name(string name) {
        base.set_page_name(name);
        
        layout.set_name(name);
    }
    
934
    public CheckerboardItem? get_item_at_pixel(double x, double y) {
935
        return layout.get_item_at_pixel(x, y);
936
    }
937 938 939
    
    private void on_items_hidden(Gee.Iterable<DataView> hidden) {
        foreach (DataView view in hidden) {
940
            CheckerboardItem item = (CheckerboardItem) view;
941 942 943 944 945 946 947 948 949 950 951
            
            if (anchor == item)
                anchor = null;
            
            if (cursor == item)
                cursor = null;
            
            if (highlighted == item)
                highlighted = null;
        }
    }
952

953 954
    protected override bool key_press_event(Gdk.EventKey event) {
        bool handled = true;
955 956 957

        // mask out the modifiers we're interested in
        uint state = event.state & Gdk.ModifierType.SHIFT_MASK;
958
        
959 960 961 962
        switch (Gdk.keyval_name(event.keyval)) {
            case "Up":
            case "KP_Up":
                move_cursor(CompassPoint.NORTH);
963
                select_anchor_to_cursor(state);
964 965 966 967 968
            break;
            
            case "Down":
            case "KP_Down":
                move_cursor(CompassPoint.SOUTH);
969
                select_anchor_to_cursor(state);
970 971 972 973 974
            break;
            
            case "Left":
            case "KP_Left":
                move_cursor(CompassPoint.WEST);
975
                select_anchor_to_cursor(state);
976 977 978 979 980
            break;
            
            case "Right":
            case "KP_Right":
                move_cursor(CompassPoint.EAST);
981
                select_anchor_to_cursor(state);
982 983 984 985
            break;
            
            case "Home":
            case "KP_Home":
986
                CheckerboardItem? first = (CheckerboardItem?) get_view().get_first();
987 988
                if (first != null)
                    cursor_to_item(first);
989
                select_anchor_to_cursor(state);
990 991 992 993
            break;
            
            case "End":
            case "KP_End":
994
                CheckerboardItem? last = (CheckerboardItem?) get_view().get_last();
995 996
                if (last != null)
                    cursor_to_item(last);
997
                select_anchor_to_cursor(state);
998 999 1000 1001
            break;
            
            case "Return":
            case "KP_Enter":
1002
                if (get_view().get_selected_count() == 1)
1003
                    on_item_activated((CheckerboardItem) get_view().get_selected_at(0));
1004
                else
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
                    handled = false;
            break;
            
            default:
                handled = false;
            break;
        }
        
        if (handled)
            return true;
        
        return (base.key_press_event != null) ? base.key_press_event(event) : true;
    }
    
    protected override bool on_left_click(Gdk.EventButton event) {
1020
        // only interested in single-click and double-clicks for now
1021
        if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS))
1022 1023 1024 1025 1026
            return false;
        
        // mask out the modifiers we're interested in
        uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK);
        
1027 1028
        // use clicks for multiple selection and activation only; single selects are handled by
        // button release, to allow for multiple items to be selected then dragged
1029
        CheckerboardItem item = get_item_at_pixel(event.x, event.y);
1030 1031
        if (item != null) {
            switch (state) {
1032
                case Gdk.ModifierType.CONTROL_MASK:
1033 1034
                    // with only Ctrl pressed, multiple selections are possible ... chosen item
                    // is toggled
1035 1036
                    Marker marker = get_view().mark(item);
                    get_view().toggle_marked(marker);
1037

1038 1039 1040 1041
                    if (item.is_selected()) {
                        anchor = item;
                        cursor = item;
                    }
1042
                break;
1043
                
1044
                case Gdk.ModifierType.SHIFT_MASK:
1045
                    get_view().unselect_all();
1046 1047 1048 1049
                    
                    if (anchor == null)
                        anchor = item;
                    
1050
                    select_between_items(anchor, item);
1051
                    
1052
                    cursor = item;
1053
                break;
1054
                
1055
                case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
1056
                    // TODO
1057
                break;
1058
                
1059
                default:
1060
                    if (event.type == Gdk.EventType.2BUTTON_PRESS) {
1061
                        activated_item = item;
1062
                    } else {
1063 1064 1065 1066 1067
                        // if the user has selected one or more items and is preparing for a drag,
                        // don't want to blindly unselect: if they've clicked on an unselected item
                        // unselect all and select that one; if they've clicked on a previously
                        // selected item, do nothing
                        if (!item.is_selected()) {
1068
                            get_view().unselect_all();
1069 1070
                            get_view().select_marked(get_view().mark(item));
                        }
1071
                    }
1072

1073 1074
                    anchor = item;
                    cursor = item;
1075
                break;
1076 1077
            }
        } else {
1078 1079 1080 1081
            // user clicked on "dead" area; only unselect if control is not pressed
            // do we want similar behavior for shift as well?
            if (state != Gdk.ModifierType.CONTROL_MASK)
                get_view().unselect_all();
1082

1083 1084 1085 1086
            // grab previously marked items
            previously_selected = new Gee.ArrayList<CheckerboardItem>();
            foreach (DataView view in get_view().get_selected())
                previously_selected.add((CheckerboardItem) view);
1087

1088
            layout.set_drag_select_origin((int) event.x, (int) event.y);
1089 1090 1091

            return true;
        }
1092

1093 1094 1095
        // need to determine if the signal should be passed to the DnD handlers
        // Return true to block the DnD handler, false otherwise

1096
        return get_view().get_selected_count() == 0;
1097 1098
    }
    
1099
    protected override bool on_left_released(Gdk.EventButton event) {
1100 1101
        previously_selected = null;

1102
        // if drag-selecting, stop here and do nothing else
1103
        if (layout.is_drag_select_active()) {
1104
            layout.clear_drag_select();
1105
            anchor = cursor;
1106

1107 1108 1109
            return true;
        }
        
1110 1111 1112
        // only interested in non-modified button releases
        if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0)
            return false;
1113 1114 1115 1116 1117
        
        // if the item was activated in the double-click, report it now
        if (activated_item != null) {
            on_item_activated(activated_item);
            activated_item = null;
1118
            
1119 1120 1121
            return true;
        }
        
1122
        CheckerboardItem item = get_item_at_pixel(event.x, event.y);
1123 1124
        if (item == null) {
            // released button on "dead" area
1125
            return true;
1126
        }
1127

1128
        if (cursor != item) {
1129 1130
            // user released mouse button after moving it off the initial item, or moved from dead
            // space onto one.  either way, unselect everything
1131
            get_view().unselect_all();
1132 1133 1134 1135
        } else {
            // the idea is, if a user single-clicks on an item with no modifiers, then all other items
            // should be deselected, however, if they single-click in order to drag one or more items,
            // they should remain selected, hence performing this here rather than on_left_click
1136 1137
            // (item may not be selected if an unimplemented modifier key was used)
            if (item.is_selected())
1138
                get_view().unselect_all_but(item);
1139
        }
1140 1141 1142 1143

        return true;
    }
    
1144
    protected override bool on_right_click(Gdk.EventButton event) {
1145
        // only interested in single-clicks for now
1146
        if (event.type != Gdk.EventType.BUTTON_PRESS)
1147 1148
            return false;
        
1149
        // get what's right-clicked upon
1150
        CheckerboardItem item = get_item_at_pixel(event.x, event.y);
1151
        if (item != null) {
1152 1153 1154 1155
            // mask out the modifiers we're interested in
            switch (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) {
                case Gdk.ModifierType.CONTROL_MASK:
                    // chosen item is toggled
1156 1157
                    Marker marker = get_view().mark(item);
                    get_view().toggle_marked(marker);
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171
                break;
                
                case Gdk.ModifierType.SHIFT_MASK:
                    // TODO
                break;
                
                case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK:
                    // TODO
                break;
                
                default:
                    // if the item is already selected, proceed; if item is not selected, a bare right
                    // click unselects everything else but it
                    if (!item.is_selected()) {
1172 1173 1174 1175
                        get_view().unselect_all();
                        
                        Marker marker = get_view().mark(item);
                        get_view().select_marked(marker);
1176 1177 1178 1179 1180
                    }
                break;
            }
        } else {
            // clicked in "dead" space, unselect everything
1181
            get_view().unselect_all();
1182
        }
1183
       
1184
        Gtk.Menu context_menu = get_context_menu();
1185
        return popup_context_menu(context_menu, event);
1186
    }
1187
    
1188
    protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) {
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210
        // if hovering over the last hovered item, or both are null (nothing highlighted and
        // hovering over empty space), do nothing
        if (item == highlighted)
            return true;
        
        // either something new is highlighted or now hovering over empty space, so dim old item
        if (highlighted != null) {
            highlighted.unbrighten();
            highlighted = null;
        }
        
        // if over empty space, done
        if (item == null)
            return true;
        
        // brighten the new item
        item.brighten();
        highlighted = item;
        
        return true;
    }
    
1211
    protected override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) {
1212 1213 1214 1215 1216
        // report what item the mouse is hovering over
        if (!on_mouse_over(get_item_at_pixel(x, y), x, y, mask))
            return false;
        
        // go no further if not drag-selecting
1217
        if (!layout.is_drag_select_active())
1218 1219
            return false;
        
1220 1221
        // set the new endpoint of the drag selection
        layout.set_drag_select_endpoint(x, y);
1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235
        
        updated_selection_band();

        // if out of bounds, schedule a check to auto-scroll the viewport
        if (!autoscroll_scheduled 
            && get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) {
            Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll);
            autoscroll_scheduled = true;
        }

        return true;
    }
    
    private void updated_selection_band() {
1236
        assert(layout.is_drag_select_active());
1237 1238
        
        // get all items inside the selection
1239
        Gee.List<CheckerboardItem>? intersection = layout.items_in_selection_band();
1240 1241
        if (intersection == null)
            return;
1242
        
1243 1244 1245 1246 1247 1248 1249 1250 1251 1252
        Marker to_unselect = get_view().start_marking();
        Marker to_select = get_view().start_marking();

        // mark all selected items to be unselected
        to_unselect.mark_many(get_view().get_selected());

        // except for the items that were selected before the drag began
        assert(previously_selected != null);
        to_unselect.unmark_many(previously_selected);        
        to_select.mark_many(previously_selected);   
1253
        
1254
        // toggle selection on everything in the intersection and update the cursor
1255
        cursor = null;
1256
        
1257
        foreach (CheckerboardItem item in intersection) {
1258 1259 1260 1261 1262
            if (to_select.toggle(item))
                to_unselect.unmark(item);
            else
                to_unselect.mark(item);

1263 1264 1265
            if (cursor == null)
                cursor = item;
        }
1266
        
1267 1268
        get_view().select_marked(to_select);
        get_view().unselect_marked(to_unselect);
1269 1270 1271
    }
    
    private bool selection_autoscroll() {
1272
        if (!layout.is_drag_select_active()) { 
1273 1274 1275 1276 1277 1278 1279
            autoscroll_scheduled = false;
            
            return false;
        }
        
        // as the viewport never scrolls horizontally, only interested in vertical
        Gtk.Adjustment vadj = get_vadjustment();
1280
        
1281 1282
        int x, y;
        Gdk.ModifierType mask;
1283
        get_event_source_pointer(out x, out y, out mask);
1284 1285 1286 1287
        
        int new_value = (int) vadj.get_value();
        switch (get_adjustment_relation(vadj, y)) {
            case AdjustmentRelation.BELOW:
1288
                // pointer above window, scroll up
1289
                new_value -= AUTOSCROLL_PIXELS;
1290
                layout.set_drag_select_endpoint(x, new_value);
1291 1292 1293
            break;
            
            case AdjustmentRelation.ABOVE:
1294
                // pointer below window, scroll down, extend selection to bottom of page
1295
                new_value += AUTOSCROLL_PIXELS;
1296
                layout.set_drag_select_endpoint(x, new_value + (int) vadj.get_page_size());
1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308
            break;
            
            case AdjustmentRelation.IN_RANGE:
                autoscroll_scheduled = false;
                
                return false;
            
            default:
                warn_if_reached();
            break;
        }
        
1309 1310 1311 1312 1313
        // It appears that in GTK+ 2.18, the adjustment is not clamped the way it was in 2.16.
        // This may have to do with how adjustments are different w/ scrollbars, that they're upper
        // clamp is upper - page_size ... either way, enforce these limits here
        vadj.set_value(new_value.clamp((int) vadj.get_lower(), 
            (int) vadj.get_upper() - (int) vadj.get_page_size()));
1314 1315 1316 1317 1318 1319
        
        updated_selection_band();
        
        return true;
    }
    
1320
    public void cursor_to_item(CheckerboardItem item) {
1321
        assert(get_view().contains(item));
1322 1323

        cursor = item;
1324 1325
        
        get_view().unselect_all();
1326
        
1327 1328
        Marker marker = get_view().mark(item);
        get_view().select_marked(marker);
1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339

        // if item is in any way out of view, scroll to it
        Gtk.Adjustment vadj = get_vadjustment();
        if (get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE
            && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == AdjustmentRelation.IN_RANGE))
            return;

        // scroll to see the new item
        int top = 0;
        if (item.allocation.y < vadj.get_value()) {
            top = item.allocation.y;
1340
            top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2;
1341 1342
        } else {
            top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size();
1343
            top += CheckerboardLayout.ROW_GUTTER_PADDING / 2;
1344 1345 1346 1347 1348 1349
        }
        
        vadj.set_value(top);
    }
    
    public void move_cursor(CompassPoint point) {
1350 1351 1352 1353
        // if no items, nothing to do
        if (get_view().get_count() == 0)
            return;
            
1354
        // if nothing is selected, simply select the first and exit
1355
        if (get_view().get_selected_count() == 0 || cursor == null) {
1356
            CheckerboardIte