Merge lp:~osomon/webbrowser-app/uriboni-tab-context-menu into lp:webbrowser-app
- uriboni-tab-context-menu
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Olivier Tilloy |
Approved revision: | 1154 |
Merged at revision: | 1207 |
Proposed branch: | lp:~osomon/webbrowser-app/uriboni-tab-context-menu |
Merge into: | lp:webbrowser-app |
Diff against target: |
1057 lines (+527/-130) 12 files modified
src/app/webbrowser/Browser.qml (+6/-5) src/app/webbrowser/BrowserTab.qml (+8/-0) src/app/webbrowser/Chrome.qml (+2/-2) src/app/webbrowser/TabItem.qml (+28/-14) src/app/webbrowser/TabsBar.qml (+62/-28) src/app/webbrowser/tabs-model.cpp (+42/-12) src/app/webbrowser/tabs-model.h (+2/-0) tests/autopilot/webbrowser_app/emulators/browser.py (+3/-3) tests/unittests/qml/qml_tree_helpers.js (+42/-0) tests/unittests/qml/tst_BrowserTab.qml (+17/-0) tests/unittests/qml/tst_TabsBar.qml (+146/-8) tests/unittests/tabs-model/tst_TabsModelTests.cpp (+169/-58) |
To merge this branch: | bzr merge lp:~osomon/webbrowser-app/uriboni-tab-context-menu |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ugo Riboni (community) | Approve | ||
Riccardo Padovani (community) | Approve | ||
PS Jenkins bot | continuous-integration | Needs Fixing | |
Review via email: mp+272148@code.launchpad.net |
Commit message
Add a context menu to each tab in the tab bar, allowing to insert a new tab just after, close or reload the current tab.
Description of the change
Olivier Tilloy (osomon) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1149
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Riccardo Padovani (rpadovani) wrote : | # |
I tested it extensively and looks good: I also looked to code and I think it's ok.
Only thing to fix: the 'x' to close a tab isn't clickable anymore, the MouseArea that intercepts the click for context menu overalls it. Except from that, for me it's ready to go
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1150
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
> Only thing to fix: the 'x' to close a tab isn't clickable anymore,
> the MouseArea that intercepts the click for context menu overalls it.
Wow, nice catch, thanks for testing. This was caused by merging the latest changes from trunk. The conflict was not as trivial as it initially appeared, I now fixed this properly.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1152
http://
Executed test runs:
UNSTABLE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Riccardo Padovani (rpadovani) wrote : | # |
lgtm now, thanks!
just a very little thing (tbh, I don't think it's fixable): you disabled the x dragging while right clicking (and that's perfect) but if I press right click and before releasing it I middle click the tab is closed (I expect it is ignored)
Riccardo Padovani (rpadovani) wrote : | # |
Now is perfect, thanks so much!
Preview Diff
1 | === modified file 'src/app/webbrowser/Browser.qml' | |||
2 | --- src/app/webbrowser/Browser.qml 2015-09-22 01:27:19 +0000 | |||
3 | +++ src/app/webbrowser/Browser.qml 2015-09-28 08:10:23 +0000 | |||
4 | @@ -353,7 +353,7 @@ | |||
5 | 353 | onRemoved: if (chrome.bookmarked && (url === chrome.webview.url)) chrome.bookmarked = false | 353 | onRemoved: if (chrome.bookmarked && (url === chrome.webview.url)) chrome.bookmarked = false |
6 | 354 | } | 354 | } |
7 | 355 | 355 | ||
9 | 356 | onRequestNewTab: browser.openUrlInNewTab("", true) | 356 | onRequestNewTab: browser.openUrlInNewTab("", makeCurrent, true, index) |
10 | 357 | 357 | ||
11 | 358 | onFindInPageModeChanged: if (!chrome.findInPageMode) internal.resetFocus() | 358 | onFindInPageModeChanged: if (!chrome.findInPageMode) internal.resetFocus() |
12 | 359 | 359 | ||
13 | @@ -1247,8 +1247,9 @@ | |||
14 | 1247 | if (share) share.shareText(text) | 1247 | if (share) share.shareText(text) |
15 | 1248 | } | 1248 | } |
16 | 1249 | 1249 | ||
19 | 1250 | function addTab(tab, setCurrent) { | 1250 | function addTab(tab, setCurrent, index) { |
20 | 1251 | var index = tabsModel.add(tab) | 1251 | if (index === undefined) index = tabsModel.add(tab) |
21 | 1252 | else index = tabsModel.insert(tab, index) | ||
22 | 1252 | if (setCurrent) { | 1253 | if (setCurrent) { |
23 | 1253 | tabsModel.currentIndex = index | 1254 | tabsModel.currentIndex = index |
24 | 1254 | chrome.requestedUrl = tab.initialUrl | 1255 | chrome.requestedUrl = tab.initialUrl |
25 | @@ -1351,10 +1352,10 @@ | |||
26 | 1351 | } | 1352 | } |
27 | 1352 | } | 1353 | } |
28 | 1353 | 1354 | ||
30 | 1354 | function openUrlInNewTab(url, setCurrent, load) { | 1355 | function openUrlInNewTab(url, setCurrent, load, index) { |
31 | 1355 | load = typeof load !== 'undefined' ? load : true | 1356 | load = typeof load !== 'undefined' ? load : true |
32 | 1356 | var tab = tabComponent.createObject(tabContainer, {"initialUrl": url, 'incognito': browser.incognito}) | 1357 | var tab = tabComponent.createObject(tabContainer, {"initialUrl": url, 'incognito': browser.incognito}) |
34 | 1357 | internal.addTab(tab, setCurrent) | 1358 | internal.addTab(tab, setCurrent, index) |
35 | 1358 | if (load) { | 1359 | if (load) { |
36 | 1359 | tabsModel.currentTab.load() | 1360 | tabsModel.currentTab.load() |
37 | 1360 | } | 1361 | } |
38 | 1361 | 1362 | ||
39 | === modified file 'src/app/webbrowser/BrowserTab.qml' | |||
40 | --- src/app/webbrowser/BrowserTab.qml 2015-09-02 10:35:36 +0000 | |||
41 | +++ src/app/webbrowser/BrowserTab.qml 2015-09-28 08:10:23 +0000 | |||
42 | @@ -71,6 +71,14 @@ | |||
43 | 71 | } | 71 | } |
44 | 72 | } | 72 | } |
45 | 73 | 73 | ||
46 | 74 | function reload() { | ||
47 | 75 | if (webview) { | ||
48 | 76 | webview.reload() | ||
49 | 77 | } else { | ||
50 | 78 | load() | ||
51 | 79 | } | ||
52 | 80 | } | ||
53 | 81 | |||
54 | 74 | function close() { | 82 | function close() { |
55 | 75 | unload() | 83 | unload() |
56 | 76 | if (preview && preview.toString()) { | 84 | if (preview && preview.toString()) { |
57 | 77 | 85 | ||
58 | === modified file 'src/app/webbrowser/Chrome.qml' | |||
59 | --- src/app/webbrowser/Chrome.qml 2015-09-17 09:15:51 +0000 | |||
60 | +++ src/app/webbrowser/Chrome.qml 2015-09-28 08:10:23 +0000 | |||
61 | @@ -39,7 +39,7 @@ | |||
62 | 39 | property alias showFaviconInAddressBar: navigationBar.showFaviconInAddressBar | 39 | property alias showFaviconInAddressBar: navigationBar.showFaviconInAddressBar |
63 | 40 | readonly property alias bookmarkTogglePlaceHolder: navigationBar.bookmarkTogglePlaceHolder | 40 | readonly property alias bookmarkTogglePlaceHolder: navigationBar.bookmarkTogglePlaceHolder |
64 | 41 | 41 | ||
66 | 42 | signal requestNewTab() | 42 | signal requestNewTab(int index, bool makeCurrent) |
67 | 43 | 43 | ||
68 | 44 | backgroundColor: incognito ? UbuntuColors.darkGrey : "#bcbcbc" | 44 | backgroundColor: incognito ? UbuntuColors.darkGrey : "#bcbcbc" |
69 | 45 | 45 | ||
70 | @@ -67,7 +67,7 @@ | |||
71 | 67 | sourceComponent: TabsBar { | 67 | sourceComponent: TabsBar { |
72 | 68 | model: tabsModel | 68 | model: tabsModel |
73 | 69 | incognito: chrome.incognito | 69 | incognito: chrome.incognito |
75 | 70 | onRequestNewTab: chrome.requestNewTab() | 70 | onRequestNewTab: chrome.requestNewTab(index, makeCurrent) |
76 | 71 | } | 71 | } |
77 | 72 | 72 | ||
78 | 73 | anchors { | 73 | anchors { |
79 | 74 | 74 | ||
80 | === modified file 'src/app/webbrowser/TabItem.qml' | |||
81 | --- src/app/webbrowser/TabItem.qml 2015-09-17 18:32:59 +0000 | |||
82 | +++ src/app/webbrowser/TabItem.qml 2015-09-28 08:10:23 +0000 | |||
83 | @@ -26,13 +26,18 @@ | |||
84 | 26 | property bool incognito: false | 26 | property bool incognito: false |
85 | 27 | property bool active: false | 27 | property bool active: false |
86 | 28 | property bool hoverable: true | 28 | property bool hoverable: true |
88 | 29 | property int rightMargin: tabImage.anchors.rightMargin | 29 | property real rightMargin: 0 |
89 | 30 | 30 | ||
90 | 31 | property alias title: label.text | 31 | property alias title: label.text |
91 | 32 | property alias icon: favicon.source | 32 | property alias icon: favicon.source |
92 | 33 | 33 | ||
93 | 34 | property real dragMin: 0 | ||
94 | 35 | property real dragMax: 0 | ||
95 | 36 | readonly property bool dragging: mouseArea.drag.active | ||
96 | 37 | |||
97 | 34 | signal selected() | 38 | signal selected() |
98 | 35 | signal closed() | 39 | signal closed() |
99 | 40 | signal contextMenu() | ||
100 | 36 | 41 | ||
101 | 37 | BorderImage { | 42 | BorderImage { |
102 | 38 | id: tabImage | 43 | id: tabImage |
103 | @@ -83,14 +88,33 @@ | |||
104 | 83 | } | 88 | } |
105 | 84 | 89 | ||
106 | 85 | MouseArea { | 90 | MouseArea { |
107 | 91 | id: hoverArea | ||
108 | 92 | anchors.fill: parent | ||
109 | 93 | hoverEnabled: !tabItem.active && tabItem.hoverable | ||
110 | 94 | } | ||
111 | 95 | |||
112 | 96 | MouseArea { | ||
113 | 97 | id: mouseArea | ||
114 | 86 | anchors.left: parent.left | 98 | anchors.left: parent.left |
115 | 87 | anchors.top: parent.top | 99 | anchors.top: parent.top |
116 | 88 | anchors.bottom: parent.bottom | 100 | anchors.bottom: parent.bottom |
117 | 89 | anchors.right: closeButton.left | 101 | anchors.right: closeButton.left |
119 | 90 | onClicked: tabItem.selected() | 102 | acceptedButtons: Qt.AllButtons |
120 | 103 | onPressed: { | ||
121 | 104 | if (mouse.button === Qt.LeftButton) { | ||
122 | 105 | tabItem.selected() | ||
123 | 106 | } else if (mouse.button === Qt.RightButton) { | ||
124 | 107 | tabItem.contextMenu() | ||
125 | 108 | } | ||
126 | 109 | } | ||
127 | 110 | onClicked: { | ||
128 | 111 | if ((mouse.buttons === 0) && (mouse.button === Qt.MiddleButton)) { | ||
129 | 112 | tabItem.closed() | ||
130 | 113 | } | ||
131 | 114 | } | ||
132 | 91 | } | 115 | } |
133 | 92 | 116 | ||
135 | 93 | AbstractButton { | 117 | MouseArea { |
136 | 94 | id: closeButton | 118 | id: closeButton |
137 | 95 | objectName: "closeButton" | 119 | objectName: "closeButton" |
138 | 96 | 120 | ||
139 | @@ -108,17 +132,7 @@ | |||
140 | 108 | name: "close" | 132 | name: "close" |
141 | 109 | } | 133 | } |
142 | 110 | 134 | ||
153 | 111 | onTriggered: closed() | 135 | onClicked: closed() |
144 | 112 | } | ||
145 | 113 | |||
146 | 114 | MouseArea { | ||
147 | 115 | id: hoverArea | ||
148 | 116 | anchors.fill: parent | ||
149 | 117 | hoverEnabled: !tabItem.active && tabItem.hoverable | ||
150 | 118 | propagateComposedEvents: true | ||
151 | 119 | acceptedButtons: Qt.MiddleButton | ||
152 | 120 | onClicked: tabItem.closed() | ||
154 | 121 | } | 136 | } |
155 | 122 | } | 137 | } |
156 | 123 | } | 138 | } |
157 | 124 | |||
158 | 125 | 139 | ||
159 | === modified file 'src/app/webbrowser/TabsBar.qml' | |||
160 | --- src/app/webbrowser/TabsBar.qml 2015-09-04 17:48:37 +0000 | |||
161 | +++ src/app/webbrowser/TabsBar.qml 2015-09-28 08:10:23 +0000 | |||
162 | @@ -18,6 +18,7 @@ | |||
163 | 18 | 18 | ||
164 | 19 | import QtQuick 2.4 | 19 | import QtQuick 2.4 |
165 | 20 | import Ubuntu.Components 1.3 | 20 | import Ubuntu.Components 1.3 |
166 | 21 | import Ubuntu.Components.Popups 1.3 | ||
167 | 21 | import ".." | 22 | import ".." |
168 | 22 | 23 | ||
169 | 23 | Item { | 24 | Item { |
170 | @@ -31,7 +32,7 @@ | |||
171 | 31 | 32 | ||
172 | 32 | property bool incognito: false | 33 | property bool incognito: false |
173 | 33 | 34 | ||
175 | 34 | signal requestNewTab() | 35 | signal requestNewTab(int index, bool makeCurrent) |
176 | 35 | 36 | ||
177 | 36 | MouseArea { | 37 | MouseArea { |
178 | 37 | anchors.fill: parent | 38 | anchors.fill: parent |
179 | @@ -67,7 +68,36 @@ | |||
180 | 67 | color: incognito ? "white" : UbuntuColors.darkGrey | 68 | color: incognito ? "white" : UbuntuColors.darkGrey |
181 | 68 | } | 69 | } |
182 | 69 | 70 | ||
184 | 70 | onClicked: root.requestNewTab() | 71 | onClicked: root.requestNewTab(root.model.count, true) |
185 | 72 | } | ||
186 | 73 | |||
187 | 74 | Component { | ||
188 | 75 | id: contextualOptionsComponent | ||
189 | 76 | ActionSelectionPopover { | ||
190 | 77 | id: menu | ||
191 | 78 | objectName: "tabContextualActions" | ||
192 | 79 | property int targetIndex | ||
193 | 80 | readonly property var tab: root.model.get(targetIndex) | ||
194 | 81 | |||
195 | 82 | actions: ActionList { | ||
196 | 83 | Action { | ||
197 | 84 | objectName: "tab_action_new_tab" | ||
198 | 85 | text: i18n.tr("New Tab") | ||
199 | 86 | onTriggered: root.requestNewTab(menu.targetIndex + 1, false) | ||
200 | 87 | } | ||
201 | 88 | Action { | ||
202 | 89 | objectName: "tab_action_reload" | ||
203 | 90 | text: i18n.tr("Reload") | ||
204 | 91 | enabled: menu.tab.url.toString().length > 0 | ||
205 | 92 | onTriggered: menu.tab.reload() | ||
206 | 93 | } | ||
207 | 94 | Action { | ||
208 | 95 | objectName: "tab_action_close_tab" | ||
209 | 96 | text: i18n.tr("Close Tab") | ||
210 | 97 | onTriggered: internal.closeTab(menu.targetIndex) | ||
211 | 98 | } | ||
212 | 99 | } | ||
213 | 100 | } | ||
214 | 71 | } | 101 | } |
215 | 72 | 102 | ||
216 | 73 | Item { | 103 | Item { |
217 | @@ -88,54 +118,58 @@ | |||
218 | 88 | 118 | ||
219 | 89 | property bool reordering: false | 119 | property bool reordering: false |
220 | 90 | 120 | ||
222 | 91 | delegate: TabItem { | 121 | delegate: MouseArea { |
223 | 92 | id: tabDelegate | 122 | id: tabDelegate |
224 | 93 | objectName: "tabDelegate" | 123 | objectName: "tabDelegate" |
225 | 124 | |||
226 | 94 | readonly property int tabIndex: index | 125 | readonly property int tabIndex: index |
227 | 95 | 126 | ||
228 | 96 | active: index === root.model.currentIndex | ||
229 | 97 | hoverable: true | ||
230 | 98 | incognito: root.incognito | ||
231 | 99 | title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab")) | ||
232 | 100 | icon: model.icon | ||
233 | 101 | |||
234 | 102 | anchors.top: tabsContainer.top | 127 | anchors.top: tabsContainer.top |
235 | 128 | property real rightMargin: units.dp(1) | ||
236 | 103 | width: tabWidth + rightMargin | 129 | width: tabWidth + rightMargin |
237 | 104 | height: tabsContainer.height | 130 | height: tabsContainer.height |
245 | 105 | rightMargin: units.dp(1) | 131 | |
246 | 106 | 132 | acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton | |
247 | 107 | onClosed: internal.closeTab(index) | 133 | readonly property bool dragging: drag.active |
248 | 108 | onSelected: root.model.currentIndex = index | 134 | drag { |
249 | 109 | 135 | target: (pressedButtons === Qt.LeftButton) ? tabDelegate : null | |
250 | 110 | MouseArea { | 136 | axis: Drag.XAxis |
251 | 111 | id: mouseArea | 137 | minimumX: 0 |
252 | 138 | maximumX: root.width - tabDelegate.width | ||
253 | 139 | filterChildren: true | ||
254 | 140 | } | ||
255 | 141 | |||
256 | 142 | TabItem { | ||
257 | 112 | anchors.fill: parent | 143 | anchors.fill: parent |
267 | 113 | // XXX: should not start a drag when middle button was pressed | 144 | |
268 | 114 | drag { | 145 | active: tabIndex === root.model.currentIndex |
269 | 115 | target: tabDelegate | 146 | hoverable: true |
270 | 116 | axis: Drag.XAxis | 147 | incognito: root.incognito |
271 | 117 | minimumX: 0 | 148 | title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab")) |
272 | 118 | maximumX: root.width - tabDelegate.width | 149 | icon: model.icon |
273 | 119 | } | 150 | |
274 | 120 | onReleased: root.model.currentIndex = index | 151 | rightMargin: tabDelegate.rightMargin |
275 | 121 | propagateComposedEvents: true | 152 | |
276 | 153 | onClosed: internal.closeTab(index) | ||
277 | 154 | onSelected: root.model.currentIndex = index | ||
278 | 155 | onContextMenu: PopupUtils.open(contextualOptionsComponent, tabDelegate, {"targetIndex": index}) | ||
279 | 122 | } | 156 | } |
280 | 123 | 157 | ||
281 | 124 | Binding { | 158 | Binding { |
282 | 125 | target: repeater | 159 | target: repeater |
283 | 126 | property: "reordering" | 160 | property: "reordering" |
285 | 127 | value: mouseArea.drag.active | 161 | value: dragging |
286 | 128 | } | 162 | } |
287 | 129 | 163 | ||
288 | 130 | Binding on x { | 164 | Binding on x { |
290 | 131 | when: !mouseArea.drag.active | 165 | when: !dragging |
291 | 132 | value: index * width | 166 | value: index * width |
292 | 133 | } | 167 | } |
293 | 134 | 168 | ||
294 | 135 | Behavior on x { NumberAnimation { duration: 250 } } | 169 | Behavior on x { NumberAnimation { duration: 250 } } |
295 | 136 | 170 | ||
296 | 137 | onXChanged: { | 171 | onXChanged: { |
298 | 138 | if (!mouseArea.drag.active) return | 172 | if (!dragging) return |
299 | 139 | if (x < (index * width - width / 2)) { | 173 | if (x < (index * width - width / 2)) { |
300 | 140 | root.model.move(index, index - 1) | 174 | root.model.move(index, index - 1) |
301 | 141 | } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) { | 175 | } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) { |
302 | 142 | 176 | ||
303 | === modified file 'src/app/webbrowser/tabs-model.cpp' | |||
304 | --- src/app/webbrowser/tabs-model.cpp 2015-09-02 16:48:01 +0000 | |||
305 | +++ src/app/webbrowser/tabs-model.cpp 2015-09-28 08:10:23 +0000 | |||
306 | @@ -21,6 +21,7 @@ | |||
307 | 21 | // Qt | 21 | // Qt |
308 | 22 | #include <QtCore/QDebug> | 22 | #include <QtCore/QDebug> |
309 | 23 | #include <QtCore/QObject> | 23 | #include <QtCore/QObject> |
310 | 24 | #include <QtCore/QtGlobal> | ||
311 | 24 | 25 | ||
312 | 25 | /*! | 26 | /*! |
313 | 26 | \class TabsModel | 27 | \class TabsModel |
314 | @@ -101,35 +102,58 @@ | |||
315 | 101 | 102 | ||
316 | 102 | QObject* TabsModel::currentTab() const | 103 | QObject* TabsModel::currentTab() const |
317 | 103 | { | 104 | { |
319 | 104 | if (m_tabs.isEmpty()) { | 105 | if (m_tabs.isEmpty() || !checkValidTabIndex(m_currentIndex)) { |
320 | 105 | return nullptr; | 106 | return nullptr; |
321 | 106 | } | 107 | } |
322 | 107 | return m_tabs.at(m_currentIndex); | 108 | return m_tabs.at(m_currentIndex); |
323 | 108 | } | 109 | } |
324 | 109 | 110 | ||
325 | 110 | /*! | 111 | /*! |
327 | 111 | Add a tab to the model and return the corresponding index in the model. | 112 | Append a tab to the model and return the corresponding index in the model. |
328 | 112 | 113 | ||
329 | 113 | It is the responsibility of the caller to instantiate the corresponding | 114 | It is the responsibility of the caller to instantiate the corresponding |
330 | 114 | Tab beforehand. | 115 | Tab beforehand. |
331 | 115 | */ | 116 | */ |
332 | 116 | int TabsModel::add(QObject* tab) | 117 | int TabsModel::add(QObject* tab) |
333 | 117 | { | 118 | { |
334 | 119 | return insert(tab, m_tabs.count()); | ||
335 | 120 | } | ||
336 | 121 | |||
337 | 122 | /*! | ||
338 | 123 | Add a tab to the model at the specified index, and return the index itself, | ||
339 | 124 | or -1 if the operation failed. | ||
340 | 125 | |||
341 | 126 | It is the responsibility of the caller to instantiate the corresponding | ||
342 | 127 | Tab beforehand. | ||
343 | 128 | */ | ||
344 | 129 | int TabsModel::insert(QObject* tab, int index) | ||
345 | 130 | { | ||
346 | 118 | if (tab == nullptr) { | 131 | if (tab == nullptr) { |
347 | 119 | qWarning() << "Invalid Tab"; | 132 | qWarning() << "Invalid Tab"; |
348 | 120 | return -1; | 133 | return -1; |
349 | 121 | } | 134 | } |
351 | 122 | int index = m_tabs.count(); | 135 | index = qMax(qMin(index, m_tabs.count()), 0); |
352 | 123 | beginInsertRows(QModelIndex(), index, index); | 136 | beginInsertRows(QModelIndex(), index, index); |
354 | 124 | m_tabs.append(tab); | 137 | m_tabs.insert(index, tab); |
355 | 125 | connect(tab, SIGNAL(urlChanged()), SLOT(onUrlChanged())); | 138 | connect(tab, SIGNAL(urlChanged()), SLOT(onUrlChanged())); |
356 | 126 | connect(tab, SIGNAL(titleChanged()), SLOT(onTitleChanged())); | 139 | connect(tab, SIGNAL(titleChanged()), SLOT(onTitleChanged())); |
357 | 127 | connect(tab, SIGNAL(iconChanged()), SLOT(onIconChanged())); | 140 | connect(tab, SIGNAL(iconChanged()), SLOT(onIconChanged())); |
358 | 128 | endInsertRows(); | 141 | endInsertRows(); |
359 | 129 | Q_EMIT countChanged(); | 142 | Q_EMIT countChanged(); |
362 | 130 | if (index == 0) { | 143 | |
363 | 131 | setCurrentIndex(0); | 144 | if (m_currentIndex == -1) { |
364 | 145 | // Set the index to zero if this is the first item that gets added to the | ||
365 | 146 | // model, as it should not be possible to have items in the model but no | ||
366 | 147 | // current tab. | ||
367 | 148 | m_currentIndex = 0; | ||
368 | 149 | Q_EMIT currentIndexChanged(); | ||
369 | 150 | Q_EMIT currentTabChanged(); | ||
370 | 151 | } else if (index <= m_currentIndex) { | ||
371 | 152 | // Increment the index if we are inserting items before the current index. | ||
372 | 153 | m_currentIndex++; | ||
373 | 154 | Q_EMIT currentIndexChanged(); | ||
374 | 132 | } | 155 | } |
375 | 156 | |||
376 | 133 | return index; | 157 | return index; |
377 | 134 | } | 158 | } |
378 | 135 | 159 | ||
379 | @@ -150,15 +174,21 @@ | |||
380 | 150 | tab->disconnect(this); | 174 | tab->disconnect(this); |
381 | 151 | endRemoveRows(); | 175 | endRemoveRows(); |
382 | 152 | Q_EMIT countChanged(); | 176 | Q_EMIT countChanged(); |
386 | 153 | if (index == m_currentIndex) { | 177 | |
387 | 154 | if (!checkValidTabIndex(index)) { | 178 | if (index < m_currentIndex) { |
388 | 155 | m_currentIndex = m_tabs.count() - 1; | 179 | // If we removed any tab before the current one, decrease the |
389 | 180 | // current index to match. | ||
390 | 181 | m_currentIndex--; | ||
391 | 182 | Q_EMIT currentIndexChanged(); | ||
392 | 183 | } else if (index == m_currentIndex) { | ||
393 | 184 | // If the current tab was removed, the following one (if any) is made | ||
394 | 185 | // current. If it was the last tab in the model, the current index needs | ||
395 | 186 | // to be decreased. | ||
396 | 187 | if (m_currentIndex == m_tabs.count()) { | ||
397 | 188 | m_currentIndex--; | ||
398 | 156 | Q_EMIT currentIndexChanged(); | 189 | Q_EMIT currentIndexChanged(); |
399 | 157 | } | 190 | } |
400 | 158 | Q_EMIT currentTabChanged(); | 191 | Q_EMIT currentTabChanged(); |
401 | 159 | } else if (m_currentIndex > index) { | ||
402 | 160 | m_currentIndex -= 1; | ||
403 | 161 | Q_EMIT currentIndexChanged(); | ||
404 | 162 | } | 192 | } |
405 | 163 | return tab; | 193 | return tab; |
406 | 164 | } | 194 | } |
407 | 165 | 195 | ||
408 | === modified file 'src/app/webbrowser/tabs-model.h' | |||
409 | --- src/app/webbrowser/tabs-model.h 2015-06-18 14:28:11 +0000 | |||
410 | +++ src/app/webbrowser/tabs-model.h 2015-09-28 08:10:23 +0000 | |||
411 | @@ -57,6 +57,7 @@ | |||
412 | 57 | QObject* currentTab() const; | 57 | QObject* currentTab() const; |
413 | 58 | 58 | ||
414 | 59 | Q_INVOKABLE int add(QObject* tab); | 59 | Q_INVOKABLE int add(QObject* tab); |
415 | 60 | Q_INVOKABLE int insert(QObject* tab, int index); | ||
416 | 60 | Q_INVOKABLE QObject* remove(int index); | 61 | Q_INVOKABLE QObject* remove(int index); |
417 | 61 | Q_INVOKABLE QObject* get(int index) const; | 62 | Q_INVOKABLE QObject* get(int index) const; |
418 | 62 | Q_INVOKABLE void move(int from, int to); | 63 | Q_INVOKABLE void move(int from, int to); |
419 | @@ -76,6 +77,7 @@ | |||
420 | 76 | int m_currentIndex; | 77 | int m_currentIndex; |
421 | 77 | 78 | ||
422 | 78 | bool checkValidTabIndex(int index) const; | 79 | bool checkValidTabIndex(int index) const; |
423 | 80 | void setCurrentIndexNoCheck(int index); | ||
424 | 79 | void onDataChanged(QObject* tab, int role); | 81 | void onDataChanged(QObject* tab, int role); |
425 | 80 | }; | 82 | }; |
426 | 81 | 83 | ||
427 | 82 | 84 | ||
428 | === modified file 'tests/autopilot/webbrowser_app/emulators/browser.py' | |||
429 | --- tests/autopilot/webbrowser_app/emulators/browser.py 2015-09-13 21:24:35 +0000 | |||
430 | +++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-09-28 08:10:23 +0000 | |||
431 | @@ -329,10 +329,10 @@ | |||
432 | 329 | self.pointing_device.click_object(button) | 329 | self.pointing_device.click_object(button) |
433 | 330 | 330 | ||
434 | 331 | def get_tabs(self): | 331 | def get_tabs(self): |
436 | 332 | return self.select_many("QQuickItem", objectName="tabDelegate") | 332 | return self.select_many("QQuickMouseArea", objectName="tabDelegate") |
437 | 333 | 333 | ||
438 | 334 | def get_tab(self, index): | 334 | def get_tab(self, index): |
440 | 335 | return self.select_single("QQuickItem", objectName="tabDelegate", | 335 | return self.select_single("QQuickMouseArea", objectName="tabDelegate", |
441 | 336 | tabIndex=index) | 336 | tabIndex=index) |
442 | 337 | 337 | ||
443 | 338 | @autopilot.logging.log_action(logger.info) | 338 | @autopilot.logging.log_action(logger.info) |
444 | @@ -342,7 +342,7 @@ | |||
445 | 342 | @autopilot.logging.log_action(logger.info) | 342 | @autopilot.logging.log_action(logger.info) |
446 | 343 | def close_tab(self, index): | 343 | def close_tab(self, index): |
447 | 344 | tab = self.get_tab(index) | 344 | tab = self.get_tab(index) |
449 | 345 | close_button = tab.select_single("Icon", objectName="closeButton") | 345 | close_button = tab.select_single(objectName="closeButton") |
450 | 346 | self.pointing_device.click_object(close_button) | 346 | self.pointing_device.click_object(close_button) |
451 | 347 | 347 | ||
452 | 348 | 348 | ||
453 | 349 | 349 | ||
454 | === added file 'tests/unittests/qml/qml_tree_helpers.js' | |||
455 | --- tests/unittests/qml/qml_tree_helpers.js 1970-01-01 00:00:00 +0000 | |||
456 | +++ tests/unittests/qml/qml_tree_helpers.js 2015-09-28 08:10:23 +0000 | |||
457 | @@ -0,0 +1,42 @@ | |||
458 | 1 | /* | ||
459 | 2 | * Copyright 2015 Canonical Ltd. | ||
460 | 3 | * | ||
461 | 4 | * This file is part of webbrowser-app. | ||
462 | 5 | * | ||
463 | 6 | * webbrowser-app is free software; you can redistribute it and/or modify | ||
464 | 7 | * it under the terms of the GNU General Public License as published by | ||
465 | 8 | * the Free Software Foundation; version 3. | ||
466 | 9 | * | ||
467 | 10 | * webbrowser-app is distributed in the hope that it will be useful, | ||
468 | 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
469 | 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
470 | 13 | * GNU General Public License for more details. | ||
471 | 14 | * | ||
472 | 15 | * You should have received a copy of the GNU General Public License | ||
473 | 16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
474 | 17 | */ | ||
475 | 18 | |||
476 | 19 | // Given the naming convention in QML for class names, we should | ||
477 | 20 | // never have a class name with underscores in it, so the following | ||
478 | 21 | // should be a safe way to remove the rest of the extra metatype | ||
479 | 22 | // information produced by converting QML objects to strings. | ||
480 | 23 | function qmlType(item) { | ||
481 | 24 | return String(item).split("_")[0] | ||
482 | 25 | } | ||
483 | 26 | |||
484 | 27 | function findDescendantsByType(item, type, list) { | ||
485 | 28 | list = list || [] | ||
486 | 29 | if (qmlType(item) === type) list.push(item) | ||
487 | 30 | for (var i in item.children) { | ||
488 | 31 | findDescendantsByType(item.children[i], type, list) | ||
489 | 32 | } | ||
490 | 33 | return list | ||
491 | 34 | } | ||
492 | 35 | |||
493 | 36 | function findAncestorByType(item, type) { | ||
494 | 37 | while (item.parent) { | ||
495 | 38 | if (qmlType(item.parent) === type) return item.parent | ||
496 | 39 | item = item.parent | ||
497 | 40 | } | ||
498 | 41 | return null | ||
499 | 42 | } | ||
500 | 0 | 43 | ||
501 | === modified file 'tests/unittests/qml/tst_BrowserTab.qml' | |||
502 | --- tests/unittests/qml/tst_BrowserTab.qml 2015-08-10 15:22:00 +0000 | |||
503 | +++ tests/unittests/qml/tst_BrowserTab.qml 2015-09-28 08:10:23 +0000 | |||
504 | @@ -36,6 +36,9 @@ | |||
505 | 36 | property url icon | 36 | property url icon |
506 | 37 | property var request | 37 | property var request |
507 | 38 | property string currentState | 38 | property string currentState |
508 | 39 | |||
509 | 40 | property int reloaded: 0 | ||
510 | 41 | function reload() { reloaded++ } | ||
511 | 39 | } | 42 | } |
512 | 40 | readonly property bool webviewPresent: webview | 43 | readonly property bool webviewPresent: webview |
513 | 41 | } | 44 | } |
514 | @@ -76,6 +79,20 @@ | |||
515 | 76 | tab.destroy() | 79 | tab.destroy() |
516 | 77 | } | 80 | } |
517 | 78 | 81 | ||
518 | 82 | function test_reload() { | ||
519 | 83 | var tab = tabComponent.createObject(root) | ||
520 | 84 | verify(!tab.webviewPresent) | ||
521 | 85 | |||
522 | 86 | tab.initialUrl = "http://example.org" | ||
523 | 87 | tab.reload() | ||
524 | 88 | tryCompare(tab, 'webviewPresent', true) | ||
525 | 89 | compare(tab.webview.reloaded, 0) | ||
526 | 90 | |||
527 | 91 | tab.reload() | ||
528 | 92 | verify(tab.webviewPresent) | ||
529 | 93 | compare(tab.webview.reloaded, 1) | ||
530 | 94 | } | ||
531 | 95 | |||
532 | 79 | function test_create_with_request() { | 96 | function test_create_with_request() { |
533 | 80 | var tab = tabComponent.createObject(root, {'request': "foobar"}) | 97 | var tab = tabComponent.createObject(root, {'request': "foobar"}) |
534 | 81 | tryCompare(tab, 'webviewPresent', true) | 98 | tryCompare(tab, 'webviewPresent', true) |
535 | 82 | 99 | ||
536 | === modified file 'tests/unittests/qml/tst_TabsBar.qml' | |||
537 | --- tests/unittests/qml/tst_TabsBar.qml 2015-08-10 15:22:00 +0000 | |||
538 | +++ tests/unittests/qml/tst_TabsBar.qml 2015-09-28 08:10:23 +0000 | |||
539 | @@ -21,12 +21,14 @@ | |||
540 | 21 | import Ubuntu.Test 1.0 | 21 | import Ubuntu.Test 1.0 |
541 | 22 | import "../../../src/app/webbrowser" | 22 | import "../../../src/app/webbrowser" |
542 | 23 | import webbrowserapp.private 0.1 | 23 | import webbrowserapp.private 0.1 |
543 | 24 | import "qml_tree_helpers.js" as Tree | ||
544 | 24 | 25 | ||
545 | 25 | Item { | 26 | Item { |
546 | 26 | id: root | 27 | id: root |
547 | 27 | 28 | ||
548 | 28 | width: 600 | 29 | width: 600 |
550 | 29 | height: 50 | 30 | height: 200 |
551 | 31 | signal reload(string url) | ||
552 | 30 | 32 | ||
553 | 31 | TabsModel { | 33 | TabsModel { |
554 | 32 | id: tabsModel | 34 | id: tabsModel |
555 | @@ -35,22 +37,35 @@ | |||
556 | 35 | Component { | 37 | Component { |
557 | 36 | id: tabComponent | 38 | id: tabComponent |
558 | 37 | QtObject { | 39 | QtObject { |
559 | 40 | id: tab | ||
560 | 38 | property url url | 41 | property url url |
561 | 39 | property string title | 42 | property string title |
562 | 40 | property url icon | 43 | property url icon |
563 | 41 | function close() { destroy() } | 44 | function close() { destroy() } |
564 | 45 | function reload() { root.reload(tab.url) } | ||
565 | 42 | } | 46 | } |
566 | 43 | } | 47 | } |
567 | 44 | 48 | ||
568 | 45 | TabsBar { | 49 | TabsBar { |
569 | 46 | id: tabs | 50 | id: tabs |
571 | 47 | anchors.fill: parent | 51 | |
572 | 52 | // Make the tabs bar smaller than the window and aligned in the middle | ||
573 | 53 | // to leave room for the context menu to pop up and have all its items | ||
574 | 54 | // visible within the screen. | ||
575 | 55 | anchors.left: parent.left | ||
576 | 56 | anchors.right: parent.right | ||
577 | 57 | anchors.verticalCenter: parent.verticalCenter | ||
578 | 58 | height: 50 | ||
579 | 59 | |||
580 | 48 | model: tabsModel | 60 | model: tabsModel |
582 | 49 | onRequestNewTab: appendTab("", "", "") | 61 | onRequestNewTab: insertTab("", "", "", index) |
583 | 50 | function appendTab(url, title, icon) { | 62 | function appendTab(url, title, icon) { |
584 | 63 | insertTab(url, title, icon, model.count) | ||
585 | 64 | model.currentIndex = model.count - 1 | ||
586 | 65 | } | ||
587 | 66 | function insertTab(url, title, icon, index) { | ||
588 | 51 | var tab = tabComponent.createObject(root, {"url": url, "title": title, "icon": icon}) | 67 | var tab = tabComponent.createObject(root, {"url": url, "title": title, "icon": icon}) |
591 | 52 | model.add(tab) | 68 | model.insert(tab, index) |
590 | 53 | model.currentIndex = model.count - 1 | ||
592 | 54 | } | 69 | } |
593 | 55 | } | 70 | } |
594 | 56 | 71 | ||
595 | @@ -60,13 +75,46 @@ | |||
596 | 60 | signalName: "requestNewTab" | 75 | signalName: "requestNewTab" |
597 | 61 | } | 76 | } |
598 | 62 | 77 | ||
599 | 78 | SignalSpy { | ||
600 | 79 | id: reloadSpy | ||
601 | 80 | target: root | ||
602 | 81 | signalName: "reload" | ||
603 | 82 | } | ||
604 | 83 | |||
605 | 84 | // Ideally we would get menu items by their objectName, however they are | ||
606 | 85 | // created dynamically by the ActionSelectionPopover and the objectName | ||
607 | 86 | // of the source action is not copied to the generated item. | ||
608 | 87 | // So we first select the source Action by objectName then look for an item | ||
609 | 88 | // with the same text as the action. This is not ideal but it will work | ||
610 | 89 | // since we don't have items with the same text. | ||
611 | 90 | // https://launchpad.net/bugs/1205144 tracks the issue, and as of 2015-09-23 | ||
612 | 91 | // is fixed in the vivid overlay PPA but not in wily yet. | ||
613 | 92 | function getMenuItemForAction(menu, actionName) { | ||
614 | 93 | actionName = "tab_action_" + actionName | ||
615 | 94 | var text = "" | ||
616 | 95 | var actions = menu.actions.actions | ||
617 | 96 | for (var i = 0; i < actions.length; i++) { | ||
618 | 97 | if (actions[i].objectName === actionName) { | ||
619 | 98 | text = actions[i].text | ||
620 | 99 | break | ||
621 | 100 | } | ||
622 | 101 | } | ||
623 | 102 | if (text === "") return null | ||
624 | 103 | |||
625 | 104 | var menuItems = Tree.findDescendantsByType(menu, "Label") | ||
626 | 105 | var matching = menuItems.filter(function(item) { return item.text === text }) | ||
627 | 106 | if (matching.length === 0) return null | ||
628 | 107 | else return Tree.findAncestorByType(matching[0], "Empty") | ||
629 | 108 | } | ||
630 | 109 | |||
631 | 63 | UbuntuTestCase { | 110 | UbuntuTestCase { |
632 | 64 | name: "TabsBar" | 111 | name: "TabsBar" |
633 | 65 | when: windowShown | 112 | when: windowShown |
634 | 66 | 113 | ||
636 | 67 | function clickItem(item) { | 114 | function clickItem(item, button) { |
637 | 115 | if (button === undefined) button = Qt.LeftButton | ||
638 | 68 | var center = centerOf(item) | 116 | var center = centerOf(item) |
640 | 69 | mouseClick(item, center.x, center.y) | 117 | mouseClick(item, center.x, center.y, button) |
641 | 70 | } | 118 | } |
642 | 71 | 119 | ||
643 | 72 | function getTabDelegate(index) { | 120 | function getTabDelegate(index) { |
644 | @@ -80,11 +128,22 @@ | |||
645 | 80 | return null | 128 | return null |
646 | 81 | } | 129 | } |
647 | 82 | 130 | ||
648 | 131 | function popupMenuOnTab(index) { | ||
649 | 132 | var tab = getTabDelegate(index) | ||
650 | 133 | if (tab) { | ||
651 | 134 | clickItem(tab, Qt.RightButton) | ||
652 | 135 | var menu = findChild(root, "tabContextualActions") | ||
653 | 136 | waitForRendering(menu) | ||
654 | 137 | return menu | ||
655 | 138 | } else return null | ||
656 | 139 | } | ||
657 | 140 | |||
658 | 83 | function cleanup() { | 141 | function cleanup() { |
659 | 84 | while (tabsModel.count > 0) { | 142 | while (tabsModel.count > 0) { |
660 | 85 | tabsModel.remove(0).destroy() | 143 | tabsModel.remove(0).destroy() |
661 | 86 | } | 144 | } |
662 | 87 | newTabRequestSpy.clear() | 145 | newTabRequestSpy.clear() |
663 | 146 | reloadSpy.clear() | ||
664 | 88 | } | 147 | } |
665 | 89 | 148 | ||
666 | 90 | function populateTabs() { | 149 | function populateTabs() { |
667 | @@ -101,6 +160,9 @@ | |||
668 | 101 | clickItem(newTabButton) | 160 | clickItem(newTabButton) |
669 | 102 | } | 161 | } |
670 | 103 | compare(newTabRequestSpy.count, 3) | 162 | compare(newTabRequestSpy.count, 3) |
671 | 163 | compare(newTabRequestSpy.signalArguments[0][0], 0) | ||
672 | 164 | compare(newTabRequestSpy.signalArguments[1][0], 1) | ||
673 | 165 | compare(newTabRequestSpy.signalArguments[2][0], 2) | ||
674 | 104 | } | 166 | } |
675 | 105 | 167 | ||
676 | 106 | function test_mouse_left_click() { | 168 | function test_mouse_left_click() { |
677 | @@ -117,11 +179,19 @@ | |||
678 | 117 | populateTabs() | 179 | populateTabs() |
679 | 118 | for (var i = 2; i >= 0; --i) { | 180 | for (var i = 2; i >= 0; --i) { |
680 | 119 | var tab0 = getTabDelegate(0) | 181 | var tab0 = getTabDelegate(0) |
682 | 120 | mouseClick(tab0, centerOf(tab0).x, centerOf(tab0).y, Qt.MiddleButton) | 182 | clickItem(tab0, Qt.MiddleButton) |
683 | 121 | compare(tabsModel.count, i) | 183 | compare(tabsModel.count, i) |
684 | 122 | } | 184 | } |
685 | 123 | } | 185 | } |
686 | 124 | 186 | ||
687 | 187 | function test_mouse_right_click() { | ||
688 | 188 | // Right click pops up the contextual actions menu | ||
689 | 189 | populateTabs() | ||
690 | 190 | var menu = popupMenuOnTab(0) | ||
691 | 191 | verify(menu) | ||
692 | 192 | verify(menu.visible) | ||
693 | 193 | } | ||
694 | 194 | |||
695 | 125 | function test_mouse_wheel() { | 195 | function test_mouse_wheel() { |
696 | 126 | // Wheel events cycle through open tabs | 196 | // Wheel events cycle through open tabs |
697 | 127 | populateTabs() | 197 | populateTabs() |
698 | @@ -141,6 +211,16 @@ | |||
699 | 141 | compare(tabsModel.currentIndex, 1) | 211 | compare(tabsModel.currentIndex, 1) |
700 | 142 | } | 212 | } |
701 | 143 | 213 | ||
702 | 214 | function test_close_tabs() { | ||
703 | 215 | populateTabs() | ||
704 | 216 | for (var i = 2; i >= 0; --i) { | ||
705 | 217 | var tab0 = getTabDelegate(0) | ||
706 | 218 | var closeButton = findChild(tab0, "closeButton") | ||
707 | 219 | clickItem(closeButton, Qt.LeftButton) | ||
708 | 220 | compare(tabsModel.count, i) | ||
709 | 221 | } | ||
710 | 222 | } | ||
711 | 223 | |||
712 | 144 | function test_drag_tab() { | 224 | function test_drag_tab() { |
713 | 145 | populateTabs() | 225 | populateTabs() |
714 | 146 | 226 | ||
715 | @@ -169,5 +249,63 @@ | |||
716 | 169 | tab = getTabDelegate(1) | 249 | tab = getTabDelegate(1) |
717 | 170 | dragTab(tab, -tab.width * 2, 0) | 250 | dragTab(tab, -tab.width * 2, 0) |
718 | 171 | } | 251 | } |
719 | 252 | |||
720 | 253 | function test_menu_states_on_new_tab() { | ||
721 | 254 | populateTabs() | ||
722 | 255 | var menu = popupMenuOnTab(0) | ||
723 | 256 | var item = getMenuItemForAction(menu, "new_tab") | ||
724 | 257 | verify(item.enabled) | ||
725 | 258 | item = getMenuItemForAction(menu, "reload") | ||
726 | 259 | verify(!item.enabled) | ||
727 | 260 | item = getMenuItemForAction(menu, "close_tab") | ||
728 | 261 | verify(item.enabled) | ||
729 | 262 | } | ||
730 | 263 | |||
731 | 264 | function test_menu_states_on_page() { | ||
732 | 265 | tabs.appendTab("http://localhost/", "tab", "") | ||
733 | 266 | var menu = popupMenuOnTab(0) | ||
734 | 267 | var item = getMenuItemForAction(menu, "new_tab") | ||
735 | 268 | verify(item.enabled) | ||
736 | 269 | item = getMenuItemForAction(menu, "reload") | ||
737 | 270 | verify(item.enabled) | ||
738 | 271 | item = getMenuItemForAction(menu, "close_tab") | ||
739 | 272 | verify(item.enabled) | ||
740 | 273 | } | ||
741 | 274 | |||
742 | 275 | function test_context_menu_close() { | ||
743 | 276 | populateTabs() | ||
744 | 277 | var menu = popupMenuOnTab(1) | ||
745 | 278 | var item = getMenuItemForAction(menu, "close_tab") | ||
746 | 279 | clickItem(item) | ||
747 | 280 | compare(tabsModel.count, 2) | ||
748 | 281 | compare(tabsModel.get(0).title, "tab 0") | ||
749 | 282 | compare(tabsModel.get(1).title, "tab 2") | ||
750 | 283 | } | ||
751 | 284 | |||
752 | 285 | function test_context_menu_reload() { | ||
753 | 286 | var baseUrl = "http://localhost/" | ||
754 | 287 | tabs.appendTab(baseUrl + "1", "tab 1", "") | ||
755 | 288 | tabs.appendTab(baseUrl + "2", "tab 2", "") | ||
756 | 289 | var menu = popupMenuOnTab(1) | ||
757 | 290 | var item = getMenuItemForAction(menu, "reload") | ||
758 | 291 | clickItem(item) | ||
759 | 292 | compare(reloadSpy.count, 1) | ||
760 | 293 | compare(reloadSpy.signalArguments[0][0], baseUrl + "2") | ||
761 | 294 | } | ||
762 | 295 | |||
763 | 296 | function test_context_menu_new_tab() { | ||
764 | 297 | var baseUrl = "http://localhost/" | ||
765 | 298 | tabs.appendTab(baseUrl + "1", "tab 1", "") | ||
766 | 299 | tabs.appendTab(baseUrl + "2", "tab 2", "") | ||
767 | 300 | var menu = popupMenuOnTab(0) | ||
768 | 301 | var item = getMenuItemForAction(menu, "new_tab") | ||
769 | 302 | clickItem(item) | ||
770 | 303 | compare(newTabRequestSpy.count, 1) | ||
771 | 304 | compare(newTabRequestSpy.signalArguments[0][0], 1) | ||
772 | 305 | compare(tabsModel.count, 3) | ||
773 | 306 | compare(tabsModel.get(0).url, baseUrl + "1") | ||
774 | 307 | compare(tabsModel.get(1).url, "") | ||
775 | 308 | compare(tabsModel.get(2).url, baseUrl + "2") | ||
776 | 309 | } | ||
777 | 172 | } | 310 | } |
778 | 173 | } | 311 | } |
779 | 174 | 312 | ||
780 | === modified file 'tests/unittests/tabs-model/tst_TabsModelTests.cpp' | |||
781 | --- tests/unittests/tabs-model/tst_TabsModelTests.cpp 2015-09-02 16:48:01 +0000 | |||
782 | +++ tests/unittests/tabs-model/tst_TabsModelTests.cpp 2015-09-28 08:10:23 +0000 | |||
783 | @@ -17,6 +17,7 @@ | |||
784 | 17 | */ | 17 | */ |
785 | 18 | 18 | ||
786 | 19 | // Qt | 19 | // Qt |
787 | 20 | #include <QtCore/QStringList> | ||
788 | 20 | #include <QtQml/QQmlComponent> | 21 | #include <QtQml/QQmlComponent> |
789 | 21 | #include <QtQml/QQmlEngine> | 22 | #include <QtQml/QQmlEngine> |
790 | 22 | #include <QtQml/QQmlProperty> | 23 | #include <QtQml/QQmlProperty> |
791 | @@ -47,6 +48,20 @@ | |||
792 | 47 | return item; | 48 | return item; |
793 | 48 | } | 49 | } |
794 | 49 | 50 | ||
795 | 51 | QQuickItem* createTabWithTitle(const QString& title) { | ||
796 | 52 | QQuickItem* tab = createTab(); | ||
797 | 53 | tab->setProperty("title", title); | ||
798 | 54 | return tab; | ||
799 | 55 | } | ||
800 | 56 | |||
801 | 57 | void verifyTabsOrder(QStringList orderedTitles) { | ||
802 | 58 | QCOMPARE(model->rowCount(), orderedTitles.count()); | ||
803 | 59 | int i = 0; | ||
804 | 60 | Q_FOREACH(QString title, orderedTitles) { | ||
805 | 61 | QCOMPARE(model->get(i++)->property("title").toString(), title); | ||
806 | 62 | } | ||
807 | 63 | } | ||
808 | 64 | |||
809 | 50 | private Q_SLOTS: | 65 | private Q_SLOTS: |
810 | 51 | void init() | 66 | void init() |
811 | 52 | { | 67 | { |
812 | @@ -113,6 +128,51 @@ | |||
813 | 113 | QCOMPARE(model->rowCount(), 1); | 128 | QCOMPARE(model->rowCount(), 1); |
814 | 114 | } | 129 | } |
815 | 115 | 130 | ||
816 | 131 | void shouldNotInsertNullTab() | ||
817 | 132 | { | ||
818 | 133 | QCOMPARE(model->insert(0, 0), -1); | ||
819 | 134 | QCOMPARE(model->rowCount(), 0); | ||
820 | 135 | } | ||
821 | 136 | |||
822 | 137 | void shouldReturnIndexWhenInsertingTab() | ||
823 | 138 | { | ||
824 | 139 | for(int i = 0; i < 3; ++i) { | ||
825 | 140 | model->add(createTab()); | ||
826 | 141 | } | ||
827 | 142 | for(int i = 2; i >= 0; --i) { | ||
828 | 143 | QCOMPARE(model->insert(createTab(), i), i); | ||
829 | 144 | } | ||
830 | 145 | } | ||
831 | 146 | |||
832 | 147 | void shouldUpdateCountWhenInsertingTab() | ||
833 | 148 | { | ||
834 | 149 | QSignalSpy spy(model, SIGNAL(countChanged())); | ||
835 | 150 | model->insert(createTab(), 0); | ||
836 | 151 | QCOMPARE(spy.count(), 1); | ||
837 | 152 | QCOMPARE(model->rowCount(), 1); | ||
838 | 153 | } | ||
839 | 154 | |||
840 | 155 | void shouldInsertAtCorrectIndex() | ||
841 | 156 | { | ||
842 | 157 | model->insert(createTabWithTitle("B"), 0); | ||
843 | 158 | model->insert(createTabWithTitle("A"), 0); | ||
844 | 159 | verifyTabsOrder(QStringList({"A", "B"})); | ||
845 | 160 | model->insert(createTabWithTitle("X"), 1); | ||
846 | 161 | verifyTabsOrder(QStringList({"A", "X", "B"})); | ||
847 | 162 | model->insert(createTabWithTitle("C"), 3); | ||
848 | 163 | verifyTabsOrder(QStringList({"A", "X", "B", "C"})); | ||
849 | 164 | } | ||
850 | 165 | |||
851 | 166 | void shouldClampIndexWhenInsertingTabOutOfBounds() | ||
852 | 167 | { | ||
853 | 168 | model->add(createTabWithTitle("A")); | ||
854 | 169 | model->add(createTabWithTitle("B")); | ||
855 | 170 | model->insert(createTabWithTitle("C"), 3); | ||
856 | 171 | verifyTabsOrder(QStringList({"A", "B", "C"})); | ||
857 | 172 | model->insert(createTabWithTitle("X"), -1); | ||
858 | 173 | verifyTabsOrder(QStringList({"X", "A", "B", "C"})); | ||
859 | 174 | } | ||
860 | 175 | |||
861 | 116 | void shouldUpdateCountWhenRemovingTab() | 176 | void shouldUpdateCountWhenRemovingTab() |
862 | 117 | { | 177 | { |
863 | 118 | model->add(createTab()); | 178 | model->add(createTab()); |
864 | @@ -138,19 +198,6 @@ | |||
865 | 138 | delete removed; | 198 | delete removed; |
866 | 139 | } | 199 | } |
867 | 140 | 200 | ||
868 | 141 | void shouldNotChangeCurrentTabWhenAddingUnlessModelWasEmpty() | ||
869 | 142 | { | ||
870 | 143 | QSignalSpy spy(model, SIGNAL(currentTabChanged())); | ||
871 | 144 | QQuickItem* tab = createTab(); | ||
872 | 145 | model->add(tab); | ||
873 | 146 | QCOMPARE(spy.count(), 1); | ||
874 | 147 | QCOMPARE(model->currentTab(), tab); | ||
875 | 148 | spy.clear(); | ||
876 | 149 | model->add(createTab()); | ||
877 | 150 | QVERIFY(spy.isEmpty()); | ||
878 | 151 | QCOMPARE(model->currentTab(), tab); | ||
879 | 152 | } | ||
880 | 153 | |||
881 | 154 | void shouldNotDeleteTabWhenRemoving() | 201 | void shouldNotDeleteTabWhenRemoving() |
882 | 155 | { | 202 | { |
883 | 156 | QQuickItem* tab = createTab(); | 203 | QQuickItem* tab = createTab(); |
884 | @@ -244,64 +291,128 @@ | |||
885 | 244 | QCOMPARE(model->currentTab(), tab2); | 291 | QCOMPARE(model->currentTab(), tab2); |
886 | 245 | } | 292 | } |
887 | 246 | 293 | ||
898 | 247 | void shouldUpdateCurrentTabWhenRemoving() | 294 | void shouldSetCurrentTabWhenAddingFirstTab() |
899 | 248 | { | 295 | { |
900 | 249 | QSignalSpy tabSpy(model, SIGNAL(currentTabChanged())); | 296 | // Adding a tab to an empty model should update the current tab |
901 | 250 | QSignalSpy indexSpy(model, SIGNAL(currentIndexChanged())); | 297 | // to that tab |
902 | 251 | 298 | QSignalSpy spytab(model, SIGNAL(currentTabChanged())); | |
903 | 252 | // Adding a tab to an empty model should update the current tab. | 299 | QSignalSpy spyindex(model, SIGNAL(currentIndexChanged())); |
904 | 253 | // Removing the last tab from the model should update it too. | 300 | QCOMPARE(model->currentIndex(), -1); |
905 | 254 | model->add(createTab()); | 301 | QCOMPARE(model->currentTab(), (QObject*) nullptr); |
906 | 255 | tabSpy.clear(); | 302 | |
907 | 256 | indexSpy.clear(); | 303 | QQuickItem* tab1 = createTab(); |
908 | 304 | model->add(tab1); | ||
909 | 305 | |||
910 | 306 | QCOMPARE(spytab.count(), 1); | ||
911 | 307 | QCOMPARE(spyindex.count(), 1); | ||
912 | 308 | QCOMPARE(model->currentIndex(), 0); | ||
913 | 309 | QCOMPARE(model->currentTab(), tab1); | ||
914 | 310 | |||
915 | 311 | // But adding further items should keep the index where it was | ||
916 | 312 | model->add(createTab()); | ||
917 | 313 | model->add(createTab()); | ||
918 | 314 | |||
919 | 315 | QCOMPARE(spytab.count(), 1); | ||
920 | 316 | QCOMPARE(spyindex.count(), 1); | ||
921 | 317 | QCOMPARE(model->currentIndex(), 0); | ||
922 | 318 | QCOMPARE(model->currentTab(), tab1); | ||
923 | 319 | } | ||
924 | 320 | |||
925 | 321 | void shouldSetCurrentTabWhenInsertingFirstTab() | ||
926 | 322 | { | ||
927 | 323 | // Inserting a tab to an empty model should update the current tab | ||
928 | 324 | // to that tab | ||
929 | 325 | QSignalSpy spytab(model, SIGNAL(currentTabChanged())); | ||
930 | 326 | QSignalSpy spyindex(model, SIGNAL(currentIndexChanged())); | ||
931 | 327 | QCOMPARE(model->currentIndex(), -1); | ||
932 | 328 | QCOMPARE(model->currentTab(), (QObject*) nullptr); | ||
933 | 329 | |||
934 | 330 | QQuickItem* tab1 = createTab(); | ||
935 | 331 | model->insert(tab1, 0); | ||
936 | 332 | |||
937 | 333 | QCOMPARE(spytab.count(), 1); | ||
938 | 334 | QCOMPARE(spyindex.count(), 1); | ||
939 | 335 | QCOMPARE(model->currentIndex(), 0); | ||
940 | 336 | QCOMPARE(model->currentTab(), tab1); | ||
941 | 337 | } | ||
942 | 338 | |||
943 | 339 | void shouldSetInvalidIndexWhenRemovingLastTab() | ||
944 | 340 | { | ||
945 | 341 | // Removing the last item should also set the current index to -1 | ||
946 | 342 | // and the current tab to null | ||
947 | 343 | model->add(createTab()); | ||
948 | 344 | |||
949 | 345 | QSignalSpy spytab(model, SIGNAL(currentTabChanged())); | ||
950 | 346 | QSignalSpy spyindex(model, SIGNAL(currentIndexChanged())); | ||
951 | 257 | delete model->remove(0); | 347 | delete model->remove(0); |
954 | 258 | QCOMPARE(tabSpy.count(), 1); | 348 | QCOMPARE(spytab.count(), 1); |
955 | 259 | QCOMPARE(indexSpy.count(), 1); | 349 | QCOMPARE(spyindex.count(), 1); |
956 | 350 | QCOMPARE(model->currentIndex(), -1); | ||
957 | 351 | QCOMPARE(model->currentTab(), (QObject*) nullptr); | ||
958 | 352 | } | ||
959 | 260 | 353 | ||
962 | 261 | // When removing a tab after the current one, neither the | 354 | void shouldNotChangeIndexWhenRemovingAfterCurrent() |
963 | 262 | // current tab nor the current index should change. | 355 | { |
964 | 356 | // When removing a tab after the current one, | ||
965 | 357 | // the current tab shouldn’t change. | ||
966 | 263 | QQuickItem* tab1 = createTab(); | 358 | QQuickItem* tab1 = createTab(); |
967 | 264 | model->add(tab1); | 359 | model->add(tab1); |
968 | 265 | model->add(createTab()); | 360 | model->add(createTab()); |
971 | 266 | tabSpy.clear(); | 361 | |
972 | 267 | indexSpy.clear(); | 362 | QSignalSpy spytab(model, SIGNAL(currentTabChanged())); |
973 | 363 | QSignalSpy spyindex(model, SIGNAL(currentIndexChanged())); | ||
974 | 268 | delete model->remove(1); | 364 | delete model->remove(1); |
975 | 269 | QCOMPARE(model->currentTab(), tab1); | 365 | QCOMPARE(model->currentTab(), tab1); |
978 | 270 | QVERIFY(tabSpy.isEmpty()); | 366 | QVERIFY(spytab.isEmpty()); |
979 | 271 | QVERIFY(indexSpy.isEmpty()); | 367 | QVERIFY(spyindex.isEmpty()); |
980 | 368 | } | ||
981 | 272 | 369 | ||
984 | 273 | // When removing the current tab, if there is a tab after it, it | 370 | void shouldUpdateIndexWhenRemovingCurrent() |
985 | 274 | // becomes the current one, and thus the current index doesn’t change. | 371 | { |
986 | 372 | // When removing the current tab, if there is a tab after it, | ||
987 | 373 | // it becomes the current one. | ||
988 | 374 | QQuickItem* tab1 = createTab(); | ||
989 | 275 | QQuickItem* tab2 = createTab(); | 375 | QQuickItem* tab2 = createTab(); |
990 | 276 | model->add(tab2); | ||
991 | 277 | tabSpy.clear(); | ||
992 | 278 | indexSpy.clear(); | ||
993 | 279 | delete model->remove(0); | ||
994 | 280 | QCOMPARE(tabSpy.count(), 1); | ||
995 | 281 | QVERIFY(indexSpy.isEmpty()); | ||
996 | 282 | QCOMPARE(model->currentTab(), tab2); | ||
997 | 283 | |||
998 | 284 | // When removing a tab before the current one, the current | ||
999 | 285 | // tab doesn’t change but the current index is updated. | ||
1000 | 286 | QQuickItem* tab3 = createTab(); | 376 | QQuickItem* tab3 = createTab(); |
1001 | 377 | model->add(tab1); | ||
1002 | 378 | model->add(tab2); | ||
1003 | 287 | model->add(tab3); | 379 | model->add(tab3); |
1004 | 288 | model->setCurrentIndex(1); | 380 | model->setCurrentIndex(1); |
1010 | 289 | tabSpy.clear(); | 381 | QCOMPARE(model->currentIndex(), 1); |
1011 | 290 | indexSpy.clear(); | 382 | QCOMPARE(model->currentTab(), tab2); |
1012 | 291 | delete model->remove(0); | 383 | |
1013 | 292 | QVERIFY(tabSpy.isEmpty()); | 384 | QSignalSpy spytab(model, SIGNAL(currentTabChanged())); |
1014 | 293 | QCOMPARE(indexSpy.count(), 1); | 385 | QSignalSpy spyindex(model, SIGNAL(currentIndexChanged())); |
1015 | 386 | delete model->remove(1); | ||
1016 | 387 | QCOMPARE(spyindex.count(), 0); | ||
1017 | 388 | QCOMPARE(spytab.count(), 1); | ||
1018 | 389 | QCOMPARE(model->currentTab(), tab3); | ||
1019 | 390 | |||
1020 | 391 | // If there is no tab after it but one before, that one becomes current | ||
1021 | 392 | delete model->remove(1); | ||
1022 | 393 | QCOMPARE(spyindex.count(), 1); | ||
1023 | 394 | QCOMPARE(spytab.count(), 2); | ||
1024 | 294 | QCOMPARE(model->currentIndex(), 0); | 395 | QCOMPARE(model->currentIndex(), 0); |
1035 | 295 | 396 | QCOMPARE(model->currentTab(), tab1); | |
1036 | 296 | // When removing the current tab, if it was the last one, the | 397 | } |
1037 | 297 | // current tab should be reset to 0. | 398 | |
1038 | 298 | tabSpy.clear(); | 399 | void shouldDecreaseIndexWhenRemovingBeforeCurrent() |
1039 | 299 | indexSpy.clear(); | 400 | { |
1040 | 300 | delete model->remove(0); | 401 | // When removing a tab before the current tab, the current index |
1041 | 301 | QCOMPARE(tabSpy.count(), 1); | 402 | // should decrease to match. |
1042 | 302 | QCOMPARE(indexSpy.count(), 1); | 403 | model->add(createTab()); |
1043 | 303 | QCOMPARE(model->currentTab(), (QObject*) nullptr); | 404 | model->add(createTab()); |
1044 | 304 | QCOMPARE(model->currentIndex(), -1); | 405 | QQuickItem* tab = createTab(); |
1045 | 406 | model->add(tab); | ||
1046 | 407 | model->setCurrentIndex(2); | ||
1047 | 408 | |||
1048 | 409 | QSignalSpy spytab(model, SIGNAL(currentTabChanged())); | ||
1049 | 410 | QSignalSpy spyindex(model, SIGNAL(currentIndexChanged())); | ||
1050 | 411 | delete model->remove(1); | ||
1051 | 412 | QVERIFY(spytab.isEmpty()); | ||
1052 | 413 | QCOMPARE(spyindex.count(), 1); | ||
1053 | 414 | QCOMPARE(model->currentIndex(), 1); | ||
1054 | 415 | QCOMPARE(model->currentTab(), tab); | ||
1055 | 305 | } | 416 | } |
1056 | 306 | 417 | ||
1057 | 307 | void shouldReturnData() | 418 | void shouldReturnData() |
Note to the reviewer: this is based on and supersedes a previous proposal by Ugo, see initial review at https:/ /code.launchpad .net/~uriboni/ webbrowser- app/tab- context- menu/+merge/ 268220.