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