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
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2015-11-03 11:56:01 +0000
+++ src/app/webbrowser/Browser.qml 2015-11-08 23:11:15 +0000
@@ -62,14 +62,11 @@
62 target: tabsModel62 target: tabsModel
63 onCurrentIndexChanged: {63 onCurrentIndexChanged: {
64 // Remove focus from the address bar when the current tab64 // Remove focus from the address bar when the current tab
65 // changes to ensure that its contents are updated.65 // changes to ensure that its contents are updated...
66 tabContainer.forceActiveFocus()66 tabContainer.forceActiveFocus()
6767 // ...then give focus to the either the tab or the address bar
68 // In narrow mode, the tabslist is a stack:68 // depending if we are on a new tab view or on a page
69 // the current tab is always at the top.69 internal.resetFocus()
70 if (!browser.wide) {
71 tabsModel.move(tabsModel.currentIndex, 0)
72 }
73 }70 }
74 }71 }
7572
@@ -711,9 +708,6 @@
711 onWideChanged: {708 onWideChanged: {
712 if (wide) {709 if (wide) {
713 recentView.reset()710 recentView.reset()
714 } else {
715 // In narrow mode, the tabslist is a stack: the current tab is always at the top.
716 tabsModel.move(tabsModel.currentIndex, 0)
717 }711 }
718 }712 }
719713
@@ -946,6 +940,7 @@
946940
947 TabsModel {941 TabsModel {
948 id: publicTabsModel942 id: publicTabsModel
943 behaveAsStack: !browser.wide
949 }944 }
950945
951 Loader {946 Loader {
@@ -957,6 +952,7 @@
957 id: privateTabsModelComponent952 id: privateTabsModelComponent
958953
959 TabsModel {954 TabsModel {
955 behaveAsStack: !browser.wide
960 Component.onDestruction: {956 Component.onDestruction: {
961 while (count > 0) {957 while (count > 0) {
962 var tab = remove(count - 1)958 var tab = remove(count - 1)
@@ -1485,7 +1481,7 @@
1485 internal.addTab(tab, i == 0)1481 internal.addTab(tab, i == 0)
1486 }1482 }
1487 }1483 }
1488 if ('currentIndex' in state) {1484 if ('currentIndex' in state && publicTabsModel.validIndex(state.currentIndex)) {
1489 publicTabsModel.currentIndex = state.currentIndex1485 publicTabsModel.currentIndex = state.currentIndex
1490 }1486 }
1491 }1487 }
14921488
=== modified file 'src/app/webbrowser/CMakeLists.txt'
--- src/app/webbrowser/CMakeLists.txt 2015-10-18 19:21:48 +0000
+++ src/app/webbrowser/CMakeLists.txt 2015-11-08 23:11:15 +0000
@@ -21,7 +21,6 @@
21 history-model.cpp21 history-model.cpp
22 history-timeframe-model.cpp22 history-timeframe-model.cpp
23 limit-proxy-model.cpp23 limit-proxy-model.cpp
24 tabs-model.cpp
25 text-search-filter-model.cpp24 text-search-filter-model.cpp
26 top-sites-model.cpp25 top-sites-model.cpp
27)26)
2827
=== modified file 'src/app/webbrowser/TabsBar.qml'
--- src/app/webbrowser/TabsBar.qml 2015-09-24 14:07:47 +0000
+++ src/app/webbrowser/TabsBar.qml 2015-11-08 23:11:15 +0000
@@ -24,7 +24,7 @@
24Item {24Item {
25 id: root25 id: root
2626
27 property alias model: repeater.model27 property var model
2828
29 property real minTabWidth: 0 //units.gu(6)29 property real minTabWidth: 0 //units.gu(6)
30 property real maxTabWidth: units.gu(20)30 property real maxTabWidth: units.gu(20)
@@ -88,7 +88,7 @@
88 Action {88 Action {
89 objectName: "tab_action_reload"89 objectName: "tab_action_reload"
90 text: i18n.tr("Reload")90 text: i18n.tr("Reload")
91 enabled: menu.tab.url.toString().length > 091 enabled: menu.tab && menu.tab.url.toString().length > 0
92 onTriggered: menu.tab.reload()92 onTriggered: menu.tab.reload()
93 }93 }
94 Action {94 Action {
@@ -118,6 +118,7 @@
118118
119 property bool reordering: false119 property bool reordering: false
120120
121 model: root.model.listModel
121 delegate: MouseArea {122 delegate: MouseArea {
122 id: tabDelegate123 id: tabDelegate
123 objectName: "tabDelegate"124 objectName: "tabDelegate"
@@ -145,8 +146,8 @@
145 active: tabIndex === root.model.currentIndex146 active: tabIndex === root.model.currentIndex
146 hoverable: true147 hoverable: true
147 incognito: root.incognito148 incognito: root.incognito
148 title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab"))149 title: model.tab.title ? model.tab.title : (model.tab.url.toString() ? model.tab.url : i18n.tr("New tab"))
149 icon: model.icon150 icon: model.tab.icon
150151
151 rightMargin: tabDelegate.rightMargin152 rightMargin: tabDelegate.rightMargin
152153
@@ -171,9 +172,9 @@
171 onXChanged: {172 onXChanged: {
172 if (!dragging) return173 if (!dragging) return
173 if (x < (index * width - width / 2)) {174 if (x < (index * width - width / 2)) {
174 root.model.move(index, index - 1)175 root.model.move(index, index - 1, 1)
175 } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) {176 } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) {
176 root.model.move(index + 1, index)177 root.model.move(index + 1, index, 1)
177 }178 }
178 }179 }
179180
180181
=== modified file 'src/app/webbrowser/TabsList.qml'
--- src/app/webbrowser/TabsList.qml 2015-09-04 17:48:37 +0000
+++ src/app/webbrowser/TabsList.qml 2015-11-08 23:11:15 +0000
@@ -24,7 +24,7 @@
2424
25 property real delegateHeight25 property real delegateHeight
26 property real chromeOffset26 property real chromeOffset
27 property alias model: repeater.model27 property var model
28 readonly property int count: repeater.count28 readonly property int count: repeater.count
29 property bool incognito29 property bool incognito
3030
@@ -62,6 +62,7 @@
62 Repeater {62 Repeater {
63 id: repeater63 id: repeater
6464
65 model: tabslist.model.listModel
65 delegate: Loader {66 delegate: Loader {
66 id: delegate67 id: delegate
6768
@@ -91,8 +92,8 @@
91 }92 }
92 }93 }
9394
94 readonly property string title: model.title ? model.title : (model.url.toString() ? model.url : i18n.tr("New tab"))95 readonly property string title: model.tab.title ? model.tab.title : (model.tab.url.toString() ? model.tab.url : i18n.tr("New tab"))
95 readonly property string icon: model.icon96 readonly property string icon: model.tab.icon
9697
97 readonly property bool needsInstance: (index >= 0) && ((flickable.contentY + flickable.height + delegateHeight / 2) >= (index * delegateHeight))98 readonly property bool needsInstance: (index >= 0) && ((flickable.contentY + flickable.height + delegateHeight / 2) >= (index * delegateHeight))
98 sourceComponent: needsInstance ? tabPreviewComponent : undefined99 sourceComponent: needsInstance ? tabPreviewComponent : undefined
@@ -103,13 +104,14 @@
103 id: tabPreviewComponent104 id: tabPreviewComponent
104105
105 TabPreview {106 TabPreview {
107 id: preview
106 title: delegate.title108 title: delegate.title
107 icon: delegate.icon109 icon: delegate.icon
108 incognito: tabslist.incognito110 incognito: tabslist.incognito
109 tab: model.tab111 tab: model.tab
110 chromeHeight: firstItemChromeBackground.height112 chromeHeight: firstItemChromeBackground.height
111 showContent: (index > 0) || (delegate.y > flickable.contentY) ||113 showContent: (index > 0) || (delegate.y > flickable.contentY) ||
112 !(tab.webview && tab.webview.visible)114 !(preview.tab.webview && preview.tab.webview.visible)
113115
114 onSelected: tabslist.selectAndAnimateTab(index)116 onSelected: tabslist.selectAndAnimateTab(index)
115 onClosed: tabslist.tabClosed(index)117 onClosed: tabslist.tabClosed(index)
116118
=== added file 'src/app/webbrowser/TabsModel.qml'
--- src/app/webbrowser/TabsModel.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/TabsModel.qml 2015-11-08 23:11:15 +0000
@@ -0,0 +1,137 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20
21QtObject {
22 property int currentIndex: -1
23 property var currentTab: null
24 property bool behaveAsStack: false
25 property alias count: tabs.count
26
27 // Expose the ListModel through a property instead of inheriting directly
28 // from it due to a bug in QML that does not allow calling base class
29 // methods: https://bugreports.qt.io/browse/QTBUG-25942
30 // This allows us to create our own methods that implement the required
31 // semantics (such as correctly tracking and updating currentIndex and
32 // currentTab) at the price of the minor inconvenience of having to access
33 // TabsModel.listModel when wanting to use it as model for a view.
34 property ListModel listModel: ListModel {
35 id: tabs
36
37 function updateCurrentTab() {
38 var newCurrentTab = null
39 if (validIndex(currentIndex)) {
40 if (behaveAsStack) {
41 tabs.move(currentIndex, 0, 1)
42 currentIndex = 0
43 }
44 newCurrentTab = tabs.get(currentIndex).tab
45 }
46 if (newCurrentTab != currentTab) {
47 currentTab = newCurrentTab
48 }
49 }
50 }
51
52 onCurrentIndexChanged: {
53 // Clamp the model index to valid values. -1 is a valid value only
54 // if the model is empty.
55 currentIndex = Math.max((tabs.count === 0) ? -1 : 0,
56 Math.min(currentIndex, tabs.count - 1))
57 tabs.updateCurrentTab()
58 }
59 onBehaveAsStackChanged: if (behaveAsStack) tabs.updateCurrentTab()
60
61 function validIndex(index) { return index >= 0 && index < tabs.count }
62
63 function add(tab) { return insert(tab, tabs.count) }
64
65 function insert(tab, index) {
66 if (tab) {
67 if (index === undefined) index = tabs.count
68 else if (index < 0 || index > tabs.count) {
69 console.warn("Invalid insert index:", index)
70 return
71 }
72 tabs.insert(index, {tab: tab})
73 if (currentIndex == -1) {
74 // if the current index was -1 then the model must have been
75 // empty, and the first item added to an empty model always
76 // becomes the new current item
77 currentIndex = 0
78 tabs.updateCurrentTab()
79 } else if (currentIndex == tabs.count - 1) {
80 // index was set out of range, but adding the new item made it
81 // valid, so we need to update the current item
82 tabs.updateCurrentTab()
83 } else if (index <= currentIndex) {
84 if (validIndex(currentIndex)) {
85 // Increment the index if we are inserting items before the
86 // current index, and the current index has not been set to
87 // a position past the end of the list.
88 currentIndex++
89 }
90 }
91 return index
92 } else {
93 console.warn("Invalid Tab:", tab)
94 return -1
95 }
96 }
97
98 function remove(index) {
99 if (!validIndex(index)) return null
100 var tab = tabs.get(index).tab
101 tabs.remove(index)
102
103 if (index < currentIndex) {
104 // if we removed the current index and it is the last item OR
105 // if we removed any item before the current index,
106 // then decrement the current index
107 --currentIndex
108 } else if (index === currentIndex) {
109 // If the current tab was removed, the following one (if any) is made
110 // current. If it was the last tab in the model, the current index needs
111 // to be decreased.
112 if (index === tabs.count) --currentIndex
113 else tabs.updateCurrentTab()
114 }
115
116 return tab
117 }
118
119 function get(index) {
120 return validIndex(index) ? tabs.get(index).tab : null
121 }
122
123 function move(from, to)
124 {
125 if (from == to || !validIndex(from) || !validIndex(to)) {
126 return
127 }
128 tabs.move(from, to, 1)
129 if (currentIndex == from) {
130 currentIndex = to
131 } else if (currentIndex >= to && currentIndex < from) {
132 currentIndex++
133 } else if (currentIndex > from && currentIndex <= to) {
134 currentIndex--
135 }
136 }
137}
0138
=== removed file 'src/app/webbrowser/tabs-model.cpp'
--- src/app/webbrowser/tabs-model.cpp 2015-09-23 16:50:21 +0000
+++ src/app/webbrowser/tabs-model.cpp 1970-01-01 00:00:00 +0000
@@ -1,254 +0,0 @@
1/*
2 * Copyright 2013-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#include "tabs-model.h"
20
21// Qt
22#include <QtCore/QDebug>
23#include <QtCore/QObject>
24#include <QtCore/QtGlobal>
25
26/*!
27 \class TabsModel
28 \brief List model that stores the list of currently open tabs.
29
30 TabsModel is a list model that stores the list of currently open tabs.
31 Each tab holds a pointer to a Tab and associated metadata (URL, title,
32 icon).
33
34 The model doesn’t own the Tab, so it is the responsibility of whoever
35 adds a tab to instantiate the corresponding Tab, and to destroy it after
36 it’s removed from the model.
37*/
38TabsModel::TabsModel(QObject* parent)
39 : QAbstractListModel(parent)
40 , m_currentIndex(-1)
41{
42}
43
44TabsModel::~TabsModel()
45{
46}
47
48QHash<int, QByteArray> TabsModel::roleNames() const
49{
50 static QHash<int, QByteArray> roles;
51 if (roles.isEmpty()) {
52 roles[Url] = "url";
53 roles[Title] = "title";
54 roles[Icon] = "icon";
55 roles[Tab] = "tab";
56 }
57 return roles;
58}
59
60int TabsModel::rowCount(const QModelIndex& parent) const
61{
62 Q_UNUSED(parent);
63 return m_tabs.count();
64}
65
66QVariant TabsModel::data(const QModelIndex& index, int role) const
67{
68 if (!index.isValid()) {
69 return QVariant();
70 }
71 QObject* tab = m_tabs.at(index.row());
72 switch (role) {
73 case Url:
74 return tab->property("url");
75 case Title:
76 return tab->property("title");
77 case Icon:
78 return tab->property("icon");
79 case Tab:
80 return QVariant::fromValue(tab);
81 default:
82 return QVariant();
83 }
84}
85
86int TabsModel::currentIndex() const
87{
88 return m_currentIndex;
89}
90
91void TabsModel::setCurrentIndex(int index)
92{
93 if (!checkValidTabIndex(index)) {
94 return;
95 }
96 if (index != m_currentIndex) {
97 m_currentIndex = index;
98 Q_EMIT currentIndexChanged();
99 Q_EMIT currentTabChanged();
100 }
101}
102
103QObject* TabsModel::currentTab() const
104{
105 if (m_tabs.isEmpty() || !checkValidTabIndex(m_currentIndex)) {
106 return nullptr;
107 }
108 return m_tabs.at(m_currentIndex);
109}
110
111/*!
112 Append a tab to the model and return the corresponding index in the model.
113
114 It is the responsibility of the caller to instantiate the corresponding
115 Tab beforehand.
116*/
117int TabsModel::add(QObject* tab)
118{
119 return insert(tab, m_tabs.count());
120}
121
122/*!
123 Add a tab to the model at the specified index, and return the index itself,
124 or -1 if the operation failed.
125
126 It is the responsibility of the caller to instantiate the corresponding
127 Tab beforehand.
128*/
129int TabsModel::insert(QObject* tab, int index)
130{
131 if (tab == nullptr) {
132 qWarning() << "Invalid Tab";
133 return -1;
134 }
135 index = qMax(qMin(index, m_tabs.count()), 0);
136 beginInsertRows(QModelIndex(), index, index);
137 m_tabs.insert(index, tab);
138 connect(tab, SIGNAL(urlChanged()), SLOT(onUrlChanged()));
139 connect(tab, SIGNAL(titleChanged()), SLOT(onTitleChanged()));
140 connect(tab, SIGNAL(iconChanged()), SLOT(onIconChanged()));
141 endInsertRows();
142 Q_EMIT countChanged();
143
144 if (m_currentIndex == -1) {
145 // Set the index to zero if this is the first item that gets added to the
146 // model, as it should not be possible to have items in the model but no
147 // current tab.
148 m_currentIndex = 0;
149 Q_EMIT currentIndexChanged();
150 Q_EMIT currentTabChanged();
151 } else if (index <= m_currentIndex) {
152 // Increment the index if we are inserting items before the current index.
153 m_currentIndex++;
154 Q_EMIT currentIndexChanged();
155 }
156
157 return index;
158}
159
160/*!
161 Given its index, remove a tab from the model, and return the corresponding
162 Tab.
163
164 It is the responsibility of the caller to destroy the corresponding
165 Tab afterwards.
166*/
167QObject* TabsModel::remove(int index)
168{
169 if (!checkValidTabIndex(index)) {
170 return nullptr;
171 }
172 beginRemoveRows(QModelIndex(), index, index);
173 QObject* tab = m_tabs.takeAt(index);
174 tab->disconnect(this);
175 endRemoveRows();
176 Q_EMIT countChanged();
177
178 if (index < m_currentIndex) {
179 // If we removed any tab before the current one, decrease the
180 // current index to match.
181 m_currentIndex--;
182 Q_EMIT currentIndexChanged();
183 } else if (index == m_currentIndex) {
184 // If the current tab was removed, the following one (if any) is made
185 // current. If it was the last tab in the model, the current index needs
186 // to be decreased.
187 if (m_currentIndex == m_tabs.count()) {
188 m_currentIndex--;
189 Q_EMIT currentIndexChanged();
190 }
191 Q_EMIT currentTabChanged();
192 }
193 return tab;
194}
195
196QObject* TabsModel::get(int index) const
197{
198 if (!checkValidTabIndex(index)) {
199 return nullptr;
200 }
201 return m_tabs.at(index);
202}
203
204void TabsModel::move(int from, int to)
205{
206 if ((from == to) || !checkValidTabIndex(from) || !checkValidTabIndex(to)) {
207 return;
208 }
209 beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
210 m_tabs.move(from, to);
211 endMoveRows();
212 if (m_currentIndex == from) {
213 m_currentIndex = to;
214 Q_EMIT currentIndexChanged();
215 } else if ((m_currentIndex >= to) && (m_currentIndex < from)) {
216 m_currentIndex++;
217 Q_EMIT currentIndexChanged();
218 } else if ((m_currentIndex > from) && (m_currentIndex <= to)) {
219 m_currentIndex--;
220 Q_EMIT currentIndexChanged();
221 }
222}
223
224bool TabsModel::checkValidTabIndex(int index) const
225{
226 if ((index < 0) || (index >= m_tabs.count())) {
227 qWarning() << "Invalid tab index:" << index;
228 return false;
229 }
230 return true;
231}
232
233void TabsModel::onDataChanged(QObject* tab, int role)
234{
235 int index = m_tabs.indexOf(tab);
236 if (checkValidTabIndex(index)) {
237 Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << role);
238 }
239}
240
241void TabsModel::onUrlChanged()
242{
243 onDataChanged(sender(), Url);
244}
245
246void TabsModel::onTitleChanged()
247{
248 onDataChanged(sender(), Title);
249}
250
251void TabsModel::onIconChanged()
252{
253 onDataChanged(sender(), Icon);
254}
2550
=== removed file 'src/app/webbrowser/tabs-model.h'
--- src/app/webbrowser/tabs-model.h 2015-09-02 16:37:20 +0000
+++ src/app/webbrowser/tabs-model.h 1970-01-01 00:00:00 +0000
@@ -1,84 +0,0 @@
1/*
2 * Copyright 2013-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#ifndef __TABS_MODEL_H__
20#define __TABS_MODEL_H__
21
22// Qt
23#include <QtCore/QAbstractListModel>
24#include <QtCore/QList>
25
26class QObject;
27
28class TabsModel : public QAbstractListModel
29{
30 Q_OBJECT
31
32 Q_ENUMS(Roles)
33
34 Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
35 Q_PROPERTY(QObject* currentTab READ currentTab NOTIFY currentTabChanged)
36 Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
37
38public:
39 TabsModel(QObject* parent=0);
40 ~TabsModel();
41
42 enum Roles {
43 Url = Qt::UserRole + 1,
44 Title,
45 Icon,
46 Tab
47 };
48
49 // reimplemented from QAbstractListModel
50 QHash<int, QByteArray> roleNames() const;
51 int rowCount(const QModelIndex& parent=QModelIndex()) const;
52 QVariant data(const QModelIndex& index, int role) const;
53
54 int currentIndex() const;
55 void setCurrentIndex(int index);
56
57 QObject* currentTab() const;
58
59 Q_INVOKABLE int add(QObject* tab);
60 Q_INVOKABLE int insert(QObject* tab, int index);
61 Q_INVOKABLE QObject* remove(int index);
62 Q_INVOKABLE QObject* get(int index) const;
63 Q_INVOKABLE void move(int from, int to);
64
65Q_SIGNALS:
66 void currentIndexChanged() const;
67 void currentTabChanged() const;
68 void countChanged() const;
69
70private Q_SLOTS:
71 void onUrlChanged();
72 void onTitleChanged();
73 void onIconChanged();
74
75private:
76 QList<QObject*> m_tabs;
77 int m_currentIndex;
78
79 bool checkValidTabIndex(int index) const;
80 void setCurrentIndexNoCheck(int index);
81 void onDataChanged(QObject* tab, int role);
82};
83
84#endif // __TABS_MODEL_H__
850
=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
--- src/app/webbrowser/webbrowser-app.cpp 2015-10-22 15:07:26 +0000
+++ src/app/webbrowser/webbrowser-app.cpp 2015-11-08 23:11:15 +0000
@@ -30,7 +30,6 @@
30#include "limit-proxy-model.h"30#include "limit-proxy-model.h"
31#include "searchengine.h"31#include "searchengine.h"
32#include "text-search-filter-model.h"32#include "text-search-filter-model.h"
33#include "tabs-model.h"
34#include "top-sites-model.h"33#include "top-sites-model.h"
35#include "webbrowser-app.h"34#include "webbrowser-app.h"
3635
@@ -89,7 +88,6 @@
89 qmlRegisterType<HistoryLastVisitDateListModel>(uri, 0, 1, "HistoryLastVisitDateListModel");88 qmlRegisterType<HistoryLastVisitDateListModel>(uri, 0, 1, "HistoryLastVisitDateListModel");
90 qmlRegisterType<HistoryLastVisitDateModel>(uri, 0, 1, "HistoryLastVisitDateModel");89 qmlRegisterType<HistoryLastVisitDateModel>(uri, 0, 1, "HistoryLastVisitDateModel");
91 qmlRegisterType<LimitProxyModel>(uri, 0 , 1, "LimitProxyModel");90 qmlRegisterType<LimitProxyModel>(uri, 0 , 1, "LimitProxyModel");
92 qmlRegisterType<TabsModel>(uri, 0, 1, "TabsModel");
93 qmlRegisterSingletonType<BookmarksModel>(uri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory);91 qmlRegisterSingletonType<BookmarksModel>(uri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory);
94 qmlRegisterType<BookmarksFolderListModel>(uri, 0, 1, "BookmarksFolderListModel");92 qmlRegisterType<BookmarksFolderListModel>(uri, 0, 1, "BookmarksFolderListModel");
95 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);93 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
9694
=== modified file 'tests/unittests/CMakeLists.txt'
--- tests/unittests/CMakeLists.txt 2015-10-06 09:48:38 +0000
+++ tests/unittests/CMakeLists.txt 2015-11-08 23:11:15 +0000
@@ -10,7 +10,6 @@
10add_subdirectory(history-lastvisitdatelist-model)10add_subdirectory(history-lastvisitdatelist-model)
11add_subdirectory(top-sites-model)11add_subdirectory(top-sites-model)
12add_subdirectory(session-utils)12add_subdirectory(session-utils)
13add_subdirectory(tabs-model)
14add_subdirectory(bookmarks-model)13add_subdirectory(bookmarks-model)
15add_subdirectory(bookmarks-folder-model)14add_subdirectory(bookmarks-folder-model)
16add_subdirectory(bookmarks-folderlist-model)15add_subdirectory(bookmarks-folderlist-model)
1716
=== modified file 'tests/unittests/qml/CMakeLists.txt'
--- tests/unittests/qml/CMakeLists.txt 2015-10-06 09:48:38 +0000
+++ tests/unittests/qml/CMakeLists.txt 2015-11-08 23:11:15 +0000
@@ -27,7 +27,6 @@
27 ${webbrowser-app_SOURCE_DIR}/history-timeframe-model.cpp27 ${webbrowser-app_SOURCE_DIR}/history-timeframe-model.cpp
28 ${webbrowser-app_SOURCE_DIR}/limit-proxy-model.cpp28 ${webbrowser-app_SOURCE_DIR}/limit-proxy-model.cpp
29 ${webbrowser-app_SOURCE_DIR}/searchengine.cpp29 ${webbrowser-app_SOURCE_DIR}/searchengine.cpp
30 ${webbrowser-app_SOURCE_DIR}/tabs-model.cpp
31 ${webbrowser-app_SOURCE_DIR}/text-search-filter-model.cpp30 ${webbrowser-app_SOURCE_DIR}/text-search-filter-model.cpp
32 ${webbrowser-app_SOURCE_DIR}/top-sites-model.cpp31 ${webbrowser-app_SOURCE_DIR}/top-sites-model.cpp
33 tst_QmlTests.cpp32 tst_QmlTests.cpp
3433
=== modified file 'tests/unittests/qml/tst_QmlTests.cpp'
--- tests/unittests/qml/tst_QmlTests.cpp 2015-10-22 15:07:26 +0000
+++ tests/unittests/qml/tst_QmlTests.cpp 2015-11-08 23:11:15 +0000
@@ -35,7 +35,6 @@
35#include "history-lastvisitdatelist-model.h"35#include "history-lastvisitdatelist-model.h"
36#include "limit-proxy-model.h"36#include "limit-proxy-model.h"
37#include "searchengine.h"37#include "searchengine.h"
38#include "tabs-model.h"
39#include "text-search-filter-model.h"38#include "text-search-filter-model.h"
40#include "top-sites-model.h"39#include "top-sites-model.h"
4140
@@ -189,7 +188,6 @@
189188
190 const char* browserUri = "webbrowserapp.private";189 const char* browserUri = "webbrowserapp.private";
191 qmlRegisterType<SearchEngine>(browserUri, 0, 1, "SearchEngine");190 qmlRegisterType<SearchEngine>(browserUri, 0, 1, "SearchEngine");
192 qmlRegisterType<TabsModel>(browserUri, 0, 1, "TabsModel");
193 qmlRegisterSingletonType<BookmarksModel>(browserUri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory);191 qmlRegisterSingletonType<BookmarksModel>(browserUri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory);
194 qmlRegisterType<BookmarksFolderListModel>(browserUri, 0, 1, "BookmarksFolderListModel");192 qmlRegisterType<BookmarksFolderListModel>(browserUri, 0, 1, "BookmarksFolderListModel");
195 qmlRegisterSingletonType<HistoryModel>(browserUri, 0, 1, "HistoryModel", HistoryModel_singleton_factory);193 qmlRegisterSingletonType<HistoryModel>(browserUri, 0, 1, "HistoryModel", HistoryModel_singleton_factory);
196194
=== added file 'tests/unittests/qml/tst_TabsModel.qml'
--- tests/unittests/qml/tst_TabsModel.qml 1970-01-01 00:00:00 +0000
+++ tests/unittests/qml/tst_TabsModel.qml 2015-11-08 23:11:15 +0000
@@ -0,0 +1,424 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import QtTest 1.0
21import "../../../src/app/webbrowser"
22
23Item {
24 width: 1
25 height: 1
26
27 property alias model: modelLoader.item
28
29 Loader {
30 id: modelLoader
31 active: false
32 asynchronous: false
33 sourceComponent: TabsModel { }
34 }
35
36 Component {
37 id: tab
38 Item {
39 property url url
40 property string title
41 property url icon
42 }
43 }
44
45 SignalSpy {
46 id: countSpy
47 target: model
48 signalName: "countChanged"
49 }
50
51 SignalSpy {
52 id: currentIndexSpy
53 target: model
54 signalName: "currentIndexChanged"
55 }
56
57 SignalSpy {
58 id: currentTabSpy
59 target: model
60 signalName: "currentTabChanged"
61 }
62
63 TestCase {
64 name: "TabsModel"
65 when: windowShown
66
67 function createTab()
68 {
69 return tab.createObject()
70 }
71
72 function init()
73 {
74 modelLoader.active = true
75 }
76
77 function cleanup()
78 {
79 modelLoader.active = false
80 countSpy.clear()
81 currentIndexSpy.clear()
82 currentTabSpy.clear()
83 }
84
85 function createTabWithTitle(title) {
86 var tab = createTab()
87 tab.title = title
88 return tab
89 }
90
91 function verifyTabsOrder(orderedTitles) {
92 compare(model.count, orderedTitles.length)
93 orderedTitles.forEach(function(title, i) {
94 compare(model.get(i).title, title)
95 })
96 }
97
98 function test_shouldBeInitiallyEmpty()
99 {
100 compare(model.count, 0)
101 compare(model.currentIndex, -1)
102 compare(model.currentTab, null)
103 }
104
105 function test_currentTabShouldBeNullOnInvalidIndex()
106 {
107 model.add(createTab())
108 model.remove(0)
109 compare(model.currentIndex, -1)
110 compare(model.currentTab, null)
111 }
112
113 function test_shouldNotAllowOutOfBoundsCurrentIndex()
114 {
115 model.currentIndex = 0;
116 compare(currentIndexSpy.count, 2)
117 compare(model.currentIndex, -1)
118
119 model.currentIndex = -2;
120 compare(currentIndexSpy.count, 4)
121 compare(model.currentIndex, -1)
122
123 // verify that when the model is not empty -1 is not a valid
124 // current index
125 model.add(createTab())
126 compare(model.currentIndex, 0)
127 currentIndexSpy.clear()
128 model.currentIndex = -1;
129 compare(currentIndexSpy.count, 2)
130 compare(model.currentIndex, 0)
131 }
132
133 function test_shouldNotAddNullTab()
134 {
135 ignoreWarning("Invalid Tab: null")
136 compare(model.add(null), -1)
137 compare(model.count, 0)
138 }
139
140 function test_shouldNotInsertTabAtInvalidIndex()
141 {
142 ignoreWarning("Invalid insert index: " + -1)
143 model.insert(createTab(), -1)
144 compare(model.count, 0)
145 compare(model.currentIndex, -1)
146 compare(model.currentTab, null)
147
148 ignoreWarning("Invalid insert index: " + 1)
149 model.insert(createTab(), 1)
150 compare(model.count, 0)
151 compare(model.currentIndex, -1)
152 compare(model.currentTab, null)
153 }
154
155 function test_shouldAppendWhenInsertingAtUndefinedIndex()
156 {
157 compare(model.insert(createTab()), 0)
158 compare(model.count, 1)
159
160 compare(model.insert(createTab(), undefined), 1)
161 compare(model.count, 2)
162 }
163
164 function test_shouldReturnIndexWhenAddingTab()
165 {
166 for (var i = 0; i < 3; ++i) {
167 compare(model.add(createTab()), i)
168 }
169 }
170
171 function test_shouldUpdateCountWhenAddingTab()
172 {
173 model.add(createTab())
174 compare(countSpy.count, 1)
175 compare(model.count, 1)
176 }
177
178 function test_shouldUpdateCountWhenRemovingTab()
179 {
180 model.add(createTab())
181 countSpy.clear()
182 delete model.remove(0)
183 compare(countSpy.count, 1)
184 compare(model.count, 0)
185 }
186
187 function test_shouldNotAllowRemovingAtInvalidIndex()
188 {
189 compare(model.remove(0), null)
190 compare(model.remove(2), null)
191 compare(model.remove(-2), null)
192 }
193
194 function test_shouldReturnTabWhenRemoving()
195 {
196 var tab = createTab()
197 model.add(tab)
198 var removed = model.remove(0)
199 compare(removed, tab)
200 delete removed
201 }
202
203 function test_shouldUpdateCurrentTabWhenSettingCurrentIndex()
204 {
205 var tab1 = createTab()
206 model.add(tab1)
207 compare(model.currentIndex, 0)
208 compare(currentTabSpy.count, 1)
209 compare(model.currentTab, tab1)
210
211 var tab2 = createTab()
212 model.add(tab2)
213 model.currentIndex = 1
214 compare(model.currentIndex, 1)
215 compare(currentTabSpy.count, 2)
216 compare(model.currentTab, tab2)
217 }
218
219 function test_shouldSetCurrentTabWhenAddingFirstTab()
220 {
221 // Adding a tab to an empty model should update the current tab
222 // to that tab
223 var tab1 = createTab()
224 model.add(tab1)
225
226 compare(currentTabSpy.count, 1)
227 compare(currentIndexSpy.count, 1)
228 compare(model.currentIndex, 0)
229 compare(model.currentTab, tab1)
230
231 // But adding further items should keep the index where it was
232 model.add(createTab())
233 model.add(createTab())
234
235 compare(currentTabSpy.count, 1)
236 compare(currentIndexSpy.count, 1)
237 compare(model.currentIndex, 0)
238 compare(model.currentTab, tab1)
239 }
240
241 function test_shouldSetCurrentTabWhenInsertingFirstTab()
242 {
243 // Inserting a tab to an empty model should update the current tab
244 // to that tab
245 var tab1 = createTab()
246 model.insert(tab1, 0)
247
248 compare(currentTabSpy.count, 1)
249 compare(currentIndexSpy.count, 1)
250 compare(model.currentIndex, 0)
251 compare(model.currentTab, tab1)
252 }
253
254 function test_shouldSetInvalidIndexWhenRemovingLastTab()
255 {
256 // Removing the last item should also set the current index to -1
257 // and the current tab to null
258 model.add(createTab())
259
260 currentTabSpy.clear()
261 currentIndexSpy.clear()
262 model.remove(0)
263 compare(currentTabSpy.count, 1)
264 compare(currentIndexSpy.count, 1)
265 compare(model.currentIndex, -1)
266 compare(model.currentTab, null)
267 }
268
269 function test_shouldNotChangeIndexWhenRemovingAfterCurrent()
270 {
271 // When removing a tab after the current one,
272 // the current tab shouldn’t change.
273 var tab1 = createTab()
274 model.add(tab1)
275 model.add(createTab())
276
277 currentTabSpy.clear()
278 currentIndexSpy.clear()
279 model.remove(1)
280 compare(model.currentTab, tab1)
281 compare(currentTabSpy.count, 0)
282 compare(currentIndexSpy.count, 0)
283 }
284
285 function test_shouldUpdateIndexWhenRemovingCurrent()
286 {
287 // When removing the current tab, if there is a tab after it,
288 // it becomes the current one.
289 var tab1 = createTab()
290 var tab2 = createTab()
291 var tab3 = createTab()
292 model.add(tab1)
293 model.add(tab2)
294 model.add(tab3)
295 model.currentIndex = 1
296 compare(model.currentIndex, 1)
297 compare(model.currentTab, tab2)
298
299 currentTabSpy.clear()
300 currentIndexSpy.clear()
301 model.remove(1)
302 compare(currentIndexSpy.count, 0)
303 compare(currentTabSpy.count, 1)
304 compare(model.currentTab, tab3)
305
306 // If there is no tab after it but one before, that one becomes current
307 model.remove(1)
308 compare(currentIndexSpy.count, 1)
309 compare(currentTabSpy.count, 2)
310 compare(model.currentIndex, 0)
311 compare(model.currentTab, tab1)
312 }
313
314 function test_shouldDecreaseIndexWhenRemovingBeforeCurrent()
315 {
316 // When removing a tab before the current tab, the current index
317 // should decrease to match.
318 model.add(createTab())
319 model.add(createTab())
320 var tab = createTab()
321 model.add(tab)
322 model.currentIndex = 2
323
324 currentTabSpy.clear()
325 currentIndexSpy.clear()
326 model.remove(1)
327 compare(currentTabSpy.count, 0)
328 compare(currentIndexSpy.count, 1)
329 compare(model.currentIndex, 1)
330 compare(model.currentTab, tab)
331 }
332
333 function test_shouldReturnTabAtIndex()
334 {
335 // valid indexes
336 var tab1 = createTab()
337 model.add(tab1)
338 var tab2 = createTab()
339 model.add(tab2)
340 var tab3 = createTab()
341 model.add(tab3)
342 compare(model.get(0), tab1)
343 compare(model.get(1), tab2)
344 compare(model.get(2), tab3)
345
346 // invalid indexes
347 compare(model.get(-1), null)
348 compare(model.get(3), null)
349 }
350
351 function test_shouldPreserveCurrentIndexAndTabWhenMoving_data()
352 {
353 return [
354 {initial: 0, from: 0, to: 2, result: 2}, // moving tab that has the current index
355 {initial: 1, from: 0, to: 2, result: 0}, // moving from before current index to after current index
356 {initial: 1, from: 2, to: 0, result: 2} // moving from after current index to before current index
357 ]
358 }
359 function test_shouldPreserveCurrentIndexAndTabWhenMoving(data)
360 {
361 for (var i = 0; i < 3; ++i) {
362 model.add(createTab())
363 }
364 model.currentIndex = data.initial
365 var currentTabInitial = model.currentTab
366 model.move(data.from, data.to)
367 compare(model.currentIndex, data.result)
368 compare(model.currentTab, currentTabInitial)
369 }
370
371 function test_shouldMoveToTopWhenSettingCurrentIndexInStackMode() {
372 var tab1 = createTab()
373 model.add(tab1)
374 var tab2 = createTab()
375 model.add(tab2)
376 var tab3 = createTab()
377 model.add(tab3)
378 model.behaveAsStack = true
379 compare(model.currentIndex, 0)
380
381 model.currentIndex = 2
382 compare(model.currentIndex, 0)
383 compare(model.currentTab, tab3)
384 compare(model.get(1), tab1)
385 compare(model.get(2), tab2)
386 }
387
388 function test_shouldNotInsertNullTab()
389 {
390 ignoreWarning("Invalid Tab: null")
391 compare(model.insert(null, 0), -1)
392 compare(model.count, 0)
393 }
394
395 function test_shouldReturnIndexWhenInsertingTab()
396 {
397 for (var i = 0; i < 3; ++i) {
398 model.add(createTab())
399 }
400
401 for (var i = 2; i >= 0; --i) {
402 compare(model.insert(createTab(), i), i)
403 }
404 }
405
406 function test_shouldUpdateCountWhenInsertingTab()
407 {
408 model.insert(createTab(), 0)
409 compare(countSpy.count, 1)
410 compare(model.count, 1)
411 }
412
413 function test_shouldInsertAtCorrectIndex()
414 {
415 model.insert(createTabWithTitle("B"), 0)
416 model.insert(createTabWithTitle("A"), 0)
417 verifyTabsOrder(["A", "B"])
418 model.insert(createTabWithTitle("X"), 1)
419 verifyTabsOrder(["A", "X", "B"])
420 model.insert(createTabWithTitle("C"), 3)
421 verifyTabsOrder(["A", "X", "B", "C"])
422 }
423 }
424}
0425
=== removed directory 'tests/unittests/tabs-model'
=== removed file 'tests/unittests/tabs-model/CMakeLists.txt'
--- tests/unittests/tabs-model/CMakeLists.txt 2015-06-22 10:29:20 +0000
+++ tests/unittests/tabs-model/CMakeLists.txt 1970-01-01 00:00:00 +0000
@@ -1,18 +0,0 @@
1find_package(Qt5Core REQUIRED)
2find_package(Qt5Qml REQUIRED)
3find_package(Qt5Quick REQUIRED)
4find_package(Qt5Sql REQUIRED)
5find_package(Qt5Test REQUIRED)
6set(TEST tst_TabsModelTests)
7add_executable(${TEST} tst_TabsModelTests.cpp)
8include_directories(${webbrowser-app_SOURCE_DIR})
9target_link_libraries(${TEST}
10 Qt5::Core
11 Qt5::Qml
12 Qt5::Quick
13 Qt5::Sql
14 Qt5::Test
15 webbrowser-app-models
16)
17add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
18set_tests_properties(${TEST} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal")
190
=== removed file 'tests/unittests/tabs-model/tst_TabsModelTests.cpp'
--- tests/unittests/tabs-model/tst_TabsModelTests.cpp 2015-09-23 17:14:30 +0000
+++ tests/unittests/tabs-model/tst_TabsModelTests.cpp 1970-01-01 00:00:00 +0000
@@ -1,493 +0,0 @@
1/*
2 * Copyright 2013-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19// Qt
20#include <QtCore/QStringList>
21#include <QtQml/QQmlComponent>
22#include <QtQml/QQmlEngine>
23#include <QtQml/QQmlProperty>
24#include <QtQuick/QQuickItem>
25#include <QtTest/QSignalSpy>
26#include <QtTest/QtTest>
27
28// local
29#include "tabs-model.h"
30
31class TabsModelTests : public QObject
32{
33 Q_OBJECT
34
35private:
36 TabsModel* model;
37
38 QQuickItem* createTab()
39 {
40 QQmlEngine engine;
41 QQmlComponent component(&engine);
42 QByteArray data("import QtQuick 2.4\nItem {\nproperty url url\n"
43 "property string title\nproperty url icon\n}");
44 component.setData(data, QUrl());
45 QObject* object = component.create();
46 object->setParent(this);
47 QQuickItem* item = qobject_cast<QQuickItem*>(object);
48 return item;
49 }
50
51 QQuickItem* createTabWithTitle(const QString& title) {
52 QQuickItem* tab = createTab();
53 tab->setProperty("title", title);
54 return tab;
55 }
56
57 void verifyTabsOrder(QStringList orderedTitles) {
58 QCOMPARE(model->rowCount(), orderedTitles.count());
59 int i = 0;
60 Q_FOREACH(QString title, orderedTitles) {
61 QCOMPARE(model->get(i++)->property("title").toString(), title);
62 }
63 }
64
65private Q_SLOTS:
66 void init()
67 {
68 model = new TabsModel;
69 }
70
71 void cleanup()
72 {
73 while (model->rowCount() > 0) {
74 delete model->remove(0);
75 }
76 delete model;
77 }
78
79 void shouldBeInitiallyEmpty()
80 {
81 QCOMPARE(model->rowCount(), 0);
82 QCOMPARE(model->currentTab(), (QObject*) nullptr);
83 }
84
85 void shouldExposeRoleNames()
86 {
87 QList<QByteArray> roleNames = model->roleNames().values();
88 QVERIFY(roleNames.contains("url"));
89 QVERIFY(roleNames.contains("title"));
90 QVERIFY(roleNames.contains("icon"));
91 QVERIFY(roleNames.contains("tab"));
92 }
93
94 void shouldNotAllowSettingTheIndexToAnInvalidValue_data()
95 {
96 QTest::addColumn<int>("index");
97 QTest::newRow("zero") << 0;
98 QTest::newRow("too high") << 2;
99 QTest::newRow("too low") << -2;
100 }
101
102 void shouldNotAllowSettingTheIndexToAnInvalidValue()
103 {
104 QFETCH(int, index);
105 model->setCurrentIndex(index);
106 QCOMPARE(model->currentIndex(), -1);
107 QCOMPARE(model->currentTab(), (QObject*) nullptr);
108 }
109
110 void shouldNotAddNullTab()
111 {
112 QCOMPARE(model->add(0), -1);
113 QCOMPARE(model->rowCount(), 0);
114 }
115
116 void shouldReturnIndexWhenAddingTab()
117 {
118 for(int i = 0; i < 3; ++i) {
119 QCOMPARE(model->add(createTab()), i);
120 }
121 }
122
123 void shouldUpdateCountWhenAddingTab()
124 {
125 QSignalSpy spy(model, SIGNAL(countChanged()));
126 model->add(createTab());
127 QCOMPARE(spy.count(), 1);
128 QCOMPARE(model->rowCount(), 1);
129 }
130
131 void shouldNotInsertNullTab()
132 {
133 QCOMPARE(model->insert(0, 0), -1);
134 QCOMPARE(model->rowCount(), 0);
135 }
136
137 void shouldReturnIndexWhenInsertingTab()
138 {
139 for(int i = 0; i < 3; ++i) {
140 model->add(createTab());
141 }
142 for(int i = 2; i >= 0; --i) {
143 QCOMPARE(model->insert(createTab(), i), i);
144 }
145 }
146
147 void shouldUpdateCountWhenInsertingTab()
148 {
149 QSignalSpy spy(model, SIGNAL(countChanged()));
150 model->insert(createTab(), 0);
151 QCOMPARE(spy.count(), 1);
152 QCOMPARE(model->rowCount(), 1);
153 }
154
155 void shouldInsertAtCorrectIndex()
156 {
157 model->insert(createTabWithTitle("B"), 0);
158 model->insert(createTabWithTitle("A"), 0);
159 verifyTabsOrder(QStringList({"A", "B"}));
160 model->insert(createTabWithTitle("X"), 1);
161 verifyTabsOrder(QStringList({"A", "X", "B"}));
162 model->insert(createTabWithTitle("C"), 3);
163 verifyTabsOrder(QStringList({"A", "X", "B", "C"}));
164 }
165
166 void shouldClampIndexWhenInsertingTabOutOfBounds()
167 {
168 model->add(createTabWithTitle("A"));
169 model->add(createTabWithTitle("B"));
170 model->insert(createTabWithTitle("C"), 3);
171 verifyTabsOrder(QStringList({"A", "B", "C"}));
172 model->insert(createTabWithTitle("X"), -1);
173 verifyTabsOrder(QStringList({"X", "A", "B", "C"}));
174 }
175
176 void shouldUpdateCountWhenRemovingTab()
177 {
178 model->add(createTab());
179 QSignalSpy spy(model, SIGNAL(countChanged()));
180 delete model->remove(0);
181 QCOMPARE(spy.count(), 1);
182 QCOMPARE(model->rowCount(), 0);
183 }
184
185 void shouldNotAllowRemovingAtInvalidIndex()
186 {
187 QCOMPARE(model->remove(0), (QObject*) nullptr);
188 QCOMPARE(model->remove(2), (QObject*) nullptr);
189 QCOMPARE(model->remove(-2), (QObject*) nullptr);
190 }
191
192 void shouldReturnTabWhenRemoving()
193 {
194 QQuickItem* tab = createTab();
195 model->add(tab);
196 QObject* removed = model->remove(0);
197 QCOMPARE(removed, tab);
198 delete removed;
199 }
200
201 void shouldNotDeleteTabWhenRemoving()
202 {
203 QQuickItem* tab = createTab();
204 model->add(tab);
205 model->remove(0);
206 QCOMPARE(tab->parent(), this);
207 delete tab;
208 }
209
210 void shouldNotifyWhenAddingTab()
211 {
212 QSignalSpy spy(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)));
213 for(int i = 0; i < 3; ++i) {
214 model->add(createTab());
215 QCOMPARE(spy.count(), 1);
216 QList<QVariant> args = spy.takeFirst();
217 QCOMPARE(args.at(1).toInt(), i);
218 QCOMPARE(args.at(2).toInt(), i);
219 }
220 }
221
222 void shouldNotifyWhenRemovingTab()
223 {
224 QSignalSpy spy(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)));
225 for(int i = 0; i < 5; ++i) {
226 model->add(createTab());
227 }
228 delete model->remove(3);
229 QCOMPARE(spy.count(), 1);
230 QList<QVariant> args = spy.takeFirst();
231 QCOMPARE(args.at(1).toInt(), 3);
232 QCOMPARE(args.at(2).toInt(), 3);
233 for(int i = 3; i >= 0; --i) {
234 delete model->remove(i);
235 QCOMPARE(spy.count(), 1);
236 args = spy.takeFirst();
237 QCOMPARE(args.at(1).toInt(), i);
238 QCOMPARE(args.at(2).toInt(), i);
239 }
240 }
241
242 void shouldNotifyWhenTabPropertiesChange()
243 {
244 qRegisterMetaType<QVector<int> >();
245 QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
246 QQuickItem* tab = createTab();
247 model->add(tab);
248
249 QQmlProperty(tab, "url").write(QUrl("http://ubuntu.com"));
250 QCOMPARE(spy.count(), 1);
251 QList<QVariant> args = spy.takeFirst();
252 QCOMPARE(args.at(0).toModelIndex().row(), 0);
253 QCOMPARE(args.at(1).toModelIndex().row(), 0);
254 QVector<int> roles = args.at(2).value<QVector<int> >();
255 QCOMPARE(roles.size(), 1);
256 QVERIFY(roles.contains(TabsModel::Url));
257
258 QQmlProperty(tab, "title").write(QString("Lorem Ipsum"));
259 QCOMPARE(spy.count(), 1);
260 args = spy.takeFirst();
261 QCOMPARE(args.at(0).toModelIndex().row(), 0);
262 QCOMPARE(args.at(1).toModelIndex().row(), 0);
263 roles = args.at(2).value<QVector<int> >();
264 QCOMPARE(roles.size(), 1);
265 QVERIFY(roles.contains(TabsModel::Title));
266
267 QQmlProperty(tab, "icon").write(QUrl("image://webicon/123"));
268 QCOMPARE(spy.count(), 1);
269 args = spy.takeFirst();
270 QCOMPARE(args.at(0).toModelIndex().row(), 0);
271 QCOMPARE(args.at(1).toModelIndex().row(), 0);
272 roles = args.at(2).value<QVector<int> >();
273 QCOMPARE(roles.size(), 1);
274 QVERIFY(roles.contains(TabsModel::Icon));
275 }
276
277 void shouldUpdateCurrentTabWhenSettingCurrentIndex()
278 {
279 QQuickItem* tab1 = createTab();
280 model->add(tab1);
281 QSignalSpy spy(model, SIGNAL(currentTabChanged()));
282 model->setCurrentIndex(0);
283 QCOMPARE(model->currentIndex(), 0);
284 QVERIFY(spy.isEmpty());
285 QCOMPARE(model->currentTab(), tab1);
286 QQuickItem* tab2 = createTab();
287 model->add(tab2);
288 model->setCurrentIndex(1);
289 QCOMPARE(model->currentIndex(), 1);
290 QCOMPARE(spy.count(), 1);
291 QCOMPARE(model->currentTab(), tab2);
292 }
293
294 void shouldSetCurrentTabWhenAddingFirstTab()
295 {
296 // Adding a tab to an empty model should update the current tab
297 // to that tab
298 QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
299 QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
300 QCOMPARE(model->currentIndex(), -1);
301 QCOMPARE(model->currentTab(), (QObject*) nullptr);
302
303 QQuickItem* tab1 = createTab();
304 model->add(tab1);
305
306 QCOMPARE(spytab.count(), 1);
307 QCOMPARE(spyindex.count(), 1);
308 QCOMPARE(model->currentIndex(), 0);
309 QCOMPARE(model->currentTab(), tab1);
310
311 // But adding further items should keep the index where it was
312 model->add(createTab());
313 model->add(createTab());
314
315 QCOMPARE(spytab.count(), 1);
316 QCOMPARE(spyindex.count(), 1);
317 QCOMPARE(model->currentIndex(), 0);
318 QCOMPARE(model->currentTab(), tab1);
319 }
320
321 void shouldSetCurrentTabWhenInsertingFirstTab()
322 {
323 // Inserting a tab to an empty model should update the current tab
324 // to that tab
325 QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
326 QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
327 QCOMPARE(model->currentIndex(), -1);
328 QCOMPARE(model->currentTab(), (QObject*) nullptr);
329
330 QQuickItem* tab1 = createTab();
331 model->insert(tab1, 0);
332
333 QCOMPARE(spytab.count(), 1);
334 QCOMPARE(spyindex.count(), 1);
335 QCOMPARE(model->currentIndex(), 0);
336 QCOMPARE(model->currentTab(), tab1);
337 }
338
339 void shouldSetInvalidIndexWhenRemovingLastTab()
340 {
341 // Removing the last item should also set the current index to -1
342 // and the current tab to null
343 model->add(createTab());
344
345 QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
346 QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
347 delete model->remove(0);
348 QCOMPARE(spytab.count(), 1);
349 QCOMPARE(spyindex.count(), 1);
350 QCOMPARE(model->currentIndex(), -1);
351 QCOMPARE(model->currentTab(), (QObject*) nullptr);
352 }
353
354 void shouldNotChangeIndexWhenRemovingAfterCurrent()
355 {
356 // When removing a tab after the current one,
357 // the current tab shouldn’t change.
358 QQuickItem* tab1 = createTab();
359 model->add(tab1);
360 model->add(createTab());
361
362 QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
363 QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
364 delete model->remove(1);
365 QCOMPARE(model->currentTab(), tab1);
366 QVERIFY(spytab.isEmpty());
367 QVERIFY(spyindex.isEmpty());
368 }
369
370 void shouldUpdateIndexWhenRemovingCurrent()
371 {
372 // When removing the current tab, if there is a tab after it,
373 // it becomes the current one.
374 QQuickItem* tab1 = createTab();
375 QQuickItem* tab2 = createTab();
376 QQuickItem* tab3 = createTab();
377 model->add(tab1);
378 model->add(tab2);
379 model->add(tab3);
380 model->setCurrentIndex(1);
381 QCOMPARE(model->currentIndex(), 1);
382 QCOMPARE(model->currentTab(), tab2);
383
384 QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
385 QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
386 delete model->remove(1);
387 QCOMPARE(spyindex.count(), 0);
388 QCOMPARE(spytab.count(), 1);
389 QCOMPARE(model->currentTab(), tab3);
390
391 // If there is no tab after it but one before, that one becomes current
392 delete model->remove(1);
393 QCOMPARE(spyindex.count(), 1);
394 QCOMPARE(spytab.count(), 2);
395 QCOMPARE(model->currentIndex(), 0);
396 QCOMPARE(model->currentTab(), tab1);
397 }
398
399 void shouldDecreaseIndexWhenRemovingBeforeCurrent()
400 {
401 // When removing a tab before the current tab, the current index
402 // should decrease to match.
403 model->add(createTab());
404 model->add(createTab());
405 QQuickItem* tab = createTab();
406 model->add(tab);
407 model->setCurrentIndex(2);
408
409 QSignalSpy spytab(model, SIGNAL(currentTabChanged()));
410 QSignalSpy spyindex(model, SIGNAL(currentIndexChanged()));
411 delete model->remove(1);
412 QVERIFY(spytab.isEmpty());
413 QCOMPARE(spyindex.count(), 1);
414 QCOMPARE(model->currentIndex(), 1);
415 QCOMPARE(model->currentTab(), tab);
416 }
417
418 void shouldReturnData()
419 {
420 QQuickItem* tab = createTab();
421 QQmlProperty(tab, "url").write(QUrl("http://ubuntu.com/"));
422 QQmlProperty(tab, "title").write(QString("Lorem Ipsum"));
423 QQmlProperty(tab, "icon").write(QUrl("image://webicon/123"));
424 model->add(tab);
425 QVERIFY(!model->data(QModelIndex(), TabsModel::Url).isValid());
426 QVERIFY(!model->data(model->index(-1, 0), TabsModel::Url).isValid());
427 QVERIFY(!model->data(model->index(3, 0), TabsModel::Url).isValid());
428 QCOMPARE(model->data(model->index(0, 0), TabsModel::Url).toUrl(), QUrl("http://ubuntu.com/"));
429 QCOMPARE(model->data(model->index(0, 0), TabsModel::Title).toString(), QString("Lorem Ipsum"));
430 QCOMPARE(model->data(model->index(0, 0), TabsModel::Icon).toUrl(), QUrl("image://webicon/123"));
431 QCOMPARE(model->data(model->index(0, 0), TabsModel::Tab).value<QQuickItem*>(), tab);
432 QVERIFY(!model->data(model->index(0, 0), TabsModel::Tab + 3).isValid());
433 }
434
435 void shouldReturnTabAtIndex()
436 {
437 // valid indexes
438 QQuickItem* tab1 = createTab();
439 model->add(tab1);
440 QQuickItem* tab2 = createTab();
441 model->add(tab2);
442 QQuickItem* tab3 = createTab();
443 model->add(tab3);
444 QCOMPARE(model->get(0), tab1);
445 QCOMPARE(model->get(1), tab2);
446 QCOMPARE(model->get(2), tab3);
447
448 // invalid indexes
449 QCOMPARE(model->get(-1), (QObject*) nullptr);
450 QCOMPARE(model->get(3), (QObject*) nullptr);
451 }
452
453private:
454 void moveTabs(int from, int to, bool moved, bool indexChanged, int newIndex)
455 {
456 QSignalSpy spyMoved(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)));
457 QSignalSpy spyIndex(model, SIGNAL(currentIndexChanged()));
458 QSignalSpy spyTab(model, SIGNAL(currentTabChanged()));
459
460 model->move(from, to);
461 QCOMPARE(spyMoved.count(), moved ? 1 : 0);
462 if (moved) {
463 QList<QVariant> args = spyMoved.takeFirst();
464 QCOMPARE(args.at(1).toInt(), from);
465 QCOMPARE(args.at(2).toInt(), from);
466 QCOMPARE(args.at(4).toInt(), to);
467 }
468 QCOMPARE(spyIndex.count(), indexChanged ? 1 : 0);
469 QCOMPARE(model->currentIndex(), newIndex);
470 QVERIFY(spyTab.isEmpty());
471 }
472
473private Q_SLOTS:
474 void shouldNotifyWhenMovingTabs()
475 {
476 for (int i = 0; i < 3; ++i) {
477 model->add(createTab());
478 }
479
480 moveTabs(1, 1, false, false, 0);
481 moveTabs(4, 1, false, false, 0);
482 moveTabs(1, 4, false, false, 0);
483 moveTabs(0, 2, true, true, 2);
484 moveTabs(1, 0, true, false, 2);
485 moveTabs(2, 1, true, true, 1);
486 moveTabs(2, 0, true, true, 2);
487 moveTabs(2, 1, true, true, 1);
488 moveTabs(0, 2, true, true, 0);
489 }
490};
491
492QTEST_MAIN(TabsModelTests)
493#include "tst_TabsModelTests.moc"

Subscribers

People subscribed via source and target branches

to status/vote changes: