Merge lp:~uriboni/webbrowser-app/qml-tabs-model into lp:webbrowser-app

Proposed by Ugo Riboni
Status: Needs review
Proposed branch: lp:~uriboni/webbrowser-app/qml-tabs-model
Merge into: lp:webbrowser-app
Diff against target: 1675 lines (+581/-877)
14 files modified
src/app/webbrowser/Browser.qml (+7/-11)
src/app/webbrowser/CMakeLists.txt (+0/-1)
src/app/webbrowser/TabsBar.qml (+7/-6)
src/app/webbrowser/TabsList.qml (+6/-4)
src/app/webbrowser/TabsModel.qml (+137/-0)
src/app/webbrowser/tabs-model.cpp (+0/-254)
src/app/webbrowser/tabs-model.h (+0/-84)
src/app/webbrowser/webbrowser-app.cpp (+0/-2)
tests/unittests/CMakeLists.txt (+0/-1)
tests/unittests/qml/CMakeLists.txt (+0/-1)
tests/unittests/qml/tst_QmlTests.cpp (+0/-2)
tests/unittests/qml/tst_TabsModel.qml (+424/-0)
tests/unittests/tabs-model/CMakeLists.txt (+0/-18)
tests/unittests/tabs-model/tst_TabsModelTests.cpp (+0/-493)
To merge this branch: bzr merge lp:~uriboni/webbrowser-app/qml-tabs-model
Reviewer Review Type Date Requested Status
system-apps-ci-bot continuous-integration Needs Fixing
PS Jenkins bot continuous-integration Approve
Olivier Tilloy Needs Fixing
Review via email: mp+270408@code.launchpad.net

Commit message

Reimplement the tabs model in QML.

Description of the change

Reimplement the tabs model in QML.

The behaviour is not 100% the same as the previous model, due to differences in what can be done in QML and C++, but the application can be easily adapted to cope (which this MR does too), with no changes in app functionality.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
1176. By Ugo Riboni

Merge changes from trunk

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: Needs Fixing (continuous-integration)
1177. By Ugo Riboni

Partial merge from trunk

1178. By Ugo Riboni

Partial merge from trunk

1179. By Ugo Riboni

Partial merge from trunk

1180. By Ugo Riboni

Partial merge from trunk

1181. By Ugo Riboni

Partial merge from trunk

1182. By Ugo Riboni

Partial merge from trunk

1183. By Ugo Riboni

Partial merge from trunk

1184. By Ugo Riboni

Partial merge from trunk

1185. By Ugo Riboni

Partial merge from trunk

1186. By Ugo Riboni

Partial merge from trunk

1187. By Ugo Riboni

Merge all remaining changes from trunk

1188. By Ugo Riboni

Fix style issue

1189. By Ugo Riboni

Remove mistakenly added merge conflict file

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

Overall this looks good to me, getting rid of custom C++ code in favour of QML with (supposedly) no performance hit is always good, thanks for working on this.

I have a few minor comments:

56 - return tabComponent.createObject(tabContainer, properties)
57 + var tab = tabComponent.createObject(tabContainer, properties)
58 + return tab

that change is unnecessary, can it be reverted?

Is QTBUG-26357 the correct bug report for the lack of a way to override methods and call their base implementation? It doesn’t look like it from the description.

TabsModel could be a QtObject instead of an Item (you could move the updateCurrentTab() function to inside the ListModel).

There’s a couple of lines of JS code in TabsModel.qml that end with a semicolon (and one line in test_shouldReturnTabWhenRemoving), can you please remove them?

What is the point of allowing currentIndex to be invalid? Shouldn’t it be reset to a valid value?

Related to the above, what is the point of allowing the second parameter of insert() to be invalid, and have the model clamp it? Wouldn’t it be better to spit out an error if the index is not valid?

Because these tests don’t exercise user input, the width and height of the top-level item in tst_TabsModel.qml don’t need to be that large (they can be set to 1px).

Camel-casing on test_shouldUpdateCurrentTabWhenSettingcurrentIndex is not correct.

The first two compare() in test_shouldSetCurrentTabWhenAddingFirstTab and in test_shouldSetCurrentTabWhenInsertingFirstTab are useless, as this is already being tested by test_shouldBeInitiallyEmpty.

review: Needs Fixing
1190. By Ugo Riboni

Make TabsModel a QtObject. Fix the reference to the QML bug that prevents calling base class methods. Remove semicolons and a pointless change.

1191. By Ugo Riboni

No need for the window to be large as there is no input being tested

1192. By Ugo Riboni

Fix test name case

1193. By Ugo Riboni

Remove useless checks, as that code path is already tested elsewhere

1194. By Ugo Riboni

Do not allow inserting outside of valid boundaries

1195. By Ugo Riboni

Do not allow invalid current indexes, except for -1

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

There is one extraneous blank line in updateCurrentTab, can it be removed?

In insert(), if index is invalid, shouldn’t the fuction return early after printing out the warning? And in test_shouldNotInsertTabAtInvalidIndex, you should also verify that currentIndex and currentTab remain unchanged.

Should we really allow setting currentIndex to -1 when there’s at least one tab in the model? What real-world use case does this cover?

review: Needs Fixing
1196. By Ugo Riboni

Append when insert index is undefined, and return immediately when index is specified and out of bounds. Add unit test to verify the undefined index behavior.

1197. By Ugo Riboni

Prevent the currentIndex from being -1 unless the model is empty

1198. 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 :

webbrowser_app.tests.test_tabs.TestTabsFocus.test_focus_on_close is broken on desktop

> And in test_shouldNotInsertTabAtInvalidIndex, you should also verify
> that currentIndex and currentTab remain unchanged.

This hasn’t been done.

846 + // verify that when the model is not empty -1 is not a valid
847 + // current index anymore

s/anymore// : -1 never was a valid value for currentIndex, at least not in the current implementation in trunk

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

> webbrowser_app.tests.test_tabs.TestTabsFocus.test_focus_on_close is broken on
> desktop

It is really odd that this was working on jenkins, but not when we run it on our machines.
Fixed in any case

> > And in test_shouldNotInsertTabAtInvalidIndex, you should also verify
> > that currentIndex and currentTab remain unchanged.
>
> This hasn’t been done.

Fixed

> 846 + // verify that when the model is not empty -1 is not a valid
> 847 + // current index anymore
>
> s/anymore// : -1 never was a valid value for currentIndex, at least not in the
> current implementation in trunk

I mean that -1 stops being a valid index when the model is not empty. But removed "anymore" to avoid confusion anyway.

1199. By Ugo Riboni

Give back focus to the address bar if we close a tab and we land on a new tab view. This caused some AP tests to fail on desktops (but not in CI, oddly).

1200. By Ugo Riboni

Check some additional things in unit tests and clarify a comment

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

> It is really odd that this was working on jenkins, but not when we run
> it on our machines. Fixed in any case

As I explained in a comment on another merge request, CI didn’t complain because it doesn’t run autopilot tests on desktop, only on a phone.

> I mean that -1 stops being a valid index when the model is not empty.
> But removed "anymore" to avoid confusion anyway.

Right, I had misunderstood the comment. It was slightly confusing (or maybe it was just me who was confused). Thanks for clarifying!

Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :
review: Needs Fixing (continuous-integration)

Unmerged revisions

1200. By Ugo Riboni

Check some additional things in unit tests and clarify a comment

1199. By Ugo Riboni

Give back focus to the address bar if we close a tab and we land on a new tab view. This caused some AP tests to fail on desktops (but not in CI, oddly).

1198. By Ugo Riboni

Merge changes from trunk

1197. By Ugo Riboni

Prevent the currentIndex from being -1 unless the model is empty

1196. By Ugo Riboni

Append when insert index is undefined, and return immediately when index is specified and out of bounds. Add unit test to verify the undefined index behavior.

1195. By Ugo Riboni

Do not allow invalid current indexes, except for -1

1194. By Ugo Riboni

Do not allow inserting outside of valid boundaries

1193. By Ugo Riboni

Remove useless checks, as that code path is already tested elsewhere

1192. By Ugo Riboni

Fix test name case

1191. By Ugo Riboni

No need for the window to be large as there is no input being tested

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-08 23:11:15 +0000
4@@ -62,14 +62,11 @@
5 target: tabsModel
6 onCurrentIndexChanged: {
7 // Remove focus from the address bar when the current tab
8- // changes to ensure that its contents are updated.
9+ // changes to ensure that its contents are updated...
10 tabContainer.forceActiveFocus()
11-
12- // In narrow mode, the tabslist is a stack:
13- // the current tab is always at the top.
14- if (!browser.wide) {
15- tabsModel.move(tabsModel.currentIndex, 0)
16- }
17+ // ...then give focus to the either the tab or the address bar
18+ // depending if we are on a new tab view or on a page
19+ internal.resetFocus()
20 }
21 }
22
23@@ -711,9 +708,6 @@
24 onWideChanged: {
25 if (wide) {
26 recentView.reset()
27- } else {
28- // In narrow mode, the tabslist is a stack: the current tab is always at the top.
29- tabsModel.move(tabsModel.currentIndex, 0)
30 }
31 }
32
33@@ -946,6 +940,7 @@
34
35 TabsModel {
36 id: publicTabsModel
37+ behaveAsStack: !browser.wide
38 }
39
40 Loader {
41@@ -957,6 +952,7 @@
42 id: privateTabsModelComponent
43
44 TabsModel {
45+ behaveAsStack: !browser.wide
46 Component.onDestruction: {
47 while (count > 0) {
48 var tab = remove(count - 1)
49@@ -1485,7 +1481,7 @@
50 internal.addTab(tab, i == 0)
51 }
52 }
53- if ('currentIndex' in state) {
54+ if ('currentIndex' in state && publicTabsModel.validIndex(state.currentIndex)) {
55 publicTabsModel.currentIndex = state.currentIndex
56 }
57 }
58
59=== modified file 'src/app/webbrowser/CMakeLists.txt'
60--- src/app/webbrowser/CMakeLists.txt 2015-10-18 19:21:48 +0000
61+++ src/app/webbrowser/CMakeLists.txt 2015-11-08 23:11:15 +0000
62@@ -21,7 +21,6 @@
63 history-model.cpp
64 history-timeframe-model.cpp
65 limit-proxy-model.cpp
66- tabs-model.cpp
67 text-search-filter-model.cpp
68 top-sites-model.cpp
69 )
70
71=== modified file 'src/app/webbrowser/TabsBar.qml'
72--- src/app/webbrowser/TabsBar.qml 2015-09-24 14:07:47 +0000
73+++ src/app/webbrowser/TabsBar.qml 2015-11-08 23:11:15 +0000
74@@ -24,7 +24,7 @@
75 Item {
76 id: root
77
78- property alias model: repeater.model
79+ property var model
80
81 property real minTabWidth: 0 //units.gu(6)
82 property real maxTabWidth: units.gu(20)
83@@ -88,7 +88,7 @@
84 Action {
85 objectName: "tab_action_reload"
86 text: i18n.tr("Reload")
87- enabled: menu.tab.url.toString().length > 0
88+ enabled: menu.tab && menu.tab.url.toString().length > 0
89 onTriggered: menu.tab.reload()
90 }
91 Action {
92@@ -118,6 +118,7 @@
93
94 property bool reordering: false
95
96+ model: root.model.listModel
97 delegate: MouseArea {
98 id: tabDelegate
99 objectName: "tabDelegate"
100@@ -145,8 +146,8 @@
101 active: tabIndex === root.model.currentIndex
102 hoverable: true
103 incognito: root.incognito
104- title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab"))
105- icon: model.icon
106+ title: model.tab.title ? model.tab.title : (model.tab.url.toString() ? model.tab.url : i18n.tr("New tab"))
107+ icon: model.tab.icon
108
109 rightMargin: tabDelegate.rightMargin
110
111@@ -171,9 +172,9 @@
112 onXChanged: {
113 if (!dragging) return
114 if (x < (index * width - width / 2)) {
115- root.model.move(index, index - 1)
116+ root.model.move(index, index - 1, 1)
117 } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) {
118- root.model.move(index + 1, index)
119+ root.model.move(index + 1, index, 1)
120 }
121 }
122
123
124=== modified file 'src/app/webbrowser/TabsList.qml'
125--- src/app/webbrowser/TabsList.qml 2015-09-04 17:48:37 +0000
126+++ src/app/webbrowser/TabsList.qml 2015-11-08 23:11:15 +0000
127@@ -24,7 +24,7 @@
128
129 property real delegateHeight
130 property real chromeOffset
131- property alias model: repeater.model
132+ property var model
133 readonly property int count: repeater.count
134 property bool incognito
135
136@@ -62,6 +62,7 @@
137 Repeater {
138 id: repeater
139
140+ model: tabslist.model.listModel
141 delegate: Loader {
142 id: delegate
143
144@@ -91,8 +92,8 @@
145 }
146 }
147
148- readonly property string title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab"))
149- readonly property string icon: model.icon
150+ readonly property string title: model.tab.title ? model.tab.title : (model.tab.url.toString() ? model.tab.url : i18n.tr("New tab"))
151+ readonly property string icon: model.tab.icon
152
153 readonly property bool needsInstance: (index >= 0) && ((flickable.contentY + flickable.height + delegateHeight / 2) >= (index * delegateHeight))
154 sourceComponent: needsInstance ? tabPreviewComponent : undefined
155@@ -103,13 +104,14 @@
156 id: tabPreviewComponent
157
158 TabPreview {
159+ id: preview
160 title: delegate.title
161 icon: delegate.icon
162 incognito: tabslist.incognito
163 tab: model.tab
164 chromeHeight: firstItemChromeBackground.height
165 showContent: (index > 0) || (delegate.y > flickable.contentY) ||
166- !(tab.webview && tab.webview.visible)
167+ !(preview.tab.webview && preview.tab.webview.visible)
168
169 onSelected: tabslist.selectAndAnimateTab(index)
170 onClosed: tabslist.tabClosed(index)
171
172=== added file 'src/app/webbrowser/TabsModel.qml'
173--- src/app/webbrowser/TabsModel.qml 1970-01-01 00:00:00 +0000
174+++ src/app/webbrowser/TabsModel.qml 2015-11-08 23:11:15 +0000
175@@ -0,0 +1,137 @@
176+/*
177+ * Copyright 2015 Canonical Ltd.
178+ *
179+ * This file is part of webbrowser-app.
180+ *
181+ * webbrowser-app is free software; you can redistribute it and/or modify
182+ * it under the terms of the GNU General Public License as published by
183+ * the Free Software Foundation; version 3.
184+ *
185+ * webbrowser-app is distributed in the hope that it will be useful,
186+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
187+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
188+ * GNU General Public License for more details.
189+ *
190+ * You should have received a copy of the GNU General Public License
191+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
192+ */
193+
194+import QtQuick 2.4
195+
196+QtObject {
197+ property int currentIndex: -1
198+ property var currentTab: null
199+ property bool behaveAsStack: false
200+ property alias count: tabs.count
201+
202+ // Expose the ListModel through a property instead of inheriting directly
203+ // from it due to a bug in QML that does not allow calling base class
204+ // methods: https://bugreports.qt.io/browse/QTBUG-25942
205+ // This allows us to create our own methods that implement the required
206+ // semantics (such as correctly tracking and updating currentIndex and
207+ // currentTab) at the price of the minor inconvenience of having to access
208+ // TabsModel.listModel when wanting to use it as model for a view.
209+ property ListModel listModel: ListModel {
210+ id: tabs
211+
212+ function updateCurrentTab() {
213+ var newCurrentTab = null
214+ if (validIndex(currentIndex)) {
215+ if (behaveAsStack) {
216+ tabs.move(currentIndex, 0, 1)
217+ currentIndex = 0
218+ }
219+ newCurrentTab = tabs.get(currentIndex).tab
220+ }
221+ if (newCurrentTab != currentTab) {
222+ currentTab = newCurrentTab
223+ }
224+ }
225+ }
226+
227+ onCurrentIndexChanged: {
228+ // Clamp the model index to valid values. -1 is a valid value only
229+ // if the model is empty.
230+ currentIndex = Math.max((tabs.count === 0) ? -1 : 0,
231+ Math.min(currentIndex, tabs.count - 1))
232+ tabs.updateCurrentTab()
233+ }
234+ onBehaveAsStackChanged: if (behaveAsStack) tabs.updateCurrentTab()
235+
236+ function validIndex(index) { return index >= 0 && index < tabs.count }
237+
238+ function add(tab) { return insert(tab, tabs.count) }
239+
240+ function insert(tab, index) {
241+ if (tab) {
242+ if (index === undefined) index = tabs.count
243+ else if (index < 0 || index > tabs.count) {
244+ console.warn("Invalid insert index:", index)
245+ return
246+ }
247+ tabs.insert(index, {tab: tab})
248+ if (currentIndex == -1) {
249+ // if the current index was -1 then the model must have been
250+ // empty, and the first item added to an empty model always
251+ // becomes the new current item
252+ currentIndex = 0
253+ tabs.updateCurrentTab()
254+ } else if (currentIndex == tabs.count - 1) {
255+ // index was set out of range, but adding the new item made it
256+ // valid, so we need to update the current item
257+ tabs.updateCurrentTab()
258+ } else if (index <= currentIndex) {
259+ if (validIndex(currentIndex)) {
260+ // Increment the index if we are inserting items before the
261+ // current index, and the current index has not been set to
262+ // a position past the end of the list.
263+ currentIndex++
264+ }
265+ }
266+ return index
267+ } else {
268+ console.warn("Invalid Tab:", tab)
269+ return -1
270+ }
271+ }
272+
273+ function remove(index) {
274+ if (!validIndex(index)) return null
275+ var tab = tabs.get(index).tab
276+ tabs.remove(index)
277+
278+ if (index < currentIndex) {
279+ // if we removed the current index and it is the last item OR
280+ // if we removed any item before the current index,
281+ // then decrement the current index
282+ --currentIndex
283+ } else if (index === currentIndex) {
284+ // If the current tab was removed, the following one (if any) is made
285+ // current. If it was the last tab in the model, the current index needs
286+ // to be decreased.
287+ if (index === tabs.count) --currentIndex
288+ else tabs.updateCurrentTab()
289+ }
290+
291+ return tab
292+ }
293+
294+ function get(index) {
295+ return validIndex(index) ? tabs.get(index).tab : null
296+ }
297+
298+ function move(from, to)
299+ {
300+ if (from == to || !validIndex(from) || !validIndex(to)) {
301+ return
302+ }
303+ tabs.move(from, to, 1)
304+ if (currentIndex == from) {
305+ currentIndex = to
306+ } else if (currentIndex >= to && currentIndex < from) {
307+ currentIndex++
308+ } else if (currentIndex > from && currentIndex <= to) {
309+ currentIndex--
310+ }
311+ }
312+}
313
314=== removed file 'src/app/webbrowser/tabs-model.cpp'
315--- src/app/webbrowser/tabs-model.cpp 2015-09-23 16:50:21 +0000
316+++ src/app/webbrowser/tabs-model.cpp 1970-01-01 00:00:00 +0000
317@@ -1,254 +0,0 @@
318-/*
319- * Copyright 2013-2015 Canonical Ltd.
320- *
321- * This file is part of webbrowser-app.
322- *
323- * webbrowser-app is free software; you can redistribute it and/or modify
324- * it under the terms of the GNU General Public License as published by
325- * the Free Software Foundation; version 3.
326- *
327- * webbrowser-app is distributed in the hope that it will be useful,
328- * but WITHOUT ANY WARRANTY; without even the implied warranty of
329- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
330- * GNU General Public License for more details.
331- *
332- * You should have received a copy of the GNU General Public License
333- * along with this program. If not, see <http://www.gnu.org/licenses/>.
334- */
335-
336-#include "tabs-model.h"
337-
338-// Qt
339-#include <QtCore/QDebug>
340-#include <QtCore/QObject>
341-#include <QtCore/QtGlobal>
342-
343-/*!
344- \class TabsModel
345- \brief List model that stores the list of currently open tabs.
346-
347- TabsModel is a list model that stores the list of currently open tabs.
348- Each tab holds a pointer to a Tab and associated metadata (URL, title,
349- icon).
350-
351- The model doesn’t own the Tab, so it is the responsibility of whoever
352- adds a tab to instantiate the corresponding Tab, and to destroy it after
353- it’s removed from the model.
354-*/
355-TabsModel::TabsModel(QObject* parent)
356- : QAbstractListModel(parent)
357- , m_currentIndex(-1)
358-{
359-}
360-
361-TabsModel::~TabsModel()
362-{
363-}
364-
365-QHash<int, QByteArray> TabsModel::roleNames() const
366-{
367- static QHash<int, QByteArray> roles;
368- if (roles.isEmpty()) {
369- roles[Url] = "url";
370- roles[Title] = "title";
371- roles[Icon] = "icon";
372- roles[Tab] = "tab";
373- }
374- return roles;
375-}
376-
377-int TabsModel::rowCount(const QModelIndex& parent) const
378-{
379- Q_UNUSED(parent);
380- return m_tabs.count();
381-}
382-
383-QVariant TabsModel::data(const QModelIndex& index, int role) const
384-{
385- if (!index.isValid()) {
386- return QVariant();
387- }
388- QObject* tab = m_tabs.at(index.row());
389- switch (role) {
390- case Url:
391- return tab->property("url");
392- case Title:
393- return tab->property("title");
394- case Icon:
395- return tab->property("icon");
396- case Tab:
397- return QVariant::fromValue(tab);
398- default:
399- return QVariant();
400- }
401-}
402-
403-int TabsModel::currentIndex() const
404-{
405- return m_currentIndex;
406-}
407-
408-void TabsModel::setCurrentIndex(int index)
409-{
410- if (!checkValidTabIndex(index)) {
411- return;
412- }
413- if (index != m_currentIndex) {
414- m_currentIndex = index;
415- Q_EMIT currentIndexChanged();
416- Q_EMIT currentTabChanged();
417- }
418-}
419-
420-QObject* TabsModel::currentTab() const
421-{
422- if (m_tabs.isEmpty() || !checkValidTabIndex(m_currentIndex)) {
423- return nullptr;
424- }
425- return m_tabs.at(m_currentIndex);
426-}
427-
428-/*!
429- Append a tab to the model and return the corresponding index in the model.
430-
431- It is the responsibility of the caller to instantiate the corresponding
432- Tab beforehand.
433-*/
434-int TabsModel::add(QObject* tab)
435-{
436- return insert(tab, m_tabs.count());
437-}
438-
439-/*!
440- Add a tab to the model at the specified index, and return the index itself,
441- or -1 if the operation failed.
442-
443- It is the responsibility of the caller to instantiate the corresponding
444- Tab beforehand.
445-*/
446-int TabsModel::insert(QObject* tab, int index)
447-{
448- if (tab == nullptr) {
449- qWarning() << "Invalid Tab";
450- return -1;
451- }
452- index = qMax(qMin(index, m_tabs.count()), 0);
453- beginInsertRows(QModelIndex(), index, index);
454- m_tabs.insert(index, tab);
455- connect(tab, SIGNAL(urlChanged()), SLOT(onUrlChanged()));
456- connect(tab, SIGNAL(titleChanged()), SLOT(onTitleChanged()));
457- connect(tab, SIGNAL(iconChanged()), SLOT(onIconChanged()));
458- endInsertRows();
459- Q_EMIT countChanged();
460-
461- if (m_currentIndex == -1) {
462- // Set the index to zero if this is the first item that gets added to the
463- // model, as it should not be possible to have items in the model but no
464- // current tab.
465- m_currentIndex = 0;
466- Q_EMIT currentIndexChanged();
467- Q_EMIT currentTabChanged();
468- } else if (index <= m_currentIndex) {
469- // Increment the index if we are inserting items before the current index.
470- m_currentIndex++;
471- Q_EMIT currentIndexChanged();
472- }
473-
474- return index;
475-}
476-
477-/*!
478- Given its index, remove a tab from the model, and return the corresponding
479- Tab.
480-
481- It is the responsibility of the caller to destroy the corresponding
482- Tab afterwards.
483-*/
484-QObject* TabsModel::remove(int index)
485-{
486- if (!checkValidTabIndex(index)) {
487- return nullptr;
488- }
489- beginRemoveRows(QModelIndex(), index, index);
490- QObject* tab = m_tabs.takeAt(index);
491- tab->disconnect(this);
492- endRemoveRows();
493- Q_EMIT countChanged();
494-
495- if (index < m_currentIndex) {
496- // If we removed any tab before the current one, decrease the
497- // current index to match.
498- m_currentIndex--;
499- Q_EMIT currentIndexChanged();
500- } else if (index == m_currentIndex) {
501- // If the current tab was removed, the following one (if any) is made
502- // current. If it was the last tab in the model, the current index needs
503- // to be decreased.
504- if (m_currentIndex == m_tabs.count()) {
505- m_currentIndex--;
506- Q_EMIT currentIndexChanged();
507- }
508- Q_EMIT currentTabChanged();
509- }
510- return tab;
511-}
512-
513-QObject* TabsModel::get(int index) const
514-{
515- if (!checkValidTabIndex(index)) {
516- return nullptr;
517- }
518- return m_tabs.at(index);
519-}
520-
521-void TabsModel::move(int from, int to)
522-{
523- if ((from == to) || !checkValidTabIndex(from) || !checkValidTabIndex(to)) {
524- return;
525- }
526- beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
527- m_tabs.move(from, to);
528- endMoveRows();
529- if (m_currentIndex == from) {
530- m_currentIndex = to;
531- Q_EMIT currentIndexChanged();
532- } else if ((m_currentIndex >= to) && (m_currentIndex < from)) {
533- m_currentIndex++;
534- Q_EMIT currentIndexChanged();
535- } else if ((m_currentIndex > from) && (m_currentIndex <= to)) {
536- m_currentIndex--;
537- Q_EMIT currentIndexChanged();
538- }
539-}
540-
541-bool TabsModel::checkValidTabIndex(int index) const
542-{
543- if ((index < 0) || (index >= m_tabs.count())) {
544- qWarning() << "Invalid tab index:" << index;
545- return false;
546- }
547- return true;
548-}
549-
550-void TabsModel::onDataChanged(QObject* tab, int role)
551-{
552- int index = m_tabs.indexOf(tab);
553- if (checkValidTabIndex(index)) {
554- Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << role);
555- }
556-}
557-
558-void TabsModel::onUrlChanged()
559-{
560- onDataChanged(sender(), Url);
561-}
562-
563-void TabsModel::onTitleChanged()
564-{
565- onDataChanged(sender(), Title);
566-}
567-
568-void TabsModel::onIconChanged()
569-{
570- onDataChanged(sender(), Icon);
571-}
572
573=== removed file 'src/app/webbrowser/tabs-model.h'
574--- src/app/webbrowser/tabs-model.h 2015-09-02 16:37:20 +0000
575+++ src/app/webbrowser/tabs-model.h 1970-01-01 00:00:00 +0000
576@@ -1,84 +0,0 @@
577-/*
578- * Copyright 2013-2015 Canonical Ltd.
579- *
580- * This file is part of webbrowser-app.
581- *
582- * webbrowser-app is free software; you can redistribute it and/or modify
583- * it under the terms of the GNU General Public License as published by
584- * the Free Software Foundation; version 3.
585- *
586- * webbrowser-app is distributed in the hope that it will be useful,
587- * but WITHOUT ANY WARRANTY; without even the implied warranty of
588- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
589- * GNU General Public License for more details.
590- *
591- * You should have received a copy of the GNU General Public License
592- * along with this program. If not, see <http://www.gnu.org/licenses/>.
593- */
594-
595-#ifndef __TABS_MODEL_H__
596-#define __TABS_MODEL_H__
597-
598-// Qt
599-#include <QtCore/QAbstractListModel>
600-#include <QtCore/QList>
601-
602-class QObject;
603-
604-class TabsModel : public QAbstractListModel
605-{
606- Q_OBJECT
607-
608- Q_ENUMS(Roles)
609-
610- Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
611- Q_PROPERTY(QObject* currentTab READ currentTab NOTIFY currentTabChanged)
612- Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
613-
614-public:
615- TabsModel(QObject* parent=0);
616- ~TabsModel();
617-
618- enum Roles {
619- Url = Qt::UserRole + 1,
620- Title,
621- Icon,
622- Tab
623- };
624-
625- // reimplemented from QAbstractListModel
626- QHash<int, QByteArray> roleNames() const;
627- int rowCount(const QModelIndex& parent=QModelIndex()) const;
628- QVariant data(const QModelIndex& index, int role) const;
629-
630- int currentIndex() const;
631- void setCurrentIndex(int index);
632-
633- QObject* currentTab() const;
634-
635- Q_INVOKABLE int add(QObject* tab);
636- Q_INVOKABLE int insert(QObject* tab, int index);
637- Q_INVOKABLE QObject* remove(int index);
638- Q_INVOKABLE QObject* get(int index) const;
639- Q_INVOKABLE void move(int from, int to);
640-
641-Q_SIGNALS:
642- void currentIndexChanged() const;
643- void currentTabChanged() const;
644- void countChanged() const;
645-
646-private Q_SLOTS:
647- void onUrlChanged();
648- void onTitleChanged();
649- void onIconChanged();
650-
651-private:
652- QList<QObject*> m_tabs;
653- int m_currentIndex;
654-
655- bool checkValidTabIndex(int index) const;
656- void setCurrentIndexNoCheck(int index);
657- void onDataChanged(QObject* tab, int role);
658-};
659-
660-#endif // __TABS_MODEL_H__
661
662=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
663--- src/app/webbrowser/webbrowser-app.cpp 2015-10-22 15:07:26 +0000
664+++ src/app/webbrowser/webbrowser-app.cpp 2015-11-08 23:11:15 +0000
665@@ -30,7 +30,6 @@
666 #include "limit-proxy-model.h"
667 #include "searchengine.h"
668 #include "text-search-filter-model.h"
669-#include "tabs-model.h"
670 #include "top-sites-model.h"
671 #include "webbrowser-app.h"
672
673@@ -89,7 +88,6 @@
674 qmlRegisterType<HistoryLastVisitDateListModel>(uri, 0, 1, "HistoryLastVisitDateListModel");
675 qmlRegisterType<HistoryLastVisitDateModel>(uri, 0, 1, "HistoryLastVisitDateModel");
676 qmlRegisterType<LimitProxyModel>(uri, 0 , 1, "LimitProxyModel");
677- qmlRegisterType<TabsModel>(uri, 0, 1, "TabsModel");
678 qmlRegisterSingletonType<BookmarksModel>(uri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory);
679 qmlRegisterType<BookmarksFolderListModel>(uri, 0, 1, "BookmarksFolderListModel");
680 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
681
682=== modified file 'tests/unittests/CMakeLists.txt'
683--- tests/unittests/CMakeLists.txt 2015-10-06 09:48:38 +0000
684+++ tests/unittests/CMakeLists.txt 2015-11-08 23:11:15 +0000
685@@ -10,7 +10,6 @@
686 add_subdirectory(history-lastvisitdatelist-model)
687 add_subdirectory(top-sites-model)
688 add_subdirectory(session-utils)
689-add_subdirectory(tabs-model)
690 add_subdirectory(bookmarks-model)
691 add_subdirectory(bookmarks-folder-model)
692 add_subdirectory(bookmarks-folderlist-model)
693
694=== modified file 'tests/unittests/qml/CMakeLists.txt'
695--- tests/unittests/qml/CMakeLists.txt 2015-10-06 09:48:38 +0000
696+++ tests/unittests/qml/CMakeLists.txt 2015-11-08 23:11:15 +0000
697@@ -27,7 +27,6 @@
698 ${webbrowser-app_SOURCE_DIR}/history-timeframe-model.cpp
699 ${webbrowser-app_SOURCE_DIR}/limit-proxy-model.cpp
700 ${webbrowser-app_SOURCE_DIR}/searchengine.cpp
701- ${webbrowser-app_SOURCE_DIR}/tabs-model.cpp
702 ${webbrowser-app_SOURCE_DIR}/text-search-filter-model.cpp
703 ${webbrowser-app_SOURCE_DIR}/top-sites-model.cpp
704 tst_QmlTests.cpp
705
706=== modified file 'tests/unittests/qml/tst_QmlTests.cpp'
707--- tests/unittests/qml/tst_QmlTests.cpp 2015-10-22 15:07:26 +0000
708+++ tests/unittests/qml/tst_QmlTests.cpp 2015-11-08 23:11:15 +0000
709@@ -35,7 +35,6 @@
710 #include "history-lastvisitdatelist-model.h"
711 #include "limit-proxy-model.h"
712 #include "searchengine.h"
713-#include "tabs-model.h"
714 #include "text-search-filter-model.h"
715 #include "top-sites-model.h"
716
717@@ -189,7 +188,6 @@
718
719 const char* browserUri = "webbrowserapp.private";
720 qmlRegisterType<SearchEngine>(browserUri, 0, 1, "SearchEngine");
721- qmlRegisterType<TabsModel>(browserUri, 0, 1, "TabsModel");
722 qmlRegisterSingletonType<BookmarksModel>(browserUri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory);
723 qmlRegisterType<BookmarksFolderListModel>(browserUri, 0, 1, "BookmarksFolderListModel");
724 qmlRegisterSingletonType<HistoryModel>(browserUri, 0, 1, "HistoryModel", HistoryModel_singleton_factory);
725
726=== added file 'tests/unittests/qml/tst_TabsModel.qml'
727--- tests/unittests/qml/tst_TabsModel.qml 1970-01-01 00:00:00 +0000
728+++ tests/unittests/qml/tst_TabsModel.qml 2015-11-08 23:11:15 +0000
729@@ -0,0 +1,424 @@
730+/*
731+ * Copyright 2015 Canonical Ltd.
732+ *
733+ * This file is part of webbrowser-app.
734+ *
735+ * webbrowser-app is free software; you can redistribute it and/or modify
736+ * it under the terms of the GNU General Public License as published by
737+ * the Free Software Foundation; version 3.
738+ *
739+ * webbrowser-app is distributed in the hope that it will be useful,
740+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
741+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
742+ * GNU General Public License for more details.
743+ *
744+ * You should have received a copy of the GNU General Public License
745+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
746+ */
747+
748+import QtQuick 2.4
749+import QtTest 1.0
750+import "../../../src/app/webbrowser"
751+
752+Item {
753+ width: 1
754+ height: 1
755+
756+ property alias model: modelLoader.item
757+
758+ Loader {
759+ id: modelLoader
760+ active: false
761+ asynchronous: false
762+ sourceComponent: TabsModel { }
763+ }
764+
765+ Component {
766+ id: tab
767+ Item {
768+ property url url
769+ property string title
770+ property url icon
771+ }
772+ }
773+
774+ SignalSpy {
775+ id: countSpy
776+ target: model
777+ signalName: "countChanged"
778+ }
779+
780+ SignalSpy {
781+ id: currentIndexSpy
782+ target: model
783+ signalName: "currentIndexChanged"
784+ }
785+
786+ SignalSpy {
787+ id: currentTabSpy
788+ target: model
789+ signalName: "currentTabChanged"
790+ }
791+
792+ TestCase {
793+ name: "TabsModel"
794+ when: windowShown
795+
796+ function createTab()
797+ {
798+ return tab.createObject()
799+ }
800+
801+ function init()
802+ {
803+ modelLoader.active = true
804+ }
805+
806+ function cleanup()
807+ {
808+ modelLoader.active = false
809+ countSpy.clear()
810+ currentIndexSpy.clear()
811+ currentTabSpy.clear()
812+ }
813+
814+ function createTabWithTitle(title) {
815+ var tab = createTab()
816+ tab.title = title
817+ return tab
818+ }
819+
820+ function verifyTabsOrder(orderedTitles) {
821+ compare(model.count, orderedTitles.length)
822+ orderedTitles.forEach(function(title, i) {
823+ compare(model.get(i).title, title)
824+ })
825+ }
826+
827+ function test_shouldBeInitiallyEmpty()
828+ {
829+ compare(model.count, 0)
830+ compare(model.currentIndex, -1)
831+ compare(model.currentTab, null)
832+ }
833+
834+ function test_currentTabShouldBeNullOnInvalidIndex()
835+ {
836+ model.add(createTab())
837+ model.remove(0)
838+ compare(model.currentIndex, -1)
839+ compare(model.currentTab, null)
840+ }
841+
842+ function test_shouldNotAllowOutOfBoundsCurrentIndex()
843+ {
844+ model.currentIndex = 0;
845+ compare(currentIndexSpy.count, 2)
846+ compare(model.currentIndex, -1)
847+
848+ model.currentIndex = -2;
849+ compare(currentIndexSpy.count, 4)
850+ compare(model.currentIndex, -1)
851+
852+ // verify that when the model is not empty -1 is not a valid
853+ // current index
854+ model.add(createTab())
855+ compare(model.currentIndex, 0)
856+ currentIndexSpy.clear()
857+ model.currentIndex = -1;
858+ compare(currentIndexSpy.count, 2)
859+ compare(model.currentIndex, 0)
860+ }
861+
862+ function test_shouldNotAddNullTab()
863+ {
864+ ignoreWarning("Invalid Tab: null")
865+ compare(model.add(null), -1)
866+ compare(model.count, 0)
867+ }
868+
869+ function test_shouldNotInsertTabAtInvalidIndex()
870+ {
871+ ignoreWarning("Invalid insert index: " + -1)
872+ model.insert(createTab(), -1)
873+ compare(model.count, 0)
874+ compare(model.currentIndex, -1)
875+ compare(model.currentTab, null)
876+
877+ ignoreWarning("Invalid insert index: " + 1)
878+ model.insert(createTab(), 1)
879+ compare(model.count, 0)
880+ compare(model.currentIndex, -1)
881+ compare(model.currentTab, null)
882+ }
883+
884+ function test_shouldAppendWhenInsertingAtUndefinedIndex()
885+ {
886+ compare(model.insert(createTab()), 0)
887+ compare(model.count, 1)
888+
889+ compare(model.insert(createTab(), undefined), 1)
890+ compare(model.count, 2)
891+ }
892+
893+ function test_shouldReturnIndexWhenAddingTab()
894+ {
895+ for (var i = 0; i < 3; ++i) {
896+ compare(model.add(createTab()), i)
897+ }
898+ }
899+
900+ function test_shouldUpdateCountWhenAddingTab()
901+ {
902+ model.add(createTab())
903+ compare(countSpy.count, 1)
904+ compare(model.count, 1)
905+ }
906+
907+ function test_shouldUpdateCountWhenRemovingTab()
908+ {
909+ model.add(createTab())
910+ countSpy.clear()
911+ delete model.remove(0)
912+ compare(countSpy.count, 1)
913+ compare(model.count, 0)
914+ }
915+
916+ function test_shouldNotAllowRemovingAtInvalidIndex()
917+ {
918+ compare(model.remove(0), null)
919+ compare(model.remove(2), null)
920+ compare(model.remove(-2), null)
921+ }
922+
923+ function test_shouldReturnTabWhenRemoving()
924+ {
925+ var tab = createTab()
926+ model.add(tab)
927+ var removed = model.remove(0)
928+ compare(removed, tab)
929+ delete removed
930+ }
931+
932+ function test_shouldUpdateCurrentTabWhenSettingCurrentIndex()
933+ {
934+ var tab1 = createTab()
935+ model.add(tab1)
936+ compare(model.currentIndex, 0)
937+ compare(currentTabSpy.count, 1)
938+ compare(model.currentTab, tab1)
939+
940+ var tab2 = createTab()
941+ model.add(tab2)
942+ model.currentIndex = 1
943+ compare(model.currentIndex, 1)
944+ compare(currentTabSpy.count, 2)
945+ compare(model.currentTab, tab2)
946+ }
947+
948+ function test_shouldSetCurrentTabWhenAddingFirstTab()
949+ {
950+ // Adding a tab to an empty model should update the current tab
951+ // to that tab
952+ var tab1 = createTab()
953+ model.add(tab1)
954+
955+ compare(currentTabSpy.count, 1)
956+ compare(currentIndexSpy.count, 1)
957+ compare(model.currentIndex, 0)
958+ compare(model.currentTab, tab1)
959+
960+ // But adding further items should keep the index where it was
961+ model.add(createTab())
962+ model.add(createTab())
963+
964+ compare(currentTabSpy.count, 1)
965+ compare(currentIndexSpy.count, 1)
966+ compare(model.currentIndex, 0)
967+ compare(model.currentTab, tab1)
968+ }
969+
970+ function test_shouldSetCurrentTabWhenInsertingFirstTab()
971+ {
972+ // Inserting a tab to an empty model should update the current tab
973+ // to that tab
974+ var tab1 = createTab()
975+ model.insert(tab1, 0)
976+
977+ compare(currentTabSpy.count, 1)
978+ compare(currentIndexSpy.count, 1)
979+ compare(model.currentIndex, 0)
980+ compare(model.currentTab, tab1)
981+ }
982+
983+ function test_shouldSetInvalidIndexWhenRemovingLastTab()
984+ {
985+ // Removing the last item should also set the current index to -1
986+ // and the current tab to null
987+ model.add(createTab())
988+
989+ currentTabSpy.clear()
990+ currentIndexSpy.clear()
991+ model.remove(0)
992+ compare(currentTabSpy.count, 1)
993+ compare(currentIndexSpy.count, 1)
994+ compare(model.currentIndex, -1)
995+ compare(model.currentTab, null)
996+ }
997+
998+ function test_shouldNotChangeIndexWhenRemovingAfterCurrent()
999+ {
1000+ // When removing a tab after the current one,
1001+ // the current tab shouldn’t change.
1002+ var tab1 = createTab()
1003+ model.add(tab1)
1004+ model.add(createTab())
1005+
1006+ currentTabSpy.clear()
1007+ currentIndexSpy.clear()
1008+ model.remove(1)
1009+ compare(model.currentTab, tab1)
1010+ compare(currentTabSpy.count, 0)
1011+ compare(currentIndexSpy.count, 0)
1012+ }
1013+
1014+ function test_shouldUpdateIndexWhenRemovingCurrent()
1015+ {
1016+ // When removing the current tab, if there is a tab after it,
1017+ // it becomes the current one.
1018+ var tab1 = createTab()
1019+ var tab2 = createTab()
1020+ var tab3 = createTab()
1021+ model.add(tab1)
1022+ model.add(tab2)
1023+ model.add(tab3)
1024+ model.currentIndex = 1
1025+ compare(model.currentIndex, 1)
1026+ compare(model.currentTab, tab2)
1027+
1028+ currentTabSpy.clear()
1029+ currentIndexSpy.clear()
1030+ model.remove(1)
1031+ compare(currentIndexSpy.count, 0)
1032+ compare(currentTabSpy.count, 1)
1033+ compare(model.currentTab, tab3)
1034+
1035+ // If there is no tab after it but one before, that one becomes current
1036+ model.remove(1)
1037+ compare(currentIndexSpy.count, 1)
1038+ compare(currentTabSpy.count, 2)
1039+ compare(model.currentIndex, 0)
1040+ compare(model.currentTab, tab1)
1041+ }
1042+
1043+ function test_shouldDecreaseIndexWhenRemovingBeforeCurrent()
1044+ {
1045+ // When removing a tab before the current tab, the current index
1046+ // should decrease to match.
1047+ model.add(createTab())
1048+ model.add(createTab())
1049+ var tab = createTab()
1050+ model.add(tab)
1051+ model.currentIndex = 2
1052+
1053+ currentTabSpy.clear()
1054+ currentIndexSpy.clear()
1055+ model.remove(1)
1056+ compare(currentTabSpy.count, 0)
1057+ compare(currentIndexSpy.count, 1)
1058+ compare(model.currentIndex, 1)
1059+ compare(model.currentTab, tab)
1060+ }
1061+
1062+ function test_shouldReturnTabAtIndex()
1063+ {
1064+ // valid indexes
1065+ var tab1 = createTab()
1066+ model.add(tab1)
1067+ var tab2 = createTab()
1068+ model.add(tab2)
1069+ var tab3 = createTab()
1070+ model.add(tab3)
1071+ compare(model.get(0), tab1)
1072+ compare(model.get(1), tab2)
1073+ compare(model.get(2), tab3)
1074+
1075+ // invalid indexes
1076+ compare(model.get(-1), null)
1077+ compare(model.get(3), null)
1078+ }
1079+
1080+ function test_shouldPreserveCurrentIndexAndTabWhenMoving_data()
1081+ {
1082+ return [
1083+ {initial: 0, from: 0, to: 2, result: 2}, // moving tab that has the current index
1084+ {initial: 1, from: 0, to: 2, result: 0}, // moving from before current index to after current index
1085+ {initial: 1, from: 2, to: 0, result: 2} // moving from after current index to before current index
1086+ ]
1087+ }
1088+ function test_shouldPreserveCurrentIndexAndTabWhenMoving(data)
1089+ {
1090+ for (var i = 0; i < 3; ++i) {
1091+ model.add(createTab())
1092+ }
1093+ model.currentIndex = data.initial
1094+ var currentTabInitial = model.currentTab
1095+ model.move(data.from, data.to)
1096+ compare(model.currentIndex, data.result)
1097+ compare(model.currentTab, currentTabInitial)
1098+ }
1099+
1100+ function test_shouldMoveToTopWhenSettingCurrentIndexInStackMode() {
1101+ var tab1 = createTab()
1102+ model.add(tab1)
1103+ var tab2 = createTab()
1104+ model.add(tab2)
1105+ var tab3 = createTab()
1106+ model.add(tab3)
1107+ model.behaveAsStack = true
1108+ compare(model.currentIndex, 0)
1109+
1110+ model.currentIndex = 2
1111+ compare(model.currentIndex, 0)
1112+ compare(model.currentTab, tab3)
1113+ compare(model.get(1), tab1)
1114+ compare(model.get(2), tab2)
1115+ }
1116+
1117+ function test_shouldNotInsertNullTab()
1118+ {
1119+ ignoreWarning("Invalid Tab: null")
1120+ compare(model.insert(null, 0), -1)
1121+ compare(model.count, 0)
1122+ }
1123+
1124+ function test_shouldReturnIndexWhenInsertingTab()
1125+ {
1126+ for (var i = 0; i < 3; ++i) {
1127+ model.add(createTab())
1128+ }
1129+
1130+ for (var i = 2; i >= 0; --i) {
1131+ compare(model.insert(createTab(), i), i)
1132+ }
1133+ }
1134+
1135+ function test_shouldUpdateCountWhenInsertingTab()
1136+ {
1137+ model.insert(createTab(), 0)
1138+ compare(countSpy.count, 1)
1139+ compare(model.count, 1)
1140+ }
1141+
1142+ function test_shouldInsertAtCorrectIndex()
1143+ {
1144+ model.insert(createTabWithTitle("B"), 0)
1145+ model.insert(createTabWithTitle("A"), 0)
1146+ verifyTabsOrder(["A", "B"])
1147+ model.insert(createTabWithTitle("X"), 1)
1148+ verifyTabsOrder(["A", "X", "B"])
1149+ model.insert(createTabWithTitle("C"), 3)
1150+ verifyTabsOrder(["A", "X", "B", "C"])
1151+ }
1152+ }
1153+}
1154
1155=== removed directory 'tests/unittests/tabs-model'
1156=== removed file 'tests/unittests/tabs-model/CMakeLists.txt'
1157--- tests/unittests/tabs-model/CMakeLists.txt 2015-06-22 10:29:20 +0000
1158+++ tests/unittests/tabs-model/CMakeLists.txt 1970-01-01 00:00:00 +0000
1159@@ -1,18 +0,0 @@
1160-find_package(Qt5Core REQUIRED)
1161-find_package(Qt5Qml REQUIRED)
1162-find_package(Qt5Quick REQUIRED)
1163-find_package(Qt5Sql REQUIRED)
1164-find_package(Qt5Test REQUIRED)
1165-set(TEST tst_TabsModelTests)
1166-add_executable(${TEST} tst_TabsModelTests.cpp)
1167-include_directories(${webbrowser-app_SOURCE_DIR})
1168-target_link_libraries(${TEST}
1169- Qt5::Core
1170- Qt5::Qml
1171- Qt5::Quick
1172- Qt5::Sql
1173- Qt5::Test
1174- webbrowser-app-models
1175-)
1176-add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
1177-set_tests_properties(${TEST} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal")
1178
1179=== removed file 'tests/unittests/tabs-model/tst_TabsModelTests.cpp'
1180--- tests/unittests/tabs-model/tst_TabsModelTests.cpp 2015-09-23 17:14:30 +0000
1181+++ tests/unittests/tabs-model/tst_TabsModelTests.cpp 1970-01-01 00:00:00 +0000
1182@@ -1,493 +0,0 @@
1183-/*
1184- * Copyright 2013-2015 Canonical Ltd.
1185- *
1186- * This file is part of webbrowser-app.
1187- *
1188- * webbrowser-app is free software; you can redistribute it and/or modify
1189- * it under the terms of the GNU General Public License as published by
1190- * the Free Software Foundation; version 3.
1191- *
1192- * webbrowser-app is distributed in the hope that it will be useful,
1193- * but WITHOUT ANY WARRANTY; without even the implied warranty of
1194- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1195- * GNU General Public License for more details.
1196- *
1197- * You should have received a copy of the GNU General Public License
1198- * along with this program. If not, see <http://www.gnu.org/licenses/>.
1199- */
1200-
1201-// Qt
1202-#include <QtCore/QStringList>
1203-#include <QtQml/QQmlComponent>
1204-#include <QtQml/QQmlEngine>
1205-#include <QtQml/QQmlProperty>
1206-#include <QtQuick/QQuickItem>
1207-#include <QtTest/QSignalSpy>
1208-#include <QtTest/QtTest>
1209-
1210-// local
1211-#include "tabs-model.h"
1212-
1213-class TabsModelTests : public QObject
1214-{
1215- Q_OBJECT
1216-
1217-private:
1218- TabsModel* model;
1219-
1220- QQuickItem* createTab()
1221- {
1222- QQmlEngine engine;
1223- QQmlComponent component(&engine);
1224- QByteArray data("import QtQuick 2.4\nItem {\nproperty url url\n"
1225- "property string title\nproperty url icon\n}");
1226- component.setData(data, QUrl());
1227- QObject* object = component.create();
1228- object->setParent(this);
1229- QQuickItem* item = qobject_cast<QQuickItem*>(object);
1230- return item;
1231- }
1232-
1233- QQuickItem* createTabWithTitle(const QString& title) {
1234- QQuickItem* tab = createTab();
1235- tab->setProperty("title", title);
1236- return tab;
1237- }
1238-
1239- void verifyTabsOrder(QStringList orderedTitles) {
1240- QCOMPARE(model->rowCount(), orderedTitles.count());
1241- int i = 0;
1242- Q_FOREACH(QString title, orderedTitles) {
1243- QCOMPARE(model->get(i++)->property("title").toString(), title);
1244- }
1245- }
1246-
1247-private Q_SLOTS:
1248- void init()
1249- {
1250- model = new TabsModel;
1251- }
1252-
1253- void cleanup()
1254- {
1255- while (model->rowCount() > 0) {
1256- delete model->remove(0);
1257- }
1258- delete model;
1259- }
1260-
1261- void shouldBeInitiallyEmpty()
1262- {
1263- QCOMPARE(model->rowCount(), 0);
1264- QCOMPARE(model->currentTab(), (QObject*) nullptr);
1265- }
1266-
1267- void shouldExposeRoleNames()
1268- {
1269- QList<QByteArray> roleNames = model->roleNames().values();
1270- QVERIFY(roleNames.contains("url"));
1271- QVERIFY(roleNames.contains("title"));
1272- QVERIFY(roleNames.contains("icon"));
1273- QVERIFY(roleNames.contains("tab"));
1274- }
1275-
1276- void shouldNotAllowSettingTheIndexToAnInvalidValue_data()
1277- {
1278- QTest::addColumn<int>("index");
1279- QTest::newRow("zero") << 0;
1280- QTest::newRow("too high") << 2;
1281- QTest::newRow("too low") << -2;
1282- }
1283-
1284- void shouldNotAllowSettingTheIndexToAnInvalidValue()
1285- {
1286- QFETCH(int, index);
1287- model->setCurrentIndex(index);
1288- QCOMPARE(model->currentIndex(), -1);
1289- QCOMPARE(model->currentTab(), (QObject*) nullptr);
1290- }
1291-
1292- void shouldNotAddNullTab()
1293- {
1294- QCOMPARE(model->add(0), -1);
1295- QCOMPARE(model->rowCount(), 0);
1296- }
1297-
1298- void shouldReturnIndexWhenAddingTab()
1299- {
1300- for(int i = 0; i < 3; ++i) {
1301- QCOMPARE(model->add(createTab()), i);
1302- }
1303- }
1304-
1305- void shouldUpdateCountWhenAddingTab()
1306- {
1307- QSignalSpy spy(model, SIGNAL(countChanged()));
1308- model->add(createTab());
1309- QCOMPARE(spy.count(), 1);
1310- QCOMPARE(model->rowCount(), 1);
1311- }
1312-
1313- void shouldNotInsertNullTab()
1314- {
1315- QCOMPARE(model->insert(0, 0), -1);
1316- QCOMPARE(model->rowCount(), 0);
1317- }
1318-
1319- void shouldReturnIndexWhenInsertingTab()
1320- {
1321- for(int i = 0; i < 3; ++i) {
1322- model->add(createTab());
1323- }
1324- for(int i = 2; i >= 0; --i) {
1325- QCOMPARE(model->insert(createTab(), i), i);
1326- }
1327- }
1328-
1329- void shouldUpdateCountWhenInsertingTab()
1330- {
1331- QSignalSpy spy(model, SIGNAL(countChanged()));
1332- model->insert(createTab(), 0);
1333- QCOMPARE(spy.count(), 1);
1334- QCOMPARE(model->rowCount(), 1);
1335- }
1336-
1337- void shouldInsertAtCorrectIndex()
1338- {
1339- model->insert(createTabWithTitle("B"), 0);
1340- model->insert(createTabWithTitle("A"), 0);
1341- verifyTabsOrder(QStringList({"A", "B"}));
1342- model->insert(createTabWithTitle("X"), 1);
1343- verifyTabsOrder(QStringList({"A", "X", "B"}));
1344- model->insert(createTabWithTitle("C"), 3);
1345- verifyTabsOrder(QStringList({"A", "X", "B", "C"}));
1346- }
1347-
1348- void shouldClampIndexWhenInsertingTabOutOfBounds()
1349- {
1350- model->add(createTabWithTitle("A"));
1351- model->add(createTabWithTitle("B"));
1352- model->insert(createTabWithTitle("C"), 3);
1353- verifyTabsOrder(QStringList({"A", "B", "C"}));
1354- model->insert(createTabWithTitle("X"), -1);
1355- verifyTabsOrder(QStringList({"X", "A", "B", "C"}));
1356- }
1357-
1358- void shouldUpdateCountWhenRemovingTab()
1359- {
1360- model->add(createTab());
1361- QSignalSpy spy(model, SIGNAL(countChanged()));
1362- delete model->remove(0);
1363- QCOMPARE(spy.count(), 1);
1364- QCOMPARE(model->rowCount(), 0);
1365- }
1366-
1367- void shouldNotAllowRemovingAtInvalidIndex()
1368- {
1369- QCOMPARE(model->remove(0), (QObject*) nullptr);
1370- QCOMPARE(model->remove(2), (QObject*) nullptr);
1371- QCOMPARE(model->remove(-2), (QObject*) nullptr);
1372- }
1373-
1374- void shouldReturnTabWhenRemoving()
1375- {
1376- QQuickItem* tab = createTab();
1377- model->add(tab);
1378- QObject* removed = model->remove(0);
1379- QCOMPARE(removed, tab);
1380- delete removed;
1381- }
1382-
1383- void shouldNotDeleteTabWhenRemoving()
1384- {
1385- QQuickItem* tab = createTab();
1386- model->add(tab);
1387- model->remove(0);
1388- QCOMPARE(tab->parent(), this);
1389- delete tab;
1390- }
1391-
1392- void shouldNotifyWhenAddingTab()
1393- {
1394- QSignalSpy spy(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)));
1395- for(int i = 0; i < 3; ++i) {
1396- model->add(createTab());
1397- QCOMPARE(spy.count(), 1);
1398- QList<QVariant> args = spy.takeFirst();
1399- QCOMPARE(args.at(1).toInt(), i);
1400- QCOMPARE(args.at(2).toInt(), i);
1401- }
1402- }
1403-
1404- void shouldNotifyWhenRemovingTab()
1405- {
1406- QSignalSpy spy(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)));
1407- for(int i = 0; i < 5; ++i) {
1408- model->add(createTab());
1409- }
1410- delete model->remove(3);
1411- QCOMPARE(spy.count(), 1);
1412- QList<QVariant> args = spy.takeFirst();
1413- QCOMPARE(args.at(1).toInt(), 3);
1414- QCOMPARE(args.at(2).toInt(), 3);
1415- for(int i = 3; i >= 0; --i) {
1416- delete model->remove(i);
1417- QCOMPARE(spy.count(), 1);
1418- args = spy.takeFirst();
1419- QCOMPARE(args.at(1).toInt(), i);
1420- QCOMPARE(args.at(2).toInt(), i);
1421- }
1422- }
1423-
1424- void shouldNotifyWhenTabPropertiesChange()
1425- {
1426- qRegisterMetaType<QVector<int> >();
1427- QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
1428- QQuickItem* tab = createTab();
1429- model->add(tab);
1430-
1431- QQmlProperty(tab, "url").write(QUrl("http://ubuntu.com"));
1432- QCOMPARE(spy.count(), 1);
1433- QList<QVariant> args = spy.takeFirst();
1434- QCOMPARE(args.at(0).toModelIndex().row(), 0);
1435- QCOMPARE(args.at(1).toModelIndex().row(), 0);
1436- QVector<int> roles = args.at(2).value<QVector<int> >();
1437- QCOMPARE(roles.size(), 1);
1438- QVERIFY(roles.contains(TabsModel::Url));
1439-
1440- QQmlProperty(tab, "title").write(QString("Lorem Ipsum"));
1441- QCOMPARE(spy.count(), 1);
1442- args = spy.takeFirst();
1443- QCOMPARE(args.at(0).toModelIndex().row(), 0);
1444- QCOMPARE(args.at(1).toModelIndex().row(), 0);
1445- roles = args.at(2).value<QVector<int> >();
1446- QCOMPARE(roles.size(), 1);
1447- QVERIFY(roles.contains(TabsModel::Title));
1448-
1449- QQmlProperty(tab, "icon").write(QUrl("image://webicon/123"));
1450- QCOMPARE(spy.count(), 1);
1451- args = spy.takeFirst();
1452- QCOMPARE(args.at(0).toModelIndex().row(), 0);
1453- QCOMPARE(args.at(1).toModelIndex().row(), 0);
1454- roles = args.at(2).value<QVector<int> >();
1455- QCOMPARE(roles.size(), 1);
1456- QVERIFY(roles.contains(TabsModel::Icon));
1457- }
1458-
1459- void shouldUpdateCurrentTabWhenSettingCurrentIndex()
1460- {
1461- QQuickItem* tab1 = createTab();
1462- model->add(tab1);
1463- QSignalSpy spy(model, SIGNAL(currentTabChanged()));
1464- model->setCurrentIndex(0);
1465- QCOMPARE(model->currentIndex(), 0);
1466- QVERIFY(spy.isEmpty());
1467- QCOMPARE(model->currentTab(), tab1);
1468- QQuickItem* tab2 = createTab();
1469- model->add(tab2);
1470- model->setCurrentIndex(1);
1471- QCOMPARE(model->currentIndex(), 1);
1472- QCOMPARE(spy.count(), 1);
1473- QCOMPARE(model->currentTab(), tab2);
1474- }
1475-
1476- void shouldSetCurrentTabWhenAddingFirstTab()
1477- {
1478- // Adding a tab to an empty model should update the current tab
1479- // to that tab
1480- QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
1481- QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
1482- QCOMPARE(model->currentIndex(), -1);
1483- QCOMPARE(model->currentTab(), (QObject*) nullptr);
1484-
1485- QQuickItem* tab1 = createTab();
1486- model->add(tab1);
1487-
1488- QCOMPARE(spytab.count(), 1);
1489- QCOMPARE(spyindex.count(), 1);
1490- QCOMPARE(model->currentIndex(), 0);
1491- QCOMPARE(model->currentTab(), tab1);
1492-
1493- // But adding further items should keep the index where it was
1494- model->add(createTab());
1495- model->add(createTab());
1496-
1497- QCOMPARE(spytab.count(), 1);
1498- QCOMPARE(spyindex.count(), 1);
1499- QCOMPARE(model->currentIndex(), 0);
1500- QCOMPARE(model->currentTab(), tab1);
1501- }
1502-
1503- void shouldSetCurrentTabWhenInsertingFirstTab()
1504- {
1505- // Inserting a tab to an empty model should update the current tab
1506- // to that tab
1507- QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
1508- QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
1509- QCOMPARE(model->currentIndex(), -1);
1510- QCOMPARE(model->currentTab(), (QObject*) nullptr);
1511-
1512- QQuickItem* tab1 = createTab();
1513- model->insert(tab1, 0);
1514-
1515- QCOMPARE(spytab.count(), 1);
1516- QCOMPARE(spyindex.count(), 1);
1517- QCOMPARE(model->currentIndex(), 0);
1518- QCOMPARE(model->currentTab(), tab1);
1519- }
1520-
1521- void shouldSetInvalidIndexWhenRemovingLastTab()
1522- {
1523- // Removing the last item should also set the current index to -1
1524- // and the current tab to null
1525- model->add(createTab());
1526-
1527- QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
1528- QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
1529- delete model->remove(0);
1530- QCOMPARE(spytab.count(), 1);
1531- QCOMPARE(spyindex.count(), 1);
1532- QCOMPARE(model->currentIndex(), -1);
1533- QCOMPARE(model->currentTab(), (QObject*) nullptr);
1534- }
1535-
1536- void shouldNotChangeIndexWhenRemovingAfterCurrent()
1537- {
1538- // When removing a tab after the current one,
1539- // the current tab shouldn’t change.
1540- QQuickItem* tab1 = createTab();
1541- model->add(tab1);
1542- model->add(createTab());
1543-
1544- QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
1545- QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
1546- delete model->remove(1);
1547- QCOMPARE(model->currentTab(), tab1);
1548- QVERIFY(spytab.isEmpty());
1549- QVERIFY(spyindex.isEmpty());
1550- }
1551-
1552- void shouldUpdateIndexWhenRemovingCurrent()
1553- {
1554- // When removing the current tab, if there is a tab after it,
1555- // it becomes the current one.
1556- QQuickItem* tab1 = createTab();
1557- QQuickItem* tab2 = createTab();
1558- QQuickItem* tab3 = createTab();
1559- model->add(tab1);
1560- model->add(tab2);
1561- model->add(tab3);
1562- model->setCurrentIndex(1);
1563- QCOMPARE(model->currentIndex(), 1);
1564- QCOMPARE(model->currentTab(), tab2);
1565-
1566- QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
1567- QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
1568- delete model->remove(1);
1569- QCOMPARE(spyindex.count(), 0);
1570- QCOMPARE(spytab.count(), 1);
1571- QCOMPARE(model->currentTab(), tab3);
1572-
1573- // If there is no tab after it but one before, that one becomes current
1574- delete model->remove(1);
1575- QCOMPARE(spyindex.count(), 1);
1576- QCOMPARE(spytab.count(), 2);
1577- QCOMPARE(model->currentIndex(), 0);
1578- QCOMPARE(model->currentTab(), tab1);
1579- }
1580-
1581- void shouldDecreaseIndexWhenRemovingBeforeCurrent()
1582- {
1583- // When removing a tab before the current tab, the current index
1584- // should decrease to match.
1585- model->add(createTab());
1586- model->add(createTab());
1587- QQuickItem* tab = createTab();
1588- model->add(tab);
1589- model->setCurrentIndex(2);
1590-
1591- QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
1592- QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
1593- delete model->remove(1);
1594- QVERIFY(spytab.isEmpty());
1595- QCOMPARE(spyindex.count(), 1);
1596- QCOMPARE(model->currentIndex(), 1);
1597- QCOMPARE(model->currentTab(), tab);
1598- }
1599-
1600- void shouldReturnData()
1601- {
1602- QQuickItem* tab = createTab();
1603- QQmlProperty(tab, "url").write(QUrl("http://ubuntu.com/"));
1604- QQmlProperty(tab, "title").write(QString("Lorem Ipsum"));
1605- QQmlProperty(tab, "icon").write(QUrl("image://webicon/123"));
1606- model->add(tab);
1607- QVERIFY(!model->data(QModelIndex(), TabsModel::Url).isValid());
1608- QVERIFY(!model->data(model->index(-1, 0), TabsModel::Url).isValid());
1609- QVERIFY(!model->data(model->index(3, 0), TabsModel::Url).isValid());
1610- QCOMPARE(model->data(model->index(0, 0), TabsModel::Url).toUrl(), QUrl("http://ubuntu.com/"));
1611- QCOMPARE(model->data(model->index(0, 0), TabsModel::Title).toString(), QString("Lorem Ipsum"));
1612- QCOMPARE(model->data(model->index(0, 0), TabsModel::Icon).toUrl(), QUrl("image://webicon/123"));
1613- QCOMPARE(model->data(model->index(0, 0), TabsModel::Tab).value<QQuickItem*>(), tab);
1614- QVERIFY(!model->data(model->index(0, 0), TabsModel::Tab + 3).isValid());
1615- }
1616-
1617- void shouldReturnTabAtIndex()
1618- {
1619- // valid indexes
1620- QQuickItem* tab1 = createTab();
1621- model->add(tab1);
1622- QQuickItem* tab2 = createTab();
1623- model->add(tab2);
1624- QQuickItem* tab3 = createTab();
1625- model->add(tab3);
1626- QCOMPARE(model->get(0), tab1);
1627- QCOMPARE(model->get(1), tab2);
1628- QCOMPARE(model->get(2), tab3);
1629-
1630- // invalid indexes
1631- QCOMPARE(model->get(-1), (QObject*) nullptr);
1632- QCOMPARE(model->get(3), (QObject*) nullptr);
1633- }
1634-
1635-private:
1636- void moveTabs(int from, int to, bool moved, bool indexChanged, int newIndex)
1637- {
1638- QSignalSpy spyMoved(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)));
1639- QSignalSpy spyIndex(model, SIGNAL(currentIndexChanged()));
1640- QSignalSpy spyTab(model, SIGNAL(currentTabChanged()));
1641-
1642- model->move(from, to);
1643- QCOMPARE(spyMoved.count(), moved ? 1 : 0);
1644- if (moved) {
1645- QList<QVariant> args = spyMoved.takeFirst();
1646- QCOMPARE(args.at(1).toInt(), from);
1647- QCOMPARE(args.at(2).toInt(), from);
1648- QCOMPARE(args.at(4).toInt(), to);
1649- }
1650- QCOMPARE(spyIndex.count(), indexChanged ? 1 : 0);
1651- QCOMPARE(model->currentIndex(), newIndex);
1652- QVERIFY(spyTab.isEmpty());
1653- }
1654-
1655-private Q_SLOTS:
1656- void shouldNotifyWhenMovingTabs()
1657- {
1658- for (int i = 0; i < 3; ++i) {
1659- model->add(createTab());
1660- }
1661-
1662- moveTabs(1, 1, false, false, 0);
1663- moveTabs(4, 1, false, false, 0);
1664- moveTabs(1, 4, false, false, 0);
1665- moveTabs(0, 2, true, true, 2);
1666- moveTabs(1, 0, true, false, 2);
1667- moveTabs(2, 1, true, true, 1);
1668- moveTabs(2, 0, true, true, 2);
1669- moveTabs(2, 1, true, true, 1);
1670- moveTabs(0, 2, true, true, 0);
1671- }
1672-};
1673-
1674-QTEST_MAIN(TabsModelTests)
1675-#include "tst_TabsModelTests.moc"

Subscribers

People subscribed via source and target branches

to status/vote changes: