Merge lp:~uriboni/webbrowser-app/undo-close-tab into lp:webbrowser-app

Proposed by Ugo Riboni
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 1273
Merged at revision: 1271
Proposed branch: lp:~uriboni/webbrowser-app/undo-close-tab
Merge into: lp:webbrowser-app
Diff against target: 423 lines (+173/-46)
6 files modified
src/app/webbrowser/Browser.qml (+36/-0)
src/app/webbrowser/Chrome.qml (+2/-0)
src/app/webbrowser/TabsBar.qml (+3/-11)
tests/autopilot/webbrowser_app/tests/test_keyboard.py (+4/-4)
tests/autopilot/webbrowser_app/tests/test_tabs.py (+104/-16)
tests/unittests/qml/tst_TabsBar.qml (+24/-15)
To merge this branch: bzr merge lp:~uriboni/webbrowser-app/undo-close-tab
Reviewer Review Type Date Requested Status
Olivier Tilloy Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+275849@code.launchpad.net

Commit message

Add keyboard shortcuts to undo closing tabs (Ctrl+Shift+T and Ctrl+Shift+W).

Description of the change

Add keyboard shortcut to undo closing tabs (ctrl+shift+w)

To post a comment you must log in.
1260. By Ugo Riboni

Exclude the new tab view from being part of the undo logic

Revision history for this message
Olivier Tilloy (osomon) wrote :

As pointed out by mterry in https://bugs.launchpad.net/ubuntu/+source/webbrowser-app/+bug/1499767/comments/3, only saving/restoring the URL is not enough. We need to save and restore the full state of the webview, which includes back/forward history, current scroll offset, form data, … The Oxide WebView has a 'currentState' property that you can use.

Revision history for this message
Olivier Tilloy (osomon) wrote :

Also, Ctrl+Shift+W exit the application in chromium and firefox on desktop. For consistency with the competition, can we use Ctrl+Shift+T ?

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1261. By Ugo Riboni

Should work also with the tabs list

1262. By Ugo Riboni

Fix tab close unit tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
1263. By Ugo Riboni

Restore the whole state of the tab, not just its url, when undoing close

1264. By Ugo Riboni

Exclude incognito mode from the whole undo behavior

1265. By Ugo Riboni

Add AP test to verify incognito mode behavior

1266. By Ugo Riboni

Restore tabs to their previous index. Adjust AP tests.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
1267. By Ugo Riboni

Add Ctrl+Shift+T as shortcut, since other major browsers have that

1268. By Ugo Riboni

Ensure that "new tab view" tabs do not get saved and restored

Revision history for this message
Ugo Riboni (uriboni) wrote :

Note: in the bug report both CTRL+SHIFT+T (as firefox and chrome do) and CTRL+SHIFT+W (to complement CTRL+W to close a tab) were suggested as shortcuts. I implemented both because I think they both make sense.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

Incognito tabs are incorrectly being remembered: launch the browser, switch to private browsing, browse to a given page, close it with Ctrl+W, this switches back to public browsing, then press Ctrl+Shift+T: the private tab you just closed is being restored, as a public one.

review: Needs Fixing
1269. By Ugo Riboni

Make sure that closing the last tab in incognito mode does not remember it. Adjust AP tests to verify.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

In test_undo_close_tab_incognito, you’re not checking which tabs are being restored, only the number of webviews after restoring a tab. And since you’re using the same URL for all tabs, the test doesn’t actually verify that it’s not restoring an incognito tab (it could be incognito or not, we won’t know unless we use different URLs for each tab.

review: Needs Fixing
1270. By Ugo Riboni

Improve AP test to make sure we are not restoring tabs from incognito when exiting incognito, and to be a little bit more confident that there is no way to restore tabs when within incognito mode.

1271. By Ugo Riboni

Merge changes from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

At least the following two tests are failing on my desktop:

  webbrowser_app.tests.test_tabs.TestTabsFocus.test_focus_on_close
  webbrowser_app.tests.test_tabs.TestTabsFocus.test_focus_on_switch

It looks like Ctrl+T doesn’t work any longer to open a new tab (at least in the tests, it seems to work fine when I run the app).

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

276 + return count;

Please remove the trailing semicolon, for consistency.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ugo Riboni (uriboni) wrote :

> At least the following two tests are failing on my desktop:
>
> webbrowser_app.tests.test_tabs.TestTabsFocus.test_focus_on_close
> webbrowser_app.tests.test_tabs.TestTabsFocus.test_focus_on_switch
>
> It looks like Ctrl+T doesn’t work any longer to open a new tab (at least in
> the tests, it seems to work fine when I run the app).

We had the same problem in the qml-tabs-model MR, and I fixed the problem already there. Given that this code presumably will go in the same silo, and that CI seems to be happy about the tests anyway (why ?), can we avoid fixing this here too and then having to solve the conflict ?

1272. By Ugo Riboni

Style: remove trailing semicolon

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

> We had the same problem in the qml-tabs-model MR, and I fixed the
> problem already there. Given that this code presumably will go in the
> same silo, and that CI seems to be happy about the tests anyway
> (why ?), can we avoid fixing this here too and then having to solve
> the conflict ?

Nothing guarantees that this will go in the same silo as the qml-tabs-model MR (especially so close to the code freeze for OTA-8). Branches should be mergeable on their own, unless one has a hard dependency on another one (but this is not the case here).

CI didn’t complain because it doesn’t run autopilot tests on desktop, only on a phone. I’ve been bugging the CI team about that, but without success so far. Until we get automated CI for desktop, we’ll need to run the tests ourselves manually to ensure they pass everywhere.

1273. By Ugo Riboni

Fix AP tests by keeping in mind that when calling press_key the case of the letter keys is significant (uppercase triggers Shift implicitly)

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Francis Ginther (fginther) wrote :

I've restarted this MP to work around an issue triggering generic-deb-autopilot-vivid-touch on krillin hardware.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

Looks good to me now, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/webbrowser/Browser.qml'
2--- src/app/webbrowser/Browser.qml 2015-11-03 11:56:01 +0000
3+++ src/app/webbrowser/Browser.qml 2015-11-09 10:55:44 +0000
4@@ -376,6 +376,7 @@
5 }
6
7 onRequestNewTab: browser.openUrlInNewTab("", makeCurrent, true, index)
8+ onTabClosed: internal.closeTab(index)
9
10 onFindInPageModeChanged: {
11 if (!chrome.findInPageMode) internal.resetFocus()
12@@ -1285,6 +1286,7 @@
13
14 QtObject {
15 id: internal
16+ property var closedTabHistory: []
17
18 function getOpenPages() {
19 var urls = [];
20@@ -1325,8 +1327,19 @@
21 }
22
23 function closeTab(index) {
24+ // Save the incognito state before removing the tab, because
25+ // removing the last tab in the model will switch out incognito
26+ // mode, thus causing the check below to fail and save the tab
27+ // into the undo stack when it should be forgotten instead.
28+ var wasIncognito = incognito
29 var tab = tabsModel.remove(index)
30 if (tab) {
31+ if (!wasIncognito && tab.url.toString().length > 0) {
32+ closedTabHistory.push({
33+ state: session.serializeTabState(tab),
34+ index: index
35+ })
36+ }
37 tab.close()
38 }
39 if (tabsModel.count === 0) {
40@@ -1341,6 +1354,14 @@
41 }
42 }
43
44+ function undoCloseTab() {
45+ if (!incognito && closedTabHistory.length > 0) {
46+ var tabInfo = closedTabHistory.pop()
47+ var tab = session.createTabFromState(tabInfo.state)
48+ internal.addTab(tab, true, tabInfo.index)
49+ }
50+ }
51+
52 function switchToPreviousTab() {
53 if (browser.wide) {
54 internal.switchToTab((tabsModel.currentIndex - 1 + tabsModel.count) % tabsModel.count)
55@@ -1713,6 +1734,21 @@
56 onTriggered: internal.switchToPreviousTab()
57 }
58
59+ // Ctrl+Shift+W or Ctrl+Shift+T: Undo close tab
60+ KeyboardShortcut {
61+ modifiers: Qt.ControlModifier | Qt.ShiftModifier
62+ key: Qt.Key_W
63+ enabled: chrome.visible || recentView.visible
64+ onTriggered: internal.undoCloseTab()
65+ }
66+
67+ KeyboardShortcut {
68+ modifiers: Qt.ControlModifier | Qt.ShiftModifier
69+ key: Qt.Key_T
70+ enabled: chrome.visible || recentView.visible
71+ onTriggered: internal.undoCloseTab()
72+ }
73+
74 // Ctrl+W or Ctrl+F4: Close the current tab
75 KeyboardShortcut {
76 modifiers: Qt.ControlModifier
77
78=== modified file 'src/app/webbrowser/Chrome.qml'
79--- src/app/webbrowser/Chrome.qml 2015-09-23 18:43:10 +0000
80+++ src/app/webbrowser/Chrome.qml 2015-11-09 10:55:44 +0000
81@@ -40,6 +40,7 @@
82 readonly property alias bookmarkTogglePlaceHolder: navigationBar.bookmarkTogglePlaceHolder
83
84 signal requestNewTab(int index, bool makeCurrent)
85+ signal tabClosed(int index)
86
87 backgroundColor: incognito ? UbuntuColors.darkGrey : "#bcbcbc"
88
89@@ -68,6 +69,7 @@
90 model: tabsModel
91 incognito: chrome.incognito
92 onRequestNewTab: chrome.requestNewTab(index, makeCurrent)
93+ onTabClosed: chrome.tabClosed(index)
94 }
95
96 anchors {
97
98=== modified file 'src/app/webbrowser/TabsBar.qml'
99--- src/app/webbrowser/TabsBar.qml 2015-09-24 14:07:47 +0000
100+++ src/app/webbrowser/TabsBar.qml 2015-11-09 10:55:44 +0000
101@@ -33,6 +33,7 @@
102 property bool incognito: false
103
104 signal requestNewTab(int index, bool makeCurrent)
105+ signal tabClosed(int index)
106
107 MouseArea {
108 anchors.fill: parent
109@@ -94,7 +95,7 @@
110 Action {
111 objectName: "tab_action_close_tab"
112 text: i18n.tr("Close Tab")
113- onTriggered: internal.closeTab(menu.targetIndex)
114+ onTriggered: root.tabClosed(menu.targetIndex)
115 }
116 }
117 }
118@@ -150,7 +151,7 @@
119
120 rightMargin: tabDelegate.rightMargin
121
122- onClosed: internal.closeTab(index)
123+ onClosed: root.tabClosed(index)
124 onSelected: root.model.currentIndex = index
125 onContextMenu: PopupUtils.open(contextualOptionsComponent, tabDelegate, {"targetIndex": index})
126 }
127@@ -181,13 +182,4 @@
128 }
129 }
130 }
131-
132- QtObject {
133- id: internal
134-
135- function closeTab(index) {
136- var tab = root.model.remove(index)
137- if (tab) tab.close()
138- }
139- }
140 }
141
142=== modified file 'tests/autopilot/webbrowser_app/tests/test_keyboard.py'
143--- tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-10-16 15:29:02 +0000
144+++ tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-11-09 10:55:44 +0000
145@@ -67,7 +67,7 @@
146 self.address_bar = self.main_window.address_bar
147
148 def open_tab(self, url):
149- self.main_window.press_key('Ctrl+T')
150+ self.main_window.press_key('Ctrl+t')
151 new_tab_view = self.main_window.get_new_tab_view()
152 self.address_bar.go_to_url(url)
153 new_tab_view.wait_until_destroyed()
154@@ -86,7 +86,7 @@
155 Eventually(Equals(url)))
156
157 def test_new_tab(self):
158- self.main_window.press_key('Ctrl+T')
159+ self.main_window.press_key('Ctrl+t')
160
161 webview = self.main_window.get_current_webview()
162 self.assertThat(webview.url, Equals(""))
163@@ -306,7 +306,7 @@
164 bookmarks_view = self.main_window.get_bookmarks_view()
165 self.assertThat(bookmarks_view.activeFocus, Eventually(Equals(True)))
166
167- self.main_window.press_key('Ctrl+T')
168+ self.main_window.press_key('Ctrl+t')
169 bookmarks_view.wait_until_destroyed()
170
171 new_tab_view = self.main_window.get_new_tab_view()
172@@ -339,7 +339,7 @@
173 history_view = self.main_window.get_history_view()
174 self.assertThat(history_view.activeFocus, Eventually(Equals(True)))
175
176- self.main_window.press_key('Ctrl+T')
177+ self.main_window.press_key('Ctrl+t')
178 history_view.wait_until_destroyed()
179
180 new_tab_view = self.main_window.get_new_tab_view()
181
182=== modified file 'tests/autopilot/webbrowser_app/tests/test_tabs.py'
183--- tests/autopilot/webbrowser_app/tests/test_tabs.py 2015-10-26 13:06:27 +0000
184+++ tests/autopilot/webbrowser_app/tests/test_tabs.py 2015-11-09 10:55:44 +0000
185@@ -18,6 +18,7 @@
186 from autopilot.matchers import Eventually
187 from autopilot.platform import model
188
189+import time
190 import testtools
191 import unittest
192
193@@ -141,7 +142,7 @@
194 the new page view"""
195 address_bar = self.main_window.address_bar
196
197- self.main_window.press_key('Ctrl+T')
198+ self.main_window.press_key('Ctrl+t')
199 self.assertThat(address_bar.activeFocus, Eventually(Equals(True)))
200
201 self.main_window.press_key('Ctrl+Tab')
202@@ -159,31 +160,31 @@
203 allowing keyboard shortcuts to work without interruption"""
204 address_bar = self.main_window.address_bar
205
206- self.main_window.press_key('Ctrl+T')
207- self.main_window.press_key('Ctrl+T')
208+ self.main_window.press_key('Ctrl+t')
209+ self.main_window.press_key('Ctrl+t')
210 url = self.base_url + "/test1"
211 self.main_window.go_to_url(url)
212 self.main_window.wait_until_page_loaded(url)
213
214- self.main_window.press_key('Ctrl+T')
215+ self.main_window.press_key('Ctrl+t')
216 url = self.base_url + "/test2"
217 self.main_window.go_to_url(url)
218 self.main_window.wait_until_page_loaded(url)
219- self.main_window.press_key('Ctrl+T')
220- self.main_window.press_key('Ctrl+T')
221+ self.main_window.press_key('Ctrl+t')
222+ self.main_window.press_key('Ctrl+t')
223
224- self.main_window.press_key('Ctrl+W')
225+ self.main_window.press_key('Ctrl+w')
226 self.assertThat(address_bar.activeFocus, Eventually(Equals(True)))
227
228- self.main_window.press_key('Ctrl+W')
229- webview = self.main_window.get_current_webview()
230- self.assertThat(webview.activeFocus, Eventually(Equals(True)))
231-
232- self.main_window.press_key('Ctrl+W')
233- webview = self.main_window.get_current_webview()
234- self.assertThat(webview.activeFocus, Eventually(Equals(True)))
235-
236- self.main_window.press_key('Ctrl+W')
237+ self.main_window.press_key('Ctrl+w')
238+ webview = self.main_window.get_current_webview()
239+ self.assertThat(webview.activeFocus, Eventually(Equals(True)))
240+
241+ self.main_window.press_key('Ctrl+w')
242+ webview = self.main_window.get_current_webview()
243+ self.assertThat(webview.activeFocus, Eventually(Equals(True)))
244+
245+ self.main_window.press_key('Ctrl+w')
246 self.assertThat(address_bar.activeFocus, Eventually(Equals(True)))
247
248
249@@ -257,3 +258,90 @@
250 webview.wait_until_destroyed()
251 self.assert_number_webviews_eventually(1)
252 self.main_window.get_new_tab_view()
253+
254+ @testtools.skipIf(model() != "Desktop", "on desktop only")
255+ def test_undo_close_tab(self):
256+ tabs = self.main_window.chrome.get_tabs_bar()
257+ url0 = self.main_window.get_current_webview().url
258+
259+ self.open_new_tab()
260+ url1 = self.base_url + "/tab/1"
261+ self.main_window.go_to_url(url1)
262+ self.main_window.wait_until_page_loaded(url1)
263+ self.assert_number_webviews_eventually(2)
264+
265+ # Insert a "new tab view" page in the middle, without any page loaded
266+ # so that we can verify that it will not be restored
267+ self.open_new_tab()
268+
269+ self.open_new_tab()
270+ url2 = self.base_url + "/tab/2"
271+ self.main_window.go_to_url(url2)
272+ self.main_window.wait_until_page_loaded(url2)
273+ self.assert_number_webviews_eventually(4)
274+
275+ tabs.close_tab(3)
276+ self.assert_number_webviews_eventually(3)
277+ tabs.close_tab(2)
278+ self.assert_number_webviews_eventually(2)
279+ tabs.close_tab(1)
280+ self.assert_number_webviews_eventually(1)
281+ self.check_current_tab(url0)
282+
283+ # Both ctrl+shift+w and ctrl+shift+t activate the undo, so test both
284+ self.main_window.press_key('Ctrl+Shift+w')
285+ self.assert_number_webviews_eventually(2)
286+ self.check_current_tab(url1)
287+
288+ self.main_window.press_key('Ctrl+Shift+t')
289+ self.assert_number_webviews_eventually(3)
290+ self.check_current_tab(url2)
291+
292+ self.main_window.press_key('Ctrl+Shift+t')
293+ self.assert_number_webviews_eventually(3)
294+ self.check_current_tab(url2)
295+
296+ @testtools.skipIf(model() != "Desktop", "on desktop only")
297+ def test_undo_close_tab_incognito(self):
298+ start_url = self.main_window.get_current_webview().url
299+ self.open_new_tab()
300+ url = self.base_url + "/tab/1"
301+ self.main_window.go_to_url(url)
302+ self.main_window.wait_until_page_loaded(url)
303+
304+ self.main_window.press_key('Ctrl+w')
305+ self.assert_number_webviews_eventually(1)
306+ self.check_current_tab(start_url)
307+
308+ incognito_url = self.base_url + "/tab/2"
309+ self.main_window.enter_private_mode()
310+ self.assertThat(self.main_window.is_in_private_mode,
311+ Eventually(Equals(True)))
312+ self.main_window.go_to_url(incognito_url)
313+ self.main_window.wait_until_page_loaded(incognito_url)
314+
315+ self.open_new_tab()
316+ self.main_window.go_to_url(incognito_url)
317+ self.main_window.wait_until_page_loaded(incognito_url)
318+ self.main_window.press_key('Ctrl+w')
319+ self.assert_number_incognito_webviews_eventually(1)
320+
321+ # Test that no tabs will be restored in incognito mode.
322+ # We sleep for a bit after the keypress to give more confidence that
323+ # the tab still hasn't appeared within a reasonable amount of time.
324+ self.main_window.press_key('Ctrl+Shift+w')
325+ time.sleep(1)
326+ self.assert_number_incognito_webviews_eventually(1)
327+
328+ # Close the last incognito tab to exit the mode. This is done on
329+ # purpose instead of using leave_private_mode since we want to
330+ # make sure the last tab is also not saved, as it is a corner case
331+ self.main_window.press_key('Ctrl+w')
332+ self.assertThat(self.main_window.is_in_private_mode,
333+ Eventually(Equals(False)))
334+
335+ # Tabs that we closed before going incognito will be restored
336+ # when going back to default mode
337+ self.main_window.press_key('Ctrl+Shift+w')
338+ self.assert_number_webviews_eventually(2)
339+ self.check_current_tab(url)
340
341=== modified file 'tests/unittests/qml/tst_TabsBar.qml'
342--- tests/unittests/qml/tst_TabsBar.qml 2015-10-01 19:53:14 +0000
343+++ tests/unittests/qml/tst_TabsBar.qml 2015-11-09 10:55:44 +0000
344@@ -75,6 +75,12 @@
345 }
346
347 SignalSpy {
348+ id: tabClosedSpy
349+ target: tabs
350+ signalName: "tabClosed"
351+ }
352+
353+ SignalSpy {
354 id: reloadSpy
355 target: root
356 signalName: "reload"
357@@ -121,13 +127,16 @@
358 }
359 newTabRequestSpy.clear()
360 reloadSpy.clear()
361+ tabClosedSpy.clear()
362 }
363
364 function populateTabs() {
365- for (var i = 0; i < 3; ++i) {
366+ var count = 3
367+ for (var i = 0; i < count; ++i) {
368 tabs.appendTab("", "tab " + i, "")
369 }
370- compare(tabsModel.currentIndex, 2)
371+ compare(tabsModel.currentIndex, count - 1)
372+ return count
373 }
374
375 function test_create_new_tab() {
376@@ -153,11 +162,11 @@
377
378 function test_mouse_middle_click() {
379 // Middle click closes the tab
380- populateTabs()
381- for (var i = 2; i >= 0; --i) {
382- var tab0 = getTabDelegate(0)
383- clickItem(tab0, Qt.MiddleButton)
384- compare(tabsModel.count, i)
385+ var count = populateTabs()
386+ for (var i = 0; i < count; i++) {
387+ var tab = getTabDelegate(i)
388+ clickItem(tab, Qt.MiddleButton)
389+ compare(tabClosedSpy.count, i + 1)
390 }
391 }
392
393@@ -196,12 +205,13 @@
394 }
395
396 function test_close_tabs(data) {
397- populateTabs()
398- for (var i = 2; i >= 0; --i) {
399- var tab0 = getTabDelegate(0)
400- var closeButton = findChild(tab0, "closeButton")
401+ var count = populateTabs()
402+ for (var i = 0; i < count; i++) {
403+ var tab = getTabDelegate(count - (i + 1))
404+ var closeButton = findChild(tab, "closeButton")
405 clickItem(closeButton, data.button)
406- compare(tabsModel.count, i)
407+ compare(tabClosedSpy.count, i + 1)
408+ compare(tabClosedSpy.signalArguments[i][0], count - (i + 1))
409 }
410 }
411
412@@ -261,9 +271,8 @@
413 var menu = popupMenuOnTab(1)
414 var item = getMenuItemForAction(menu, "close_tab")
415 clickItem(item)
416- compare(tabsModel.count, 2)
417- compare(tabsModel.get(0).title, "tab 0")
418- compare(tabsModel.get(1).title, "tab 2")
419+ compare(tabClosedSpy.count, 1)
420+ compare(tabClosedSpy.signalArguments[0][0], 1)
421 }
422
423 function test_context_menu_reload() {

Subscribers

People subscribed via source and target branches

to status/vote changes: