1/* 2 * Copyright (C) 2019 elementary, Inc. (https://elementary.io) 3 * 2011-2013 Tom Beckmann <tom@elementaryos.org> 4 * 5 * This program or library is free software; you can redistribute it 6 * and/or modify it under the terms of the GNU Lesser General Public 7 * License as published by the Free Software Foundation; either 8 * version 3 of the License, or (at your option) any later version. 9 * 10 * This library is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 * Lesser General Public License for more details. 14 * 15 * You should have received a copy of the GNU Lesser General 16 * Public License along with this library; if not, write to the 17 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 18 * Boston, MA 02110-1301 USA. 19 */ 20 21namespace Granite { 22 // https://bugzilla.gnome.org/show_bug.cgi?id=767718 23 public delegate void WidgetsDroppedDelegate (); 24} 25 26namespace Granite.Widgets { 27 28 // a mask to ignore modifiers like num lock or caps lock that are irrelevant to keyboard shortcuts 29 internal const Gdk.ModifierType MODIFIER_MASK = ( 30 Gdk.ModifierType.SHIFT_MASK | 31 Gdk.ModifierType.SUPER_MASK | 32 Gdk.ModifierType.CONTROL_MASK | 33 Gdk.ModifierType.MOD1_MASK 34 ); 35 36 private class TabPageContainer : Gtk.EventBox { 37 private unowned Tab _tab = null; 38 39 public unowned Tab tab { 40 get { return _tab; } 41 set { _tab = value; } 42 } 43 44 DynamicNotebook dynamic_notebook { 45 get { return (get_parent () as Gtk.Notebook).get_parent () as DynamicNotebook; } 46 } 47 48 public TabPageContainer (Tab tab) { 49 Object (tab: tab); 50 } 51 52 construct { 53 add (new Gtk.Grid ()); 54 55 // delay tabs resizing until cursor leaves tab-bar 56 // tab_bar-area = DynamicNotebook-area - TabPageContainer-area - add_button 57 this.enter_notify_event.connect ((e) => { 58 dynamic_notebook.check_to_recalc_size (); 59 return false; 60 }); 61 } 62 } 63 64 /** 65 * This is a standard tab which can be used in a notebook to form a tabbed UI. 66 */ 67 public class Tab : Gtk.EventBox { 68 Gtk.Label _label; 69 public string label { 70 get { return _label.label; } 71 72 set { 73 _label.label = value; 74 _label.set_tooltip_text (value); 75 } 76 } 77 78 /** 79 * The (plain) text that will be shown in a tooltip when the tab is hovered. 80 **/ 81 public string tooltip { 82 set { 83 _label.set_tooltip_text (value); 84 } 85 } 86 87 private bool _pinned = false; 88 public bool pinned { 89 get { return _pinned; } 90 91 set { 92 if (pinnable) { 93 if (value != _pinned) { 94 if (value) { 95 _label.visible = false; 96 _icon.margin_start = 1; 97 _working.margin_start = 1; 98 } else { 99 _label.visible = true; 100 _icon.margin_start = 0; 101 _working.margin_start = 0; 102 } 103 104 _pinned = value; 105 update_close_button_visibility (); 106 this.pin_switch (); 107 } 108 } 109 } 110 } 111 112 private bool _pinnable = true; 113 public bool pinnable { 114 get { return _pinnable; } 115 set { 116 if (!value) { 117 pinned = false; 118 } 119 120 _pinnable = value; 121 } 122 } 123 124 /** 125 * Data which will be kept once the tab is deleted, and which will be used by 126 * the application to restore the data into the restored tab. Let it empty if 127 * the tab should not be restored. 128 **/ 129 public string restore_data { get; set; default=""; } 130 131 /** 132 * An optional delegate that is called when the tab is dropped from the set 133 * of restorable tabs in DynamicNotebook. 134 * A tab is dropped either when Clear All is pressed, or when 135 * the tab is the oldest tab in the set of restorable tabs and 136 * the number of restorable tabs has exceeded the upper limit. 137 */ 138 public WidgetsDroppedDelegate dropped_callback = null; 139 140 /** 141 * Accelerator label of the "Close Tab" menu item in the tab context menu. 142 */ 143 public AccelLabel? close_tab_label { get; construct; } 144 145 /** 146 * Accelerator label of the "Duplicate Tab" menu item in the tab context menu. 147 */ 148 public AccelLabel? duplicate_tab_label { get; construct; } 149 150 /** 151 * Accelerator label of "Open tab in New Window" menu item in the tab context menu. 152 */ 153 public AccelLabel? new_window_label { get; construct; } 154 155 internal TabPageContainer page_container; 156 public Gtk.Widget page { 157 get { 158 return page_container.get_child (); 159 } 160 set { 161 weak Gtk.Widget container_child = page_container.get_child (); 162 if (container_child != null) { 163 page_container.remove (container_child); 164 } 165 166 weak Gtk.Container? value_parent = value.get_parent (); 167 if (value_parent != null) { 168 value_parent.remove (value); 169 page_container.add (value); 170 } else { 171 page_container.add (value); 172 } 173 174 page_container.show_all (); 175 } 176 } 177 178 DynamicNotebook dynamic_notebook { 179 get { return (get_parent () as Gtk.Notebook).get_parent () as DynamicNotebook; } 180 } 181 182 internal Gtk.Image _icon; 183 public GLib.Icon? icon { 184 owned get { return _icon.gicon; } 185 set { _icon.gicon = value; } 186 } 187 188 Gtk.Spinner _working; 189 bool __working; 190 public bool working { 191 get { return __working; } 192 193 set { 194 __working = _working.visible = value; 195 _icon.visible = !value; 196 } 197 } 198 199 public Pango.EllipsizeMode ellipsize_mode { 200 get { return _label.ellipsize; } 201 set { _label.ellipsize = value; } 202 } 203 204 public Gtk.Menu menu { get; set; } 205 206 private bool _closable = true; 207 internal bool closable { 208 set { 209 if (value == _closable) 210 return; 211 212 _closable = value; 213 update_close_button_visibility (); 214 } 215 } 216 217 //We need to be able to toggle these from the notebook. 218 internal Gtk.MenuItem new_window_m; 219 internal Gtk.MenuItem duplicate_m; 220 internal Gtk.MenuItem pin_m; 221 222 private bool _is_current_tab = false; 223 internal bool is_current_tab { 224 set { 225 _is_current_tab = value; 226 update_close_button_visibility (); 227 } 228 } 229 230 private bool cursor_over_tab = false; 231 private bool cursor_over_close_button = false; 232 private Gtk.Revealer close_button_revealer; 233 234 internal signal void closed (); 235 internal signal void close_others (); 236 internal signal void close_others_right (); 237 internal signal void new_window (); 238 internal signal void duplicate (); 239 internal signal void pin_switch (); 240 241 /** 242 * With this you can construct a Tab. It is linked to the page that is shown on focus. 243 * A Tab can have a icon on the right side. You can pass null on the constructor to 244 * create a tab without a icon. 245 **/ 246 public Tab (string? label = null, GLib.Icon? icon = null, Gtk.Widget? page = null) { 247 this.with_accellabels (label, icon, page); 248 } 249 250 /** 251 * Create a tab with accellabels. 252 */ 253 public Tab.with_accellabels ( 254 string? label = null, 255 GLib.Icon? icon = null, 256 Gtk.Widget? page = null, 257 AccelLabel? _close_tab_label = null, 258 AccelLabel? _duplicate_tab_label = null, 259 AccelLabel? _new_window_label = null 260 ) { 261 Object ( 262 label: label, 263 icon: icon, 264 close_tab_label: _close_tab_label, 265 duplicate_tab_label: _duplicate_tab_label, 266 new_window_label: _new_window_label 267 ); 268 269 if (page != null) { 270 this.page = page; 271 } 272 } 273 274 static construct { 275 Granite.init (); 276 } 277 278 construct { 279 if (close_tab_label == null) { 280 close_tab_label = new Granite.AccelLabel (_("Close Tab")); 281 } 282 if (new_window_label == null) { 283 new_window_label = new Granite.AccelLabel (_("Open in a New Window")); 284 } 285 if (duplicate_tab_label == null) { 286 duplicate_tab_label = new Granite.AccelLabel (_("Duplicate")); 287 } 288 289 _label = new Gtk.Label (null); 290 _label.hexpand = true; 291 _label.tooltip_text = label; 292 _label.ellipsize = Pango.EllipsizeMode.END; 293 294 _icon = new Gtk.Image (); 295 _icon.icon_size = Gtk.IconSize.MENU; 296 _icon.visible = true; 297 _icon.set_size_request (16, 16); 298 299 _working = new Gtk.Spinner (); 300 _working.set_size_request (16, 16); 301 _working.start (); 302 303 var close_button = new Gtk.Button.from_icon_name ("window-close-symbolic", Gtk.IconSize.MENU); 304 close_button.tooltip_text = _("Close Tab"); 305 close_button.valign = Gtk.Align.CENTER; 306 close_button.relief = Gtk.ReliefStyle.NONE; 307 308 close_button_revealer = new Gtk.Revealer () { 309 transition_duration = TRANSITION_DURATION_IN_PLACE, 310 transition_type = Gtk.RevealerTransitionType.CROSSFADE 311 }; 312 313 close_button_revealer.add (close_button); 314 315 var tab_layout = new Gtk.Grid (); 316 tab_layout.hexpand = false; 317 tab_layout.orientation = Gtk.Orientation.HORIZONTAL; 318 tab_layout.add (close_button_revealer); 319 tab_layout.add (_label); 320 tab_layout.add (_icon); 321 tab_layout.add (_working); 322 323 visible_window = true; 324 325 add (tab_layout); 326 show_all (); 327 328 page_container = new TabPageContainer (this); 329 330 menu = new Gtk.Menu (); 331 var close_m = new Gtk.MenuItem () { child = close_tab_label }; 332 var close_other_m = new Gtk.MenuItem.with_label (""); 333 var close_other_right_m = new Gtk.MenuItem.with_label (""); 334 pin_m = new Gtk.MenuItem.with_label (""); 335 new_window_m = new Gtk.MenuItem () { child = new_window_label }; 336 duplicate_m = new Gtk.MenuItem () { child = duplicate_tab_label }; 337 menu.append (close_other_m); 338 menu.append (close_other_right_m); 339 menu.append (close_m); 340 menu.append (new_window_m); 341 menu.append (duplicate_m); 342 menu.append (pin_m); 343 menu.show_all (); 344 345 close_m.activate.connect (() => closed () ); 346 close_other_m.activate.connect (() => close_others () ); 347 close_other_right_m.activate.connect (() => close_others_right () ); 348 new_window_m.activate.connect (() => new_window () ); 349 duplicate_m.activate.connect (() => duplicate () ); 350 pin_m.activate.connect (() => pinned = !pinned); 351 352 add_events (Gdk.EventMask.SCROLL_MASK); 353 this.scroll_event.connect ((e) => { 354 switch (e.direction) { 355 case Gdk.ScrollDirection.UP: 356 case Gdk.ScrollDirection.LEFT: 357 dynamic_notebook.previous_page (); 358 return true; 359 360 case Gdk.ScrollDirection.DOWN: 361 case Gdk.ScrollDirection.RIGHT: 362 dynamic_notebook.next_page (); 363 return true; 364 } 365 366 return false; 367 }); 368 369 this.button_press_event.connect ((e) => { 370 if (e.button == 1 && e.type == Gdk.EventType.2BUTTON_PRESS && duplicate_m.visible) { 371 this.duplicate (); 372 } else if (e.button == 2) { 373 return true; // consume middle-click, prevent event propagation to DynamicNotebook 374 } else if (e.button == 3) { 375 menu.popup_at_pointer (e); 376 uint num_tabs = dynamic_notebook.n_tabs; 377 uint tab_position = dynamic_notebook.get_tab_position (this); 378 close_other_m.label = dngettext (GETTEXT_PACKAGE, _("Close Other Tab"), _("Close Other Tabs"), num_tabs - 1); 379 close_other_m.sensitive = (num_tabs != 1); 380 /// TRANSLATORS: This will close tabs to the left in right-to-left environments 381 close_other_right_m.label = dngettext ( 382 GETTEXT_PACKAGE, 383 _("Close Tab to the Right"), 384 _("Close Tabs to the Right"), 385 num_tabs - 1 - tab_position 386 ); 387 close_other_right_m.sensitive = (tab_position < num_tabs - 1); 388 new_window_m.sensitive = (num_tabs != 1); 389 pin_m.label = _("Pin"); 390 if (this.pinned) { 391 pin_m.label = _("Unpin"); 392 } 393 } else { 394 return false; 395 } 396 397 return true; 398 }); 399 400 this.button_release_event.connect ((e) => { 401 if (e.button == 2 && cursor_over_tab) { 402 e.state &= MODIFIER_MASK; 403 if (e.state == 0) { 404 dynamic_notebook.close_tab_and_keep_width (this); 405 } else if (e.state == Gdk.ModifierType.SHIFT_MASK) { 406 this.close_others (); 407 } 408 409 return true; 410 } 411 412 return false; 413 }); 414 415 this.enter_notify_event.connect ((e) => { 416 cursor_over_tab = true; 417 update_close_button_visibility (); 418 return false; 419 }); 420 421 this.leave_notify_event.connect ((e) => { 422 // We don't want to handle leave_notify events without a prior enter_notify 423 // for event parity reasons. 424 if (!cursor_over_tab) 425 return false; 426 427 cursor_over_tab = false; 428 update_close_button_visibility (); 429 return false; 430 }); 431 432 // Hovering the close button area causes a leave_notify_event on the tab EventBox. 433 // Because of that we need to watch the events from those widgets independently 434 // to avoid misbehavior. While setting "above_child" to "true" on the tab might 435 // appear to be a more proper solution, that wouldn't let us capture any event 436 // (e.g. button_press) on the button. 437 close_button.enter_notify_event.connect ((e) => { 438 cursor_over_close_button = true; 439 update_close_button_visibility (); 440 return false; 441 }); 442 443 close_button.leave_notify_event.connect ((e) => { 444 // We don't want to handle leave_notify events without a prior enter_notify 445 // for event parity reasons. 446 if (!cursor_over_close_button) 447 return false; 448 449 cursor_over_close_button = false; 450 update_close_button_visibility (); 451 return false; 452 }); 453 454 page_container.button_press_event.connect (() => { return true; }); //dont let clicks pass through 455 close_button.clicked.connect (() => { dynamic_notebook.close_tab_and_keep_width (this); }); 456 working = false; 457 458 update_close_button_visibility (); 459 } 460 461 public void close () { 462 closed (); 463 } 464 465 private void update_close_button_visibility () { 466 // If the tab is pinned, we don't want the revealer to keep 467 // the size allocation of the close button. 468 close_button_revealer.no_show_all = _pinned; 469 close_button_revealer.visible = !_pinned; 470 471 close_button_revealer.reveal_child = _closable && !_pinned 472 && (cursor_over_tab || cursor_over_close_button || _is_current_tab); 473 } 474 } 475 476 private class ClosedTabs : GLib.Object { 477 478 public signal void restored (string label, string restore_data, GLib.Icon? icon); 479 public signal void cleared (); 480 481 private int _max_restorable_tabs = 10; 482 public int max_restorable_tabs { 483 get { return _max_restorable_tabs; } 484 set { 485 assert (value > 0); 486 _max_restorable_tabs = value; 487 } 488 } 489 490 internal struct Entry { 491 string label; 492 string restore_data; 493 GLib.Icon? icon; 494 weak WidgetsDroppedDelegate? dropped_callback; 495 } 496 497 private Gee.LinkedList<Entry?> closed_tabs; 498 499 public ClosedTabs () { 500 501 } 502 503 construct { 504 closed_tabs = new Gee.LinkedList<Entry?> (); 505 } 506 507 public bool empty { 508 get { 509 return closed_tabs.size == 0; 510 } 511 } 512 513 public void push (Tab tab) { 514 foreach (var entry in closed_tabs) 515 if (tab.restore_data == entry.restore_data) 516 return; 517 518 // Insert the element at the end of the list. 519 Entry e = { tab.label, tab.restore_data, tab.icon, tab.dropped_callback }; 520 closed_tabs.add (e); 521 522 // If the maximum size is exceeded, remove from the beginning of the list. 523 if (closed_tabs.size > max_restorable_tabs) { 524 var elem = closed_tabs.poll_head (); 525 unowned WidgetsDroppedDelegate? dropped_callback = elem.dropped_callback; 526 527 if (dropped_callback != null) 528 dropped_callback (); 529 } 530 } 531 532 public Entry pop () { 533 assert (closed_tabs.size > 0); 534 return closed_tabs.poll_tail (); 535 } 536 537 public Entry pick (string search) { 538 Entry picked = {null, null, null}; 539 540 for (int i = 0; i < closed_tabs.size; i++) { 541 var entry = closed_tabs[i]; 542 543 if (entry.restore_data == search) { 544 picked = closed_tabs.remove_at (i); 545 break; 546 } 547 } 548 549 return picked; 550 } 551 552 public Gtk.Menu menu { 553 owned get { 554 var _menu = new Gtk.Menu (); 555 556 foreach (var entry in closed_tabs) { 557 var item = new Gtk.MenuItem.with_label (entry.label); 558 _menu.prepend (item); 559 560 item.activate.connect (() => { 561 var e = pick (entry.restore_data); 562 this.restored (e.label, e.restore_data, e.icon); 563 }); 564 } 565 566 if (!empty) { 567 var separator = new Gtk.SeparatorMenuItem (); 568 var item = new Gtk.MenuItem.with_label (_("Clear All")); 569 570 _menu.append (separator); 571 _menu.append (item); 572 573 item.activate.connect (() => { 574 foreach (var entry in closed_tabs) { 575 if (entry.dropped_callback != null) { 576 entry.dropped_callback (); 577 } 578 } 579 580 closed_tabs.clear (); 581 cleared (); 582 }); 583 } 584 585 return _menu; 586 } 587 } 588 } 589 590 /** 591 * Tab bar widget designed for a variable number of tabs. 592 * Supports showing a "New tab" button, restoring closed tabs, "pinning" tabs, and more. 593 * 594 * {{../doc/images/DynamicNotebook.png}} 595 */ 596 public class DynamicNotebook : Gtk.EventBox { 597 /** 598 * number of pages 599 */ 600 public int n_tabs { 601 get { return notebook.get_n_pages (); } 602 } 603 604 /** 605 * Hide the tab bar and only show the pages 606 */ 607 public bool show_tabs { 608 get { return notebook.show_tabs; } 609 set { notebook.show_tabs = value; } 610 } 611 612 /** 613 * Hide the close buttons and disable closing of tabs 614 */ 615 bool _tabs_closable = true; 616 public bool tabs_closable { 617 get { return _tabs_closable; } 618 set { 619 if (value != _tabs_closable) 620 tabs.foreach ((t) => { 621 t.closable = value; 622 }); 623 _tabs_closable = value; 624 } 625 } 626 627 /** 628 * Make tabs reorderable 629 */ 630 bool _allow_drag = true; 631 public bool allow_drag { 632 get { return _allow_drag; } 633 set { 634 _allow_drag = value; 635 this.tabs.foreach ((t) => { 636 notebook.set_tab_reorderable (t.page_container, value); 637 }); 638 } 639 } 640 641 /** 642 * Allow creating new windows by dragging a tab out 643 */ 644 bool _allow_new_window = false; 645 public bool allow_new_window { 646 get { return _allow_new_window; } 647 set { 648 _allow_new_window = value; 649 this.tabs.foreach ((t) => { 650 notebook.set_tab_detachable (t.page_container, value); 651 }); 652 } 653 } 654 655 /** 656 * Allow duplicating tabs 657 */ 658 bool _allow_duplication = false; 659 public bool allow_duplication { 660 get { return _allow_duplication; } 661 set { 662 _allow_duplication = value; 663 664 foreach (var tab in tabs) { 665 tab.duplicate_m.visible = value; 666 } 667 } 668 } 669 670 /** 671 * Allow restoring tabs 672 */ 673 bool _allow_restoring = false; 674 public bool allow_restoring { 675 get { return _allow_restoring; } 676 set { 677 _allow_restoring = value; 678 restore_tab_m.visible = value; 679 restore_button.visible = value; 680 } 681 } 682 683 /** 684 * Set or get the upper limit of the size of the set 685 * of restorable tabs. 686 */ 687 public int max_restorable_tabs { 688 get { return closed_tabs.max_restorable_tabs; } 689 set { closed_tabs.max_restorable_tabs = value; } 690 } 691 692 /** 693 * Controls the '+' add button visibility 694 */ 695 bool _add_button_visible = true; 696 public bool add_button_visible { 697 get { return _add_button_visible; } 698 set { 699 if (value != _add_button_visible) { 700 if (_add_button_visible) { 701 notebook.remove (add_button); 702 } else { 703 notebook.set_action_widget (add_button, Gtk.PackType.START); 704 } 705 706 _add_button_visible = value; 707 } 708 } 709 } 710 711 bool _allow_pinning = false; 712 public bool allow_pinning { 713 get { return _allow_pinning; } 714 set { 715 _allow_pinning = value; 716 717 foreach (var tab in tabs) { 718 tab.pinnable = value; 719 } 720 } 721 } 722 723 bool _force_left = true; 724 public bool force_left { 725 get { return _force_left; } 726 set { _force_left = value; } 727 } 728 729 /** 730 * The text shown in the add button tooltip 731 */ 732 public string add_button_tooltip { 733 get { _add_button_tooltip = add_button.tooltip_text; return _add_button_tooltip; } 734 set { add_button.tooltip_text = value; } 735 } 736 // Use temporary field to avoid breaking API this can be dropped while preparing for 0.4 737 string _add_button_tooltip; 738 739 /** 740 * Accelerator label of the "New Tab" menu item in the tab context menu. 741 */ 742 public AccelLabel new_tab_label { get; construct; } 743 744 /** 745 * Accelerator label of the "Restore Tab" menu item in the tab context menu. 746 */ 747 public AccelLabel restore_tab_label { get; construct; } 748 749 public Tab current { 750 get { return tabs.nth_data (notebook.get_current_page ()); } 751 set { notebook.set_current_page (tabs.index (value)); } 752 } 753 754 GLib.List<Tab> _tabs; 755 public GLib.List<Tab> tabs { 756 get { 757 _tabs = new GLib.List<Tab> (); 758 for (var i = 0; i < n_tabs; i++) { 759 _tabs.append (notebook.get_tab_label (notebook.get_nth_page (i)) as Tab); 760 } 761 return _tabs; 762 } 763 } 764 765 public string group_name { 766 get { return notebook.group_name; } 767 set { notebook.group_name = value; } 768 } 769 770 public enum TabBarBehavior { 771 ALWAYS = 0, 772 SINGLE = 1, 773 NEVER = 2 774 } 775 776 /** 777 * The behavior of the tab bar and its visibility 778 */ 779 public TabBarBehavior tab_bar_behavior { 780 set { 781 _tab_bar_behavior = value; 782 update_tabs_visibility (); 783 } 784 785 get { return _tab_bar_behavior; } 786 } 787 788 private TabBarBehavior _tab_bar_behavior; 789 790 /** 791 * The menu appearing when the notebook is clicked on a blank space 792 */ 793 public Gtk.Menu menu { get; private set; } 794 795 private ClosedTabs closed_tabs; 796 797 Gtk.Notebook notebook; 798 799 private const int MIN_TAB_WIDTH = 80; 800 private const int MAX_TAB_WIDTH = 220; 801 private const int TAB_WIDTH_PINNED = 18; 802 private int tab_width = MAX_TAB_WIDTH; 803 private bool wait_to_recalc_size = false; 804 805 public signal void tab_added (Tab tab); 806 public signal void tab_removed (Tab tab); 807 private Tab? old_tab; //stores a reference for tab_switched 808 public signal void tab_switched (Tab? old_tab, Tab new_tab); 809 public signal void tab_reordered (Tab tab, int new_pos); 810 public signal void tab_moved (Tab tab, int x, int y); 811 public signal void tab_duplicated (Tab duplicated_tab); 812 public signal void tab_restored (string label, string data, GLib.Icon? icon); 813 public signal void new_tab_requested (); 814 public signal bool close_tab_requested (Tab tab); 815 816 private Gtk.MenuItem new_tab_m; 817 private Gtk.MenuItem restore_tab_m; 818 819 private Gtk.Button add_button; 820 private Gtk.Button restore_button; // should be a Gtk.MenuButton when we have Gtk+ 3.6 821 822 /** 823 * Create a new dynamic notebook 824 */ 825 public DynamicNotebook () { 826 this.with_accellabels (); 827 } 828 829 /** 830 * Create a new dynamic notebook with accellabels 831 */ 832 public DynamicNotebook.with_accellabels ( 833 AccelLabel new_tab_label = new AccelLabel (_("New Tab")), 834 AccelLabel restore_tab_label = new AccelLabel (_("Undo Close Tab")) 835 ) { 836 Object ( 837 new_tab_label: new_tab_label, 838 restore_tab_label: restore_tab_label 839 ); 840 } 841 842 static construct { 843 Granite.init (); 844 } 845 846 construct { 847 notebook = new Gtk.Notebook (); 848 notebook.can_focus = false; 849 visible_window = true; // needed for leave_notify event 850 get_style_context ().add_class ("dynamic-notebook"); 851 852 notebook.scrollable = true; 853 notebook.show_border = false; 854 _tab_bar_behavior = TabBarBehavior.ALWAYS; 855 856 add (notebook); 857 858 menu = new Gtk.Menu (); 859 new_tab_m = new Gtk.MenuItem () { child = new_tab_label }; 860 restore_tab_m = new Gtk.MenuItem () { 861 child = restore_tab_label, 862 sensitive = false 863 }; 864 menu.append (new_tab_m); 865 menu.append (restore_tab_m); 866 menu.show_all (); 867 868 new_tab_m.activate.connect (() => { 869 new_tab_requested (); 870 }); 871 872 restore_tab_m.activate.connect (() => { 873 restore_last_tab (); 874 }); 875 876 closed_tabs = new ClosedTabs (); 877 closed_tabs.restored.connect ((label, restore_data, icon) => { 878 if (!allow_restoring) 879 return; 880 restore_button.sensitive = !closed_tabs.empty; 881 restore_tab_m.sensitive = !closed_tabs.empty; 882 tab_restored (label, restore_data, icon); 883 }); 884 885 closed_tabs.cleared.connect (() => { 886 restore_button.sensitive = false; 887 restore_tab_m.sensitive = false; 888 }); 889 890 add_button = new Gtk.Button.from_icon_name ("list-add-symbolic", Gtk.IconSize.MENU); 891 add_button.relief = Gtk.ReliefStyle.NONE; 892 add_button.margin_top = 6; 893 add_button.margin_bottom = 6; 894 add_button.tooltip_text = _("New Tab"); 895 896 // FIXME: Used to prevent an issue with widget overlap in Gtk+ < 3.20 897 var add_button_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 898 add_button_box.add (add_button); 899 add_button_box.show_all (); 900 901 restore_button = new Gtk.Button.from_icon_name ("document-open-recent-symbolic", Gtk.IconSize.MENU); 902 restore_button.margin_end = 3; 903 restore_button.relief = Gtk.ReliefStyle.NONE; 904 restore_button.tooltip_text = _("Closed Tabs"); 905 restore_button.sensitive = false; 906 restore_button.show (); 907 908 notebook.set_action_widget (add_button_box, Gtk.PackType.START); 909 notebook.set_action_widget (restore_button, Gtk.PackType.END); 910 911 // delay tabs resizing until cursor leaves tab-bar 912 // tab_bar-area = DynamicNotebook-area - TabPageContainer-area - add_button 913 leave_notify_event.connect ((e) => { check_to_recalc_size (); return false; }); 914 add_button.enter_notify_event.connect (() => { check_to_recalc_size (); return false; }); 915 916 917 add_button.clicked.connect (() => { 918 new_tab_requested (); 919 }); 920 921 add_button.button_press_event.connect ((e) => { 922 // Consume double-clicks 923 return e.type == Gdk.EventType.2BUTTON_PRESS && e.button == 1; 924 }); 925 926 restore_button.clicked.connect (() => { 927 var menu = closed_tabs.menu; 928 menu.attach_widget = restore_button; 929 menu.show_all (); 930 menu.popup_at_widget (restore_button, Gdk.Gravity.SOUTH_EAST, Gdk.Gravity.NORTH_EAST, null); 931 }); 932 933 restore_tab_m.visible = allow_restoring; 934 restore_button.visible = allow_restoring; 935 936 size_allocate.connect (() => { 937 if (!wait_to_recalc_size) { 938 recalc_size (); 939 } 940 }); 941 942 button_press_event.connect ((e) => { 943 if (e.type == Gdk.EventType.2BUTTON_PRESS && e.button == 1) { 944 new_tab_requested (); 945 } else if (e.button == 2 && allow_restoring) { 946 restore_last_tab (); 947 return true; 948 } else if (e.button == 3) { 949 menu.popup_at_pointer (e); 950 } 951 952 return false; 953 }); 954 955 key_press_event.connect ((e) => { 956 e.state &= MODIFIER_MASK; 957 958 switch (e.keyval) { 959 case Gdk.Key.@w: 960 case Gdk.Key.@W: 961 if (e.state == Gdk.ModifierType.CONTROL_MASK) { 962 if (!tabs_closable) { 963 break; 964 } 965 966 current.close (); 967 return true; 968 } 969 970 break; 971 972 case Gdk.Key.@t: 973 case Gdk.Key.@T: 974 if (e.state == Gdk.ModifierType.CONTROL_MASK) { 975 new_tab_requested (); 976 return true; 977 } else if ( 978 e.state == (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) && 979 allow_restoring 980 ) { 981 restore_last_tab (); 982 return true; 983 } 984 985 break; 986 987 case Gdk.Key.Page_Up: 988 if (e.state == Gdk.ModifierType.CONTROL_MASK) { 989 next_page (); 990 return true; 991 } 992 993 break; 994 995 case Gdk.Key.Page_Down: 996 if (e.state == Gdk.ModifierType.CONTROL_MASK) { 997 previous_page (); 998 return true; 999 } 1000 1001 break; 1002 1003 case Gdk.Key.@1: 1004 case Gdk.Key.@2: 1005 case Gdk.Key.@3: 1006 case Gdk.Key.@4: 1007 case Gdk.Key.@5: 1008 case Gdk.Key.@6: 1009 case Gdk.Key.@7: 1010 case Gdk.Key.@8: 1011 if ((e.state & Gdk.ModifierType.MOD1_MASK) == Gdk.ModifierType.MOD1_MASK) { 1012 var i = e.keyval - 49; 1013 var n_pages = notebook.get_n_pages (); 1014 notebook.page = (int) ((i >= n_pages) ? n_pages - 1 : i); 1015 return true; 1016 } 1017 1018 break; 1019 1020 case Gdk.Key.@9: 1021 if ((e.state & Gdk.ModifierType.MOD1_MASK) == Gdk.ModifierType.MOD1_MASK) { 1022 notebook.page = notebook.get_n_pages () - 1; 1023 return true; 1024 } 1025 1026 break; 1027 } 1028 1029 return false; 1030 }); 1031 1032 destroy.connect (() => { 1033 notebook.switch_page.disconnect (on_switch_page); 1034 notebook.page_added.disconnect (on_page_added); 1035 notebook.page_removed.disconnect (on_page_removed); 1036 notebook.page_reordered.disconnect (on_page_reordered); 1037 notebook.create_window.disconnect (on_create_window); 1038 }); 1039 1040 notebook.switch_page.connect (on_switch_page); 1041 notebook.page_added.connect (on_page_added); 1042 notebook.page_removed.connect (on_page_removed); 1043 notebook.page_reordered.connect (on_page_reordered); 1044 notebook.create_window.connect (on_create_window); 1045 } 1046 1047 void on_switch_page (Gtk.Widget page, uint pagenum) { 1048 var new_tab = (page as TabPageContainer).tab; 1049 1050 // update property accordingly for previous selected tab 1051 if (old_tab != null) 1052 old_tab.is_current_tab = false; 1053 1054 // now set the new tab as current 1055 new_tab.is_current_tab = true; 1056 1057 tab_switched (old_tab, new_tab); 1058 1059 old_tab = new_tab; 1060 } 1061 1062 void on_page_added (Gtk.Widget page, uint pagenum) { 1063 var t = (page as TabPageContainer).tab; 1064 1065 insert_callbacks (t); 1066 tab_added (t); 1067 update_tabs_visibility (); 1068 } 1069 1070 void on_page_removed (Gtk.Widget page, uint pagenum) { 1071 var t = (page as TabPageContainer).tab; 1072 1073 remove_callbacks (t); 1074 tab_removed (t); 1075 update_tabs_visibility (); 1076 } 1077 1078 void on_page_reordered (Gtk.Widget page, uint pagenum) { 1079 tab_reordered ((page as TabPageContainer).tab, (int) pagenum); 1080 recalc_order (); 1081 } 1082 1083 unowned Gtk.Notebook on_create_window (Gtk.Widget page, int x, int y) { 1084 var tab = notebook.get_tab_label (page) as Tab; 1085 tab_moved (tab, x, y); 1086 recalc_order (); 1087 return (Gtk.Notebook) null; 1088 } 1089 1090 private void recalc_order () { 1091 if (n_tabs == 0 || !force_left) 1092 return; 1093 1094 var pinned_tabs = 0; 1095 for (var i = 0; i < this.notebook.get_n_pages (); i++) { 1096 if ((this.notebook.get_tab_label (this.notebook.get_nth_page (i)) as Tab).pinned) { 1097 pinned_tabs++; 1098 } 1099 } 1100 1101 for (var p = 0; p < pinned_tabs; p++) { 1102 int sel = p; 1103 for (var i = p; i < this.notebook.get_n_pages (); i++) { 1104 if ((this.notebook.get_tab_label (this.notebook.get_nth_page (i)) as Tab).pinned) { 1105 sel = i; 1106 break; 1107 } 1108 } 1109 1110 if (sel != p) { 1111 this.notebook.reorder_child (this.notebook.get_nth_page (sel), p); 1112 } 1113 } 1114 } 1115 1116 private void recalc_size () { 1117 if (n_tabs == 0) 1118 return; 1119 1120 var pinned_tabs = 0; 1121 var unpinned_tabs = 0; 1122 for (var i = 0; i < n_tabs; i++) { 1123 if ((this.notebook.get_tab_label (this.notebook.get_nth_page (i)) as Tab).pinned) { 1124 pinned_tabs++; 1125 } else { 1126 unpinned_tabs++; 1127 } 1128 } 1129 1130 if (unpinned_tabs != 0) { 1131 var offset = 130; 1132 tab_width = (this.get_allocated_width () - offset - pinned_tabs * TAB_WIDTH_PINNED) / unpinned_tabs; 1133 1134 if (tab_width > MAX_TAB_WIDTH) { 1135 tab_width = MAX_TAB_WIDTH; 1136 } else if (tab_width < MIN_TAB_WIDTH) { 1137 tab_width = MIN_TAB_WIDTH; 1138 } 1139 } 1140 1141 foreach (var tab in tabs.copy ()) { 1142 tab.width_request = tab.pinned ? TAB_WIDTH_PINNED : tab_width; 1143 } 1144 1145 this.notebook.resize_children (); 1146 } 1147 1148 private void restore_last_tab () { 1149 if (!allow_restoring || closed_tabs.empty) { 1150 return; 1151 } 1152 1153 var restored = closed_tabs.pop (); 1154 restore_button.sensitive = !closed_tabs.empty; 1155 restore_tab_m.sensitive = !closed_tabs.empty; 1156 this.tab_restored (restored.label, restored.restore_data, restored.icon); 1157 } 1158 1159 private void switch_pin_tab (Tab tab) { 1160 if (!allow_pinning) { 1161 return; 1162 } 1163 1164 recalc_order (); 1165 recalc_size (); 1166 } 1167 1168 public void remove_tab (Tab tab) { 1169 var pos = get_tab_position (tab); 1170 1171 if (pos != -1) 1172 notebook.remove_page (pos); 1173 } 1174 1175 public void next_page () { 1176 if (this.notebook.page + 1 >= this.notebook.get_n_pages ()) { 1177 this.notebook.page = 0; 1178 } else { 1179 this.notebook.page++; 1180 } 1181 } 1182 1183 public void previous_page () { 1184 this.notebook.page = this.notebook.page - 1 < 0 ? 1185 this.notebook.page = this.notebook.get_n_pages () - 1 : this.notebook.page - 1; 1186 } 1187 1188 public override void show () { 1189 base.show (); 1190 notebook.show (); 1191 } 1192 1193 public new List<Gtk.Widget> get_children () { 1194 var list = new List<Gtk.Widget> (); 1195 1196 foreach (var child in notebook.get_children ()) { 1197 list.append ((child as Gtk.Container).get_children ().nth_data (0)); 1198 } 1199 1200 return list; 1201 } 1202 1203 public int get_tab_position (Tab tab) { 1204 return this.notebook.page_num (tab.page_container); 1205 } 1206 1207 public void set_tab_position (Tab tab, int position) { 1208 notebook.reorder_child (tab.page_container, position); 1209 tab_reordered (tab, position); 1210 recalc_order (); 1211 } 1212 1213 public Tab? get_tab_by_index (int index) { 1214 return notebook.get_tab_label (notebook.get_nth_page (index)) as Tab; 1215 } 1216 1217 public Tab? get_tab_by_widget (Gtk.Widget widget) { 1218 return notebook.get_tab_label (widget.get_parent ()) as Tab; 1219 } 1220 1221 public Gtk.Widget get_nth_page (int index) { 1222 return notebook.get_nth_page (index); 1223 } 1224 1225 public uint insert_tab (Tab tab, int index) { 1226 return_val_if_fail (tabs.index (tab) < 0, 0); 1227 1228 index = this.notebook.insert_page (tab.page_container, tab, index <= -1 ? n_tabs : index); 1229 1230 this.notebook.set_tab_reorderable (tab.page_container, this.allow_drag); 1231 this.notebook.set_tab_detachable (tab.page_container, this.allow_new_window); 1232 1233 tab.duplicate_m.visible = allow_duplication; 1234 tab.new_window_m.visible = allow_new_window; 1235 tab.pin_m.visible = allow_pinning; 1236 tab.pinnable = allow_pinning; 1237 tab.pinned = false; 1238 1239 tab.width_request = tab_width; 1240 this.recalc_size (); 1241 this.recalc_order (); 1242 1243 if (!tabs_closable) 1244 tab.closable = false; 1245 1246 return index; 1247 } 1248 1249 internal void close_tab_and_keep_width (Tab clicked_tab) { 1250 wait_to_recalc_size = true; 1251 clicked_tab.closed (); 1252 } 1253 1254 internal void check_to_recalc_size () { 1255 if (!wait_to_recalc_size) { 1256 return; 1257 } 1258 1259 recalc_size (); 1260 wait_to_recalc_size = false; 1261 } 1262 1263 private void insert_callbacks (Tab tab) { 1264 tab.closed.connect (on_tab_closed); 1265 tab.close_others.connect (on_close_others); 1266 tab.close_others_right.connect (on_close_others_right); 1267 tab.new_window.connect (on_new_window); 1268 tab.duplicate.connect (on_duplicate); 1269 tab.pin_switch.connect (on_pin_switch); 1270 } 1271 1272 private void remove_callbacks (Tab tab) { 1273 tab.closed.disconnect (on_tab_closed); 1274 tab.close_others.disconnect (on_close_others); 1275 tab.close_others_right.disconnect (on_close_others_right); 1276 tab.new_window.disconnect (on_new_window); 1277 tab.duplicate.disconnect (on_duplicate); 1278 tab.pin_switch.disconnect (on_pin_switch); 1279 } 1280 1281 private void on_tab_closed (Tab tab) { 1282 if (Signal.has_handler_pending ( 1283 this, 1284 Signal.lookup ("close-tab-requested", typeof (DynamicNotebook)), 1285 0, 1286 true 1287 )) { 1288 var sure = close_tab_requested (tab); 1289 1290 if (!sure) { 1291 return; 1292 } 1293 } 1294 1295 var pos = get_tab_position (tab); 1296 1297 remove_tab (tab); 1298 1299 if (pos != -1 && tab.page.get_parent () != null) 1300 tab.page.unparent (); 1301 1302 if (tab.label != "" && tab.restore_data != "") { 1303 closed_tabs.push (tab); 1304 restore_button.sensitive = !closed_tabs.empty; 1305 restore_tab_m.sensitive = !closed_tabs.empty; 1306 } 1307 } 1308 1309 private void on_close_others (Tab clicked_tab) { 1310 tabs.copy ().foreach ((tab) => { 1311 if (tab != clicked_tab) { 1312 tab.closed (); 1313 } 1314 }); 1315 } 1316 1317 private void on_close_others_right (Tab clicked_tab) { 1318 var is_to_the_right = false; 1319 1320 tabs.copy ().foreach ((tab) => { 1321 if (is_to_the_right) { 1322 tab.closed (); 1323 } 1324 if (tab == clicked_tab) { 1325 is_to_the_right = true; 1326 } 1327 }); 1328 } 1329 1330 private void on_new_window (Tab tab) { 1331 notebook.create_window (tab.page_container, 0, 0); 1332 } 1333 1334 private void on_duplicate (Tab tab) { 1335 tab_duplicated (tab); 1336 } 1337 1338 private void on_pin_switch (Tab tab) { 1339 switch_pin_tab (tab); 1340 } 1341 1342 private void update_tabs_visibility () { 1343 if (_tab_bar_behavior == TabBarBehavior.SINGLE) 1344 notebook.show_tabs = n_tabs > 1; 1345 else if (_tab_bar_behavior == TabBarBehavior.NEVER) 1346 notebook.show_tabs = false; 1347 else if (_tab_bar_behavior == TabBarBehavior.ALWAYS) 1348 notebook.show_tabs = true; 1349 } 1350 } 1351} 1352