1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU LGPL (version 2.1 or later). 4 * See the COPYING file in this distribution. 5 */ 6 7public class InjectionGroup { 8 public class Element { 9 public enum ItemType { 10 MENUITEM, 11 MENU, 12 SEPARATOR 13 } 14 public string name; 15 public string action; 16 public string? accellerator; 17 public ItemType kind; 18 19 public Element(string name, string? action, string? accellerator, ItemType kind) { 20 this.name = name; 21 this.action = action != null ? action : name; 22 this.accellerator = accellerator; 23 this.kind = kind; 24 } 25 } 26 27 private string path; 28 private Gee.ArrayList<Element?> elements = new Gee.ArrayList<Element?>(); 29 private int separator_id = 0; 30 31 public InjectionGroup(string path) { 32 this.path = path; 33 } 34 35 public string get_path() { 36 return path; 37 } 38 39 public Gee.List<Element?> get_elements() { 40 return elements; 41 } 42 43 public void add_menu_item(string name, string? action = null, string? accellerator = null) { 44 elements.add(new Element(name, action, accellerator, Element.ItemType.MENUITEM)); 45 } 46 47 public void add_menu(string name, string? action = null) { 48 elements.add(new Element(name, action, null, Element.ItemType.MENU)); 49 } 50 51 public void add_separator() { 52 elements.add(new Element("%d-separator".printf(separator_id++), null, 53 null, 54 Element.ItemType.SEPARATOR)); 55 } 56} 57 58public abstract class Page : Gtk.ScrolledWindow { 59 private const int CONSIDER_CONFIGURE_HALTED_MSEC = 400; 60 61 protected Gtk.Builder builder = new Gtk.Builder (); 62 protected Gtk.Toolbar toolbar; 63 protected bool in_view = false; 64 65 private string page_name; 66 private ViewCollection view = null; 67 private Gtk.Window container = null; 68 private string toolbar_path; 69 private Gdk.Rectangle last_position = Gdk.Rectangle(); 70 private Gtk.Widget event_source = null; 71 private bool dnd_enabled = false; 72 private ulong last_configure_ms = 0; 73 private bool report_move_finished = false; 74 private bool report_resize_finished = false; 75 private Gdk.Point last_down = Gdk.Point(); 76 private bool is_destroyed = false; 77 private bool ctrl_pressed = false; 78 private bool alt_pressed = false; 79 private bool shift_pressed = false; 80 private bool super_pressed = false; 81 private Gdk.CursorType last_cursor = Gdk.CursorType.LEFT_PTR; 82 private bool cursor_hidden = false; 83 private int cursor_hide_msec = 0; 84 private uint last_timeout_id = 0; 85 private int cursor_hide_time_cached = 0; 86 private bool are_actions_attached = false; 87 private OneShotScheduler? update_actions_scheduler = null; 88 89 protected Page(string page_name) { 90 this.page_name = page_name; 91 92 view = new ViewCollection("ViewCollection for Page %s".printf(page_name)); 93 94 last_down = { -1, -1 }; 95 96 set_can_focus(true); 97 98 popup_menu.connect(on_context_keypress); 99 100 realize.connect(attach_view_signals); 101 } 102 103 ~Page() { 104#if TRACE_DTORS 105 debug("DTOR: Page %s", page_name); 106#endif 107 } 108 109 // This is called by the page controller when it has removed this page ... pages should override 110 // this (or the signal) to clean up 111 public override void destroy() { 112 if (is_destroyed) 113 return; 114 115 // untie signals 116 detach_event_source(); 117 detach_view_signals(); 118 view.close(); 119 120 // remove refs to external objects which may be pointing to the Page 121 clear_container(); 122 123 if (toolbar != null) 124 toolbar.destroy(); 125 126 // halt any pending callbacks 127 if (update_actions_scheduler != null) 128 update_actions_scheduler.cancel(); 129 130 is_destroyed = true; 131 132 base.destroy(); 133 134 debug("Page %s Destroyed", get_page_name()); 135 } 136 137 public string get_page_name() { 138 return page_name; 139 } 140 141 public virtual void set_page_name(string page_name) { 142 this.page_name = page_name; 143 } 144 145 public string to_string() { 146 return page_name; 147 } 148 149 public ViewCollection get_view() { 150 return view; 151 } 152 153 public Gtk.Window? get_container() { 154 return container; 155 } 156 157 public virtual void set_container(Gtk.Window container) { 158 assert(this.container == null); 159 160 this.container = container; 161 } 162 163 public virtual void clear_container() { 164 container = null; 165 } 166 167 public void set_event_source(Gtk.Widget event_source) { 168 assert(this.event_source == null); 169 170 this.event_source = event_source; 171 event_source.set_can_focus(true); 172 173 // interested in mouse button and motion events on the event source 174 event_source.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK 175 | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.POINTER_MOTION_HINT_MASK 176 | Gdk.EventMask.BUTTON_MOTION_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK 177 | Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK); 178 event_source.button_press_event.connect(on_button_pressed_internal); 179 event_source.button_release_event.connect(on_button_released_internal); 180 event_source.motion_notify_event.connect(on_motion_internal); 181 event_source.leave_notify_event.connect(on_leave_notify_event); 182 event_source.scroll_event.connect(on_mousewheel_internal); 183 event_source.realize.connect(on_event_source_realize); 184 } 185 186 private void detach_event_source() { 187 if (event_source == null) 188 return; 189 190 event_source.button_press_event.disconnect(on_button_pressed_internal); 191 event_source.button_release_event.disconnect(on_button_released_internal); 192 event_source.motion_notify_event.disconnect(on_motion_internal); 193 event_source.leave_notify_event.disconnect(on_leave_notify_event); 194 event_source.scroll_event.disconnect(on_mousewheel_internal); 195 196 disable_drag_source(); 197 198 event_source = null; 199 } 200 201 public Gtk.Widget? get_event_source() { 202 return event_source; 203 } 204 205 private bool menubar_injected = false; 206 public GLib.MenuModel get_menubar() { 207 var model = builder.get_object ("MenuBar") as GLib.Menu; 208 if (model == null) { 209 return new GLib.Menu(); 210 } 211 212 if (!menubar_injected) { 213 // Collect injected UI elements and add them to the UI manager 214 InjectionGroup[] injection_groups = init_collect_injection_groups(); 215 foreach (InjectionGroup group in injection_groups) { 216 var items = model.get_n_items (); 217 for (int i = 0; i < items; i++) { 218 var submenu = model.get_item_link (i, GLib.Menu.LINK_SUBMENU); 219 220 var section = this.find_extension_point (submenu, 221 group.get_path ()); 222 223 if (section == null) { 224 continue; 225 } 226 227 foreach (var element in group.get_elements ()) { 228 var menu = section as GLib.Menu; 229 switch (element.kind) { 230 case InjectionGroup.Element.ItemType.MENUITEM: 231 var item = new GLib.MenuItem (element.name, 232 "win." + element.action); 233 if (element.accellerator != null) { 234 item.set_attribute ("accel", 235 "s", 236 element.accellerator); 237 } 238 239 menu.append_item (item); 240 break; 241 default: 242 break; 243 } 244 } 245 } 246 } 247 248 this.menubar_injected = true; 249 } 250 251 return model; 252 } 253 254 public virtual Gtk.Toolbar get_toolbar() { 255 if (toolbar == null) { 256 toolbar = toolbar_path == null ? new Gtk.Toolbar() : 257 builder.get_object (toolbar_path) 258 as Gtk.Toolbar; 259 toolbar.get_style_context().add_class("bottom-toolbar"); // for elementary theme 260 toolbar.set_icon_size(Gtk.IconSize.SMALL_TOOLBAR); 261 } 262 return toolbar; 263 } 264 265 public virtual Gtk.Menu? get_page_context_menu() { 266 return null; 267 } 268 269 public virtual void switching_from() { 270 in_view = false; 271 //remove_actions(AppWindow.get_instance()); 272 var map = get_container() as GLib.ActionMap; 273 if (map != null) { 274 remove_actions(map); 275 } 276 if (toolbar_path != null) 277 toolbar = null; 278 } 279 280 public virtual void switched_to() { 281 in_view = true; 282 add_ui(); 283 var map = get_container() as GLib.ActionMap; 284 if (map != null) { 285 add_actions(map); 286 } 287 int selected_count = get_view().get_selected_count(); 288 int count = get_view().get_count(); 289 init_actions(selected_count, count); 290 update_actions(selected_count, count); 291 update_modifiers(); 292 } 293 294 public virtual void ready() { 295 } 296 297 public bool is_in_view() { 298 return in_view; 299 } 300 301 public virtual void switching_to_fullscreen(FullscreenWindow fsw) { 302 add_actions(fsw); 303 } 304 305 public virtual void returning_from_fullscreen(FullscreenWindow fsw) { 306 remove_actions(fsw); 307 switched_to(); 308 } 309 310 public GLib.Action? get_action (string name) { 311 GLib.ActionMap? map = null; 312 if (container is FullscreenWindow) { 313 map = container as GLib.ActionMap; 314 } else { 315 map = AppWindow.get_instance () as GLib.ActionMap; 316 } 317 318 if (map != null) { 319 return map.lookup_action(name); 320 } 321 322 return null; 323 } 324 325 public void set_action_sensitive(string name, bool sensitive) { 326 GLib.SimpleAction? action = get_action(name) as GLib.SimpleAction; 327 if (action != null) 328 action.set_enabled (sensitive); 329 } 330 331 public void set_action_details(string name, string? label, string? tooltip, bool sensitive) { 332 GLib.SimpleAction? action = get_action(name) as GLib.SimpleAction; 333 334 if (action == null) 335 return; 336 337 if (label != null) 338 this.update_menu_item_label (name, label); 339 340 action.set_enabled (sensitive); 341 } 342 343 public void activate_action(string name) { 344 var action = get_action(name); 345 346 if (action != null) 347 action.activate (null); 348 } 349 350 public GLib.Action? get_common_action(string name, bool log_warning = true) { 351 var action = get_action (name); 352 353 if (action != null) 354 return action; 355 356 if (log_warning) 357 warning("Page %s: Unable to locate common action %s", get_page_name(), name); 358 359 return null; 360 } 361 362 public void set_common_action_sensitive(string name, bool sensitive) { 363 var action = get_common_action(name) as GLib.SimpleAction; 364 if (action != null) 365 action.set_enabled (sensitive); 366 } 367 368 public void set_common_action_label(string name, string label) { 369 debug ("Trying to set common action label for %s", name); 370 } 371 372 public void set_common_action_important(string name, bool important) { 373 debug ("Setting action to important: %s", name); 374 } 375 376 public void activate_common_action(string name) { 377 var action = get_common_action(name) as GLib.SimpleAction; 378 if (action != null) 379 action.activate(null); 380 } 381 382 public bool get_ctrl_pressed() { 383 return ctrl_pressed; 384 } 385 386 public bool get_alt_pressed() { 387 return alt_pressed; 388 } 389 390 public bool get_shift_pressed() { 391 return shift_pressed; 392 } 393 394 public bool get_super_pressed() { 395 return super_pressed; 396 } 397 398 protected void set_action_active (string name, bool active) { 399 var action = get_action (name) as GLib.SimpleAction; 400 if (action != null) { 401 action.set_state (active); 402 } 403 } 404 405 private bool get_modifiers(out bool ctrl, out bool alt, out bool shift, out bool super) { 406 if (AppWindow.get_instance().get_window() == null) { 407 ctrl = false; 408 alt = false; 409 shift = false; 410 super = false; 411 412 return false; 413 } 414 415 int x, y; 416 Gdk.ModifierType mask; 417 var seat = Gdk.Display.get_default().get_default_seat(); 418 AppWindow.get_instance().get_window().get_device_position(seat.get_pointer(), out x, out y, out mask); 419 420 ctrl = (mask & Gdk.ModifierType.CONTROL_MASK) != 0; 421 alt = (mask & Gdk.ModifierType.MOD1_MASK) != 0; 422 shift = (mask & Gdk.ModifierType.SHIFT_MASK) != 0; 423 super = (mask & Gdk.ModifierType.MOD4_MASK) != 0; // not SUPER_MASK 424 425 return true; 426 } 427 428 private void update_modifiers() { 429 bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed, 430 super_currently_pressed; 431 if (!get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed, 432 out shift_currently_pressed, out super_currently_pressed)) { 433 return; 434 } 435 436 if (ctrl_pressed && !ctrl_currently_pressed) 437 on_ctrl_released(null); 438 else if (!ctrl_pressed && ctrl_currently_pressed) 439 on_ctrl_pressed(null); 440 441 if (alt_pressed && !alt_currently_pressed) 442 on_alt_released(null); 443 else if (!alt_pressed && alt_currently_pressed) 444 on_alt_pressed(null); 445 446 if (shift_pressed && !shift_currently_pressed) 447 on_shift_released(null); 448 else if (!shift_pressed && shift_currently_pressed) 449 on_shift_pressed(null); 450 451 if(super_pressed && !super_currently_pressed) 452 on_super_released(null); 453 else if (!super_pressed && super_currently_pressed) 454 on_super_pressed(null); 455 456 ctrl_pressed = ctrl_currently_pressed; 457 alt_pressed = alt_currently_pressed; 458 shift_pressed = shift_currently_pressed; 459 super_pressed = super_currently_pressed; 460 } 461 462 public PageWindow? get_page_window() { 463 Gtk.Widget p = parent; 464 while (p != null) { 465 if (p is PageWindow) 466 return (PageWindow) p; 467 468 p = p.parent; 469 } 470 471 return null; 472 } 473 474 public CommandManager get_command_manager() { 475 return AppWindow.get_command_manager(); 476 } 477 478 protected virtual void add_actions (GLib.ActionMap map) { } 479 protected virtual void remove_actions (GLib.ActionMap map) { } 480 481 protected void on_action_toggle (GLib.Action action, Variant? value) { 482 Variant new_state = ! (bool) action.get_state (); 483 action.change_state (new_state); 484 } 485 486 protected void on_action_radio (GLib.Action action, Variant? value) { 487 action.change_state (value); 488 } 489 490 private void add_ui() { 491 // Collect all UI filenames and load them into the UI manager 492 Gee.List<string> ui_filenames = new Gee.ArrayList<string>(); 493 init_collect_ui_filenames(ui_filenames); 494 if (ui_filenames.size == 0) 495 message("No UI file specified for %s", get_page_name()); 496 497 foreach (string ui_filename in ui_filenames) 498 init_load_ui(ui_filename); 499 500 //ui.insert_action_group(action_group, 0); 501 } 502 503 public void init_toolbar(string path) { 504 toolbar_path = path; 505 } 506 507 // Called from "realize" 508 private void attach_view_signals() { 509 if (are_actions_attached) 510 return; 511 512 // initialize the Gtk.Actions according to current state 513 int selected_count = get_view().get_selected_count(); 514 int count = get_view().get_count(); 515 init_actions(selected_count, count); 516 update_actions(selected_count, count); 517 518 // monitor state changes to update actions 519 get_view().items_state_changed.connect(on_update_actions); 520 get_view().selection_group_altered.connect(on_update_actions); 521 get_view().items_visibility_changed.connect(on_update_actions); 522 get_view().contents_altered.connect(on_update_actions); 523 524 are_actions_attached = true; 525 } 526 527 // Called from destroy() 528 private void detach_view_signals() { 529 if (!are_actions_attached) 530 return; 531 532 get_view().items_state_changed.disconnect(on_update_actions); 533 get_view().selection_group_altered.disconnect(on_update_actions); 534 get_view().items_visibility_changed.disconnect(on_update_actions); 535 get_view().contents_altered.disconnect(on_update_actions); 536 537 are_actions_attached = false; 538 } 539 540 private void on_update_actions() { 541 if (update_actions_scheduler == null) { 542 update_actions_scheduler = new OneShotScheduler( 543 "Update actions scheduler for %s".printf(get_page_name()), 544 on_update_actions_on_idle); 545 } 546 547 update_actions_scheduler.at_priority_idle(Priority.LOW); 548 } 549 550 private void on_update_actions_on_idle() { 551 if (is_destroyed) 552 return; 553 554 if (!this.in_view) 555 return; 556 557 update_actions(get_view().get_selected_count(), get_view().get_count()); 558 } 559 560 private void init_load_ui(string ui_filename) { 561 var ui_resource = Resources.get_ui(ui_filename); 562 try { 563 builder.add_from_resource(ui_resource); 564 this.menubar_injected = false; 565 } catch (Error err) { 566 AppWindow.error_message("Error loading UI resource %s: %s".printf( 567 ui_resource, err.message)); 568 Application.get_instance().panic(); 569 } 570 } 571 572 // This is called during add_ui() to collect all the UI files to be loaded into the UI 573 // manager. Because order is important here, call the base method *first*, then add the 574 // classes' filename. 575 protected virtual void init_collect_ui_filenames(Gee.List<string> ui_filenames) { 576 } 577 578 // This is called during add_ui() to collect all Page.InjectedUIElements for the page. They 579 // should be added to the MultiSet using the injection path as the key. 580 protected virtual InjectionGroup[] init_collect_injection_groups() { 581 return new InjectionGroup[0]; 582 } 583 584 // This is called during "map" allowing for Gtk.Actions to be updated at 585 // initialization time. 586 protected virtual void init_actions(int selected_count, int count) { 587 } 588 589 // This is called during "map" and during ViewCollection selection, visibility, 590 // and collection content altered events. This can be used to both initialize Gtk.Actions and 591 // update them when selection or visibility has been altered. 592 protected virtual void update_actions(int selected_count, int count) { 593 } 594 595 // This method enables drag-and-drop on the event source and routes its events through this 596 // object 597 public void enable_drag_source(Gdk.DragAction actions, Gtk.TargetEntry[] source_target_entries) { 598 if (dnd_enabled) 599 return; 600 601 assert(event_source != null); 602 603 Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, source_target_entries, actions); 604 605 // hook up handlers which route the event_source's DnD signals to the Page's (necessary 606 // because Page is a NO_WINDOW widget and cannot support DnD on its own). 607 event_source.drag_begin.connect(on_drag_begin); 608 event_source.drag_data_get.connect(on_drag_data_get); 609 event_source.drag_data_delete.connect(on_drag_data_delete); 610 event_source.drag_end.connect(on_drag_end); 611 event_source.drag_failed.connect(on_drag_failed); 612 613 dnd_enabled = true; 614 } 615 616 public void disable_drag_source() { 617 if (!dnd_enabled) 618 return; 619 620 assert(event_source != null); 621 622 event_source.drag_begin.disconnect(on_drag_begin); 623 event_source.drag_data_get.disconnect(on_drag_data_get); 624 event_source.drag_data_delete.disconnect(on_drag_data_delete); 625 event_source.drag_end.disconnect(on_drag_end); 626 event_source.drag_failed.disconnect(on_drag_failed); 627 Gtk.drag_source_unset(event_source); 628 629 dnd_enabled = false; 630 } 631 632 public bool is_dnd_enabled() { 633 return dnd_enabled; 634 } 635 636 private void on_drag_begin(Gdk.DragContext context) { 637 drag_begin(context); 638 } 639 640 private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data, 641 uint info, uint time) { 642 drag_data_get(context, selection_data, info, time); 643 } 644 645 private void on_drag_data_delete(Gdk.DragContext context) { 646 drag_data_delete(context); 647 } 648 649 private void on_drag_end(Gdk.DragContext context) { 650 drag_end(context); 651 } 652 653 // wierdly, Gtk 2.16.1 doesn't supply a drag_failed virtual method in the GtkWidget impl ... 654 // Vala binds to it, but it's not available in gtkwidget.h, and so gcc complains. Have to 655 // makeshift one for now. 656 // https://bugzilla.gnome.org/show_bug.cgi?id=584247 657 public virtual bool source_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) { 658 return false; 659 } 660 661 private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) { 662 return source_drag_failed(context, drag_result); 663 } 664 665 // Use this function rather than GDK or GTK's get_pointer, especially if called during a 666 // button-down mouse drag (i.e. a window grab). 667 // 668 // For more information, see: https://bugzilla.gnome.org/show_bug.cgi?id=599937 669 public bool get_event_source_pointer(out int x, out int y, out Gdk.ModifierType mask) { 670 if (event_source == null) { 671 x = 0; 672 y = 0; 673 mask = 0; 674 675 return false; 676 } 677 678 var seat = Gdk.Display.get_default().get_default_seat(); 679 event_source.get_window().get_device_position(seat.get_pointer(), out x, out y, out mask); 680 681 if (last_down.x < 0 || last_down.y < 0) 682 return true; 683 684 // check for bogus values inside a drag which goes outside the window 685 // caused by (most likely) X windows signed 16-bit int overflow and fixup 686 // (https://bugzilla.gnome.org/show_bug.cgi?id=599937) 687 688 if ((x - last_down.x).abs() >= 0x7FFF) 689 x += 0xFFFF; 690 691 if ((y - last_down.y).abs() >= 0x7FFF) 692 y += 0xFFFF; 693 694 return true; 695 } 696 697 protected virtual bool on_left_click(Gdk.EventButton event) { 698 return false; 699 } 700 701 protected virtual bool on_middle_click(Gdk.EventButton event) { 702 return false; 703 } 704 705 protected virtual bool on_right_click(Gdk.EventButton event) { 706 return false; 707 } 708 709 protected virtual bool on_left_released(Gdk.EventButton event) { 710 return false; 711 } 712 713 protected virtual bool on_middle_released(Gdk.EventButton event) { 714 return false; 715 } 716 717 protected virtual bool on_right_released(Gdk.EventButton event) { 718 return false; 719 } 720 721 private bool on_button_pressed_internal(Gdk.EventButton event) { 722 switch (event.button) { 723 case 1: 724 if (event_source != null) 725 event_source.grab_focus(); 726 727 // stash location of mouse down for drag fixups 728 last_down.x = (int) event.x; 729 last_down.y = (int) event.y; 730 731 return on_left_click(event); 732 733 case 2: 734 return on_middle_click(event); 735 736 case 3: 737 return on_right_click(event); 738 739 default: 740 return false; 741 } 742 } 743 744 private bool on_button_released_internal(Gdk.EventButton event) { 745 switch (event.button) { 746 case 1: 747 // clear when button released, only for drag fixups 748 last_down = { -1, -1 }; 749 750 return on_left_released(event); 751 752 case 2: 753 return on_middle_released(event); 754 755 case 3: 756 return on_right_released(event); 757 758 default: 759 return false; 760 } 761 } 762 763 protected virtual bool on_ctrl_pressed(Gdk.EventKey? event) { 764 return false; 765 } 766 767 protected virtual bool on_ctrl_released(Gdk.EventKey? event) { 768 return false; 769 } 770 771 protected virtual bool on_alt_pressed(Gdk.EventKey? event) { 772 return false; 773 } 774 775 protected virtual bool on_alt_released(Gdk.EventKey? event) { 776 return false; 777 } 778 779 protected virtual bool on_shift_pressed(Gdk.EventKey? event) { 780 return false; 781 } 782 783 protected virtual bool on_shift_released(Gdk.EventKey? event) { 784 return false; 785 } 786 787 protected virtual bool on_super_pressed(Gdk.EventKey? event) { 788 return false; 789 } 790 791 protected virtual bool on_super_released(Gdk.EventKey? event) { 792 return false; 793 } 794 795 protected virtual bool on_app_key_pressed(Gdk.EventKey event) { 796 return false; 797 } 798 799 protected virtual bool on_app_key_released(Gdk.EventKey event) { 800 return false; 801 } 802 803 public bool notify_app_key_pressed(Gdk.EventKey event) { 804 bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed, 805 super_currently_pressed; 806 get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed, 807 out shift_currently_pressed, out super_currently_pressed); 808 809 switch (Gdk.keyval_name(event.keyval)) { 810 case "Control_L": 811 case "Control_R": 812 if (!ctrl_currently_pressed || ctrl_pressed) 813 return false; 814 815 ctrl_pressed = true; 816 817 return on_ctrl_pressed(event); 818 819 case "Meta_L": 820 case "Meta_R": 821 case "Alt_L": 822 case "Alt_R": 823 if (!alt_currently_pressed || alt_pressed) 824 return false; 825 826 alt_pressed = true; 827 828 return on_alt_pressed(event); 829 830 case "Shift_L": 831 case "Shift_R": 832 if (!shift_currently_pressed || shift_pressed) 833 return false; 834 835 shift_pressed = true; 836 837 return on_shift_pressed(event); 838 839 case "Super_L": 840 case "Super_R": 841 if (!super_currently_pressed || super_pressed) 842 return false; 843 844 super_pressed = true; 845 846 return on_super_pressed(event); 847 } 848 849 return on_app_key_pressed(event); 850 } 851 852 public bool notify_app_key_released(Gdk.EventKey event) { 853 bool ctrl_currently_pressed, alt_currently_pressed, shift_currently_pressed, 854 super_currently_pressed; 855 get_modifiers(out ctrl_currently_pressed, out alt_currently_pressed, 856 out shift_currently_pressed, out super_currently_pressed); 857 858 switch (Gdk.keyval_name(event.keyval)) { 859 case "Control_L": 860 case "Control_R": 861 if (ctrl_currently_pressed || !ctrl_pressed) 862 return false; 863 864 ctrl_pressed = false; 865 866 return on_ctrl_released(event); 867 868 case "Meta_L": 869 case "Meta_R": 870 case "Alt_L": 871 case "Alt_R": 872 if (alt_currently_pressed || !alt_pressed) 873 return false; 874 875 alt_pressed = false; 876 877 return on_alt_released(event); 878 879 case "Shift_L": 880 case "Shift_R": 881 if (shift_currently_pressed || !shift_pressed) 882 return false; 883 884 shift_pressed = false; 885 886 return on_shift_released(event); 887 888 case "Super_L": 889 case "Super_R": 890 if (super_currently_pressed || !super_pressed) 891 return false; 892 893 super_pressed = false; 894 895 return on_super_released(event); 896 } 897 898 return on_app_key_released(event); 899 } 900 901 public bool notify_app_focus_in(Gdk.EventFocus event) { 902 update_modifiers(); 903 904 return false; 905 } 906 907 public bool notify_app_focus_out(Gdk.EventFocus event) { 908 return false; 909 } 910 911 protected virtual void on_move(Gdk.Rectangle rect) { 912 } 913 914 protected virtual void on_move_start(Gdk.Rectangle rect) { 915 } 916 917 protected virtual void on_move_finished(Gdk.Rectangle rect) { 918 } 919 920 protected virtual void on_resize(Gdk.Rectangle rect) { 921 } 922 923 protected virtual void on_resize_start(Gdk.Rectangle rect) { 924 } 925 926 protected virtual void on_resize_finished(Gdk.Rectangle rect) { 927 } 928 929 protected virtual bool on_configure(Gdk.EventConfigure event, Gdk.Rectangle rect) { 930 return false; 931 } 932 933 public bool notify_configure_event(Gdk.EventConfigure event) { 934 Gdk.Rectangle rect = Gdk.Rectangle(); 935 rect.x = event.x; 936 rect.y = event.y; 937 rect.width = event.width; 938 rect.height = event.height; 939 940 // special case events, to report when a configure first starts (and appears to end) 941 if (last_configure_ms == 0) { 942 if (last_position.x != rect.x || last_position.y != rect.y) { 943 on_move_start(rect); 944 report_move_finished = true; 945 } 946 947 if (last_position.width != rect.width || last_position.height != rect.height) { 948 on_resize_start(rect); 949 report_resize_finished = true; 950 } 951 952 // need to check more often then the timeout, otherwise it could be up to twice the 953 // wait time before it's noticed 954 Timeout.add(CONSIDER_CONFIGURE_HALTED_MSEC / 8, check_configure_halted); 955 } 956 957 if (last_position.x != rect.x || last_position.y != rect.y) 958 on_move(rect); 959 960 if (last_position.width != rect.width || last_position.height != rect.height) 961 on_resize(rect); 962 963 last_position = rect; 964 last_configure_ms = now_ms(); 965 966 return on_configure(event, rect); 967 } 968 969 private bool check_configure_halted() { 970 if (is_destroyed) 971 return false; 972 973 if ((now_ms() - last_configure_ms) < CONSIDER_CONFIGURE_HALTED_MSEC) 974 return true; 975 976 Gtk.Allocation allocation; 977 get_allocation(out allocation); 978 979 if (report_move_finished) 980 on_move_finished((Gdk.Rectangle) allocation); 981 982 if (report_resize_finished) 983 on_resize_finished((Gdk.Rectangle) allocation); 984 985 last_configure_ms = 0; 986 report_move_finished = false; 987 report_resize_finished = false; 988 989 return false; 990 } 991 992 protected virtual bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) { 993 check_cursor_hiding(); 994 995 return false; 996 } 997 998 protected virtual bool on_leave_notify_event() { 999 return false; 1000 } 1001 1002 private bool on_motion_internal(Gdk.EventMotion event) { 1003 int x, y; 1004 Gdk.ModifierType mask; 1005 if (event.is_hint == 1) { 1006 get_event_source_pointer(out x, out y, out mask); 1007 } else { 1008 x = (int) event.x; 1009 y = (int) event.y; 1010 mask = event.state; 1011 } 1012 1013 return on_motion(event, x, y, mask); 1014 } 1015 1016 private bool on_mousewheel_internal(Gdk.EventScroll event) { 1017 switch (event.direction) { 1018 case Gdk.ScrollDirection.UP: 1019 return on_mousewheel_up(event); 1020 1021 case Gdk.ScrollDirection.DOWN: 1022 return on_mousewheel_down(event); 1023 1024 case Gdk.ScrollDirection.LEFT: 1025 return on_mousewheel_left(event); 1026 1027 case Gdk.ScrollDirection.RIGHT: 1028 return on_mousewheel_right(event); 1029 1030 case Gdk.ScrollDirection.SMOOTH: 1031 { 1032 double dx, dy; 1033 event.get_scroll_deltas(out dx, out dy); 1034 1035 if (dy < 0) 1036 return on_mousewheel_up(event); 1037 else if (dy > 0) 1038 return on_mousewheel_down(event); 1039 else if (dx < 0) 1040 return on_mousewheel_left(event); 1041 else if (dx > 0) 1042 return on_mousewheel_right(event); 1043 else 1044 return false; 1045 } 1046 1047 default: 1048 return false; 1049 } 1050 } 1051 1052 protected virtual bool on_mousewheel_up(Gdk.EventScroll event) { 1053 return false; 1054 } 1055 1056 protected virtual bool on_mousewheel_down(Gdk.EventScroll event) { 1057 return false; 1058 } 1059 1060 protected virtual bool on_mousewheel_left(Gdk.EventScroll event) { 1061 return false; 1062 } 1063 1064 protected virtual bool on_mousewheel_right(Gdk.EventScroll event) { 1065 return false; 1066 } 1067 1068 protected virtual bool on_context_keypress() { 1069 return false; 1070 } 1071 1072 protected virtual bool on_context_buttonpress(Gdk.EventButton event) { 1073 return false; 1074 } 1075 1076 protected virtual bool on_context_invoked() { 1077 return true; 1078 } 1079 1080 protected bool popup_context_menu(Gtk.Menu? context_menu, 1081 Gdk.EventButton? event = null) { 1082 1083 if (context_menu == null || !on_context_invoked()) 1084 return false; 1085 1086 context_menu.popup_at_pointer(event); 1087 1088 return true; 1089 } 1090 1091 protected void on_event_source_realize() { 1092 assert(event_source.get_window() != null); // the realize event means the Widget has a window 1093 1094 if (event_source.get_window().get_cursor() != null) { 1095 last_cursor = event_source.get_window().get_cursor().get_cursor_type(); 1096 return; 1097 } 1098 1099 // no custom cursor defined, check parents 1100 Gdk.Window? parent_window = event_source.get_window(); 1101 do { 1102 parent_window = parent_window.get_parent(); 1103 } while (parent_window != null && parent_window.get_cursor() == null); 1104 1105 if (parent_window != null) 1106 last_cursor = parent_window.get_cursor().get_cursor_type(); 1107 } 1108 1109 public void set_cursor_hide_time(int hide_time) { 1110 cursor_hide_msec = hide_time; 1111 } 1112 1113 public void start_cursor_hiding() { 1114 check_cursor_hiding(); 1115 } 1116 1117 public void stop_cursor_hiding() { 1118 if (last_timeout_id != 0) 1119 Source.remove(last_timeout_id); 1120 } 1121 1122 public void suspend_cursor_hiding() { 1123 cursor_hide_time_cached = cursor_hide_msec; 1124 1125 if (last_timeout_id != 0) 1126 Source.remove(last_timeout_id); 1127 1128 cursor_hide_msec = 0; 1129 } 1130 1131 public void restore_cursor_hiding() { 1132 cursor_hide_msec = cursor_hide_time_cached; 1133 check_cursor_hiding(); 1134 } 1135 1136 // Use this method to set the cursor for a page, NOT window.set_cursor(...) 1137 protected virtual void set_page_cursor(Gdk.CursorType cursor_type) { 1138 last_cursor = cursor_type; 1139 1140 if (!cursor_hidden && event_source != null) { 1141 var display = event_source.get_window ().get_display (); 1142 event_source.get_window().set_cursor(new Gdk.Cursor.for_display(display, cursor_type)); 1143 } 1144 } 1145 1146 private void check_cursor_hiding() { 1147 if (cursor_hidden) { 1148 cursor_hidden = false; 1149 set_page_cursor(last_cursor); 1150 } 1151 1152 if (cursor_hide_msec != 0) { 1153 if (last_timeout_id != 0) 1154 Source.remove(last_timeout_id); 1155 last_timeout_id = Timeout.add(cursor_hide_msec, on_hide_cursor); 1156 } 1157 } 1158 1159 private bool on_hide_cursor() { 1160 cursor_hidden = true; 1161 1162 if (event_source != null) { 1163 var display = event_source.get_window().get_display (); 1164 event_source.get_window().set_cursor(new Gdk.Cursor.for_display(display, Gdk.CursorType.BLANK_CURSOR)); 1165 } 1166 1167 // We remove the timeout so reset the id 1168 last_timeout_id = 0; 1169 1170 return false; 1171 } 1172 1173 protected void update_menu_item_label (string id, 1174 string new_label) { 1175 AppWindow.get_instance().update_menu_item_label (id, new_label); 1176 } 1177 1178 protected GLib.MenuModel? find_extension_point (GLib.MenuModel model, 1179 string extension_point) { 1180 var items = model.get_n_items (); 1181 GLib.MenuModel? section = null; 1182 1183 for (int i = 0; i < items && section == null; i++) { 1184 string? name = null; 1185 model.get_item_attribute (i, "id", "s", out name); 1186 if (name == extension_point) { 1187 section = model.get_item_link (i, GLib.Menu.LINK_SECTION); 1188 } else { 1189 var subsection = model.get_item_link (i, GLib.Menu.LINK_SECTION); 1190 1191 if (subsection == null) 1192 continue; 1193 1194 // Recurse into submenus 1195 var sub_items = subsection.get_n_items (); 1196 for (int j = 0; j < sub_items && section == null; j++) { 1197 var submenu = subsection.get_item_link 1198 (j, GLib.Menu.LINK_SUBMENU); 1199 if (submenu != null) { 1200 section = this.find_extension_point (submenu, 1201 extension_point); 1202 } 1203 } 1204 } 1205 } 1206 1207 return section; 1208 } 1209 1210} 1211 1212public abstract class CheckerboardPage : Page { 1213 private const int AUTOSCROLL_PIXELS = 50; 1214 private const int AUTOSCROLL_TICKS_MSEC = 50; 1215 1216 private CheckerboardLayout layout; 1217 private string item_context_menu_path = null; 1218 private string page_context_menu_path = null; 1219 private Gtk.Viewport viewport = new Gtk.Viewport(null, null); 1220 protected CheckerboardItem anchor = null; 1221 protected CheckerboardItem cursor = null; 1222 private CheckerboardItem current_hovered_item = null; 1223 private bool autoscroll_scheduled = false; 1224 private CheckerboardItem activated_item = null; 1225 private Gee.ArrayList<CheckerboardItem> previously_selected = null; 1226 1227 public enum Activator { 1228 KEYBOARD, 1229 MOUSE 1230 } 1231 1232 public struct KeyboardModifiers { 1233 public KeyboardModifiers(Page page) { 1234 ctrl_pressed = page.get_ctrl_pressed(); 1235 alt_pressed = page.get_alt_pressed(); 1236 shift_pressed = page.get_shift_pressed(); 1237 super_pressed = page.get_super_pressed(); 1238 } 1239 1240 public bool ctrl_pressed; 1241 public bool alt_pressed; 1242 public bool shift_pressed; 1243 public bool super_pressed; 1244 } 1245 1246 protected CheckerboardPage(string page_name) { 1247 base (page_name); 1248 1249 layout = new CheckerboardLayout(get_view()); 1250 layout.set_name(page_name); 1251 1252 set_event_source(layout); 1253 1254 set_border_width(0); 1255 set_shadow_type(Gtk.ShadowType.NONE); 1256 1257 viewport.set_border_width(0); 1258 viewport.set_shadow_type(Gtk.ShadowType.NONE); 1259 1260 viewport.add(layout); 1261 1262 // want to set_adjustments before adding to ScrolledWindow to let our signal handlers 1263 // run first ... otherwise, the thumbnails draw late 1264 layout.set_adjustments(get_hadjustment(), get_vadjustment()); 1265 1266 add(viewport); 1267 1268 // need to monitor items going hidden when dealing with anchor/cursor/highlighted items 1269 get_view().items_hidden.connect(on_items_hidden); 1270 get_view().contents_altered.connect(on_contents_altered); 1271 get_view().items_state_changed.connect(on_items_state_changed); 1272 get_view().items_visibility_changed.connect(on_items_visibility_changed); 1273 1274 // scrollbar policy 1275 set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); 1276 } 1277 1278 public void init_item_context_menu(string path) { 1279 item_context_menu_path = path; 1280 } 1281 1282 public void init_page_context_menu(string path) { 1283 page_context_menu_path = path; 1284 } 1285 1286 public Gtk.Menu? get_context_menu() { 1287 // show page context menu if nothing is selected 1288 return (get_view().get_selected_count() != 0) ? get_item_context_menu() : 1289 get_page_context_menu(); 1290 } 1291 1292 private Gtk.Menu item_context_menu; 1293 public virtual Gtk.Menu? get_item_context_menu() { 1294 if (item_context_menu == null) { 1295 var model = this.builder.get_object (item_context_menu_path) 1296 as GLib.MenuModel; 1297 item_context_menu = new Gtk.Menu.from_model (model); 1298 item_context_menu.attach_to_widget (this, null); 1299 } 1300 1301 return item_context_menu; 1302 } 1303 1304 private Gtk.Menu page_context_menu; 1305 public override Gtk.Menu? get_page_context_menu() { 1306 if (page_context_menu_path == null) 1307 return null; 1308 1309 if (page_context_menu == null) { 1310 var model = this.builder.get_object (page_context_menu_path) 1311 as GLib.MenuModel; 1312 page_context_menu = new Gtk.Menu.from_model (model); 1313 page_context_menu.attach_to_widget (this, null); 1314 } 1315 1316 return page_context_menu; 1317 } 1318 1319 protected override bool on_context_keypress() { 1320 return popup_context_menu(get_context_menu()); 1321 } 1322 1323 protected virtual string get_view_empty_message() { 1324 return _("No photos/videos"); 1325 } 1326 1327 protected virtual string get_filter_no_match_message() { 1328 return _("No photos/videos found which match the current filter"); 1329 } 1330 1331 protected virtual void on_item_activated(CheckerboardItem item, Activator activator, 1332 KeyboardModifiers modifiers) { 1333 } 1334 1335 public CheckerboardLayout get_checkerboard_layout() { 1336 return layout; 1337 } 1338 1339 // Gets the search view filter for this page. 1340 public abstract SearchViewFilter get_search_view_filter(); 1341 1342 public virtual Core.ViewTracker? get_view_tracker() { 1343 return null; 1344 } 1345 1346 public override void switching_from() { 1347 layout.set_in_view(false); 1348 get_search_view_filter().refresh.disconnect(on_view_filter_refresh); 1349 1350 // unselect everything so selection won't persist after page loses focus 1351 get_view().unselect_all(); 1352 1353 base.switching_from(); 1354 } 1355 1356 public override void switched_to() { 1357 layout.set_in_view(true); 1358 get_search_view_filter().refresh.connect(on_view_filter_refresh); 1359 on_view_filter_refresh(); 1360 1361 if (get_view().get_selected_count() > 0) { 1362 CheckerboardItem? item = (CheckerboardItem?) get_view().get_selected_at(0); 1363 1364 // if item is in any way out of view, scroll to it 1365 Gtk.Adjustment vadj = get_vadjustment(); 1366 if (!(get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE 1367 && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == AdjustmentRelation.IN_RANGE))) { 1368 1369 // scroll to see the new item 1370 int top = 0; 1371 if (item.allocation.y < vadj.get_value()) { 1372 top = item.allocation.y; 1373 top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2; 1374 } else { 1375 top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size(); 1376 top += CheckerboardLayout.ROW_GUTTER_PADDING / 2; 1377 } 1378 1379 vadj.set_value(top); 1380 1381 } 1382 } 1383 1384 base.switched_to(); 1385 } 1386 1387 private void on_view_filter_refresh() { 1388 update_view_filter_message(); 1389 } 1390 1391 private void on_contents_altered(Gee.Iterable<DataObject>? added, 1392 Gee.Iterable<DataObject>? removed) { 1393 update_view_filter_message(); 1394 } 1395 1396 private void on_items_state_changed(Gee.Iterable<DataView> changed) { 1397 update_view_filter_message(); 1398 } 1399 1400 private void on_items_visibility_changed(Gee.Collection<DataView> changed) { 1401 update_view_filter_message(); 1402 } 1403 1404 private void update_view_filter_message() { 1405 if (get_view().are_items_filtered_out() && get_view().get_count() == 0) { 1406 set_page_message(get_filter_no_match_message()); 1407 } else if (get_view().get_count() == 0) { 1408 set_page_message(get_view_empty_message()); 1409 } else { 1410 unset_page_message(); 1411 } 1412 } 1413 1414 public void set_page_message(string message) { 1415 layout.set_message(message); 1416 if (is_in_view()) 1417 layout.queue_draw(); 1418 } 1419 1420 public void unset_page_message() { 1421 layout.unset_message(); 1422 if (is_in_view()) 1423 layout.queue_draw(); 1424 } 1425 1426 public override void set_page_name(string name) { 1427 base.set_page_name(name); 1428 1429 layout.set_name(name); 1430 } 1431 1432 public CheckerboardItem? get_item_at_pixel(double x, double y) { 1433 return layout.get_item_at_pixel(x, y); 1434 } 1435 1436 private void on_items_hidden(Gee.Iterable<DataView> hidden) { 1437 foreach (DataView view in hidden) { 1438 CheckerboardItem item = (CheckerboardItem) view; 1439 1440 if (anchor == item) 1441 anchor = null; 1442 1443 if (cursor == item) 1444 cursor = null; 1445 1446 if (current_hovered_item == item) 1447 current_hovered_item = null; 1448 } 1449 } 1450 1451 protected override bool key_press_event(Gdk.EventKey event) { 1452 bool handled = true; 1453 1454 // mask out the modifiers we're interested in 1455 uint state = event.state & Gdk.ModifierType.SHIFT_MASK; 1456 1457 switch (Gdk.keyval_name(event.keyval)) { 1458 case "Up": 1459 case "KP_Up": 1460 move_cursor(CompassPoint.NORTH); 1461 select_anchor_to_cursor(state); 1462 break; 1463 1464 case "Down": 1465 case "KP_Down": 1466 move_cursor(CompassPoint.SOUTH); 1467 select_anchor_to_cursor(state); 1468 break; 1469 1470 case "Left": 1471 case "KP_Left": 1472 move_cursor(CompassPoint.WEST); 1473 select_anchor_to_cursor(state); 1474 break; 1475 1476 case "Right": 1477 case "KP_Right": 1478 move_cursor(CompassPoint.EAST); 1479 select_anchor_to_cursor(state); 1480 break; 1481 1482 case "Home": 1483 case "KP_Home": 1484 CheckerboardItem? first = (CheckerboardItem?) get_view().get_first(); 1485 if (first != null) 1486 cursor_to_item(first); 1487 select_anchor_to_cursor(state); 1488 break; 1489 1490 case "End": 1491 case "KP_End": 1492 CheckerboardItem? last = (CheckerboardItem?) get_view().get_last(); 1493 if (last != null) 1494 cursor_to_item(last); 1495 select_anchor_to_cursor(state); 1496 break; 1497 1498 case "Return": 1499 case "KP_Enter": 1500 if (get_view().get_selected_count() == 1) 1501 on_item_activated((CheckerboardItem) get_view().get_selected_at(0), 1502 Activator.KEYBOARD, KeyboardModifiers(this)); 1503 else 1504 handled = false; 1505 break; 1506 1507 case "space": 1508 Marker marker = get_view().mark(layout.get_cursor()); 1509 get_view().toggle_marked(marker); 1510 break; 1511 1512 default: 1513 handled = false; 1514 break; 1515 } 1516 1517 if (handled) 1518 return true; 1519 1520 return (base.key_press_event != null) ? base.key_press_event(event) : true; 1521 } 1522 1523 protected override bool on_left_click(Gdk.EventButton event) { 1524 // only interested in single-click and double-clicks for now 1525 if ((event.type != Gdk.EventType.BUTTON_PRESS) && (event.type != Gdk.EventType.2BUTTON_PRESS)) 1526 return false; 1527 1528 // mask out the modifiers we're interested in 1529 uint state = event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK); 1530 1531 // use clicks for multiple selection and activation only; single selects are handled by 1532 // button release, to allow for multiple items to be selected then dragged ... 1533 CheckerboardItem item = get_item_at_pixel(event.x, event.y); 1534 if (item != null) { 1535 // ... however, there is no dragging if the user clicks on an interactive part of the 1536 // CheckerboardItem (e.g. a tag) 1537 if (layout.handle_left_click(item, event.x, event.y, event.state)) 1538 return true; 1539 1540 switch (state) { 1541 case Gdk.ModifierType.CONTROL_MASK: 1542 // with only Ctrl pressed, multiple selections are possible ... chosen item 1543 // is toggled 1544 Marker marker = get_view().mark(item); 1545 get_view().toggle_marked(marker); 1546 1547 if (item.is_selected()) { 1548 anchor = item; 1549 cursor = item; 1550 } 1551 break; 1552 1553 case Gdk.ModifierType.SHIFT_MASK: 1554 get_view().unselect_all(); 1555 1556 if (anchor == null) 1557 anchor = item; 1558 1559 select_between_items(anchor, item); 1560 1561 cursor = item; 1562 break; 1563 1564 case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK: 1565 // Ticket #853 - Make Ctrl + Shift + Mouse Button 1 able to start a new run 1566 // of contiguous selected items without unselecting previously-selected items 1567 // a la Nautilus. 1568 // Same as the case for SHIFT_MASK, but don't unselect anything first. 1569 if (anchor == null) 1570 anchor = item; 1571 1572 select_between_items(anchor, item); 1573 1574 cursor = item; 1575 break; 1576 1577 default: 1578 if (event.type == Gdk.EventType.2BUTTON_PRESS) { 1579 activated_item = item; 1580 } else { 1581 // if the user has selected one or more items and is preparing for a drag, 1582 // don't want to blindly unselect: if they've clicked on an unselected item 1583 // unselect all and select that one; if they've clicked on a previously 1584 // selected item, do nothing 1585 if (!item.is_selected()) { 1586 Marker all = get_view().start_marking(); 1587 all.mark_many(get_view().get_selected()); 1588 1589 get_view().unselect_and_select_marked(all, get_view().mark(item)); 1590 } 1591 } 1592 1593 anchor = item; 1594 cursor = item; 1595 break; 1596 } 1597 layout.set_cursor(item); 1598 } else { 1599 // user clicked on "dead" area; only unselect if control is not pressed 1600 // do we want similar behavior for shift as well? 1601 if (state != Gdk.ModifierType.CONTROL_MASK) 1602 get_view().unselect_all(); 1603 1604 // grab previously marked items 1605 previously_selected = new Gee.ArrayList<CheckerboardItem>(); 1606 foreach (DataView view in get_view().get_selected()) 1607 previously_selected.add((CheckerboardItem) view); 1608 1609 layout.set_drag_select_origin((int) event.x, (int) event.y); 1610 1611 return true; 1612 } 1613 1614 // need to determine if the signal should be passed to the DnD handlers 1615 // Return true to block the DnD handler, false otherwise 1616 1617 return get_view().get_selected_count() == 0; 1618 } 1619 1620 protected override bool on_left_released(Gdk.EventButton event) { 1621 previously_selected = null; 1622 1623 // if drag-selecting, stop here and do nothing else 1624 if (layout.is_drag_select_active()) { 1625 layout.clear_drag_select(); 1626 anchor = cursor; 1627 1628 return true; 1629 } 1630 1631 // only interested in non-modified button releases 1632 if ((event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) != 0) 1633 return false; 1634 1635 // if the item was activated in the double-click, report it now 1636 if (activated_item != null) { 1637 on_item_activated(activated_item, Activator.MOUSE, KeyboardModifiers(this)); 1638 activated_item = null; 1639 1640 return true; 1641 } 1642 1643 CheckerboardItem item = get_item_at_pixel(event.x, event.y); 1644 if (item == null) { 1645 // released button on "dead" area 1646 return true; 1647 } 1648 1649 if (cursor != item) { 1650 // user released mouse button after moving it off the initial item, or moved from dead 1651 // space onto one. either way, unselect everything 1652 get_view().unselect_all(); 1653 } else { 1654 // the idea is, if a user single-clicks on an item with no modifiers, then all other items 1655 // should be deselected, however, if they single-click in order to drag one or more items, 1656 // they should remain selected, hence performing this here rather than on_left_click 1657 // (item may not be selected if an unimplemented modifier key was used) 1658 if (item.is_selected()) 1659 get_view().unselect_all_but(item); 1660 } 1661 1662 return true; 1663 } 1664 1665 protected override bool on_right_click(Gdk.EventButton event) { 1666 // only interested in single-clicks for now 1667 if (event.type != Gdk.EventType.BUTTON_PRESS) 1668 return false; 1669 1670 // get what's right-clicked upon 1671 CheckerboardItem item = get_item_at_pixel(event.x, event.y); 1672 if (item != null) { 1673 // mask out the modifiers we're interested in 1674 switch (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)) { 1675 case Gdk.ModifierType.CONTROL_MASK: 1676 // chosen item is toggled 1677 Marker marker = get_view().mark(item); 1678 get_view().toggle_marked(marker); 1679 break; 1680 1681 case Gdk.ModifierType.SHIFT_MASK: 1682 // TODO 1683 break; 1684 1685 case Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK: 1686 // TODO 1687 break; 1688 1689 default: 1690 // if the item is already selected, proceed; if item is not selected, a bare right 1691 // click unselects everything else but it 1692 if (!item.is_selected()) { 1693 Marker all = get_view().start_marking(); 1694 all.mark_many(get_view().get_selected()); 1695 1696 get_view().unselect_and_select_marked(all, get_view().mark(item)); 1697 } 1698 break; 1699 } 1700 } else { 1701 // clicked in "dead" space, unselect everything 1702 get_view().unselect_all(); 1703 } 1704 1705 Gtk.Menu context_menu = get_context_menu(); 1706 return popup_context_menu(context_menu, event); 1707 } 1708 1709 protected virtual bool on_mouse_over(CheckerboardItem? item, int x, int y, Gdk.ModifierType mask) { 1710 if (item != null) 1711 layout.handle_mouse_motion(item, x, y, mask); 1712 1713 // if hovering over the last hovered item, or both are null (nothing highlighted and 1714 // hovering over empty space), do nothing 1715 if (item == current_hovered_item) 1716 return true; 1717 1718 // either something new is highlighted or now hovering over empty space, so dim old item 1719 if (current_hovered_item != null) { 1720 current_hovered_item.handle_mouse_leave(); 1721 current_hovered_item = null; 1722 } 1723 1724 // if over empty space, done 1725 if (item == null) 1726 return true; 1727 1728 // brighten the new item 1729 current_hovered_item = item; 1730 current_hovered_item.handle_mouse_enter(); 1731 1732 return true; 1733 } 1734 1735 protected override bool on_motion(Gdk.EventMotion event, int x, int y, Gdk.ModifierType mask) { 1736 // report what item the mouse is hovering over 1737 if (!on_mouse_over(get_item_at_pixel(x, y), x, y, mask)) 1738 return false; 1739 1740 // go no further if not drag-selecting 1741 if (!layout.is_drag_select_active()) 1742 return false; 1743 1744 // set the new endpoint of the drag selection 1745 layout.set_drag_select_endpoint(x, y); 1746 1747 updated_selection_band(); 1748 1749 // if out of bounds, schedule a check to auto-scroll the viewport 1750 if (!autoscroll_scheduled 1751 && get_adjustment_relation(get_vadjustment(), y) != AdjustmentRelation.IN_RANGE) { 1752 Timeout.add(AUTOSCROLL_TICKS_MSEC, selection_autoscroll); 1753 autoscroll_scheduled = true; 1754 } 1755 1756 // return true to stop a potential drag-and-drop operation 1757 return true; 1758 } 1759 1760 private void updated_selection_band() { 1761 assert(layout.is_drag_select_active()); 1762 1763 // get all items inside the selection 1764 Gee.List<CheckerboardItem>? intersection = layout.items_in_selection_band(); 1765 if (intersection == null) 1766 return; 1767 1768 Marker to_unselect = get_view().start_marking(); 1769 Marker to_select = get_view().start_marking(); 1770 1771 // mark all selected items to be unselected 1772 to_unselect.mark_many(get_view().get_selected()); 1773 1774 // except for the items that were selected before the drag began 1775 assert(previously_selected != null); 1776 to_unselect.unmark_many(previously_selected); 1777 to_select.mark_many(previously_selected); 1778 1779 // toggle selection on everything in the intersection and update the cursor 1780 cursor = null; 1781 1782 foreach (CheckerboardItem item in intersection) { 1783 if (to_select.toggle(item)) 1784 to_unselect.unmark(item); 1785 else 1786 to_unselect.mark(item); 1787 1788 if (cursor == null) 1789 cursor = item; 1790 } 1791 1792 get_view().select_marked(to_select); 1793 get_view().unselect_marked(to_unselect); 1794 } 1795 1796 private bool selection_autoscroll() { 1797 if (!layout.is_drag_select_active()) { 1798 autoscroll_scheduled = false; 1799 1800 return false; 1801 } 1802 1803 // as the viewport never scrolls horizontally, only interested in vertical 1804 Gtk.Adjustment vadj = get_vadjustment(); 1805 1806 int x, y; 1807 Gdk.ModifierType mask; 1808 get_event_source_pointer(out x, out y, out mask); 1809 1810 int new_value = (int) vadj.get_value(); 1811 switch (get_adjustment_relation(vadj, y)) { 1812 case AdjustmentRelation.BELOW: 1813 // pointer above window, scroll up 1814 new_value -= AUTOSCROLL_PIXELS; 1815 layout.set_drag_select_endpoint(x, new_value); 1816 break; 1817 1818 case AdjustmentRelation.ABOVE: 1819 // pointer below window, scroll down, extend selection to bottom of page 1820 new_value += AUTOSCROLL_PIXELS; 1821 layout.set_drag_select_endpoint(x, new_value + (int) vadj.get_page_size()); 1822 break; 1823 1824 case AdjustmentRelation.IN_RANGE: 1825 autoscroll_scheduled = false; 1826 1827 return false; 1828 1829 default: 1830 warn_if_reached(); 1831 break; 1832 } 1833 1834 // It appears that in GTK+ 2.18, the adjustment is not clamped the way it was in 2.16. 1835 // This may have to do with how adjustments are different w/ scrollbars, that they're upper 1836 // clamp is upper - page_size ... either way, enforce these limits here 1837 vadj.set_value(new_value.clamp((int) vadj.get_lower(), 1838 (int) vadj.get_upper() - (int) vadj.get_page_size())); 1839 1840 updated_selection_band(); 1841 1842 return true; 1843 } 1844 1845 public void cursor_to_item(CheckerboardItem item) { 1846 assert(get_view().contains(item)); 1847 1848 cursor = item; 1849 1850 if (!get_ctrl_pressed()) { 1851 get_view().unselect_all(); 1852 Marker marker = get_view().mark(item); 1853 get_view().select_marked(marker); 1854 } 1855 layout.set_cursor(item); 1856 1857 // if item is in any way out of view, scroll to it 1858 Gtk.Adjustment vadj = get_vadjustment(); 1859 if (get_adjustment_relation(vadj, item.allocation.y) == AdjustmentRelation.IN_RANGE 1860 && (get_adjustment_relation(vadj, item.allocation.y + item.allocation.height) == AdjustmentRelation.IN_RANGE)) 1861 return; 1862 1863 // scroll to see the new item 1864 int top = 0; 1865 if (item.allocation.y < vadj.get_value()) { 1866 top = item.allocation.y; 1867 top -= CheckerboardLayout.ROW_GUTTER_PADDING / 2; 1868 } else { 1869 top = item.allocation.y + item.allocation.height - (int) vadj.get_page_size(); 1870 top += CheckerboardLayout.ROW_GUTTER_PADDING / 2; 1871 } 1872 1873 vadj.set_value(top); 1874 } 1875 1876 public void move_cursor(CompassPoint point) { 1877 // if no items, nothing to do 1878 if (get_view().get_count() == 0) 1879 return; 1880 1881 // if there is no better starting point, simply select the first and exit 1882 // The right half of the or is related to Bug #732334, the cursor might be non-null and still not contained in 1883 // the view, if the user dragged a full screen Photo off screen 1884 if (cursor == null && layout.get_cursor() == null || cursor != null && !get_view().contains(cursor)) { 1885 CheckerboardItem item = layout.get_item_at_coordinate(0, 0); 1886 cursor_to_item(item); 1887 anchor = item; 1888 1889 return; 1890 } 1891 1892 if (cursor == null) { 1893 cursor = layout.get_cursor() as CheckerboardItem; 1894 } 1895 1896 // move the cursor relative to the "first" item 1897 CheckerboardItem? item = layout.get_item_relative_to(cursor, point); 1898 if (item != null) 1899 cursor_to_item(item); 1900 } 1901 1902 public void set_cursor(CheckerboardItem item) { 1903 Marker marker = get_view().mark(item); 1904 get_view().select_marked(marker); 1905 1906 cursor = item; 1907 anchor = item; 1908 } 1909 1910 public void select_between_items(CheckerboardItem item_start, CheckerboardItem item_end) { 1911 Marker marker = get_view().start_marking(); 1912 1913 bool passed_start = false; 1914 bool passed_end = false; 1915 1916 foreach (DataObject object in get_view().get_all()) { 1917 CheckerboardItem item = (CheckerboardItem) object; 1918 1919 if (item_start == item) 1920 passed_start = true; 1921 1922 if (item_end == item) 1923 passed_end = true; 1924 1925 if (passed_start || passed_end) 1926 marker.mark((DataView) object); 1927 1928 if (passed_start && passed_end) 1929 break; 1930 } 1931 1932 get_view().select_marked(marker); 1933 } 1934 1935 public void select_anchor_to_cursor(uint state) { 1936 if (cursor == null || anchor == null) 1937 return; 1938 1939 if (state == Gdk.ModifierType.SHIFT_MASK) { 1940 get_view().unselect_all(); 1941 select_between_items(anchor, cursor); 1942 } else { 1943 anchor = cursor; 1944 } 1945 } 1946 1947 protected virtual void set_display_titles(bool display) { 1948 get_view().freeze_notifications(); 1949 get_view().set_property(CheckerboardItem.PROP_SHOW_TITLES, display); 1950 get_view().thaw_notifications(); 1951 } 1952 1953 protected virtual void set_display_comments(bool display) { 1954 get_view().freeze_notifications(); 1955 get_view().set_property(CheckerboardItem.PROP_SHOW_COMMENTS, display); 1956 get_view().thaw_notifications(); 1957 } 1958} 1959 1960public abstract class SinglePhotoPage : Page { 1961 public const Gdk.InterpType FAST_INTERP = Gdk.InterpType.NEAREST; 1962 public const Gdk.InterpType QUALITY_INTERP = Gdk.InterpType.BILINEAR; 1963 public const int KEY_REPEAT_INTERVAL_MSEC = 200; 1964 1965 public enum UpdateReason { 1966 NEW_PIXBUF, 1967 QUALITY_IMPROVEMENT, 1968 RESIZED_CANVAS 1969 } 1970 1971 protected Gtk.DrawingArea canvas = new Gtk.DrawingArea(); 1972 protected Gtk.Viewport viewport = new Gtk.Viewport(null, null); 1973 1974 private bool scale_up_to_viewport; 1975 private TransitionClock transition_clock; 1976 private int transition_duration_msec = 0; 1977 private Cairo.Surface pixmap = null; 1978 private Cairo.Context pixmap_ctx = null; 1979 private Cairo.Context text_ctx = null; 1980 private Dimensions pixmap_dim = Dimensions(); 1981 private Gdk.Pixbuf unscaled = null; 1982 private Dimensions max_dim = Dimensions(); 1983 private Gdk.Pixbuf scaled = null; 1984 private Gdk.Pixbuf old_scaled = null; // previous scaled image 1985 private Gdk.Rectangle scaled_pos = Gdk.Rectangle(); 1986 private ZoomState static_zoom_state; 1987 private bool zoom_high_quality = true; 1988 private ZoomState saved_zoom_state; 1989 private bool has_saved_zoom_state = false; 1990 private uint32 last_nav_key = 0; 1991 1992 protected SinglePhotoPage(string page_name, bool scale_up_to_viewport) { 1993 base(page_name); 1994 1995 this.scale_up_to_viewport = scale_up_to_viewport; 1996 1997 transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock(); 1998 1999 // With the current code automatically resizing the image to the viewport, scrollbars 2000 // should never be shown, but this may change if/when zooming is supported 2001 set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); 2002 2003 set_border_width(0); 2004 set_shadow_type(Gtk.ShadowType.NONE); 2005 2006 viewport.set_shadow_type(Gtk.ShadowType.NONE); 2007 viewport.set_border_width(0); 2008 viewport.add(canvas); 2009 2010 add(viewport); 2011 2012 canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK 2013 | Gdk.EventMask.SUBSTRUCTURE_MASK); 2014 2015 viewport.size_allocate.connect(on_viewport_resize); 2016 canvas.draw.connect(on_canvas_exposed); 2017 2018 set_event_source(canvas); 2019 Config.Facade.get_instance().colors_changed.connect(on_colors_changed); 2020 } 2021 2022 ~SinglePhotoPage() { 2023 Config.Facade.get_instance().colors_changed.disconnect(on_colors_changed); 2024 } 2025 2026 public bool is_transition_in_progress() { 2027 return transition_clock.is_in_progress(); 2028 } 2029 2030 public void cancel_transition() { 2031 if (transition_clock.is_in_progress()) 2032 transition_clock.cancel(); 2033 } 2034 2035 public void set_transition(string effect_id, int duration_msec) { 2036 cancel_transition(); 2037 2038 transition_clock = TransitionEffectsManager.get_instance().create_transition_clock(effect_id); 2039 if (transition_clock == null) 2040 transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock(); 2041 2042 transition_duration_msec = duration_msec; 2043 } 2044 2045 // This method includes a call to pixmap_ctx.paint(). 2046 private void render_zoomed_to_pixmap(ZoomState zoom_state) { 2047 assert(is_zoom_supported()); 2048 2049 Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle_wrt_content(); 2050 2051 Gdk.Pixbuf zoomed; 2052 if (get_zoom_buffer() != null) { 2053 zoomed = (zoom_high_quality) ? get_zoom_buffer().get_zoomed_image(zoom_state) : 2054 get_zoom_buffer().get_zoom_preview_image(zoom_state); 2055 } else { 2056 Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(unscaled); 2057 2058 Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(unscaled, view_rect_proj.x, 2059 view_rect_proj.y, view_rect_proj.width, view_rect_proj.height); 2060 2061 zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height, 2062 Gdk.InterpType.BILINEAR); 2063 } 2064 2065 if (zoomed == null) { 2066 return; 2067 } 2068 2069 int draw_x = (pixmap_dim.width - view_rect.width) / 2; 2070 draw_x = draw_x.clamp(0, int.MAX); 2071 2072 int draw_y = (pixmap_dim.height - view_rect.height) / 2; 2073 draw_y = draw_y.clamp(0, int.MAX); 2074 paint_pixmap_with_background(pixmap_ctx, zoomed, draw_x, draw_y); 2075 } 2076 2077 protected void on_interactive_zoom(ZoomState interactive_zoom_state) { 2078 assert(is_zoom_supported()); 2079 2080 set_source_color_from_string(pixmap_ctx, "#000"); 2081 pixmap_ctx.paint(); 2082 2083 bool old_quality_setting = zoom_high_quality; 2084 zoom_high_quality = false; 2085 render_zoomed_to_pixmap(interactive_zoom_state); 2086 zoom_high_quality = old_quality_setting; 2087 2088 canvas.queue_draw(); 2089 } 2090 2091 protected void on_interactive_pan(ZoomState interactive_zoom_state) { 2092 assert(is_zoom_supported()); 2093 2094 set_source_color_from_string(pixmap_ctx, "#000"); 2095 pixmap_ctx.paint(); 2096 2097 bool old_quality_setting = zoom_high_quality; 2098 zoom_high_quality = true; 2099 render_zoomed_to_pixmap(interactive_zoom_state); 2100 zoom_high_quality = old_quality_setting; 2101 2102 canvas.queue_draw(); 2103 } 2104 2105 protected virtual bool is_zoom_supported() { 2106 return false; 2107 } 2108 2109 protected virtual void cancel_zoom() { 2110 if (pixmap != null) { 2111 set_source_color_from_string(pixmap_ctx, "#000"); 2112 pixmap_ctx.paint(); 2113 } 2114 } 2115 2116 protected virtual void save_zoom_state() { 2117 saved_zoom_state = static_zoom_state; 2118 has_saved_zoom_state = true; 2119 } 2120 2121 protected virtual void restore_zoom_state() { 2122 if (!has_saved_zoom_state) 2123 return; 2124 2125 static_zoom_state = saved_zoom_state; 2126 repaint(); 2127 has_saved_zoom_state = false; 2128 } 2129 2130 protected virtual ZoomBuffer? get_zoom_buffer() { 2131 return null; 2132 } 2133 2134 protected ZoomState get_saved_zoom_state() { 2135 return saved_zoom_state; 2136 } 2137 2138 protected void set_zoom_state(ZoomState zoom_state) { 2139 assert(is_zoom_supported()); 2140 2141 static_zoom_state = zoom_state; 2142 } 2143 2144 protected ZoomState get_zoom_state() { 2145 assert(is_zoom_supported()); 2146 2147 return static_zoom_state; 2148 } 2149 2150 public override void switched_to() { 2151 base.switched_to(); 2152 2153 if (unscaled != null) 2154 repaint(); 2155 } 2156 2157 public override void set_container(Gtk.Window container) { 2158 base.set_container(container); 2159 2160 // scrollbar policy in fullscreen mode needs to be auto/auto, else the pixbuf will shift 2161 // off the screen 2162 if (container is FullscreenWindow) 2163 set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); 2164 } 2165 2166 // max_dim represents the maximum size of the original pixbuf (i.e. pixbuf may be scaled and 2167 // the caller capable of producing larger ones depending on the viewport size). max_dim 2168 // is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if 2169 // max_dim should be ignored (i.e. scale_up_to_viewport is false). 2170 public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim, Direction? direction = null) { 2171 static_zoom_state = ZoomState(max_dim, pixmap_dim, 2172 static_zoom_state.get_interpolation_factor(), 2173 static_zoom_state.get_viewport_center()); 2174 2175 cancel_transition(); 2176 2177 this.unscaled = unscaled; 2178 this.max_dim = max_dim; 2179 this.old_scaled = scaled; 2180 scaled = null; 2181 2182 // need to make sure this has happened 2183 canvas.realize(); 2184 2185 repaint(direction); 2186 } 2187 2188 public void blank_display() { 2189 unscaled = null; 2190 max_dim = Dimensions(); 2191 scaled = null; 2192 pixmap = null; 2193 2194 // this has to have happened 2195 canvas.realize(); 2196 2197 // force a redraw 2198 invalidate_all(); 2199 } 2200 2201 public Cairo.Surface? get_surface() { 2202 return pixmap; 2203 } 2204 2205 public Dimensions get_surface_dim() { 2206 return pixmap_dim; 2207 } 2208 2209 public Cairo.Context get_cairo_context() { 2210 return pixmap_ctx; 2211 } 2212 2213 public void paint_text(Pango.Layout pango_layout, int x, int y) { 2214 text_ctx.move_to(x, y); 2215 Pango.cairo_show_layout(text_ctx, pango_layout); 2216 } 2217 2218 public Scaling get_canvas_scaling() { 2219 return (get_container() is FullscreenWindow) ? Scaling.for_screen(AppWindow.get_instance(), scale_up_to_viewport) 2220 : Scaling.for_widget(viewport, scale_up_to_viewport); 2221 } 2222 2223 public Gdk.Pixbuf? get_unscaled_pixbuf() { 2224 return unscaled; 2225 } 2226 2227 public Gdk.Pixbuf? get_scaled_pixbuf() { 2228 return scaled; 2229 } 2230 2231 // Returns a rectangle describing the pixbuf in relation to the canvas 2232 public Gdk.Rectangle get_scaled_pixbuf_position() { 2233 return scaled_pos; 2234 } 2235 2236 public bool is_inside_pixbuf(int x, int y) { 2237 return coord_in_rectangle(x, y, scaled_pos); 2238 } 2239 2240 public void invalidate(Gdk.Rectangle rect) { 2241 if (canvas.get_window() != null) 2242 canvas.get_window().invalidate_rect(rect, false); 2243 } 2244 2245 public void invalidate_all() { 2246 if (canvas.get_window() != null) 2247 canvas.get_window().invalidate_rect(null, false); 2248 } 2249 2250 private void on_viewport_resize() { 2251 // do fast repaints while resizing 2252 internal_repaint(true, null); 2253 } 2254 2255 protected override void on_resize_finished(Gdk.Rectangle rect) { 2256 base.on_resize_finished(rect); 2257 2258 // when the resize is completed, do a high-quality repaint 2259 repaint(); 2260 } 2261 2262 private bool on_canvas_exposed(Cairo.Context exposed_ctx) { 2263 // draw pixmap onto canvas unless it's not been instantiated, in which case draw black 2264 // (so either old image or contents of another page is not left on screen) 2265 if (pixmap != null) 2266 exposed_ctx.set_source_surface(pixmap, 0, 0); 2267 else 2268 set_source_color_from_string(exposed_ctx, "#000"); 2269 2270 exposed_ctx.rectangle(0, 0, get_allocated_width(), get_allocated_height()); 2271 exposed_ctx.paint(); 2272 2273 return true; 2274 } 2275 2276 protected virtual void new_surface(Cairo.Context ctx, Dimensions ctx_dim) { 2277 } 2278 2279 protected virtual void updated_pixbuf(Gdk.Pixbuf pixbuf, UpdateReason reason, Dimensions old_dim) { 2280 } 2281 2282 protected virtual void paint(Cairo.Context ctx, Dimensions ctx_dim) { 2283 if (is_zoom_supported() && (!static_zoom_state.is_default())) { 2284 set_source_color_from_string(ctx, "#000"); 2285 ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height); 2286 ctx.fill(); 2287 2288 render_zoomed_to_pixmap(static_zoom_state); 2289 } else if (!transition_clock.paint(ctx, ctx_dim.width, ctx_dim.height)) { 2290 // transition is not running, so paint the full image on a black background 2291 set_source_color_from_string(ctx, "#000"); 2292 2293 ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height); 2294 ctx.fill(); 2295 2296 paint_pixmap_with_background(ctx, scaled, scaled_pos.x, scaled_pos.y); 2297 } 2298 } 2299 2300 private void repaint_pixmap() { 2301 if (pixmap_ctx == null) 2302 return; 2303 2304 paint(pixmap_ctx, pixmap_dim); 2305 invalidate_all(); 2306 } 2307 2308 public void repaint(Direction? direction = null) { 2309 internal_repaint(false, direction); 2310 } 2311 2312 private void internal_repaint(bool fast, Direction? direction) { 2313 // if not in view, assume a full repaint needed in future but do nothing more 2314 if (!is_in_view()) { 2315 pixmap = null; 2316 scaled = null; 2317 2318 return; 2319 } 2320 2321 // no image or window, no painting 2322 if (unscaled == null || canvas.get_window() == null) 2323 return; 2324 2325 Gtk.Allocation allocation; 2326 viewport.get_allocation(out allocation); 2327 2328 int width = allocation.width; 2329 int height = allocation.height; 2330 2331 if (width <= 0 || height <= 0) 2332 return; 2333 2334 bool new_pixbuf = (scaled == null); 2335 2336 // save if reporting an image being rescaled 2337 Dimensions old_scaled_dim = Dimensions.for_rectangle(scaled_pos); 2338 Gdk.Rectangle old_scaled_pos = scaled_pos; 2339 2340 // attempt to reuse pixmap 2341 if (pixmap_dim.width != width || pixmap_dim.height != height) 2342 pixmap = null; 2343 2344 // if necessary, create a pixmap as large as the entire viewport 2345 bool new_pixmap = false; 2346 if (pixmap == null) { 2347 init_pixmap(width, height); 2348 new_pixmap = true; 2349 } 2350 2351 if (new_pixbuf || new_pixmap) { 2352 Dimensions unscaled_dim = Dimensions.for_pixbuf(unscaled); 2353 2354 // determine scaled size of pixbuf ... if a max dimensions is set and not scaling up, 2355 // respect it 2356 Dimensions scaled_dim = Dimensions(); 2357 if (!scale_up_to_viewport && max_dim.has_area() && max_dim.width < width && max_dim.height < height) 2358 scaled_dim = max_dim; 2359 else 2360 scaled_dim = unscaled_dim.get_scaled_proportional(pixmap_dim); 2361 2362 assert(width >= scaled_dim.width); 2363 assert(height >= scaled_dim.height); 2364 2365 // center pixbuf on the canvas 2366 scaled_pos.x = (width - scaled_dim.width) / 2; 2367 scaled_pos.y = (height - scaled_dim.height) / 2; 2368 scaled_pos.width = scaled_dim.width; 2369 scaled_pos.height = scaled_dim.height; 2370 } 2371 2372 Gdk.InterpType interp = (fast) ? FAST_INTERP : QUALITY_INTERP; 2373 2374 // rescale if canvas rescaled or better quality is requested 2375 if (scaled == null) { 2376 scaled = resize_pixbuf(unscaled, Dimensions.for_rectangle(scaled_pos), interp); 2377 2378 UpdateReason reason = UpdateReason.RESIZED_CANVAS; 2379 if (new_pixbuf) 2380 reason = UpdateReason.NEW_PIXBUF; 2381 else if (!new_pixmap && interp == QUALITY_INTERP) 2382 reason = UpdateReason.QUALITY_IMPROVEMENT; 2383 2384 static_zoom_state = ZoomState(max_dim, pixmap_dim, 2385 static_zoom_state.get_interpolation_factor(), 2386 static_zoom_state.get_viewport_center()); 2387 2388 updated_pixbuf(scaled, reason, old_scaled_dim); 2389 } 2390 2391 zoom_high_quality = !fast; 2392 2393 if (direction != null && !transition_clock.is_in_progress()) { 2394 Spit.Transitions.Visuals visuals = new Spit.Transitions.Visuals(old_scaled, 2395 old_scaled_pos, scaled, scaled_pos, parse_color("#000")); 2396 2397 transition_clock.start(visuals, direction.to_transition_direction(), transition_duration_msec, 2398 repaint_pixmap); 2399 } 2400 2401 if (!transition_clock.is_in_progress()) 2402 repaint_pixmap(); 2403 } 2404 2405 private void init_pixmap(int width, int height) { 2406 assert(unscaled != null); 2407 assert(canvas.get_window() != null); 2408 2409 // Cairo backing surface (manual double-buffering) 2410 pixmap = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); 2411 pixmap_dim = Dimensions(width, height); 2412 2413 // Cairo context for drawing on the pixmap 2414 pixmap_ctx = new Cairo.Context(pixmap); 2415 2416 // need a new pixbuf to fit this scale 2417 scaled = null; 2418 2419 // Cairo context for drawing text on the pixmap 2420 text_ctx = new Cairo.Context(pixmap); 2421 set_source_color_from_string(text_ctx, "#fff"); 2422 2423 2424 // no need to resize canvas, viewport does that automatically 2425 2426 new_surface(pixmap_ctx, pixmap_dim); 2427 } 2428 2429 protected override bool on_context_keypress() { 2430 return popup_context_menu(get_page_context_menu()); 2431 } 2432 2433 protected virtual void on_previous_photo() { 2434 } 2435 2436 protected virtual void on_next_photo() { 2437 } 2438 2439 public override bool key_press_event(Gdk.EventKey event) { 2440 // if the user holds the arrow keys down, we will receive a steady stream of key press 2441 // events for an operation that isn't designed for a rapid succession of output ... 2442 // we staunch the supply of new photos to under a quarter second (#533) 2443 bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC; 2444 2445 bool handled = true; 2446 switch (Gdk.keyval_name(event.keyval)) { 2447 case "Left": 2448 case "KP_Left": 2449 case "BackSpace": 2450 if (nav_ok) { 2451 on_previous_photo(); 2452 last_nav_key = event.time; 2453 } 2454 break; 2455 2456 case "Right": 2457 case "KP_Right": 2458 case "space": 2459 if (nav_ok) { 2460 on_next_photo(); 2461 last_nav_key = event.time; 2462 } 2463 break; 2464 2465 default: 2466 handled = false; 2467 break; 2468 } 2469 2470 if (handled) 2471 return true; 2472 2473 return (base.key_press_event != null) ? base.key_press_event(event) : true; 2474 } 2475 2476 private void on_colors_changed() { 2477 invalidate_transparent_background(); 2478 repaint(); 2479 } 2480} 2481 2482// 2483// DragAndDropHandler attaches signals to a Page to properly handle drag-and-drop requests for the 2484// Page as a DnD Source. (DnD Destination handling is handled by the appropriate AppWindow, i.e. 2485// LibraryWindow and DirectWindow). Assumes the Page's ViewCollection holds MediaSources. 2486// 2487public class DragAndDropHandler { 2488 private enum TargetType { 2489 XDS, 2490 MEDIA_LIST 2491 } 2492 2493 private const Gtk.TargetEntry[] SOURCE_TARGET_ENTRIES = { 2494 { "XdndDirectSave0", Gtk.TargetFlags.OTHER_APP, TargetType.XDS }, 2495 { "shotwell/media-id-atom", Gtk.TargetFlags.SAME_APP, TargetType.MEDIA_LIST } 2496 }; 2497 2498 private static Gdk.Atom? XDS_ATOM = null; 2499 private static Gdk.Atom? TEXT_ATOM = null; 2500 private static uint8[]? XDS_FAKE_TARGET = null; 2501 2502 private weak Page page; 2503 private Gtk.Widget event_source; 2504 private File? drag_destination = null; 2505 private ExporterUI exporter = null; 2506 2507 public DragAndDropHandler(Page page) { 2508 this.page = page; 2509 this.event_source = page.get_event_source(); 2510 assert(event_source != null); 2511 assert(event_source.get_has_window()); 2512 2513 // Need to do this because static member variables are not properly handled 2514 if (XDS_ATOM == null) 2515 XDS_ATOM = Gdk.Atom.intern_static_string("XdndDirectSave0"); 2516 2517 if (TEXT_ATOM == null) 2518 TEXT_ATOM = Gdk.Atom.intern_static_string("text/plain"); 2519 2520 if (XDS_FAKE_TARGET == null) 2521 XDS_FAKE_TARGET = string_to_uchar_array("shotwell.txt"); 2522 2523 // register what's available on this DnD Source 2524 Gtk.drag_source_set(event_source, Gdk.ModifierType.BUTTON1_MASK, SOURCE_TARGET_ENTRIES, 2525 Gdk.DragAction.COPY); 2526 2527 // attach to the event source's DnD signals, not the Page's, which is a NO_WINDOW widget 2528 // and does not emit them 2529 event_source.drag_begin.connect(on_drag_begin); 2530 event_source.drag_data_get.connect(on_drag_data_get); 2531 event_source.drag_end.connect(on_drag_end); 2532 event_source.drag_failed.connect(on_drag_failed); 2533 } 2534 2535 ~DragAndDropHandler() { 2536 if (event_source != null) { 2537 event_source.drag_begin.disconnect(on_drag_begin); 2538 event_source.drag_data_get.disconnect(on_drag_data_get); 2539 event_source.drag_end.disconnect(on_drag_end); 2540 event_source.drag_failed.disconnect(on_drag_failed); 2541 } 2542 2543 page = null; 2544 event_source = null; 2545 } 2546 2547 private void on_drag_begin(Gdk.DragContext context) { 2548 debug("on_drag_begin (%s)", page.get_page_name()); 2549 2550 if (page == null || page.get_view().get_selected_count() == 0 || exporter != null) 2551 return; 2552 2553 drag_destination = null; 2554 2555 // use the first media item as the icon 2556 ThumbnailSource thumb = (ThumbnailSource) page.get_view().get_selected_at(0).get_source(); 2557 2558 try { 2559 Gdk.Pixbuf icon = thumb.get_thumbnail(AppWindow.DND_ICON_SCALE); 2560 Gtk.drag_source_set_icon_pixbuf(event_source, icon); 2561 } catch (Error err) { 2562 warning("Unable to fetch icon for drag-and-drop from %s: %s", thumb.to_string(), 2563 err.message); 2564 } 2565 2566 // set the XDS property to indicate an XDS save is available 2567#if VALA_0_20 2568 Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE, 2569 XDS_FAKE_TARGET, 1); 2570#else 2571 Gdk.property_change(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 8, Gdk.PropMode.REPLACE, 2572 XDS_FAKE_TARGET); 2573#endif 2574 } 2575 2576 private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData selection_data, 2577 uint target_type, uint time) { 2578 debug("on_drag_data_get (%s)", page.get_page_name()); 2579 2580 if (page == null || page.get_view().get_selected_count() == 0) 2581 return; 2582 2583 switch (target_type) { 2584 case TargetType.XDS: 2585 // Fetch the XDS property that has been set with the destination path 2586 uchar[] data = new uchar[4096]; 2587 Gdk.Atom actual_type; 2588 int actual_format = 0; 2589 bool fetched = Gdk.property_get(context.get_source_window(), XDS_ATOM, TEXT_ATOM, 2590 0, data.length, 0, out actual_type, out actual_format, out data); 2591 2592 // the destination path is actually for our XDS_FAKE_TARGET, use its parent 2593 // to determine where the file(s) should go 2594 if (fetched && data != null && data.length > 0) 2595 drag_destination = File.new_for_uri(uchar_array_to_string(data)).get_parent(); 2596 2597 debug("on_drag_data_get (%s): %s", page.get_page_name(), 2598 (drag_destination != null) ? drag_destination.get_path() : "(no path)"); 2599 2600 // Set the property to "S" for Success or "E" for Error 2601 selection_data.set(XDS_ATOM, 8, 2602 string_to_uchar_array((drag_destination != null) ? "S" : "E")); 2603 break; 2604 2605 case TargetType.MEDIA_LIST: 2606 Gee.Collection<MediaSource> sources = 2607 (Gee.Collection<MediaSource>) page.get_view().get_selected_sources(); 2608 2609 // convert the selected media sources to Gdk.Atom-encoded sourceID strings for 2610 // internal drag-and-drop 2611 selection_data.set(Gdk.Atom.intern_static_string("SourceIDAtom"), (int) sizeof(Gdk.Atom), 2612 serialize_media_sources(sources)); 2613 break; 2614 2615 default: 2616 warning("on_drag_data_get (%s): unknown target type %u", page.get_page_name(), 2617 target_type); 2618 break; 2619 } 2620 } 2621 2622 private void on_drag_end() { 2623 debug("on_drag_end (%s)", page.get_page_name()); 2624 2625 if (page == null || page.get_view().get_selected_count() == 0 || drag_destination == null 2626 || exporter != null) { 2627 return; 2628 } 2629 2630 debug("Exporting to %s", drag_destination.get_path()); 2631 2632 // drag-and-drop export doesn't pop up an export dialog, so use what are likely the 2633 // most common export settings (the current -- or "working" -- file format, with 2634 // all transformations applied, at the image's original size). 2635 if (drag_destination.get_path() != null) { 2636 exporter = new ExporterUI(new Exporter( 2637 (Gee.Collection<Photo>) page.get_view().get_selected_sources(), 2638 drag_destination, Scaling.for_original(), ExportFormatParameters.current())); 2639 exporter.export(on_export_completed); 2640 } else { 2641 AppWindow.error_message(_("Photos cannot be exported to this directory.")); 2642 } 2643 2644 drag_destination = null; 2645 } 2646 2647 private bool on_drag_failed(Gdk.DragContext context, Gtk.DragResult drag_result) { 2648 debug("on_drag_failed (%s): %d", page.get_page_name(), (int) drag_result); 2649 2650 if (page == null) 2651 return false; 2652 2653 drag_destination = null; 2654 2655 return false; 2656 } 2657 2658 private void on_export_completed() { 2659 exporter = null; 2660 } 2661 2662} 2663