Merge lp:~julien-spautz/granite/undo-close-tab into lp:~elementary-pantheon/granite/granite

Proposed by Julien Spautz
Status: Merged
Merged at revision: 589
Proposed branch: lp:~julien-spautz/granite/undo-close-tab
Merge into: lp:~elementary-pantheon/granite/granite
Diff against target: 527 lines (+264/-57)
2 files modified
demo/main.vala (+49/-23)
lib/Widgets/DynamicNotebook.vala (+215/-34)
To merge this branch: bzr merge lp:~julien-spautz/granite/undo-close-tab
Reviewer Review Type Date Requested Status
xapantu (community) Approve
Cody Garver (community) Needs Fixing
Danielle Foré Approve
David Gomes (community) Needs Fixing
Review via email: mp+155041@code.launchpad.net

Description of the change

Fixes bug #1020350 in granite by implementing this blueprint: https://blueprints.launchpad.net/granite/+spec/restoring-closed-tabs

I also updated granite-demo to allow duplicating and restoring tabs.

Existing applications will have to be patched to implement this feature, but this branch won't break anything, since it's disabled by default.

To post a comment you must log in.
Revision history for this message
David Gomes (davidgomes) wrote :

Align lines 30, 31, 47, 53 and 54.
Newline after lines 54, 304, 150, 157, 189, 193.
Align line 57.
Remove all trailing whitespace (I wouldn't normally ask you to do this, but you added a lot of trailing whitespace on this branch).

That is just code style, I don't have time to read actual code or test it now.

review: Needs Fixing
Revision history for this message
David Gomes (davidgomes) wrote :

>void restore_menu_positioning
Also, maybe that function should be called void restore_menu_position.

Revision history for this message
David Gomes (davidgomes) wrote :

Newlines after lines 199, 186 too.

Revision history for this message
David Gomes (davidgomes) wrote :

Also, the lines mentioned are diff lines.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Looks good here Julien. Nice work!

review: Approve
Revision history for this message
David Gomes (davidgomes) wrote :

Daniel, don't you think that clicking the trash when there's only one tab to restore should just restore it instead of showing a list of to-restore tabs (that is actually 1 element long)? My 2 cents here.

Revision history for this message
Danielle Foré (danrabbit) wrote :

David, hmm. Possibly, but maybe not because what if you're not sure what's in the trash?

Revision history for this message
David Gomes (davidgomes) wrote :

I guess you're right. I'll finish this review tomorrow, it's a lot of code and it needs a lot of testing.

Also Daniel, which apps would ship the trash button? I can't think of any it'd look good on except for maybe Scratch.

It clutters the view on a web browser IMO.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Midori already includes the option to have the trash can (but in the toolbar). Do you think we should make showing the trash can optional?

Revision history for this message
Julien Spautz (julien-spautz) wrote :

One thing I'm not sure about is what should happen when the closed tabs menu gets extremely long. In a web browser this can easily happen. The number of tabs (open and closed) can only grow, but never shrink. One solution would be to implement a maximum amount of closed tabs. There's no use in having a list with 50 closed tabs, also that's what history is for.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Hey Julien,

Perhaps we should look to Midori to see how their trash behaves. I agree with setting a reasonable limit. Perhaps 10 or 15?

I also noticed something small: I think the trash button should remain in the :active state while the menu is shown.

Revision history for this message
Julien Spautz (julien-spautz) wrote :

:active meaning 'pressed'? I think it's currently using a normal button but I can change it to a toggle button. That should fix it…

Revision history for this message
Danielle Foré (danrabbit) wrote :

Yep that's what I meant. Thanks!

Revision history for this message
Cody Garver (codygarver) wrote :

This branch is delayed until DynamicNotebook is fixed.

Revision history for this message
Cody Garver (codygarver) wrote :

bzr merge lp:~julien-spautz/granite/undo-close-tab
 M demo/main.vala
 M lib/Widgets/DynamicNotebook.vala
Text conflict in lib/Widgets/DynamicNotebook.vala
1 conflicts encountered.

review: Needs Fixing
556. By Julien Spautz

fixed conflicts

557. By Julien Spautz

fixed regressions

Revision history for this message
Julien Spautz (julien-spautz) wrote :

The conflicts should now be resolved.

Revision history for this message
xapantu (xapantu) wrote :

It looks good. However, I think that adding a tab member to the Tab class is not necessary and may be confusing. If you want to attach some data to a GObject, you can use the builtin set_data functions.

But apart from that, it looks great, so, I'll remove that and adapt the code in the demo before merging. Good work!

review: Approve
Revision history for this message
xapantu (xapantu) wrote :

Oh, I read the code too quickly, I didn't see that it was actually used...

Revision history for this message
xapantu (xapantu) wrote :

Merged, I just rename data to restore_data, the name data was a bit too generic for this in my opinion (and it is a public API, so we can't afford changing it every release ;) ).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'demo/main.vala'
2--- demo/main.vala 2013-06-17 00:40:26 +0000
3+++ demo/main.vala 2013-06-18 17:46:27 +0000
4@@ -377,35 +377,61 @@
5 }
6
7 private Granite.Widgets.DynamicNotebook create_dynamic_notebook () {
8+
9+ int i = 3;
10+
11 var dynamic_notebook = new Granite.Widgets.DynamicNotebook ();
12
13- var tab = new Granite.Widgets.Tab ("user@elementaryos: ~",
14- new ThemedIcon ("empty"),
15- new Gtk.Label ("Page 1"));
16+ dynamic_notebook.allow_duplication = true;
17+ dynamic_notebook.allow_restoring = true;
18+
19+ var tab = new Granite.Widgets.Tab ("user1@elementaryos: ~",
20+ new ThemedIcon ("empty"),
21+ new Gtk.Label ("Page 1"));
22+ tab.data = "1";
23 tab.working = true;
24-
25 dynamic_notebook.insert_tab (tab, -1);
26- dynamic_notebook.insert_tab (new Granite.Widgets.Tab ("user2@elementaryos: ~",
27- new ThemedIcon ("empty"),
28- new Gtk.Label ("Page 2")), -1);
29+
30+ var tab2 = new Granite.Widgets.Tab ("user2@elementaryos: ~",
31+ new ThemedIcon ("empty"),
32+ new Gtk.Label ("Page 2"));
33+ tab2.data = "2";
34+ dynamic_notebook.insert_tab (tab2, -1);
35
36 dynamic_notebook.tab_added.connect ((t) => {
37- t.page = new Gtk.Label ("newuser@elementaryos: ~");
38- t.label = "newuser@elementaryos: ~";
39- });
40-
41- dynamic_notebook.tab_moved.connect ((t, p) => {
42- print ("Moved tab %s to %i\n", t.label, p);
43- });
44-
45- dynamic_notebook.tab_switched.connect ((old_t, new_t) => {
46- print ("Switched from %s to %s\n", old_t.label, new_t.label);
47- });
48-
49- dynamic_notebook.tab_removed.connect ((t) => {
50- print ("Going to remove %s\n", t.label);
51- return true;
52- });
53+ t.page = new Gtk.Label (@"Page $i");
54+ t.label = @"user$i@elementaryos: ~";
55+ t.data = i.to_string ();
56+ i++;
57+ });
58+
59+ dynamic_notebook.tab_restored.connect ((t) => {
60+ print ("Restored tab %s\n", t.label);
61+ t.page = new Gtk.Label ("Page " + t.data);
62+ });
63+
64+ dynamic_notebook.tab_duplicated.connect ((t) => {
65+ var num = t.data;
66+ var t2 = new Granite.Widgets.Tab (@"user$num@elementaryos: ~",
67+ new ThemedIcon ("empty"),
68+ new Gtk.Label (@"Page $num"));
69+ t2.data = t.data;
70+ dynamic_notebook.insert_tab (t2, -1);
71+ print ("Duplicated tab %s\n", t2.label);
72+ });
73+
74+ dynamic_notebook.tab_moved.connect ((t, p) => {
75+ print ("Moved tab %s to %i\n", t.label, p);
76+ });
77+
78+ dynamic_notebook.tab_switched.connect ((old_t, new_t) => {
79+ print ("Switched from %s to %s\n", old_t.label, new_t.label);
80+ });
81+
82+ dynamic_notebook.tab_removed.connect ((t) => {
83+ print ("Going to remove %s\n", t.label);
84+ return true;
85+ });
86
87 return dynamic_notebook;
88 }
89
90=== modified file 'lib/Widgets/DynamicNotebook.vala'
91--- lib/Widgets/DynamicNotebook.vala 2013-06-17 18:38:22 +0000
92+++ lib/Widgets/DynamicNotebook.vala 2013-06-18 17:46:27 +0000
93@@ -28,12 +28,15 @@
94 * This is a standard tab which can be used in a notebook to form a tabbed UI.
95 */
96 public class Tab : Gtk.Box {
97+
98 Gtk.Label _label;
99 public string label {
100 get { return _label.label; }
101 set { _label.label = value; }
102 }
103
104+ public string data { get; set; }
105+
106 internal Gtk.EventBox page_container;
107 public Gtk.Widget page {
108 get {
109@@ -101,7 +104,7 @@
110 this.close = new Gtk.Button ();
111
112 close.add (new Gtk.Image.from_icon_name ("window-close-symbolic", Gtk.IconSize.MENU));
113- close.tooltip_text = _("Close tab");
114+ close.tooltip_text = _("Close Tab");
115 close.relief = Gtk.ReliefStyle.NONE;
116
117 var lbl = new Gtk.EventBox ();
118@@ -147,7 +150,7 @@
119 return true;
120 }
121 break;
122-
123+
124 case Gdk.ScrollDirection.DOWN:
125 case Gdk.ScrollDirection.RIGHT:
126 if (notebook.page < notebook.get_n_pages ()) {
127@@ -156,14 +159,14 @@
128 }
129 break;
130 }
131-
132+
133 return false;
134 });
135
136 lbl.button_press_event.connect ((e) => {
137-
138+
139 e.state &= MODIFIER_MASK;
140-
141+
142 if (e.button == 2 && e.state == 0 && close.visible) {
143 this.closed ();
144 } else if (e.button == 2 && e.state == Gdk.ModifierType.SHIFT_MASK && close.visible) {
145@@ -174,25 +177,106 @@
146 menu.popup (null, null, null, 3, e.time);
147 uint num_tabs = (this.get_parent () as Gtk.Container).get_children ().length ();
148 close_other_m.label = ngettext (_("Close Other Tab"), _("Close Other Tabs"), num_tabs - 1);
149- if (num_tabs == 1)
150- close_other_m.sensitive = false;
151+ close_other_m.sensitive = (num_tabs != 1);
152 } else {
153 return false;
154 }
155-
156+
157 return true;
158 });
159-
160+
161 this.button_press_event.connect ((e) => {
162 return (e.type == Gdk.EventType.2BUTTON_PRESS || e.button != 1);
163 });
164-
165+
166 page_container.button_press_event.connect (() => { return true; }); //dont let clicks pass through
167 close.clicked.connect ( () => this.closed () );
168 working = false;
169 }
170 }
171
172+ private class ClosedTabs : GLib.Object {
173+
174+ public signal void restored (Tab tab);
175+
176+ private struct Entry {
177+ string title;
178+ string data;
179+ GLib.Icon? icon;
180+ }
181+
182+ private Entry[] closed_tabs;
183+
184+ public ClosedTabs () {
185+ closed_tabs = {};
186+ }
187+
188+ public bool empty {
189+ get {
190+ return closed_tabs.length == 0;
191+ }
192+ }
193+
194+ public void push (Tab tab) {
195+ foreach (var entry in closed_tabs)
196+ if (tab.data == entry.data)
197+ return;
198+ Entry tmp = { tab.label, tab.data, tab.icon };
199+ closed_tabs += tmp;
200+ }
201+
202+ public Tab pop () {
203+ assert (closed_tabs.length > 0);
204+ Entry entry = closed_tabs[closed_tabs.length - 1];
205+ var tab = new Tab (entry.title);
206+ tab.data = entry.data;
207+ tab.icon = entry.icon;
208+ closed_tabs.resize (closed_tabs.length - 1);
209+ return tab;
210+ }
211+
212+ public Tab? pick (string search) {
213+ var tab = (Tab) null;
214+ Entry[] copy = {};
215+
216+ foreach (var entry in closed_tabs) {
217+ if (entry.data != search) {
218+ copy += entry;
219+ } else {
220+ tab = new Tab (entry.title);
221+ tab.data = entry.data;
222+ tab.icon = entry.icon;
223+ }
224+ }
225+
226+ closed_tabs = copy;
227+ return tab;
228+ }
229+
230+ private Gtk.Menu _menu;
231+ public Gtk.Menu menu {
232+ get {
233+ _menu = new Gtk.Menu ();
234+
235+ foreach (var entry in closed_tabs) {
236+ var item = new Gtk.ImageMenuItem.with_label (entry.title);
237+ item.set_always_show_image (true);
238+ if (entry.icon != null) {
239+ var icon = new Gtk.Image.from_gicon (entry.icon, Gtk.IconSize.MENU);
240+ item.set_image (icon);
241+ }
242+ _menu.prepend (item);
243+
244+ item.activate.connect (() => {
245+ this.restored (pick (entry.data));
246+ });
247+ }
248+
249+ return _menu;
250+ }
251+ }
252+ }
253+
254 public class DynamicNotebook : Gtk.EventBox {
255
256 /**
257@@ -210,10 +294,10 @@
258 set { notebook.show_tabs = value; }
259 }
260
261+ /**
262+ * Toggle icon display
263+ */
264 bool _show_icons;
265- /**
266- * Toggle icon display
267- */
268 public bool show_icons {
269 get { return _show_icons; }
270 set {
271@@ -282,6 +366,19 @@
272 }
273 }
274
275+ /**
276+ * Allow restoring tabs
277+ */
278+ bool _allow_restoring = false;
279+ public bool allow_restoring {
280+ get { return _allow_restoring; }
281+ set {
282+ _allow_restoring = value;
283+ restore_tab_m.visible = value;
284+ restore_button.visible = value;
285+ }
286+ }
287+
288 public Tab current {
289 get { return tabs.nth_data (notebook.get_current_page ()); }
290 set { notebook.set_current_page (tabs.index (value)); }
291@@ -298,6 +395,7 @@
292 }
293 }
294
295+
296 public string group_name {
297 get { return notebook.group_name; }
298 set { notebook.group_name = value; }
299@@ -308,6 +406,8 @@
300 */
301 public Gtk.Menu menu { get; private set; }
302
303+ private ClosedTabs closed_tabs;
304+
305 Gtk.Notebook notebook;
306 private Gtk.CssProvider button_fix;
307
308@@ -320,6 +420,13 @@
309 public signal void tab_switched (Tab? old_tab, Tab new_tab);
310 public signal void tab_moved (Tab tab, int new_pos, bool new_window, int x, int y);
311 public signal void tab_duplicated (Tab duplicated_tab);
312+ public signal void tab_restored (Tab tab);
313+
314+ private Gtk.MenuItem new_tab_m;
315+ private Gtk.MenuItem restore_tab_m;
316+
317+ private Gtk.Button add_button;
318+ private Gtk.Button restore_button; // should be a Gtk.MenuButton when we have Gtk+ 3.6
319
320 private static const string CLOSE_BUTTON_STYLE = """
321 * {
322@@ -357,19 +464,72 @@
323 this.add (this.notebook);
324
325 menu = new Gtk.Menu ();
326-
327- var new_tab_m = new Gtk.MenuItem.with_label (_("New Tab"));
328+ new_tab_m = new Gtk.MenuItem.with_label (_("New Tab"));
329+ restore_tab_m = new Gtk.MenuItem.with_label (_("Undo Close Tab"));
330+ restore_tab_m.sensitive = false;
331 menu.append (new_tab_m);
332-
333+ menu.append (restore_tab_m);
334 menu.show_all ();
335
336 new_tab_m.activate.connect (() => {
337 insert_new_tab_at_end ();
338 });
339
340+ restore_tab_m.activate.connect (() => {
341+ restore_last_tab ();
342+ });
343+
344+ closed_tabs = new ClosedTabs ();
345+ closed_tabs.restored.connect ((tab) => {
346+ if (!allow_restoring)
347+ return;
348+ restore_button.sensitive = !closed_tabs.empty;
349+ insert_tab (tab, -1);
350+ this.tab_restored (tab);
351+ });
352+
353+ add_button = new Gtk.Button ();
354+ add_button.add (new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.MENU));
355+ add_button.margin_left = 6;
356+ add_button.relief = Gtk.ReliefStyle.NONE;
357+ add_button.tooltip_text = _("New Tab");
358+ this.notebook.set_action_widget (add_button, Gtk.PackType.START);
359+ add_button.show_all ();
360+ add_button.get_style_context ().add_provider (button_fix, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
361+
362+ restore_button = new Gtk.Button ();
363+ restore_button.add (new Gtk.Image.from_icon_name ("user-trash-symbolic", Gtk.IconSize.MENU));
364+ restore_button.margin_right = 5;
365+ restore_button.set_relief (Gtk.ReliefStyle.NONE);
366+ restore_button.tooltip_text = _("Closed Tabs");
367+ restore_button.sensitive = false;
368+ this.notebook.set_action_widget (restore_button, Gtk.PackType.END);
369+ restore_button.show_all ();
370+
371+ add_button.clicked.connect (() => {
372+ insert_new_tab_at_end ();
373+ });
374+
375+ restore_button.clicked.connect (() => {
376+ var menu = closed_tabs.menu;
377+ menu.attach_widget = restore_button;
378+ menu.show_all ();
379+ menu.popup (null, null, this.restore_menu_position, 1, 0);
380+ });
381+
382+ restore_tab_m.visible = allow_restoring;
383+ restore_button.visible = allow_restoring;
384+
385+ this.size_allocate.connect (() => {
386+ this.recalc_size ();
387+ });
388+
389 this.button_press_event.connect ((e) => {
390 if (e.type == Gdk.EventType.2BUTTON_PRESS && e.button == 1) {
391 insert_new_tab_at_end ();
392+ } else if (e.button == 2 && allow_restoring) {
393+ restore_last_tab ();
394+ return true;
395 } else if (e.button == 3) {
396 menu.popup (null, null, null, 3, e.time);
397 }
398@@ -377,28 +537,18 @@
399 return false;
400 });
401
402- var add = new Gtk.Button ();
403- add.add (new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.MENU));
404- add.margin_left = 6;
405- add.relief = Gtk.ReliefStyle.NONE;
406- add.tooltip_text = _("New tab");
407- this.notebook.set_action_widget (add, Gtk.PackType.START);
408- add.show_all ();
409- add.get_style_context ().add_provider (button_fix, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
410-
411- add.clicked.connect ( () => {
412- insert_new_tab_at_end ();
413- });
414-
415 this.size_allocate.connect (() => {
416 this.recalc_size ();
417 });
418
419 this.key_press_event.connect ((e) => {
420+
421+ e.state &= MODIFIER_MASK;
422+
423 switch (e.keyval) {
424 case Gdk.Key.@w:
425 case Gdk.Key.@W:
426- if ((e.state & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK) {
427+ if (e.state == Gdk.ModifierType.CONTROL_MASK) {
428 if (!tabs_closable) break;
429 remove_tab (current);
430 return true;
431@@ -408,15 +558,18 @@
432
433 case Gdk.Key.@t:
434 case Gdk.Key.@T:
435- if ((e.state & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK) {
436+ if (e.state == Gdk.ModifierType.CONTROL_MASK) {
437 insert_new_tab_at_end ();
438 return true;
439+ } else if (e.state == (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) && allow_restoring) {
440+ restore_last_tab ();
441+ return true;
442 }
443
444 break;
445
446 case Gdk.Key.Page_Up:
447- if ((e.state & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK) {
448+ if (e.state == Gdk.ModifierType.CONTROL_MASK) {
449 next_page ();
450 return true;
451 }
452@@ -424,7 +577,7 @@
453 break;
454
455 case Gdk.Key.Page_Down:
456- if ((e.state & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK) {
457+ if (e.state == Gdk.ModifierType.CONTROL_MASK) {
458 previous_page ();
459 return true;
460 }
461@@ -447,6 +600,7 @@
462 }
463
464 break;
465+
466 case Gdk.Key.@9:
467 if ((e.state & Gdk.ModifierType.MOD1_MASK) == Gdk.ModifierType.MOD1_MASK) {
468 notebook.page = notebook.get_n_pages () - 1;
469@@ -470,6 +624,15 @@
470 notebook.create_window.disconnect (on_create_window);
471 }
472
473+ void restore_menu_position (Gtk.Menu menu, out int x, out int y, out bool p) {
474+ Gtk.Allocation button_alloc, menu_alloc;
475+ restore_button.get_allocation (out button_alloc);
476+ menu.get_allocation (out menu_alloc);
477+ restore_button.get_window ().get_origin (out x, out y);
478+ x += button_alloc.x - menu_alloc.width + button_alloc.width + 5;
479+ y += button_alloc.y + button_alloc.height + 1;
480+ }
481+
482 void on_switch_page (Gtk.Widget page, uint pagenum) {
483 var new_tab = notebook.get_tab_label (page) as Tab;
484
485@@ -484,7 +647,7 @@
486 unowned Gtk.Notebook on_create_window (Gtk.Widget page, int x, int y) {
487 var tab = notebook.get_tab_label (page) as Tab;
488 tab_moved (tab, 0, true, x, y);
489- return null;
490+ return (Gtk.Notebook) null;
491 }
492
493 private void recalc_size () {
494@@ -504,6 +667,17 @@
495 }
496 }
497
498+ private void restore_last_tab () {
499+ if (!allow_restoring || closed_tabs.empty)
500+ return;
501+
502+ var tab = closed_tabs.pop ();
503+ restore_button.sensitive = !closed_tabs.empty;
504+ restore_tab_m.sensitive = !closed_tabs.empty;
505+ insert_tab (tab, -1);
506+ this.tab_restored (tab);
507+ }
508+
509 public void remove_tab (Tab tab) {
510 if (Signal.has_handler_pending (this, Signal.lookup ("tab-removed", typeof (DynamicNotebook)), 0, true)) {
511 var sure = tab_removed (tab);
512@@ -512,8 +686,15 @@
513 }
514
515 var pos = get_tab_position (tab);
516+
517 if (pos != -1)
518 notebook.remove_page (pos);
519+
520+ if (tab.label != "" && tab.data != "") {
521+ closed_tabs.push (tab);
522+ restore_button.sensitive = !closed_tabs.empty;
523+ restore_tab_m.sensitive = !closed_tabs.empty;
524+ }
525 }
526
527 [Deprecated (since=0.2)]

Subscribers

People subscribed via source and target branches