← Back to team overview

elementaryart team mailing list archive

[Merge] lp:~tombeckmann/granite/granite-dynamic-notebook-gkt-based into lp:granite

 

Tom Beckmann has proposed merging lp:~tombeckmann/granite/granite-dynamic-notebook-gkt-based into lp:granite.

Requested reviews:
  Christian Dywan (kalikiana)

For more details, see:
https://code.launchpad.net/~tombeckmann/granite/granite-dynamic-notebook-gkt-based/+merge/104050

Replace the current DynamicNotebook class with a new Gtk Based one.

Short excerpt from last meeting:
DanRabbit: So, we've had like a billion dynamic notebook widgets so far right?
mamemame187: XD yes
DanRabbit: Well Tom decided to do one by just working with a plain gtk.notebook
agent00tai: DanRabbit: correct xD
DanRabbit: and in its pure simplicity, it seems to be the only one that actually like.. works.
codygarver2: unless DanRabbit cares about a notebook, I refuse to think about them
tom95: we can either take this stable one or wait for luna+1 for whenever until xapantus is ready
DanRabbit: It's not fancy, but it works.
DanRabbit: The only issue I have is with the tab overflow appearance/behavior
hcabaguio: why did we try to do custom one's when working with gtk.notebook is pretty good?
DanRabbit: other than that, it's no worse than our currently very inconsistent one
DanRabbit: hcabaguio: getting overly ambitious with animations and stuff
DanRabbit: So, I think kalikiana should be the one to review this (because of this experience with Midori)
DanRabbit: and if he okay's it, I say we go for it.
codygarver2: so it shall be
 * agent00tai fully approves
codygarver2: tom95: request a review and assign kalikiana

So I think mefrio had some suggestions for additional methods and improvements.
Kalikana, if you miss anything, please list it here.
-- 
https://code.launchpad.net/~tombeckmann/granite/granite-dynamic-notebook-gkt-based/+merge/104050
Your team elementaryart (old) is subscribed to branch lp:granite.
=== modified file 'demo/main.vala'
--- demo/main.vala	2012-04-27 13:26:24 +0000
+++ demo/main.vala	2012-04-29 23:45:22 +0000
@@ -209,10 +209,10 @@
         /* DynamicNotebook */
         var dynamic_notebook = new DynamicNotebook ();
         notebook.append_page (dynamic_notebook, new Gtk.Label ("Dynamic Notebook"));
-        dynamic_notebook.append_page (new Gtk.Label ("Page 1"), "Page 1");
-        dynamic_notebook.append_page (new Gtk.Label ("Page 2"), "Page 2");
-        dynamic_notebook.append_page (new Gtk.Label ("Page 3"), "Page 3");
-        dynamic_notebook.add_button_clicked.connect ( () => { dynamic_notebook.append_page (new Gtk.Label("New page"), "New tab"); });
+        dynamic_notebook.append_page ("Page 1", new Gtk.Label ("Page 1"));
+        dynamic_notebook.append_page ("Page 2", new Gtk.Label ("Page 2"));
+        dynamic_notebook.append_page ("Page 3", new Gtk.Label ("Page 3"));
+        dynamic_notebook.new_page.connect ( () => { return {"New tab", new Gtk.Label("New page"), null}; });
 
         /*Light window*/
         var light_window_button = new Gtk.Button.with_label ("Show LightWindow");

=== modified file 'lib/Widgets/DynamicNotebook.vala'
--- lib/Widgets/DynamicNotebook.vala	2012-04-06 09:49:00 +0000
+++ lib/Widgets/DynamicNotebook.vala	2012-04-29 23:45:22 +0000
@@ -1,903 +1,292 @@
-using Granite.Widgets;
-
-using Gtk;
-
-
-public class Granite.Widgets.Tab : Object {
-    public string text;
-    public  Gdk.Pixbuf? pixbuf { set; get; default = null; }
-
-    internal Gtk.StateFlags close_button = Gtk.StateFlags.NORMAL;
-    internal Gtk.StateFlags state = Gtk.StateFlags.NORMAL;
-    internal double offset = 0.0;
-    internal double draw_offset = 0.0;
-    internal double drag_origin = 0.0;
-    internal  bool removed = false;
-    double initial_offset = 1.0;
-    double initial_draw_offset = 0.0;
-    public bool loading { set; get; default = false;}
-    internal Cairo.Surface surface;
-
-    public Gtk.Widget widget;
-    
-    public signal bool close_button_clicked ();
-    public signal void need_redraw ();
-    public signal void need_recache ();
-
-    public bool is_animated () {
-        bool return_value =  (!removed && initial_offset != 1.0) ||
-                             (drag_origin == 0.0 && initial_draw_offset != 0.0) ||
-                             (removed && initial_offset != 0.0);
-        return return_value;
-    }
-
-    public Tab (string text, string? stock_id = null, bool loading = false) {
-        this.text = text;
-        if (stock_id != null) pixbuf = Gtk.IconTheme.get_default ().load_icon (stock_id, 16, 0);
-        this.loading = loading;
-        notify["pixbuf"].connect ( () => { need_recache (); });
-    }
-
-    internal void start_animation () {
-        initial_offset = offset;
-        initial_draw_offset = draw_offset;
-    }
-
-    internal void do_animation (double x) {
-        x = (double)Math.sin ((double)x * Math.PI/2);
-
-        draw_offset = initial_draw_offset * (1.0 - x);
-        if (!removed)
-            offset = initial_offset * (1.0 - x) + 1.0 * x;
-        else
-            offset = initial_offset * (1.0 - x);
-    }
-
-    internal void select () {
-        state = Gtk.StateFlags.ACTIVE;
-    }
-    
-    internal void unselect () {
-        state = Gtk.StateFlags.NORMAL;
-    }
-    
-    internal void hover () {
-        if (state == Gtk.StateFlags.NORMAL)
-            state = Gtk.StateFlags.PRELIGHT;
-    }
-
-    internal void shrunk () {
-        offset = 0.0;
-    }
-    
-    internal bool draw_with_cache (Cairo.Context cr, double x) {
-        if (offset == 1.0 && surface != null && state == Gtk.StateFlags.NORMAL &&
-            close_button == Gtk.StateFlags.NORMAL && !loading) {
-            cr.set_source_surface (surface, x + draw_offset, 0);
-            cr.paint ();
-            return true;
-        }
-        return false;
-    }
+
+const string BUTTON_STYLE = """
+* {
+    -GtkButton-default-border : 0;
+    -GtkButton-default-outside-border : 0;
+    -GtkButton-inner-border: 0;
+    -GtkWidget-focus-line-width : 0;
+    -GtkWidget-focus-padding : 0;
+    padding: 0;
 }
-
-
-internal class Granite.Widgets.Tabs : Gtk.EventBox {
-
-    Gtk.StyleContext tab_context;
-    Gtk.StyleContext label_context;
-    Gtk.StyleContext button_context;
-    
-    internal Gee.ArrayList<Tab> tabs;
-    internal static Gtk.CssProvider style_provider;
-    private const string STYLESHEET_AMBIANCE = """
-        .dynamic-notebook tab:active {
-            background-color:#000;
-            background-image: -gtk-gradient (linear, left bottom, left top,
-                                     from (shade (@dark_bg_color, 0.96)),
-                                     to (shade (@dark_bg_color, 1.4)));
-        }
-        .dynamic-notebook tab .dynamic-label:active {
-            color: @dark_fg_color;
-        }
-        .dynamic-label {
-            color: @fg_color;
-        }
-    """;
-    private const string STYLESHEET_ADWAITA = """
-        .dynamic-label {
-            color: @fg_color;
-        }
-    """;
-    internal const int style_priority = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION;
-
-    Gdk.Pixbuf close_pixbuf;
-    double scrolling = 0.0;
-
-    const double radius = 5;
-    const double max_width = 200;
-    const double min_width = 120;
-    double width = max_width;
-    protected double overlap = 3;
-    const int close_size = 16;
-    const double close_margin = 1;
-    const double y = 5;
-    const int shadow_size = 3;
-
-    Gdk.EventMotion? saved_event_motion = null;
-
-    Cairo.Surface left_surface;
-    Cairo.Surface right_surface;
-    Cairo.Pattern center_pattern;
-    
-    uint timeout_remove = -1;
-    uint scroll_timeout = -1;
-    uint timeout_anim = -1;
-
-    int _page = 0;
-    public int page {
-        get {
-            return _page;
-        }
-        set {
-            if (_page != value) {
-                if (_page < tabs.size) tabs[_page].unselect ();
-                _page = value;
-                if (0 <= _page && _page < tabs.size)
-                    tabs[_page].select ();
+""";
+
+namespace Granite.Widgets {
+    
+    public struct Tab {
+        string label;
+        Gtk.Widget page;
+        string? icon;
+    }
+    
+    public class DynamicNotebook : Gtk.EventBox {
+        
+        /**
+         * the underlying GtkNotebook, in case you need a method not provided by this class
+         **/
+        public Gtk.Notebook notebook;
+        /**
+         * connect to this signal to be notified when a page is closed. 
+         * @return return true to let the tab be closed
+         **/
+        public signal bool page_closed (Gtk.Widget page, uint num);
+        /**
+         * the plus button was pressed, you should return a Tab struct and fill it with the 
+         * appropriate content
+         **/
+        public signal Tab? new_page ();
+        /**
+         * the notebook page was swtiched
+         **/
+        public signal void switch_page (Gtk.Widget page, uint num);
+        /**
+         * Show or hide tab icons. Doesn't apply to existing tabs.
+         **/
+        public bool show_icon;
+        
+        private Gtk.CssProvider button_fix;
+        
+        private int tab_width = 150;
+        private int max_tab_width = 150;
+        
+        /**
+         * create a new dynamic notebook
+         **/
+        public DynamicNotebook () {
+            
+            this.button_fix = new Gtk.CssProvider ();
+            try {
+                this.button_fix.load_from_data (BUTTON_STYLE, -1);
+            } catch (Error e) { warning (e.message); }
+            
+            this.notebook = new Gtk.Notebook ();
+            this.visible_window = false;
+            this.get_style_context ().add_class ("dynamic-notebook");
+            
+            this.notebook.scrollable = true;
+            this.notebook.show_border = false;
+            
+            this.draw.connect ( (ctx) => {
+                this.get_style_context ().render_activity (ctx, 0, 0, this.get_allocated_width (), 27);
+                return false;
+            });
+            
+            this.add (this.notebook);
+            
+            
+            var add = new Gtk.Button ();
+            add.add (new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.MENU));
+            add.margin_left = 6;
+            add.relief = Gtk.ReliefStyle.NONE;
+            this.notebook.set_action_widget (add, Gtk.PackType.START);
+            add.show_all ();
+            add.get_style_context ().add_provider (button_fix, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+            
+            add.clicked.connect ( () => {
+                var t = this.new_page ();
+                this.append_page (t.label, t.page, t.icon);
+            });
+            
+            this.size_allocate.connect ( () => {
+                this.recalc_size ();
+            });
+            
+            this.key_press_event.connect ( (e) => {
+                switch (e.keyval){
+                    case 119: //ctrl+w
+                        if (Signal.has_handler_pending (this, //if no one listens, just kill it!
+                            Signal.lookup ("page-closed", typeof (DynamicNotebook)), 0, true)) {
+                            var sure = this.page_closed (this.notebook.get_nth_page (this.notebook.page), 
+                                this.notebook.page);
+                            if (sure)
+                                this.notebook.remove_page (this.notebook.page);
+                        } else {
+                            this.notebook.remove_page (this.notebook.page);
+                        }
+                        return true;
+                    case 116: //ctrl+t
+                        var t = this.new_page ();
+                        this.append_page (t.label, t.page, t.icon);
+                        return true;
+                    case 49: //ctrl+[1-8]
+                    case 50:
+                    case 51:
+                    case 52:
+                    case 53:
+                    case 54:
+                    case 55:
+                    case 56:
+                        if ((e.state & Gdk.ModifierType.CONTROL_MASK) != 0){
+                            var i = e.keyval - 49;
+                            this.notebook.page = (int)((i >= this.notebook.get_n_pages ()) ? 
+                                this.notebook.get_n_pages () - 1 : i);
+                            return true;
+                        }
+                        break;
+                    /*case 65289: //tab (and shift+tab)    not working :(  (Gtk seems to move focus)
+                    case 65056:
+                        if ((e.state & Gdk.ModifierType.SHIFT_MASK) != 0){
+                            this.prev ();
+                            return true;
+                        }else if ((e.state & Gdk.ModifierType.CONTROL_MASK) != 0){
+                            this.next ();
+                            return true;
+                        }
+                        break;*/
+                }
+                return false;
+            });
+            
+            this.notebook.button_press_event.connect ( (e) => {
+                /*if (e.button == 1) {
+                    this.get_parent_window ().begin_move_drag ((int)e.button, (int)e.x_root, (int)e.y_root, e.time);
+                    return true;
+                }*/
+                return false;
+            });
+        }
+        
+        private void recalc_size () {
+            if (this.notebook.get_n_pages () == 0)
+                return;
+            
+            var offset = 130;
+            this.tab_width = (this.get_allocated_width () - offset) / this.notebook.get_n_pages ();
+            if (this.tab_width > max_tab_width)
+                this.tab_width = max_tab_width;
+            
+            for (var i=0;i<this.notebook.get_n_pages ();i++) {
+                this.notebook.get_tab_label (this.notebook.get_nth_page (i)).width_request = tab_width;
+            }
+        }
+        
+        private void next () {
+            this.notebook.page = (this.notebook.page + 1 >= this.notebook.get_n_pages ())?
+                this.notebook.page = 0 : this.notebook.page + 1;
+        }
+        private void prev () {
+            this.notebook.page = (this.notebook.page - 1 < 0)?this.notebook.get_n_pages () - 1:
+                this.notebook.page-1;
+        }
+        
+        public uint append_tab (Tab tab) {
+            return this.append_page (tab.label, tab.page, tab.icon);
+        }
+        
+        /**
+         * add a page to the notebook
+         * @param label The label for the tab
+         * @param page The tab page
+         * @param icon An optional icon for the tab. Use a Gtk icon_name
+         **/
+        public uint append_page (string label, Gtk.Widget page, string? icon = null) {
+            var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+            
+            var close = new Gtk.Button ();
+            close.add (new Gtk.Image.from_stock (Gtk.Stock.CLOSE, Gtk.IconSize.MENU));
+            close.get_style_context ().add_provider (button_fix, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+            close.relief = Gtk.ReliefStyle.NONE;
+            
+            var lbl = new Gtk.EventBox ();
+            var l = new Gtk.Label (label);
+            l.set_tooltip_text (label);
+            lbl.add (l);
+            l.ellipsize = Pango.EllipsizeMode.END;
+            lbl.visible_window = false;
+            
+            var spinner = new Gtk.Spinner ();
+            
+            box.width_request = tab_width;
+            box.pack_start (close, false);
+            box.pack_start (lbl);
+            
+            Gtk.Image img;
+            if (icon != null)
+                img = new Gtk.Image.from_icon_name (icon, Gtk.IconSize.MENU);
+            else
+                img = new Gtk.Image.from_icon_name ("empty", Gtk.IconSize.MENU);
+            box.pack_start (img, false);
+            box.pack_start (spinner, false);
+            
+            var idx = this.notebook.append_page (page, box);
+            this.notebook.set_tab_reorderable (page, true);
+            
+            page.show_all ();
+            this.notebook.page = idx;
+            
+            box.show_all ();
+            if (!show_icon)
+                img.hide ();
+            spinner.hide ();
+            
+            lbl.button_release_event.connect ( (e) => {
+                if (e.button == 2) {
+                    this.close_by_button (close);
+                    return true;
+                }
+                return false;
+            });
+            
+            lbl.scroll_event.connect ( (e) => {
+                if (e.direction == Gdk.ScrollDirection.UP)
+                    this.prev ();
                 else
-                    error ("The selected tab doesn't exist.");
-                queue_draw ();
-            }
-        }
-    }
-
-    public Gtk.PositionType tab_position { set; get; default = Gtk.PositionType.BOTTOM; }
-    public bool draw_unselected_background { set; get; default = true; }
-
-    int start_dragging = -1;
-    int spinner_count = 0;
-
-    public signal void switch_page (Tab tab);
-    public signal void page_removed (Tab tab);
-
-    /**
-     * Emitted when the user makes a double click on an empty space.
-     **/
-    public signal void need_new_tab ();
-
-    // FIXME: probably needs to be moved somewhere with a public API
-    Gdk.Pixbuf load_pixbuf_with_fallbacks (string[] icons_name, int icon_size) {
-        Gdk.Pixbuf pixbuf = null;
-        try {
-            var icon_info = Gtk.IconTheme.get_default ().choose_icon (icons_name, icon_size, 0);
-            if (icon_info != null) {
-                pixbuf = icon_info.load_symbolic_for_context (button_context, null);
-            }
-        }
-        catch (Error e) {
-            try {
-                pixbuf = Gtk.IconTheme.get_default ().load_icon ("gtk-missing-image", icon_size, 0);
-            }
-            catch (Error e) {
-                error ("gtk-missing-image not found");
-            }
-        }
-        return pixbuf;
-    }
-
-    static construct {
-        install_style_property (new GLib.ParamSpecDouble ("tab-overlap",
-                                                       "Tab overlap",
-                                                       "Tab overlap",
-                                                       0, 50, 3,
-                                                       ParamFlags.READABLE));
-    }
-
-    public Tabs () {
-        tabs = new Gee.ArrayList<Tab>();
-        style_get ("tab-overlap", out overlap, null);
-        
-        if (style_provider == null) {
-            style_provider = new Gtk.CssProvider ();
-            try {
-                if (Gtk.Settings.get_default ().gtk_theme_name == "Ambiance") {
-                    //style_provider.load_from_data (STYLESHEET_AMBIANCE, -1);
-                }
-                else if (Gtk.Settings.get_default ().gtk_theme_name == "Adwaita") {
-                    style_provider.load_from_data (STYLESHEET_ADWAITA, -1);
-                }
-            } catch (Error e) {
-                warning ("The tab bar will not look as intended: %s", e.message);
-            }
-        }
-
-
-        height_request = 35;
-        width_request = (int)(2*max_width);
-        
-        /* Set up the StyleContexts */
-        tab_context = new Gtk.StyleContext ();
-        label_context = get_style_context ();;
-        button_context = new Gtk.StyleContext ();
-        
-        var path =  get_style_context ().get_path ().copy ();
-
-        var pos = path.append_type (typeof (Gtk.Notebook));
-        path.iter_add_class (pos, "notebook");
-        path.iter_add_class (pos, "dynamic-notebook");
-        path.iter_add_region (pos, "tab", Gtk.RegionFlags.EVEN); /* for a tab */
-
-        var path_label = path.copy ();
-        pos = path_label.append_type (typeof (Gtk.Label));
-        path_label.iter_add_class (pos, "dynamic-label"); /* for a label */
-
-        var path_button = path.copy ();
-        pos = path_label.append_type (typeof (Gtk.Button)); /* for a button */
-        
-        tab_context.set_path (path);
-        label_context.set_path (path_label);
-        button_context.set_path (path_button);
-        
-        get_style_context ().add_class ("dynamic-notebook");
-        
-        /* Add our nice provider... */
-        get_style_context ().add_provider (style_provider, style_priority);
-        tab_context.add_provider (style_provider, style_priority);
-        //label_context.add_provider (style_provider, style_priority);
-        button_context.add_provider (style_provider, style_priority);
-
-        size_allocate.connect (on_size_allocate);
-
-        add_events (Gdk.EventMask.POINTER_MOTION_MASK);
-
-        Timeout.add (80, () => {
-            foreach (var tab in tabs) {
-                if(tab.loading) {
-                    spinner_count++;
-                    queue_draw ();
+                    this.next ();
+                return false;
+            });
+            
+            close.clicked.connect ( () => this.close_by_button (close) );
+            
+            this.recalc_size ();
+            
+            return idx;
+        }
+        
+        private void close_by_button (Gtk.Button close) {
+            int i; //find the label widget that fits the close button's parent
+            for (i=0;i<this.notebook.get_n_pages (); i++) {
+                if (close.get_parent () == 
+                    this.notebook.get_tab_label (this.notebook.get_nth_page (i)))
                     break;
-                }
-            }
-            return true;
-        });
-        close_pixbuf = load_pixbuf_with_fallbacks ( {"window-close-symbolic", "window-close", "gtk-close"}, close_size);
-    }
-
-    public void add_tab (Tab tab) {
-        tabs.add (tab);
-        tab.need_redraw.connect (() => { queue_draw (); });
-        tab.need_recache.connect (() => { cache_tab (tab); queue_draw (); });
-
-        switch_page (tab);
-        page = tabs.size -1;
-        
-        update_tab_size (get_allocated_width ());
-        tab.shrunk ();
-        launch_animations ();
-    }
-
-    public override bool draw (Cairo.Context cr) {
-        double x = radius;
-
-        /* First; the background */
-        Gtk.render_background (get_style_context (), cr, 0, 0, get_allocated_width (), get_allocated_height ());
-        
-        /* Scroll */
-        cr.translate (scrolling, 0);
-        cr.save ();
-
-        /* We have to save these tabs because we want them to be drawn on top of the other ones. */
-        double x_selected = 0;
-        Tab? tab_selected = null;
-        double x_dragged = 0;
-        Tab? tab_dragged = null;
-
-        foreach (var tab in tabs) {
-            if (tab.state != Gtk.StateFlags.ACTIVE && tabs.index_of (tab) != start_dragging) {
-                draw_tab (cr, x, tab);
-            }
-            else if (tab.state == Gtk.StateFlags.ACTIVE) {
-                x_selected = x;
-                tab_selected = tab;
-            }
-            else {
-                x_dragged = x;
-                tab_dragged = tab;
-            }
-            x += width*tab.offset - overlap;
-        }
-
-        if (tab_selected != null) {
-            draw_tab (cr, x_selected, tab_selected);
-        }
-        
-        if (tab_dragged != null) {
-            draw_tab (cr, x_dragged, tab_dragged);
-        }
-
-        return true;
-    }
-
-    void draw_tab (Cairo.Context cr, double x, Tab tab, bool use_cache = true) {
-        double height = get_allocated_height () - y;
-        double y_origin = tab_position == Gtk.PositionType.TOP ? 0 : y;
-        
-        if (!(use_cache && tab.draw_with_cache (cr, x - radius))) {
-            if (width*tab.offset < 2*radius) /* Then it is too small */
-                return;
-
-            draw_tab_background (cr, x + tab.draw_offset, y_origin, width*tab.offset, height, radius, tab);
-            draw_label (cr, x + tab.draw_offset, y_origin, height, tab.text, tab);
-            draw_close_button (cr, x + tab.draw_offset, y_origin, height, tab);
-            draw_pixbuf_icon (cr, x + tab.draw_offset, y_origin, height, tab);
-            cr.restore ();
-            cr.save ();
-        }
-    }
-
-    void draw_pixbuf_icon (Cairo.Context cr, double x, double y, double height, Tab tab) {
-        if (tab.loading)
-            Gtk.paint_spinner (get_style (), cr, Gtk.StateType.ACTIVE, this, "",
-                               spinner_count, (int)(x + width*tab.offset - overlap - close_margin - close_size),
-                               (int)(y +  height /2 - close_size/2), close_size, close_size);
-        else if (tab.pixbuf != null) {
-            Gdk.cairo_set_source_pixbuf (cr, tab.pixbuf,
-                                         (int)(x + width*tab.offset - overlap - close_margin - close_size),
-                                         (int)(y +  height /2 - close_size/2));
-            cr.paint ();
-        }
-    }
-
-    void draw_close_button (Cairo.Context cr, double x, double y, double height, Tab tab) {
-        if (tab.close_button == Gtk.StateFlags.PRELIGHT) {
-            Gtk.render_background (button_context, cr,
-                x + overlap, y + height/2 - (close_size + 2*close_margin)/2,
-                close_size + 2*close_margin, close_size + 2*close_margin);
-            Gtk.render_frame (button_context, cr,
-                x + overlap, y + height/2 - (close_size + 2*close_margin)/2,
-                close_size + 2*close_margin, close_size + 2*close_margin);
-        }
-        Gdk.cairo_set_source_pixbuf (cr, close_pixbuf, x + overlap + close_margin, y +  height /2 - close_size/2);
-        cr.paint ();
-    }
-
-    void draw_label (Cairo.Context cr, double x, double y, double height, string text, Tab tab) {
-        double left_padding = 5 + overlap + close_size + 2*close_margin;
-
-        var layout = create_pango_layout (text);
-        double layout_width = width - 2 * radius - 2* close_margin - close_size;
-        /* Do we need to take into account the icon or the loading spinner? */
-        if (tab.pixbuf != null || tab.loading) {
-            layout_width -= 2*close_margin + close_size;
-        }
-        layout.set_width (Pango.units_from_double (layout_width));
-        layout.set_ellipsize (Pango.EllipsizeMode.END);
-
-        Pango.Rectangle extents;
-        layout.get_extents (null, out extents);
-        double layout_height = Pango.units_to_double (extents.height);
-
-        label_context.set_state (tab.state);
-
-        Gtk.render_layout (label_context, cr, x + left_padding, y + height/2 - layout_height/2, layout);
-    }
-    
-    void update_tab_size (double alloc_width) {
-        var old_width = width;
-        width = double.min (double.max ((int)((alloc_width + (tabs.size - 1)*overlap - 2*radius)/tabs.size), min_width),
-                            max_width);
-        
-        double offset = old_width/width;
-        /* Let's create the new tab cache. */
-        foreach (var tab in tabs) {
-            cache_tab (tab);
-            /* This is old_width/width, useful to have the tabs dynamically resized */
-            tab.offset = offset;
-        }
-    }
-
-    void cache_tab (Tab tab) {
-        /* Reset some values */
-        tab.offset = 1.0;
-        var draw_offset = tab.draw_offset;
-        var state = tab.state;
-        var loading = tab.loading;
-        tab.loading = false;
-        tab.draw_offset = 0;
-        tab.state = Gtk.StateFlags.NORMAL;
-
-        var buf = new Granite.Drawing.BufferSurface ( (int)(width +2*radius), get_allocated_height ());
-        draw_tab (buf.context, radius, tab, false /* don't use cache */);
-
-        tab.surface = buf.surface;
-        
-        /* Restore the values */
-        tab.state = state;
-        tab.loading = loading;
-        tab.draw_offset = draw_offset;
-    }
-
-    void on_size_allocate (Gtk.Allocation alloc) {
-    
-        var border_color = tab_context.get_border_color (Gtk.StateFlags.NORMAL);
-        border_color.alpha -= 0.2;
-
-
-        var buf = new Granite.Drawing.BufferSurface ( (int)(2*radius), (int)(alloc.height + 2*shadow_size));
-        draw_tab_background_shape (buf.context, radius, shadow_size, 50, alloc.height - 5, radius, radius);
-        Gdk.cairo_set_source_rgba (buf.context, border_color);
-        buf.context.fill ();
-        buf.gaussian_blur (shadow_size);
-        left_surface = buf.surface;
-        
-        buf = new Granite.Drawing.BufferSurface ( (int)(2*radius), (int)(alloc.height + 2*shadow_size));
-        draw_tab_background_shape (buf.context, -50 + radius, shadow_size, 50, alloc.height - 5, radius, radius);
-        Gdk.cairo_set_source_rgba (buf.context, border_color);
-        buf.context.fill ();
-        buf.gaussian_blur (shadow_size);
-        right_surface = buf.surface;
-        
-        buf = new Granite.Drawing.BufferSurface ( (int)(2), (int)(alloc.height + 2*shadow_size));
-        double y = tab_position == Gtk.PositionType.TOP ? 0 : this.y;
-        draw_tab_background_shape (buf.context, -25, y, 50, alloc.height - 5, radius, radius);
-        Gdk.cairo_set_source_rgba (buf.context, border_color);
-        buf.context.fill ();
-        buf.gaussian_blur (shadow_size);
-
-        center_pattern = new Cairo.Pattern.for_surface (buf.surface);
-        center_pattern.set_extend (Cairo.Extend.REPEAT);
-        
-        update_tab_size (alloc.width);
-    }
-
-    void draw_tab_background (Cairo.Context cr, double x, double y, double width, double height,
-                              double radius, Tab tab) {
-        double border_size = 0.8;
-
-        var border_color = tab_context.get_border_color (tab.state);
-        if (draw_unselected_background || tab.state == Gtk.StateFlags.ACTIVE) {
-            cr.set_source_surface (left_surface, x - radius, y - shadow_size);
-            cr.paint ();
-            cr.set_source_surface (right_surface, x + width - radius, y - shadow_size);
-            cr.paint ();
-
-            
-            cr.rectangle (x + radius, y - shadow_size, width - 2*radius, height + 2*shadow_size);
-            cr.set_source (center_pattern);
-            cr.fill ();
-
-            tab_context.set_state (tab.state);
-            Gdk.cairo_set_source_rgba (cr, border_color);
-            draw_tab_background_shape (cr, x, y, width, height, radius, radius);
-            cr.fill ();
-            
-            double y_origin = y;
-            if (tab_position == Gtk.PositionType.BOTTOM)
-                y_origin += border_size;
-
-            draw_tab_background_shape (cr, x + border_size, y_origin,
-                width - 2*border_size, height - border_size, radius, radius - border_size);
-            cr.set_source_rgba (1, 1, 1, 0.8);
-            cr.clip ();
-            Gtk.render_background ( tab_context, cr, x - radius, y - 3, width + 2* radius, height + 6);
-        }
-        else {
-            /* Just a light gradient */
-            cr.move_to (x + width - overlap/2, y);
-            cr.line_to (x + width - overlap/2, y + height);
-            var gradient = new Cairo.Pattern.linear (0, 0, 0, height);
-            gradient.add_color_stop_rgba (0.0, border_color.red, border_color.green, border_color.blue, 0.0);
-            gradient.add_color_stop_rgba (1.0, border_color.red, border_color.green, border_color.blue, 1.0);
-            cr.set_source (gradient);
-            cr.set_line_width (1.0);
-            cr.stroke ();
-        }
-    }
-
-    void draw_tab_background_shape (Cairo.Context cr, double x, double y, double width,
-                         double height, double radius_t, double radius_l) {
-        switch (tab_position) {
-        case Gtk.PositionType.BOTTOM:
-            cr.move_to (x - radius_l, y + height);
-            cr.curve_to (x, y + height, x, y + height - radius_t, x, y + height - radius_t);
-            cr.line_to (x, y + radius_t);
-            cr.curve_to (x, y, x + radius_l, y, x + radius_l, y);
-            cr.line_to (x + width - radius_l, y);
-            cr.curve_to (x + width, y, x + width, y + radius_t, x + width, y + radius_t);
-            cr.line_to (x + width, y + height - radius_t);
-            cr.curve_to (x + width, y + height,
-                         x + width + radius_l, y + height,
-                         x + width + radius_l, y + height);
-            break;
-
-        case Gtk.PositionType.TOP:
-            cr.move_to (x - radius_l, y);
-            cr.curve_to (x, y, x, y + radius_t, x, y + radius_t);
-            cr.line_to (x, y  + height - radius_t);
-            cr.curve_to (x, y + height, x + radius_l, y + height, x + radius_l, y + height);
-            cr.line_to (x + width - radius_l, y + height);
-            cr.curve_to (x + width, y + height,
-                         x + width, y + height - radius_t,
-                         x + width, y + height - radius_t);
-            cr.line_to (x + width, y + radius_t);
-            cr.curve_to (x + width, y, x + width + radius_l, y, x + width + radius_l, y);
-            break;
-        }
-    }
-
-    public override bool scroll_event (Gdk.EventScroll event) {
-        Source.remove (scroll_timeout);
-        double impulse = 0.0;
-        double step = 0.1;
-        if (event.direction == Gdk.ScrollDirection.DOWN) {
-            step = -step;
-        }
-        double start = scrolling;
-        scroll_timeout = Timeout.add (30, () => {
-            impulse += step;
-            double dt = Math.sin ((impulse) * Math.PI/2);
-            scrolling = double.min (double.max (-(tabs.size * (width - overlap) + 2*radius - get_allocated_width ()), start -  200*dt), 0);
-            queue_draw ();
-
-            if(impulse > 1.0 || impulse < -1.0)
-                return false;
-            return true;
-        });
-        return true;
-    }
-
-    public override bool leave_notify_event (Gdk.EventCrossing event) {
-        foreach (var tab in tabs) {
-            if (tab.state != Gtk.StateFlags.ACTIVE)
-                tab.state = Gtk.StateFlags.NORMAL;
-            tab.close_button = Gtk.StateFlags.NORMAL;
-        }
-        saved_event_motion = null;
-        queue_draw ();
-        return true;
-    }
-
-    public override bool button_press_event (Gdk.EventButton event) {
-        event.x -= radius + scrolling;
-        if (event.button == 1) {
-            start_dragging = (int)(event.x/(width - overlap));
-            if (start_dragging >= tabs.size) {
-                start_dragging = -1;
-                /* click on an empty space */
-                if (event.type == Gdk.EventType.2BUTTON_PRESS)
-                    need_new_tab ();
-            }
-            else {
-                tabs[start_dragging].drag_origin = event.x - start_dragging * (width - overlap);
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Internally remove a tab.
-     *
-     * @param n_tab the tab index in the tabs array. Errors aren't handled, so, it must be
-     * between 0 and the maximum index.
-     **/
-    void remove_tab_internal (int n_tab) {
-        /* Prepare the tab */
-        tabs[n_tab].state = Gtk.StateFlags.NORMAL;
-        tabs[n_tab].removed = true;
-        tabs[n_tab].offset = 1.0;
-        
-        /* Launch the removing animation */
-        launch_animations ();
-        
-        /* If it was selected */
-        if (page == n_tab && page > 0)
-            page--;
-        else if (n_tab == 0 && page == 0)
-            page = int.min (page + 1, tabs.size - 1);
-        if (page < tabs.size)
-        tabs[page].select ();
-        page_removed (tabs[n_tab]);
-        if (page < tabs.size)
-        switch_page (tabs[page]);
-    }
-
-    public override bool button_release_event (Gdk.EventButton event) {
-        Tab? tab_removed = null;
-        foreach (var tab in tabs) {
-            if (tab.removed) tab_removed = tab;
-        }
-        if (tab_removed != null) remove_tab (tab_removed);
-
-        event.x -= radius + scrolling;
-
-        /* Wich tab are we on? */
-        int n_tab = (int)(event.x/(width - overlap));
-        if (n_tab < tabs.size && n_tab >= 0) {
-            if (event.button == 1) { /* It is a left click */
-                /* we unselect all tabs */
-                foreach (var tab in tabs) {
-                    tab.state = Gtk.StateFlags.NORMAL;
-                }
-                /* we select the good one */
-                tabs[n_tab].select ();
-                /* Let's see of it is on the close button */
-                double offset = event.x - n_tab * (width - overlap) - overlap;
-                if (0 < offset < close_margin*2 + close_size) { /* then it is a click on the close_button */
-                    if (tabs[n_tab].close_button_clicked ())
-                        remove_tab_internal (n_tab);
-                }
-                else {
-                    page = n_tab;
-                    switch_page (tabs[page]);
-                }
-            }
-            else if (event.button == 2) {
-                remove_tab_internal (n_tab);
-            }
-        }
-        
-        /* If a tab was dragged, we need to release it. */
-        if (start_dragging != -1) {
-            var tab = tabs[start_dragging];
-            tab.drag_origin = 0.0;
-            launch_animations ();
-            start_dragging = -1;
-        }
-        
-        queue_draw ();
-        return true;
-    }
-
-    void launch_animations () {
-        Source.remove (timeout_anim);
-        
-        foreach (var tab in tabs) {
-            tab.start_animation ();
-        }
-        double dt = 0.0;
-        timeout_anim = Timeout.add (35, () => {
-            bool need_continue = true;
-            dt += 0.2;
-            if (dt >= 1.0) {
-                dt = 1.0;
-                need_continue = false;
-            }
-            Tab? tab_to_remove = null;
-            foreach (var tab in tabs) {
-                bool tab_animated = tab.is_animated ();
-                if (tab_animated)
-                    tab.do_animation (dt);
-                if (tab.removed) {
-                    tab_to_remove = tab;
-                }
-            }
-            if (!need_continue && tab_to_remove != null) {
-                remove_tab (tab_to_remove);
-            }
-
-            if (!need_continue && saved_event_motion != null) {
-                motion_notify_event (saved_event_motion);
-            }
-
-            queue_draw ();
-            return need_continue;
-        });
-    }
-
-    public void remove_tab (Tab tab) {
-        int n_tab = tabs.index_of (tab);
-        tabs.remove (tab);
-        if (_page >= n_tab)
-            _page--;
-        Source.remove (timeout_remove);
-        timeout_remove = Timeout.add (2000, () => {
-            update_tab_size (get_allocated_width ());
-            launch_animations ();
-            timeout_remove = -1;
-            return false;
-        });
-    }
-    
-    public override bool motion_notify_event (Gdk.EventMotion event) {
-        event.x -= radius + scrolling;
-        bool need_draw = false;
-
-        if (start_dragging != -1) {
-            need_draw = true;
-            tabs[start_dragging].draw_offset = -(tabs[start_dragging].drag_origin +
-                                                 start_dragging * (width - overlap) - event.x);
-            if (tabs[start_dragging].draw_offset < - width/2 && start_dragging > 0) {
-                var tab = tabs[start_dragging];
-                tabs[start_dragging] = tabs[start_dragging - 1];
-                var old_tab = tabs[start_dragging];
-                start_dragging --;
-                tabs[start_dragging] = tab;
-                tabs[start_dragging].draw_offset = -(tabs[start_dragging].drag_origin +
-                                                     start_dragging * (width - overlap) - event.x);
-                old_tab.draw_offset = - width + overlap;
-                launch_animations ();
-            }
-            else if (tabs[start_dragging].draw_offset > width/2 && start_dragging < tabs.size - 1) {
-                var tab = tabs[start_dragging];
-                tabs[start_dragging] = tabs[start_dragging + 1];
-                var old_tab = tabs[start_dragging];
-                start_dragging ++;
-                tabs[start_dragging] = tab;
-                tabs[start_dragging].draw_offset = -(tabs[start_dragging].drag_origin +
-                                                     start_dragging * (width - overlap) - event.x);
-                old_tab.draw_offset = width - overlap;
-                launch_animations ();
-            }
-        }
-        else {
-            int n_tab = (int)(event.x/(width - overlap));
-            
-            foreach (var tab in tabs) {
-                tab.close_button = Gtk.StateFlags.NORMAL;
-                if (tab.state != Gtk.StateFlags.ACTIVE && tab.state != Gtk.StateFlags.NORMAL) {
-                    tab.state = Gtk.StateFlags.NORMAL;
-                    need_draw = true;
-                }
-            }
-
-            if (n_tab < tabs.size) {
-
-                double offset = event.x - n_tab * (width - overlap) - overlap;
-                if (0 < offset < close_margin *2 + close_size &&
-                    get_allocated_height ()/2 - close_size/2 - close_margin < event.y - y <
-                    get_allocated_height ()/2 + close_size/2 + close_margin) {
-                    tabs[n_tab].close_button = Gtk.StateFlags.PRELIGHT;
-                }
-                tabs[n_tab].hover ();
-                need_draw = true;
-            }
-        }
-
-        if (need_draw)
-            queue_draw ();
-
-        event.x += radius + scrolling;
-        saved_event_motion = event;
-
-
-        return true;
-    }
-}
-
-public class Granite.Widgets.DynamicNotebook : Gtk.Grid {
-    Granite.Widgets.Tabs tabs;
-    public signal void add_button_clicked ();
-    public signal void new_tab_created (Tab tab);
-
-    public signal void switch_page (Widget page, uint num);
-    public signal void page_added (Widget page, uint num);
-    public signal void page_removed (Widget page, uint num);
-    public signal void is_empty ();
-
-    public int page { set {
-        /* Hide the old one */
-        tabs.tabs[tabs.page].widget.set_child_visible (false);
-        tabs.page = value;
-        /* Show the new one */
-        tabs.tabs[tabs.page].widget.set_child_visible (true);
-        show_all ();
-    } get { return tabs.page; } }
-
-    Gtk.EventBox add_eventbox;
-    Gtk.Button add_button;
-    public DynamicNotebook () {
-        add_eventbox = new Gtk.EventBox ();
-        add_button = new Gtk.Button();
-        add_button.set_image (new Gtk.Image.from_pixbuf (Gtk.IconTheme.get_default ().load_icon ("add", 16, 0)));
-        add_button.set_relief (Gtk.ReliefStyle.NONE);
-        add_eventbox.add (add_button);
-        add_eventbox.get_style_context ().add_class ("dynamic-notebook");
-        add_eventbox.get_style_context ().add_provider (Tabs.style_provider, Tabs.style_priority);
-        add_button.clicked.connect ( () => { add_button_clicked (); });
-        tabs = new Granite.Widgets.Tabs ();
-        tabs.hexpand = true;
-        attach (tabs, 0, 0, 1, 1);
-        attach (add_eventbox, 1, 0, 1, 1);
-
-        get_style_context ().add_class ("notebook");
-
-        tabs.switch_page.connect ( (t) => {
-            page = tabs.tabs.index_of (t);
-        });
-
-        tabs.page_removed.connect ( (t) => {
-            page_removed (t.widget, 0);
-            if (tabs.tabs.size == 0) {
-                is_empty ();
-            }
-        });
-        tabs.need_new_tab.connect ( () => { add_button_clicked (); });
-
-        tabs.show_all ();
-        add_eventbox.show_all ();
-    }
-
-    public Tab new_tab () {
-        var tab = append_page (new Gtk.Grid (), "New", "gtk-file");
-        new_tab_created (tab);
-        show_all ();
-        return tab;
-    }
-    
-    /**
-     * granite_widgets_dynamic_notebook_append_page:
-     *
-     * Return value: (transfer full): a tab
-     */
-    public Tab append_page (Gtk.Widget widget, string label, string? icon_id = null) {
-        var tab = new Tab (label, icon_id);
-        //widget.set_has_window (true);
-        tab.widget = widget;
-        widget.hexpand = widget.vexpand = true;
-        attach (widget, 0, 1, 2, 1);
-        tabs.add_tab (tab);
-        page_added (widget, tabs.tabs.size - 1);
-        return tab;
-    }
-    
-    public void remove_tab (Tab tab) {
-        tabs.remove_tab (tab);
-    }
-    
-    public void set_scrollable (bool scrollable) {
-    }
-
-    public void set_group_name (string name) {
-    }
-    
-    public override void forall_internal (bool internals, Gtk.Callback callback) {
-        if (internals) {
-            callback (tabs);
-            callback (add_eventbox);
-        }
-        foreach (var tab in tabs.tabs) {
-            callback (tab.widget);
-        }
-    }
-
-    public int page_num (Gtk.Widget widget) {
-        foreach (var tab in tabs.tabs) {
-            if (tab.widget == widget) return tabs.tabs.index_of (tab);
-        }
-        return -1;
-    }
-    
-    public override void add (Gtk.Widget widget) {
-        var tab = new Tab ("[Untitled]");
-        tab.widget = widget;
-        widget.hexpand = true;
-        widget.vexpand = true;
-        tabs.add_tab (tab);
-        base.add (widget);
-    }
-    
-
-    public void set_current_page (int page) {
-        this.page  = page;
-    }
-    
-    public int get_current_page () {
-        return page;
-    }
-    
-    public int get_n_pages () {
-        return (int) get_children ().length ();
-    }
-    
-    public Tab get_nth_page (int page) {
-        if (page < tabs.tabs.size && page >= 0)
-            return tabs.tabs[page];
-        else
-            return null;
-    }
-}
-
-
-
+            }
+            if (Signal.has_handler_pending (this, //if no one listens, just kill it!
+                Signal.lookup ("page-closed", typeof (DynamicNotebook)), 0, true)) {
+                var sure = this.page_closed (this.notebook.get_nth_page (i), i);
+                if (sure)
+                    this.notebook.remove_page (i);
+            } else {
+                this.notebook.remove_page (i);
+            }
+        }
+        
+        /**
+         * toggle the working state of the tab, which will cause a spinner to appear if it's working
+         **/
+        public void toggle_working (int num, bool enable) {
+            var box = (Gtk.Container)this.notebook.get_tab_label (this.notebook.get_nth_page (num));
+            if (enable) {
+                box.get_children ().nth_data (3).show_all ();
+                box.get_children ().nth_data (2).hide ();
+            } else {
+                box.get_children ().nth_data (2).show_all ();
+                box.get_children ().nth_data (3).hide ();
+            }
+            
+        }
+        
+        /**
+         * toggle whether the tab should be an app, e.g. show only its icon
+         **/
+        public void toggle_app_tab (int num, bool enable) {
+            var box = (Gtk.Container)this.notebook.get_tab_label (this.notebook.get_nth_page (num));
+            if (enable) {
+                box.get_children ().nth_data (0).hide ();
+                box.get_children ().nth_data (1).hide ();
+            } else {
+                box.get_children ().nth_data (0).show_all ();
+                box.get_children ().nth_data (1).show_all ();
+            }
+        }
+        
+    }
+    
+}


Follow ups