Merge lp:~uriboni/webbrowser-app/newtabview-listviews into lp:webbrowser-app
- newtabview-listviews
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~uriboni/webbrowser-app/newtabview-listviews |
Merge into: | lp:webbrowser-app |
Prerequisite: | lp:~artmello/webbrowser-app/webbrowser-app-bookmarks_view |
Diff against target: |
2461 lines (+1569/-407) 25 files modified
src/app/webbrowser/BookmarksFoldersView.qml (+151/-140) src/app/webbrowser/BookmarksFoldersViewWide.qml (+1/-2) src/app/webbrowser/BookmarksHeader.qml (+89/-0) src/app/webbrowser/BookmarksSection.qml (+74/-0) src/app/webbrowser/CMakeLists.txt (+2/-0) src/app/webbrowser/NewTabView.qml (+50/-120) src/app/webbrowser/UrlsList.qml (+40/-22) src/app/webbrowser/bookmarks-folderlist-model.cpp (+5/-0) src/app/webbrowser/bookmarks-folderlist-model.h (+1/-0) src/app/webbrowser/bookmarks-model.cpp (+12/-0) src/app/webbrowser/bookmarks-model.h (+5/-1) src/app/webbrowser/list-aggregator-model.cpp (+240/-0) src/app/webbrowser/list-aggregator-model.h (+82/-0) src/app/webbrowser/roles-adapter-model.cpp (+111/-0) src/app/webbrowser/roles-adapter-model.h (+50/-0) src/app/webbrowser/webbrowser-app.cpp (+4/-0) tests/autopilot/webbrowser_app/emulators/browser.py (+48/-17) tests/autopilot/webbrowser_app/tests/test_bookmark_options.py (+15/-15) tests/autopilot/webbrowser_app/tests/test_new_tab_view.py (+97/-89) tests/unittests/CMakeLists.txt (+2/-0) tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp (+6/-1) tests/unittests/list-aggregator-model/CMakeLists.txt (+13/-0) tests/unittests/list-aggregator-model/tst_ListAggregatorModelTests.cpp (+361/-0) tests/unittests/roles-adapter-model/CMakeLists.txt (+13/-0) tests/unittests/roles-adapter-model/tst_RolesAdapterModelTests.cpp (+97/-0) |
To merge this branch: | bzr merge lp:~uriboni/webbrowser-app/newtabview-listviews |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
system-apps-ci-bot | continuous-integration | Needs Fixing | |
PS Jenkins bot | continuous-integration | Needs Fixing | |
Olivier Tilloy | Needs Fixing | ||
Review via email: mp+274986@code.launchpad.net |
Commit message
Refactor BookmarksFolder
Description of the change
Refactor BookmarksFolder
PS Jenkins bot (ps-jenkins) wrote : | # |
- 1250. By Ugo Riboni
-
Fix AP tests
- 1251. By Ugo Riboni
-
Merge changes from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1251
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1252. By Ugo Riboni
-
Fix more AP tests
- 1253. By Ugo Riboni
-
Merge changes from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1252
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1253
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1254. By Ugo Riboni
-
Merge changes from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1254
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1255. By Ugo Riboni
-
Merge changes from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1255
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
This works, but is a rather complex implementation.
I’ve taken a stab at something much simpler that doesn’t involve aggregator models: the idea is to use a SortFilterModel indeed to sort bookmarks by folder, but instead of filtering out bookmarks for which the folder is collapsed (which as you pointed out wouldn’t work because the corresponding section wouldn’t be displayed by the list view), I’m keeping track of expanded/collapsed state of each section in a separate dictionary, and delegates in the list view are visible or not depending on that state.
This results in much simpler code, with no need for custom C++ models, all is done in QML.
The only thing that this doesn’t achieve, afaict, is prepending the homepage to the model as a hardcoded bookmark. I’ve been thinking about that, and in fact I think modifying the model is the wrong approach. We can leverage the 'header' property of ListView to display a first hardcoded item that’s not in the model.
See a fully functional standalone example at http://
Comments welcome.
Olivier Tilloy (osomon) wrote : | # |
I did some profiling on my laptop, comparing the performance of this branch and trunk with a large bookmarks database.
The first set of tests was with a database of 10000 bookmarks (100 folders with 100 URLs in each). In that case, this branch performs slightly better than trunk. The binding on internal.
With a database of 100000 bookmarks (100 folders with 1000 URLs in each), trunk performs significantly better than this branch. The binding on internal.
If we want to optimize for large bookmark databases, this branch doesn’t appear to be improving things, quite the contrary. All of this is anecdotal when we look at the time it takes for the application itself to launch and become responsive though. With 100000 bookmarks, the app takes ~20s to launch. So if we really want to optimize for this use case, we need to remove the loading of the model from the critical startup path, before doing any optimization work on the bookmarks view.
The databases I used for profiling are available there: http://
Olivier Tilloy (osomon) wrote : | # |
Small correction: the application takes ~20s to launch only if the current tab is a new tab view (e.g. if I pressed Ctrl+Tab in the last browsing session, then exited the browser, and launched it again). If it’s a normal tab (with a page loaded), there’s no noticeable slowdown (which is good).
Olivier Tilloy (osomon) wrote : | # |
I have profiled the top-level bookmarks view (was profiling the one embedded in the new tab view before), and I’m seeing similar results.
Olivier Tilloy (osomon) wrote : | # |
I have profiled the solution I was suggesting in my first comment. With a database of 100000 bookmarks, instantiating the entire view takes 600ms, and it is instantly responsive.
It works well when all sections are expanded, but it slows down considerably when all sections are collapsed (presumably because then there are 1000 invisible delegates instantiated for each section, so if there are 10 collapsed sections on screen that’s 10000 delegates instantiated at any given time).
On top of that the layout of the view is incorrect after expanding/
Nested list views may not be such a bad idea after all…
- 1256. By Ugo Riboni
-
Remove another startup bottleneck by turning the UrlsList into a ListView fed by a LimitProxyModel with the home page bookmarks aggregated at the start. This keeps NewTabView lean at startup.
- 1257. By Ugo Riboni
-
Make sure that data change signals from the aggregated models are propagated correctly
- 1258. By Ugo Riboni
-
Fix some AP tests after changes in UrlsList
- 1259. By Ugo Riboni
-
Fix one more stray reference to a non-singleton BookmarksModel
Ugo Riboni (uriboni) wrote : | # |
> I have profiled the solution I was suggesting in my first comment. With a
> database of 100000 bookmarks, instantiating the entire view takes 600ms, and
> it is instantly responsive.
>
> It works well when all sections are expanded, but it slows down considerably
> when all sections are collapsed (presumably because then there are 1000
> invisible delegates instantiated for each section, so if there are 10
> collapsed sections on screen that’s 10000 delegates instantiated at any given
> time).
All the solutions you proposed are not removing the main problem, which is that the performance is still linearly proportional to the number of bookmarks. By making the delegates leaner you are making the degrade in performance slower as the number of bookmarks grow, but the point is that we can't control the number of bookmarks, so the problem is never really solved.
Moreover you are testing with 1K bookmarks per folder, while you will see the app being completely unusable if you put 10K bookmarks in a folder and try to open it (or put them in the root folder, which is open when you open the view).
> On top of that the layout of the view is incorrect after expanding/
> a section, because the implementation of ListView computes an estimate of its
> contentHeight when using sections, instead of using the real content height.
Hiding delegates by making their height zero is essentially an hack, in my opinion: it works in some cases but it is not robust and/or scalable, and denies what is arguably the main benefit of using ListView in the first place: delegates are created and destroyed on-demand.
> Nested list views may not be such a bad idea after all…
How would you go about implementing nested ListViews ? A ListView to work needs to know the height of its delegates, and if each delegate is a ListView then they need to have height equal to contentHeight, which instantiates all the sub-list delegates. You can limit the height of each sub-ListView to a certain fixed amount, but then you have scrolling problems because either you scroll the main ListView or the sub-ListView.
Ugo Riboni (uriboni) wrote : | # |
> If we want to optimize for large bookmark databases, this branch doesn’t
> appear to be improving things, quite the contrary. All of this is anecdotal
> when we look at the time it takes for the application itself to launch and
> become responsive though. With 100000 bookmarks, the app takes ~20s to launch [when the new tab view is visible]
The problem was that the collapsed bookmarks view was still a Column+
With this change, I can't find any major bottleneck that prevent the app to be usable in any condition.
Ugo Riboni (uriboni) wrote : | # |
> The binding on internal.
> bookmarksFolder
> with trunk. But more importantly, it takes ~7s for the view to load and be
> responsive with this branch, compared to ~3s with trunk.
This bottleneck is essentially the time that it takes to expand the bookmarks view.
I did some tests with your 100K model, which has 100 folders, and another model which has 100K bookmarks but no folders.
Yours took 220ms on my machine, the other 8ms.
What is causing the difference is having to allocate each single folder/section placeholder model.
So basically the problem is now shifted from making the performance proportional to the number of bookmarks, to having the performance proportional to the number of folders. It is less of a problem but of course still a problem since we can't control the number of folders that the user can create.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1256
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1260. By Ugo Riboni
-
Prevent the home bookmark from being deleted
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1259
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1260
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1261. By Ugo Riboni
-
Fix the utility method removing the first real bookmark in the list by taking into account the peculiarity of working with a LimitProxyModel
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1261
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1261
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:1261
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 1262. By Ugo Riboni
-
Merge changes from trunk
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1262
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
308 + removable: url != homeBookmarkUrl
This condition is not correct: what if the user has added the homepage as a bookmark, manually? They should be allowed to remove it. It would probably be more correct to make that depend on the index (i.e. only the first in the list cannot be removed).
Can we consider adding the homepage bookmark to the BookmarksModel at the C++ level, instead of jumping through hoops to prepend it in javascript? We could add a 'removable' role to the model.
Olivier Tilloy (osomon) wrote : | # |
Reflecting further on the problem we’re trying to solve, I just had an idea (just thinking out loud here, let’s see if you think it can fly):
How about writing a custom proxy model that uses the BookmarksModel as input, and outputs a list that contains folders (collapsed or expanded) and bookmarks, in the expected order? There would be an additional role that describes what an entry is (is it a bookmark, is it a collapsed folder, or an expanded folder?). The model would have to keep track of the expanded/status state of each folder, and there would be a method to toggle the expanded/collapsed state of a folder (which would result in row insertions/
If I understand correctly what you did, the idea is similar, but much less generic than your implementation, and all done in C++ as opposed to a mix of C++ and QML/JS. It also doesn’t use sections, which should make it simpler (and maybe more efficient).
This would also allow adding the hardcoded homepage bookmark without modifying the source BookmarksModel.
How does that sound? Have I overlooked important details?
Olivier Tilloy (osomon) wrote : | # |
Here is the merge request that implements the idea I described above: https:/
With a single model, only the initial instantiation of the model is costly (260ms for 100000 entries, 40ms for 10000 entries), there’s no additional overhead.
The code ended up slightly more complex than I anticipated, because I made it possible to change at runtime whether to show empty folders, and whether to prepend the homepage bookmark. That’s more code and complexity, but it’s 100% unit tested, and shields us from future design changes.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:1262
https:/
Executed test runs:
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
Unmerged revisions
- 1262. By Ugo Riboni
-
Merge changes from trunk
- 1261. By Ugo Riboni
-
Fix the utility method removing the first real bookmark in the list by taking into account the peculiarity of working with a LimitProxyModel
- 1260. By Ugo Riboni
-
Prevent the home bookmark from being deleted
- 1259. By Ugo Riboni
-
Fix one more stray reference to a non-singleton BookmarksModel
- 1258. By Ugo Riboni
-
Fix some AP tests after changes in UrlsList
- 1257. By Ugo Riboni
-
Make sure that data change signals from the aggregated models are propagated correctly
- 1256. By Ugo Riboni
-
Remove another startup bottleneck by turning the UrlsList into a ListView fed by a LimitProxyModel with the home page bookmarks aggregated at the start. This keeps NewTabView lean at startup.
- 1255. By Ugo Riboni
-
Merge changes from trunk
- 1254. By Ugo Riboni
-
Merge changes from trunk
- 1253. By Ugo Riboni
-
Merge changes from trunk
Preview Diff
1 | === modified file 'src/app/webbrowser/BookmarksFoldersView.qml' |
2 | --- src/app/webbrowser/BookmarksFoldersView.qml 2015-10-22 08:12:32 +0000 |
3 | +++ src/app/webbrowser/BookmarksFoldersView.qml 2015-11-16 09:11:43 +0000 |
4 | @@ -27,163 +27,174 @@ |
5 | |
6 | property alias interactive: bookmarksFolderListView.interactive |
7 | property url homeBookmarkUrl |
8 | + property alias headerComponent: bookmarksFolderListView.header |
9 | |
10 | signal bookmarkClicked(url url) |
11 | signal bookmarkRemoved(url url) |
12 | |
13 | - height: bookmarksFolderListView.contentHeight |
14 | + /* The main model feeding this view is based on an aggregation of all the |
15 | + entries for each folder in a BookmarksFolderListModel. |
16 | + |
17 | + The reason we do this instead of using a SortFilterModel to re-order the |
18 | + BookmarkModel by folder name, is because we need to be able to "collapse" |
19 | + (i.e. hide) the folders. This we could still have done with a SortFilterModel |
20 | + by setting its filter to exclude the folders we wanted to hide. |
21 | + |
22 | + However the ListView needs to have at least 1 item inside a section to |
23 | + display the section at all, so we need to replace the hidden elements |
24 | + with placeholders, and this seems impossible with a SortFilterModel. |
25 | + |
26 | + We also need to aggregate the homepage bookmark to the root folder, and |
27 | + again using a SortFilterModel would not allow this. |
28 | + */ |
29 | + ListAggregatorModel { |
30 | + id: aggregator |
31 | + } |
32 | |
33 | BookmarksFolderListModel { |
34 | - id: bookmarksFolderListModel |
35 | + id: folders |
36 | sourceModel: BookmarksModel |
37 | + Component.onCompleted: { for (var i = 0; i < folders.count; i++) insertFolderAt(i) } |
38 | + onRowsInserted: { for (var i = first; i <= last; i++) insertFolderAt(i) } |
39 | + // NOTE: There is currently no way to remove bookmark folders from the sourceModel, |
40 | + // therefore onRowsRemoved is not implemented, and placeholders are destroyed |
41 | + // together with the view (as they are parented to it) |
42 | + |
43 | + function insertFolderAt(i) { |
44 | + var folder = folders.get(i) |
45 | + |
46 | + /* The ListView won't render a section at all if there isn't at least one item |
47 | + within that section. Therefore we need to use invisible section placeholder |
48 | + items when the section is collapsed. We do this by removing from the aggregation |
49 | + the list of items and replacing it with our placeholder: an instance of a |
50 | + ListModel with a single item with the right folder name. |
51 | + We store these placeholders in the "sections" list for convenience, |
52 | + together with the expanded state of the section they would belong to. */ |
53 | + var section = { |
54 | + // The first section starts expanded, by design. We also keep expanded any |
55 | + // sections where the folder has no items, so that the ListView will totally |
56 | + // hide them. This is also required by design. |
57 | + expanded: i === 0 || folder.entries.count === 0, |
58 | + placeholder: sectionPlaceholderModel.createObject(bookmarksFoldersViewItem, {}) |
59 | + } |
60 | + section.placeholder.sourceModel.setProperty(0, "folderName", folder.folderName); |
61 | + sections.insert(i, section) |
62 | + |
63 | + aggregator.insertModel(section.expanded ? sections.entriesFor(folder) : section.placeholder, i) |
64 | + } |
65 | + } |
66 | + |
67 | + // This model will aggregate the home bookmark (from homeBookmarkModel below) and |
68 | + // the root folder (i.e. the "All bookmarks" folder) |
69 | + ListAggregatorModel { |
70 | + id: rootFolderModel |
71 | + } |
72 | + |
73 | + RolesAdapterModel { |
74 | + id: homeBookmarkModel |
75 | + rolesSource: BookmarksModel |
76 | + sourceModel: ListModel { |
77 | + objectName: "rootfolder" |
78 | + ListElement { folderName: "__"; folder: ""; title: ""; url: ""; icon: "" } |
79 | + } |
80 | + } |
81 | + |
82 | + ListModel { |
83 | + id: sections |
84 | + |
85 | + /* Only the section property value is exposed to the section delegate. |
86 | + Our sections are unique since the model is ordered by section, so we |
87 | + can safely iterate until we find the section in the section list. */ |
88 | + function indexOf(section) { |
89 | + for (var i = 0; i < folders.count; i++) { |
90 | + if (folders.get(i).folderName === section) { |
91 | + return i; |
92 | + } |
93 | + } |
94 | + return -1; |
95 | + } |
96 | + |
97 | + function toggle(index) { |
98 | + var wasExpanded = sections.get(index).expanded |
99 | + aggregator.removeModel(index) |
100 | + var newItem = wasExpanded ? sections.get(index).placeholder : |
101 | + entriesFor(folders.get(index)) |
102 | + aggregator.insertModel(newItem, index) |
103 | + sections.setProperty(index, "expanded", !wasExpanded) |
104 | + } |
105 | + |
106 | + function entriesFor(folder) { |
107 | + if (folder.folderName === "__") { |
108 | + if (rootFolderModel.count === 0) { |
109 | + rootFolderModel.insertModel(folder.entries, 0) |
110 | + |
111 | + // ListElement only accept constants at declaration, so we need |
112 | + // to define these here before aggregating |
113 | + homeBookmarkModel.sourceModel.setProperty(0, "url", homeBookmarkUrl.toString()) |
114 | + homeBookmarkModel.sourceModel.setProperty(0, "title", i18n.tr("Homepage")) |
115 | + rootFolderModel.insertModel(homeBookmarkModel, 0) |
116 | + } |
117 | + return rootFolderModel |
118 | + } else return folder.entries |
119 | + } |
120 | + } |
121 | + |
122 | + Component { |
123 | + id: sectionPlaceholderModel |
124 | + /* We need to use a RolesAdapterModel here because ListElements have |
125 | + roles with values starting at 0, while generally our models |
126 | + implemented in C++ have roles starting at Qt::UserRole + 1 */ |
127 | + RolesAdapterModel { |
128 | + rolesSource: BookmarksModel |
129 | + sourceModel: ListModel { |
130 | + // Provide empty values to define role names, so we get no |
131 | + // warnings when using them as placeholders |
132 | + ListElement { folderName: ""; folder: ""; title: ""; url: ""; icon: "" } |
133 | + } |
134 | + } |
135 | } |
136 | |
137 | ListView { |
138 | id: bookmarksFolderListView |
139 | anchors.fill: parent |
140 | - interactive: false |
141 | focus: true |
142 | |
143 | - model: bookmarksFolderListModel |
144 | - delegate: Loader { |
145 | + model: aggregator |
146 | + section.property: "folderName" |
147 | + section.delegate: BookmarksSection { |
148 | + objectName: "bookmarkFolderDelegate" |
149 | + folderName: section |
150 | + height: units.gu(6.5) |
151 | + |
152 | anchors { |
153 | left: parent.left |
154 | right: parent.right |
155 | - } |
156 | - |
157 | - height: active ? item.height : 0 |
158 | - active: entries.count > 0 |
159 | - |
160 | - sourceComponent: Item { |
161 | - objectName: "bookmarkFolderDelegate" |
162 | - |
163 | - property string folderName: folder |
164 | - |
165 | - anchors { |
166 | - left: parent ? parent.left : undefined |
167 | - right: parent ? parent.right : undefined |
168 | - } |
169 | - |
170 | - height: delegateColumn.height |
171 | - |
172 | - Column { |
173 | - id: delegateColumn |
174 | - |
175 | - property bool expanded: folderName ? false : true |
176 | - |
177 | - anchors { |
178 | - left: parent.left |
179 | - right: parent.right |
180 | - } |
181 | - |
182 | - Item { |
183 | - objectName: "bookmarkFolderHeader" |
184 | - |
185 | - anchors { |
186 | - left: parent.left |
187 | - right: parent.right |
188 | - leftMargin: units.gu(2) |
189 | - rightMargin: units.gu(2) |
190 | - } |
191 | - |
192 | - height: units.gu(6.5) |
193 | - |
194 | - Row { |
195 | - anchors { |
196 | - left: parent.left |
197 | - leftMargin: units.gu(1.5) |
198 | - right: parent.right |
199 | - } |
200 | - |
201 | - height: units.gu(6) |
202 | - spacing: units.gu(1.5) |
203 | - |
204 | - Icon { |
205 | - id: expandedIcon |
206 | - name: delegateColumn.expanded ? "go-down" : "go-next" |
207 | - |
208 | - height: units.gu(2) |
209 | - width: height |
210 | - |
211 | - anchors { |
212 | - leftMargin: units.gu(1) |
213 | - topMargin: units.gu(2) |
214 | - top: parent.top |
215 | - } |
216 | - } |
217 | - |
218 | - Label { |
219 | - width: parent.width - expandedIcon.width - units.gu(3) |
220 | - anchors.verticalCenter: expandedIcon.verticalCenter |
221 | - |
222 | - text: folderName ? folderName : i18n.tr("All Bookmarks") |
223 | - fontSize: "small" |
224 | - } |
225 | - } |
226 | - |
227 | - ListItem.ThinDivider { |
228 | - anchors { |
229 | - left: parent.left |
230 | - right: parent.right |
231 | - bottom: parent.bottom |
232 | - bottomMargin: units.gu(1) |
233 | - } |
234 | - } |
235 | - |
236 | - MouseArea { |
237 | - anchors.fill: parent |
238 | - onClicked: delegateColumn.expanded = !delegateColumn.expanded |
239 | - } |
240 | - } |
241 | - |
242 | - Loader { |
243 | - anchors { |
244 | - left: parent.left |
245 | - right: parent.right |
246 | - } |
247 | - |
248 | - height: item ? item.contentHeight : 0 |
249 | - |
250 | - visible: status == Loader.Ready |
251 | - |
252 | - active: delegateColumn.expanded |
253 | - sourceComponent: ListView { |
254 | - readonly property bool isAllBookmarksFolder: folder === "" |
255 | - |
256 | - interactive: false |
257 | - |
258 | - model: { |
259 | - if (isAllBookmarksFolder) { |
260 | - return BookmarksModelUtils.prependHomepageToBookmarks(entries, { |
261 | - title: i18n.tr("Homepage"), |
262 | - url: bookmarksFoldersViewItem.homeBookmarkUrl |
263 | - }) |
264 | - } |
265 | - |
266 | - return entries |
267 | - } |
268 | - |
269 | - delegate: UrlDelegate{ |
270 | - id: urlDelegate |
271 | - |
272 | - property var entry: isAllBookmarksFolder ? modelData : model |
273 | - |
274 | - width: parent.width |
275 | - height: units.gu(5) |
276 | - |
277 | - removable: !isAllBookmarksFolder || index !== 0 |
278 | - |
279 | - icon: entry.icon ? entry.icon : "" |
280 | - title: entry.title ? entry.title : entry.url |
281 | - url: entry.url |
282 | - |
283 | - onClicked: bookmarksFoldersViewItem.bookmarkClicked(url) |
284 | - onRemoved: bookmarksFoldersViewItem.bookmarkRemoved(url) |
285 | - } |
286 | - } |
287 | - } |
288 | - } |
289 | - } |
290 | + leftMargin: units.gu(2) |
291 | + rightMargin: units.gu(2) |
292 | + } |
293 | + |
294 | + property int index: sections.indexOf(section) |
295 | + isExpanded: sections.get(index).expanded |
296 | + onTriggered: sections.toggle(index) |
297 | + } |
298 | + |
299 | + delegate: UrlDelegate { |
300 | + id: urlDelegate |
301 | + objectName: "bookmarkDelegate_in_" + entry.folderName |
302 | + |
303 | + property var entry: model |
304 | + |
305 | + width: parent.width |
306 | + height: url === "" ? 0 : units.gu(5) |
307 | + |
308 | + removable: url != homeBookmarkUrl |
309 | + |
310 | + icon: entry.icon ? entry.icon : "" |
311 | + title: entry.title ? entry.title : entry.url |
312 | + url: entry.url |
313 | + |
314 | + onClicked: bookmarksFoldersViewItem.bookmarkClicked(url) |
315 | + onRemoved: bookmarksFoldersViewItem.bookmarkRemoved(url) |
316 | } |
317 | } |
318 | } |
319 | |
320 | === modified file 'src/app/webbrowser/BookmarksFoldersViewWide.qml' |
321 | --- src/app/webbrowser/BookmarksFoldersViewWide.qml 2015-10-22 08:12:32 +0000 |
322 | +++ src/app/webbrowser/BookmarksFoldersViewWide.qml 2015-11-16 09:11:43 +0000 |
323 | @@ -195,8 +195,7 @@ |
324 | bookmarksList.interactive = true |
325 | |
326 | if (dragAndDrop.target && dragAndDrop.target.folderName !== folder) { |
327 | - bookmarksFoldersViewWideItem.model.update(entry.url, entry.title, |
328 | - dragAndDrop.target.folderName) |
329 | + BookmarksModel.update(entry.url, entry.title, dragAndDrop.target.folderName) |
330 | dragAndDrop.success = true |
331 | } |
332 | } |
333 | |
334 | === added file 'src/app/webbrowser/BookmarksHeader.qml' |
335 | --- src/app/webbrowser/BookmarksHeader.qml 1970-01-01 00:00:00 +0000 |
336 | +++ src/app/webbrowser/BookmarksHeader.qml 2015-11-16 09:11:43 +0000 |
337 | @@ -0,0 +1,89 @@ |
338 | +/* |
339 | + * Copyright 2015 Canonical Ltd. |
340 | + * |
341 | + * This file is part of webbrowser-app. |
342 | + * |
343 | + * webbrowser-app is free software; you can redistribute it and/or modify |
344 | + * it under the terms of the GNU General Public License as published by |
345 | + * the Free Software Foundation; version 3. |
346 | + * |
347 | + * webbrowser-app is distributed in the hope that it will be useful, |
348 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
349 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
350 | + * GNU General Public License for more details. |
351 | + * |
352 | + * You should have received a copy of the GNU General Public License |
353 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
354 | + */ |
355 | + |
356 | +import QtQuick 2.4 |
357 | +import Ubuntu.Components 1.3 |
358 | + |
359 | +Column { |
360 | + id: header |
361 | + property bool moreButtonVisible: false |
362 | + property bool moreButtonIsMore: true |
363 | + signal clicked() |
364 | + |
365 | + height: childrenRect.height |
366 | + |
367 | + Row { |
368 | + height: units.gu(6) |
369 | + spacing: units.gu(1.5) |
370 | + |
371 | + anchors { |
372 | + left: parent.left |
373 | + leftMargin: units.gu(1.5) |
374 | + right: parent.right |
375 | + } |
376 | + |
377 | + Icon { |
378 | + id: starredIcon |
379 | + color: "#dd4814" |
380 | + name: "starred" |
381 | + |
382 | + height: units.gu(2) |
383 | + width: height |
384 | + |
385 | + anchors { |
386 | + leftMargin: units.gu(1) |
387 | + topMargin: units.gu(1) |
388 | + verticalCenter: moreButton.verticalCenter |
389 | + } |
390 | + } |
391 | + |
392 | + Label { |
393 | + width: parent.width - starredIcon.width - moreButton.width - units.gu(3) |
394 | + anchors.verticalCenter: moreButton.verticalCenter |
395 | + |
396 | + text: i18n.tr("Bookmarks") |
397 | + fontSize: "small" |
398 | + } |
399 | + |
400 | + Button { |
401 | + id: moreButton |
402 | + objectName: "bookmarks.moreButton" |
403 | + height: parent.height - units.gu(2) |
404 | + |
405 | + anchors { |
406 | + top: parent.top |
407 | + topMargin: units.gu(1) |
408 | + } |
409 | + |
410 | + opacity: moreButtonVisible ? 1.0 : 0.0 |
411 | + strokeColor: UbuntuColors.darkGrey |
412 | + text: moreButtonIsMore ? i18n.tr("More") : i18n.tr("Less") |
413 | + |
414 | + onClicked: header.clicked() |
415 | + } |
416 | + } |
417 | + |
418 | + Rectangle { |
419 | + height: units.gu(0.1) |
420 | + anchors { |
421 | + left: parent.left |
422 | + right: parent.right |
423 | + } |
424 | + color: "#d3d3d3" |
425 | + } |
426 | +} |
427 | |
428 | === added file 'src/app/webbrowser/BookmarksSection.qml' |
429 | --- src/app/webbrowser/BookmarksSection.qml 1970-01-01 00:00:00 +0000 |
430 | +++ src/app/webbrowser/BookmarksSection.qml 2015-11-16 09:11:43 +0000 |
431 | @@ -0,0 +1,74 @@ |
432 | +/* |
433 | + * Copyright 2015 Canonical Ltd. |
434 | + * |
435 | + * This file is part of webbrowser-app. |
436 | + * |
437 | + * webbrowser-app is free software; you can redistribute it and/or modify |
438 | + * it under the terms of the GNU General Public License as published by |
439 | + * the Free Software Foundation; version 3. |
440 | + * |
441 | + * webbrowser-app is distributed in the hope that it will be useful, |
442 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
443 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
444 | + * GNU General Public License for more details. |
445 | + * |
446 | + * You should have received a copy of the GNU General Public License |
447 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
448 | + */ |
449 | + |
450 | +import QtQuick 2.4 |
451 | +import Ubuntu.Components 1.3 |
452 | +import Ubuntu.Components.ListItems 1.3 as ListItems |
453 | + |
454 | +AbstractButton { |
455 | + id: delegateColumn |
456 | + property string folderName |
457 | + property bool isExpanded: true |
458 | + |
459 | + Column { |
460 | + anchors.top: parent.top |
461 | + anchors.left: parent.left |
462 | + anchors.right: parent.right |
463 | + |
464 | + Row { |
465 | + anchors { |
466 | + left: parent.left |
467 | + leftMargin: units.gu(1.5) |
468 | + right: parent.right |
469 | + } |
470 | + |
471 | + height: units.gu(6) |
472 | + spacing: units.gu(1.5) |
473 | + |
474 | + Icon { |
475 | + id: expandedIcon |
476 | + name: delegateColumn.isExpanded ? "go-down" : "go-next" |
477 | + |
478 | + height: units.gu(2) |
479 | + width: height |
480 | + |
481 | + anchors { |
482 | + leftMargin: units.gu(1) |
483 | + topMargin: units.gu(2) |
484 | + top: parent.top |
485 | + } |
486 | + } |
487 | + |
488 | + Label { |
489 | + width: parent.width - expandedIcon.width - units.gu(3) |
490 | + anchors.verticalCenter: expandedIcon.verticalCenter |
491 | + |
492 | + text: folderName !== "__" ? folderName : i18n.tr("All bookmarks") |
493 | + fontSize: "small" |
494 | + } |
495 | + } |
496 | + |
497 | + ListItems.ThinDivider { |
498 | + anchors { |
499 | + left: parent.left |
500 | + right: parent.right |
501 | + bottomMargin: units.gu(1) |
502 | + } |
503 | + } |
504 | + } |
505 | +} |
506 | |
507 | === modified file 'src/app/webbrowser/CMakeLists.txt' |
508 | --- src/app/webbrowser/CMakeLists.txt 2015-10-18 19:21:48 +0000 |
509 | +++ src/app/webbrowser/CMakeLists.txt 2015-11-16 09:11:43 +0000 |
510 | @@ -21,6 +21,8 @@ |
511 | history-model.cpp |
512 | history-timeframe-model.cpp |
513 | limit-proxy-model.cpp |
514 | + list-aggregator-model.cpp |
515 | + roles-adapter-model.cpp |
516 | tabs-model.cpp |
517 | text-search-filter-model.cpp |
518 | top-sites-model.cpp |
519 | |
520 | === modified file 'src/app/webbrowser/NewTabView.qml' |
521 | --- src/app/webbrowser/NewTabView.qml 2015-10-22 08:12:32 +0000 |
522 | +++ src/app/webbrowser/NewTabView.qml 2015-11-16 09:11:43 +0000 |
523 | @@ -63,10 +63,10 @@ |
524 | } |
525 | |
526 | Flickable { |
527 | + objectName: "collapsedContents" |
528 | anchors.fill: parent |
529 | - contentHeight: internal.seeMoreBookmarksView ? |
530 | - bookmarksFolderListViewLoader.height + units.gu(6) : |
531 | - contentColumn.height |
532 | + contentHeight: contentColumn.height |
533 | + visible: !internal.seeMoreBookmarksView |
534 | |
535 | Column { |
536 | id: contentColumn |
537 | @@ -77,87 +77,20 @@ |
538 | } |
539 | height: childrenRect.height |
540 | |
541 | - Row { |
542 | - height: units.gu(6) |
543 | - anchors { |
544 | - left: parent.left |
545 | - leftMargin: units.gu(1.5) |
546 | - right: parent.right |
547 | - } |
548 | - spacing: units.gu(1.5) |
549 | - |
550 | - Icon { |
551 | - id: starredIcon |
552 | - color: "#dd4814" |
553 | - name: "starred" |
554 | - |
555 | - height: units.gu(2) |
556 | - width: height |
557 | - |
558 | - anchors { |
559 | - leftMargin: units.gu(1) |
560 | - topMargin: units.gu(1) |
561 | - verticalCenter: moreButton.verticalCenter |
562 | - } |
563 | - } |
564 | - |
565 | - Label { |
566 | - width: parent.width - starredIcon.width - moreButton.width - units.gu(3) |
567 | - anchors.verticalCenter: moreButton.verticalCenter |
568 | - |
569 | - text: i18n.tr("Bookmarks") |
570 | - fontSize: "small" |
571 | - } |
572 | - |
573 | - Button { |
574 | - id: moreButton |
575 | - objectName: "bookmarks.moreButton" |
576 | - height: parent.height - units.gu(2) |
577 | - |
578 | - anchors { top: parent.top; topMargin: units.gu(1) } |
579 | - |
580 | - strokeColor: UbuntuColors.darkGrey |
581 | - |
582 | - visible: internal.numberOfBookmarks > 4 |
583 | - |
584 | - text: internal.seeMoreBookmarksView ? i18n.tr("Less") : i18n.tr("More") |
585 | - |
586 | - onClicked: internal.seeMoreBookmarksView = !internal.seeMoreBookmarksView |
587 | - } |
588 | - } |
589 | - |
590 | - Rectangle { |
591 | - height: units.gu(0.1) |
592 | - anchors { |
593 | - left: parent.left |
594 | - leftMargin: units.gu(1.5) |
595 | - right: parent.right |
596 | - } |
597 | - color: "#d3d3d3" |
598 | - } |
599 | - |
600 | - Loader { |
601 | - id: bookmarksFolderListViewLoader |
602 | - |
603 | - anchors { |
604 | - left: parent.left |
605 | - right: parent.right |
606 | - } |
607 | - |
608 | - height: status == Loader.Ready ? item.height : 0 |
609 | - |
610 | - active: internal.seeMoreBookmarksView |
611 | - |
612 | - sourceComponent: BookmarksFoldersView { |
613 | - homeBookmarkUrl: newTabView.settingsObject.homepage |
614 | - |
615 | - onBookmarkClicked: newTabView.bookmarkClicked(url) |
616 | - onBookmarkRemoved: newTabView.bookmarkRemoved(url) |
617 | - } |
618 | - } |
619 | - |
620 | - Column { |
621 | - id: bookmarksColumn |
622 | + BookmarksHeader { |
623 | + anchors { |
624 | + left: parent.left |
625 | + leftMargin: units.gu(1.5) |
626 | + right: parent.right |
627 | + } |
628 | + |
629 | + moreButtonIsMore: true |
630 | + moreButtonVisible: internal.numberOfBookmarks > 4 |
631 | + onClicked: internal.seeMoreBookmarksView = !internal.seeMoreBookmarksView |
632 | + } |
633 | + |
634 | + UrlsList { |
635 | + objectName: "bookmarksList" |
636 | anchors { |
637 | left: parent.left |
638 | right: parent.right |
639 | @@ -167,42 +100,12 @@ |
640 | Behavior on opacity { UbuntuNumberAnimation {} } |
641 | visible: opacity > 0 |
642 | |
643 | - // Force the height to be updated when bookmarks are removed |
644 | - // in another new tab |
645 | - height: units.gu(5) * (Math.min(internal.bookmarksCountLimit, internal.numberOfBookmarks) + 1) |
646 | - spacing: 0 |
647 | - |
648 | - UrlDelegate { |
649 | - objectName: "homepageBookmark" |
650 | - anchors { |
651 | - left: parent.left |
652 | - right: parent.right |
653 | - } |
654 | - height: units.gu(5) |
655 | - |
656 | - title: i18n.tr('Homepage') |
657 | - |
658 | - leadingActions: null |
659 | - |
660 | - url: newTabView.settingsObject.homepage |
661 | - onClicked: newTabView.bookmarkClicked(url) |
662 | - } |
663 | - |
664 | - UrlsList { |
665 | - objectName: "bookmarksList" |
666 | - anchors { |
667 | - left: parent.left |
668 | - right: parent.right |
669 | - } |
670 | - |
671 | - spacing: 0 |
672 | - limit: internal.bookmarksCountLimit |
673 | - |
674 | - model: BookmarksModel |
675 | - |
676 | - onUrlClicked: newTabView.bookmarkClicked(url) |
677 | - onUrlRemoved: newTabView.bookmarkRemoved(url) |
678 | - } |
679 | + limit: internal.bookmarksCountLimit |
680 | + homeBookmarkUrl: newTabView.settingsObject.homepage |
681 | + height: contentItem.height |
682 | + |
683 | + onUrlClicked: newTabView.bookmarkClicked(url) |
684 | + onUrlRemoved: newTabView.bookmarkRemoved(url) |
685 | } |
686 | |
687 | Item { |
688 | @@ -319,4 +222,31 @@ |
689 | } |
690 | } |
691 | } |
692 | + |
693 | + Loader { |
694 | + id: bookmarksFolderListViewLoader |
695 | + |
696 | + anchors.fill: parent |
697 | + |
698 | + active: internal.seeMoreBookmarksView |
699 | + |
700 | + sourceComponent: BookmarksFoldersView { |
701 | + homeBookmarkUrl: newTabView.settingsObject.homepage |
702 | + |
703 | + onBookmarkClicked: newTabView.bookmarkClicked(url) |
704 | + onBookmarkRemoved: newTabView.bookmarkRemoved(url) |
705 | + |
706 | + headerComponent: BookmarksHeader { |
707 | + anchors { |
708 | + left: parent.left |
709 | + leftMargin: units.gu(1.5) |
710 | + right: parent.right |
711 | + rightMargin: units.gu(1.5) |
712 | + } |
713 | + moreButtonVisible: true |
714 | + moreButtonIsMore: false |
715 | + onClicked: internal.seeMoreBookmarksView = !internal.seeMoreBookmarksView |
716 | + } |
717 | + } |
718 | + } |
719 | } |
720 | |
721 | === modified file 'src/app/webbrowser/UrlsList.qml' |
722 | --- src/app/webbrowser/UrlsList.qml 2015-10-08 14:47:13 +0000 |
723 | +++ src/app/webbrowser/UrlsList.qml 2015-11-16 09:11:43 +0000 |
724 | @@ -18,35 +18,53 @@ |
725 | |
726 | import QtQuick 2.4 |
727 | import Ubuntu.Components 1.3 |
728 | +import webbrowserapp.private 0.1 |
729 | |
730 | -Column { |
731 | +ListView { |
732 | id: urlsList |
733 | |
734 | - property alias model: urlsListRepeater.model |
735 | - property int limit: -1 |
736 | + property alias limit: bookmarks.limit |
737 | + property string homeBookmarkUrl |
738 | |
739 | signal urlClicked(url url) |
740 | signal urlRemoved(url url) |
741 | |
742 | - spacing: units.gu(1) |
743 | - |
744 | - Repeater { |
745 | - id: urlsListRepeater |
746 | - |
747 | - delegate: Loader { |
748 | - active: limit < 0 || index < limit |
749 | - sourceComponent: UrlDelegate{ |
750 | - id: urlDelegate |
751 | - width: urlsList.width |
752 | - height: units.gu(5) |
753 | - |
754 | - icon: model ? model.icon : "" |
755 | - title: model.title ? model.title : model.url |
756 | - url: model ? model.url : "" |
757 | - |
758 | - onClicked: urlsList.urlClicked(model.url) |
759 | - onRemoved: urlsList.urlRemoved(model.url) |
760 | - } |
761 | + model: ListAggregatorModel {} |
762 | + |
763 | + delegate: UrlDelegate{ |
764 | + id: urlDelegate |
765 | + width: urlsList.width |
766 | + height: units.gu(5) |
767 | + |
768 | + icon: model ? model.icon : "" |
769 | + title: model.title ? model.title : model.url |
770 | + url: model ? model.url : "" |
771 | + removable: model.url !== homeBookmarkUrl |
772 | + |
773 | + onClicked: urlsList.urlClicked(model.url) |
774 | + onRemoved: urlsList.urlRemoved(model.url) |
775 | + } |
776 | + |
777 | + RolesAdapterModel { |
778 | + id: homeBookmark |
779 | + rolesSource: BookmarksModel |
780 | + sourceModel: ListModel { |
781 | + objectName: "rootfolder" |
782 | + ListElement { folderName: "__"; folder: ""; title: ""; url: ""; icon: "" } |
783 | } |
784 | } |
785 | + |
786 | + LimitProxyModel { |
787 | + id: bookmarks |
788 | + sourceModel: BookmarksModel |
789 | + } |
790 | + |
791 | + Component.onCompleted: { |
792 | + // ListElement only accept constants at declaration, so we need |
793 | + // to define these here before aggregating |
794 | + homeBookmark.sourceModel.setProperty(0, "url", homeBookmarkUrl.toString()) |
795 | + homeBookmark.sourceModel.setProperty(0, "title", i18n.tr("Homepage")) |
796 | + urlsList.model.appendModel(homeBookmark) |
797 | + urlsList.model.appendModel(bookmarks) |
798 | + } |
799 | } |
800 | |
801 | === modified file 'src/app/webbrowser/bookmarks-folderlist-model.cpp' |
802 | --- src/app/webbrowser/bookmarks-folderlist-model.cpp 2015-08-05 16:53:36 +0000 |
803 | +++ src/app/webbrowser/bookmarks-folderlist-model.cpp 2015-11-16 09:11:43 +0000 |
804 | @@ -49,6 +49,7 @@ |
805 | static QHash<int, QByteArray> roles; |
806 | if (roles.isEmpty()) { |
807 | roles[Folder] = "folder"; |
808 | + roles[FolderName] = "folderName"; |
809 | roles[Entries] = "entries"; |
810 | } |
811 | return roles; |
812 | @@ -71,6 +72,10 @@ |
813 | switch (role) { |
814 | case Folder: |
815 | return folder; |
816 | + case FolderName: |
817 | + // Provided to support creating ListView sections when using data from |
818 | + // this model. See note in BookmarksModel::data() for more details. |
819 | + return folder.length() == 0 ? BookmarksModel::RootFolderDisplayName : folder; |
820 | case Entries: |
821 | return QVariant::fromValue(entries); |
822 | default: |
823 | |
824 | === modified file 'src/app/webbrowser/bookmarks-folderlist-model.h' |
825 | --- src/app/webbrowser/bookmarks-folderlist-model.h 2015-08-05 16:53:36 +0000 |
826 | +++ src/app/webbrowser/bookmarks-folderlist-model.h 2015-11-16 09:11:43 +0000 |
827 | @@ -42,6 +42,7 @@ |
828 | |
829 | enum Roles { |
830 | Folder = Qt::UserRole + 1, |
831 | + FolderName, |
832 | Entries |
833 | }; |
834 | |
835 | |
836 | === modified file 'src/app/webbrowser/bookmarks-model.cpp' |
837 | --- src/app/webbrowser/bookmarks-model.cpp 2015-07-27 15:36:59 +0000 |
838 | +++ src/app/webbrowser/bookmarks-model.cpp 2015-11-16 09:11:43 +0000 |
839 | @@ -52,6 +52,8 @@ |
840 | QSqlDatabase::removeDatabase(CONNECTION_NAME); |
841 | } |
842 | |
843 | +const QString BookmarksModel::RootFolderDisplayName = QString("__"); |
844 | + |
845 | void BookmarksModel::resetDatabase(const QString& databaseName) |
846 | { |
847 | beginResetModel(); |
848 | @@ -174,6 +176,7 @@ |
849 | roles[Icon] = "icon"; |
850 | roles[Created] = "created"; |
851 | roles[Folder] = "folder"; |
852 | + roles[FolderName] = "folderName"; |
853 | } |
854 | return roles; |
855 | } |
856 | @@ -201,6 +204,15 @@ |
857 | return entry.created; |
858 | case Folder: |
859 | return entry.folder; |
860 | + case FolderName: |
861 | + // This role exists so that sections can be created when using the data |
862 | + // in a ListView. By convention the name of the root folder where all |
863 | + // bookmarks that are not grouped within any folder go has a string of |
864 | + // zero length for name. |
865 | + // Unfortunately a ListView won't create any section for empty values of |
866 | + // the section property, so we have to add this role and use it as the |
867 | + // section property instead. |
868 | + return entry.folder.isEmpty() ? RootFolderDisplayName : entry.folder; |
869 | default: |
870 | return QVariant(); |
871 | } |
872 | |
873 | === modified file 'src/app/webbrowser/bookmarks-model.h' |
874 | --- src/app/webbrowser/bookmarks-model.h 2015-06-09 13:11:21 +0000 |
875 | +++ src/app/webbrowser/bookmarks-model.h 2015-11-16 09:11:43 +0000 |
876 | @@ -46,7 +46,8 @@ |
877 | Title, |
878 | Icon, |
879 | Created, |
880 | - Folder |
881 | + Folder, |
882 | + FolderName |
883 | }; |
884 | |
885 | // reimplemented from QAbstractListModel |
886 | @@ -65,12 +66,15 @@ |
887 | Q_INVOKABLE void remove(const QUrl& url); |
888 | Q_INVOKABLE void update(const QUrl& url, const QString& title, const QString& folder); |
889 | |
890 | + static const QString RootFolderDisplayName; |
891 | + |
892 | Q_SIGNALS: |
893 | void databasePathChanged() const; |
894 | void folderAdded(const QString& folder) const; |
895 | void added(const QUrl& url) const; |
896 | void removed(const QUrl& url) const; |
897 | void rowCountChanged(); |
898 | + void rootFolderNameChanged() const; |
899 | |
900 | private: |
901 | QSqlDatabase m_database; |
902 | |
903 | === added file 'src/app/webbrowser/list-aggregator-model.cpp' |
904 | --- src/app/webbrowser/list-aggregator-model.cpp 1970-01-01 00:00:00 +0000 |
905 | +++ src/app/webbrowser/list-aggregator-model.cpp 2015-11-16 09:11:43 +0000 |
906 | @@ -0,0 +1,240 @@ |
907 | +/* |
908 | + * Copyright (C) 2010-2015 Canonical, Ltd. |
909 | + * |
910 | + * This file is part of webbrowser-app. |
911 | + * |
912 | + * webbrowser-app is free software; you can redistribute it and/or modify |
913 | + * it under the terms of the GNU General Public License as published by |
914 | + * the Free Software Foundation; version 3. |
915 | + * |
916 | + * webbrowser-app is distributed in the hope that it will be useful, |
917 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
918 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
919 | + * GNU General Public License for more details. |
920 | + * |
921 | + * You should have received a copy of the GNU General Public License |
922 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
923 | + */ |
924 | + |
925 | +#include "list-aggregator-model.h" |
926 | + |
927 | +// Qt |
928 | +#include <QtCore/QDebug> |
929 | +#include <QtCore/QSortFilterProxyModel> |
930 | + |
931 | +ListAggregatorModel::ListAggregatorModel(QObject* parent) : |
932 | + QAbstractListModel(parent) |
933 | +{ |
934 | +} |
935 | + |
936 | +ListAggregatorModel::~ListAggregatorModel() |
937 | +{ |
938 | +} |
939 | + |
940 | +QHash<int, QByteArray> ListAggregatorModel::roleNames() const |
941 | +{ |
942 | + return m_currentRoles; |
943 | +} |
944 | + |
945 | +void ListAggregatorModel::appendModel(const QVariant& model) |
946 | +{ |
947 | + insertModel(model, m_models.count()); |
948 | +} |
949 | + |
950 | +void ListAggregatorModel::insertModel(const QVariant& model, int index) |
951 | +{ |
952 | + static const char* errorMsg = "Unable to append a model that is not of type QAbstractListModel."; |
953 | + if (!model.isValid()) { |
954 | + qWarning() << errorMsg << "Invalid model."; |
955 | + return; |
956 | + } |
957 | + QObject* object = qvariant_cast<QObject*>(model); |
958 | + if (object == NULL) { |
959 | + qWarning() << errorMsg << model << "is of type" << model.typeName(); |
960 | + return; |
961 | + } |
962 | + |
963 | + QAbstractItemModel* list = qobject_cast<QAbstractListModel*>(object); |
964 | + if (list == NULL) { |
965 | + list = qobject_cast<QAbstractProxyModel*>(object); |
966 | + if (list == NULL) { |
967 | + qWarning() << errorMsg << object->objectName() << "is of type" << object->metaObject()->className(); |
968 | + return; |
969 | + } |
970 | + } |
971 | + aggregateListModel(list, index); |
972 | +} |
973 | + |
974 | +void ListAggregatorModel::aggregateListModel(QAbstractItemModel* model, int index) |
975 | +{ |
976 | + if (model == 0) { |
977 | + return; |
978 | + } |
979 | + |
980 | + if (m_currentRoles.count() == 0) { |
981 | + beginResetModel(); |
982 | + m_currentRoles = model->roleNames(); |
983 | + endResetModel(); |
984 | + } |
985 | + |
986 | + index = qBound(0, index, m_models.count()); |
987 | + |
988 | + int modelRowCount = model->rowCount(); |
989 | + if (modelRowCount > 0) { |
990 | + int first = rowCountBefore(index); |
991 | + int last = first + modelRowCount - 1; |
992 | + beginInsertRows(QModelIndex(), first, last); |
993 | + } |
994 | + |
995 | + m_models.insert(index, model); |
996 | + if (modelRowCount > 0) { |
997 | + endInsertRows(); |
998 | + Q_EMIT countChanged(); |
999 | + } |
1000 | + |
1001 | + connect(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)), |
1002 | + SLOT(onRowsInserted(const QModelIndex&, int, int))); |
1003 | + connect(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), |
1004 | + SLOT(onRowsRemoved(const QModelIndex&, int, int))); |
1005 | + connect(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)), |
1006 | + SLOT(onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int))); |
1007 | + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), |
1008 | + SLOT(onDataChanged(QModelIndex,QModelIndex,QVector<int>))); |
1009 | +} |
1010 | + |
1011 | +void ListAggregatorModel::removeModel(int index) |
1012 | +{ |
1013 | + if (index < 0 || index >= m_models.count()) { |
1014 | + return; |
1015 | + } |
1016 | + |
1017 | + QAbstractItemModel* model = m_models.at(index); |
1018 | + int modelRowCount = model->rowCount(); |
1019 | + if (modelRowCount > 0) { |
1020 | + int first = rowCountBefore(index); |
1021 | + int last = first + modelRowCount - 1; |
1022 | + |
1023 | + beginRemoveRows(QModelIndex(), first, last); |
1024 | + } |
1025 | + |
1026 | + m_models.removeOne(model); |
1027 | + if (modelRowCount > 0) { |
1028 | + endRemoveRows(); |
1029 | + Q_EMIT countChanged(); |
1030 | + } |
1031 | + |
1032 | + disconnect(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)), |
1033 | + this, SLOT(onRowsInserted(const QModelIndex&, int, int))); |
1034 | + disconnect(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), |
1035 | + this, SLOT(onRowsRemoved(const QModelIndex&, int, int))); |
1036 | + disconnect(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)), |
1037 | + this, SLOT(onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int))); |
1038 | + disconnect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), |
1039 | + this, SLOT(onDataChanged(QModelIndex,QModelIndex,QVector<int>))); |
1040 | +} |
1041 | + |
1042 | +int ListAggregatorModel::computeOffset(QAbstractItemModel* model) const |
1043 | +{ |
1044 | + int offset = 0; |
1045 | + QList<QAbstractItemModel*>::const_iterator iter; |
1046 | + for (iter = m_models.begin(); (iter != m_models.end()) && (*iter != model); ++iter) { |
1047 | + offset += (*iter)->rowCount(); |
1048 | + } |
1049 | + return offset; |
1050 | +} |
1051 | + |
1052 | +QAbstractItemModel* ListAggregatorModel::modelAtIndex(int index) const |
1053 | +{ |
1054 | + int offset = index; |
1055 | + Q_FOREACH(QAbstractItemModel* model, m_models) { |
1056 | + int size = model->rowCount(); |
1057 | + if (offset < size) { |
1058 | + return model; |
1059 | + } |
1060 | + offset -= size; |
1061 | + } |
1062 | + return NULL; |
1063 | +} |
1064 | + |
1065 | +void ListAggregatorModel::onRowsInserted(const QModelIndex& parent, int first, int last) |
1066 | +{ |
1067 | + QAbstractListModel* model = static_cast<QAbstractListModel*>(sender()); |
1068 | + int offset = computeOffset(model); |
1069 | + beginInsertRows(parent, first + offset, last + offset); |
1070 | + endInsertRows(); |
1071 | + Q_EMIT countChanged(); |
1072 | +} |
1073 | + |
1074 | +void ListAggregatorModel::onRowsRemoved(const QModelIndex& parent, int first, int last) |
1075 | +{ |
1076 | + QAbstractListModel* model = static_cast<QAbstractListModel*>(sender()); |
1077 | + int offset = computeOffset(model); |
1078 | + beginRemoveRows(parent, first + offset, last + offset); |
1079 | + endRemoveRows(); |
1080 | + Q_EMIT countChanged(); |
1081 | +} |
1082 | + |
1083 | +void ListAggregatorModel::onRowsMoved(const QModelIndex& sourceParent, int sourceStart, int sourceEnd, |
1084 | + const QModelIndex& destinationParent, int destinationRow) |
1085 | +{ |
1086 | + QAbstractListModel* model = static_cast<QAbstractListModel*>(sender()); |
1087 | + int offset = computeOffset(model); |
1088 | + beginMoveRows(sourceParent, sourceStart + offset, sourceEnd + offset, |
1089 | + destinationParent, destinationRow + offset); |
1090 | + endMoveRows(); |
1091 | +} |
1092 | + |
1093 | +void ListAggregatorModel::onDataChanged(const QModelIndex &topLeft, |
1094 | + const QModelIndex &bottomRight, |
1095 | + const QVector<int> &roles) |
1096 | +{ |
1097 | + QAbstractListModel* model = static_cast<QAbstractListModel*>(sender()); |
1098 | + int offset = computeOffset(model); |
1099 | + QModelIndex top = index(topLeft.row() + offset); |
1100 | + QModelIndex bottom = index(bottomRight.row() + offset); |
1101 | + Q_EMIT dataChanged(top, bottom, roles); |
1102 | +} |
1103 | + |
1104 | +int ListAggregatorModel::rowCountBefore(int index) const |
1105 | +{ |
1106 | + int count = 0; |
1107 | + for (int i = 0; i <= index - 1; ++i) { |
1108 | + count += m_models.at(i)->rowCount(); |
1109 | + } |
1110 | + return count; |
1111 | +} |
1112 | + |
1113 | +int ListAggregatorModel::rowCount(const QModelIndex& parent) const |
1114 | +{ |
1115 | + Q_UNUSED(parent) |
1116 | + |
1117 | + return rowCountBefore(m_models.count()); |
1118 | +} |
1119 | + |
1120 | +QVariant ListAggregatorModel::data(const QModelIndex& index, int role) const |
1121 | +{ |
1122 | + if (!index.isValid()) { |
1123 | + return QVariant(); |
1124 | + } |
1125 | + |
1126 | + int row = index.row(); |
1127 | + int offset = row; |
1128 | + QList<QAbstractItemModel*>::const_iterator iter; |
1129 | + for (iter = m_models.begin(); iter != m_models.end(); ++iter) { |
1130 | + int rowCount = (*iter)->rowCount(); |
1131 | + if (offset >= rowCount) { |
1132 | + offset -= rowCount; |
1133 | + } else { |
1134 | + QModelIndex new_index = (*iter)->index(offset, 0); |
1135 | + return (*iter)->data(new_index, role); |
1136 | + } |
1137 | + } |
1138 | + |
1139 | + // For the sake of completeness, should never happen. |
1140 | + return QVariant(); |
1141 | +} |
1142 | + |
1143 | +QVariant ListAggregatorModel::get(int row) const |
1144 | +{ |
1145 | + return data(QAbstractListModel::index(row), 0); |
1146 | +} |
1147 | |
1148 | === added file 'src/app/webbrowser/list-aggregator-model.h' |
1149 | --- src/app/webbrowser/list-aggregator-model.h 1970-01-01 00:00:00 +0000 |
1150 | +++ src/app/webbrowser/list-aggregator-model.h 2015-11-16 09:11:43 +0000 |
1151 | @@ -0,0 +1,82 @@ |
1152 | +/* |
1153 | + * Copyright (C) 2010-2015 Canonical, Ltd. |
1154 | + * |
1155 | + * This file is part of webbrowser-app. |
1156 | + * |
1157 | + * webbrowser-app is free software; you can redistribute it and/or modify |
1158 | + * it under the terms of the GNU General Public License as published by |
1159 | + * the Free Software Foundation; version 3. |
1160 | + * |
1161 | + * webbrowser-app is distributed in the hope that it will be useful, |
1162 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1163 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1164 | + * GNU General Public License for more details. |
1165 | + * |
1166 | + * You should have received a copy of the GNU General Public License |
1167 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1168 | + */ |
1169 | + |
1170 | +#ifndef __LIST_AGGREGATOR_MODEL_H__ |
1171 | +#define __LIST_AGGREGATOR_MODEL_H__ |
1172 | + |
1173 | +#include <QtCore/QAbstractListModel> |
1174 | + |
1175 | +/* Aggregates the data of several models and present them to the client |
1176 | + as if they were one single model. |
1177 | + The models that can be aggregated can only be QAbstractListModels or |
1178 | + QSortFilterProxyModels. |
1179 | + For this reason there are several non public methods in this class |
1180 | + that handle QAbstractItemModel (which is the common ancestor of the |
1181 | + two classes we can aggregate), but please note that this is only for |
1182 | + keeping the code simpler. |
1183 | + The public interface checks that the models it manipulates are of the |
1184 | + accepted types only. |
1185 | +*/ |
1186 | +class ListAggregatorModel : public QAbstractListModel |
1187 | +{ |
1188 | + Q_OBJECT |
1189 | + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) |
1190 | + |
1191 | +public: |
1192 | + ListAggregatorModel(QObject* parent = 0); |
1193 | + ~ListAggregatorModel(); |
1194 | + |
1195 | + /* Allow test fixtures to access protected and private members. */ |
1196 | + friend class ListAggregatorModelTest; |
1197 | + |
1198 | + Q_INVOKABLE QVariant get(int row) const; |
1199 | + |
1200 | + Q_INVOKABLE void appendModel(const QVariant& model); |
1201 | + Q_INVOKABLE void insertModel(const QVariant& model, int index); |
1202 | + Q_INVOKABLE void removeModel(int index); |
1203 | + |
1204 | + // reimplemented from QAbstractItemModel |
1205 | + QHash<int, QByteArray> roleNames() const; |
1206 | + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; |
1207 | + int rowCount(const QModelIndex& parent = QModelIndex()) const; |
1208 | + |
1209 | +Q_SIGNALS: |
1210 | + void countChanged() const; |
1211 | + |
1212 | +protected: |
1213 | + QList<QAbstractItemModel*> m_models; |
1214 | + void aggregateListModel(QAbstractItemModel* model, int index); |
1215 | + |
1216 | +private Q_SLOTS: |
1217 | + void onRowsInserted(const QModelIndex& parent, int first, int last); |
1218 | + void onRowsRemoved(const QModelIndex& parent, int first, int last); |
1219 | + void onRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int); |
1220 | + void onDataChanged(const QModelIndex &topLeft, |
1221 | + const QModelIndex &bottomRight, |
1222 | + const QVector<int> &roles); |
1223 | + |
1224 | +private: |
1225 | + int computeOffset(QAbstractItemModel* model) const; |
1226 | + QAbstractItemModel* modelAtIndex(int index) const; |
1227 | + int rowCountBefore(int index) const; |
1228 | + |
1229 | + QHash<int, QByteArray> m_currentRoles; |
1230 | +}; |
1231 | + |
1232 | +#endif // __LIST_AGGREGATOR_MODEL_H__ |
1233 | + |
1234 | |
1235 | === added file 'src/app/webbrowser/roles-adapter-model.cpp' |
1236 | --- src/app/webbrowser/roles-adapter-model.cpp 1970-01-01 00:00:00 +0000 |
1237 | +++ src/app/webbrowser/roles-adapter-model.cpp 2015-11-16 09:11:43 +0000 |
1238 | @@ -0,0 +1,111 @@ |
1239 | +/* |
1240 | + * Copyright 2015 Canonical Ltd. |
1241 | + * |
1242 | + * This file is part of webbrowser-app. |
1243 | + * |
1244 | + * webbrowser-app is free software; you can redistribute it and/or modify |
1245 | + * it under the terms of the GNU General Public License as published by |
1246 | + * the Free Software Foundation; version 3. |
1247 | + * |
1248 | + * webbrowser-app is distributed in the hope that it will be useful, |
1249 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1250 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1251 | + * GNU General Public License for more details. |
1252 | + * |
1253 | + * You should have received a copy of the GNU General Public License |
1254 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1255 | + */ |
1256 | + |
1257 | +#include "roles-adapter-model.h" |
1258 | + |
1259 | +// Qt |
1260 | +#include <QtCore/QDebug> |
1261 | + |
1262 | +/*! |
1263 | + \class RolesAdapterModel |
1264 | + \brief Identity proxy model that allows access to the data from the source |
1265 | + model by using role indexes from another model. |
1266 | + |
1267 | + RolesAdapterModel is an identity proxy model that allows accessing data from |
1268 | + the \a sourceModel using role indexes from another model, the \a rolesSource. |
1269 | + The data from the rolesSource is never accessed, only its roles map is. |
1270 | + |
1271 | + Whenever the view tries to access data, the name of the requested role is |
1272 | + searched in the \a rolesSource roles map, and if a match is found then data |
1273 | + is returned from the \a dataSource using the mapped role. If no match is |
1274 | + found then an invalid QVariant is returned. |
1275 | + |
1276 | + If no \a rolesSource is set, this model directly proxies role names and |
1277 | + from the \a sourceModel (i.e. it acts as a plain identity model) |
1278 | +*/ |
1279 | +RolesAdapterModel::RolesAdapterModel(QObject* parent) |
1280 | + : QIdentityProxyModel(parent) |
1281 | + , m_rolesSource(0) |
1282 | +{ |
1283 | +} |
1284 | + |
1285 | +RolesAdapterModel::~RolesAdapterModel() |
1286 | +{ |
1287 | +} |
1288 | + |
1289 | +QHash<int, QByteArray> RolesAdapterModel::roleNames() const |
1290 | +{ |
1291 | + return m_rolesSource ? m_rolesSource->roleNames() : QIdentityProxyModel::roleNames(); |
1292 | +} |
1293 | + |
1294 | +QVariant RolesAdapterModel::data(const QModelIndex& index, int role) const |
1295 | +{ |
1296 | + if (m_rolesSource) { |
1297 | + QHash<int, QByteArray> from = m_rolesSource->roleNames(); |
1298 | + if (from.contains(role)) { |
1299 | + const QByteArray& name = from.value(role); |
1300 | + QHash<int, QByteArray> to = sourceModel()->roleNames(); |
1301 | + int targetRole = to.key(name, -1); |
1302 | + if (targetRole != -1) { |
1303 | + QModelIndex targetIndex = sourceModel()->index(index.row(), index.column()); |
1304 | + return sourceModel()->data(targetIndex, targetRole); |
1305 | + } |
1306 | + } |
1307 | + return QVariant(); |
1308 | + } else { |
1309 | + return QIdentityProxyModel::data(index, role); |
1310 | + } |
1311 | +} |
1312 | + |
1313 | +QVariant RolesAdapterModel::rolesSource() const |
1314 | +{ |
1315 | + return QVariant::fromValue(m_rolesSource); |
1316 | +} |
1317 | + |
1318 | +void RolesAdapterModel::setRolesSource(QVariant sourceModel) |
1319 | +{ |
1320 | + static const char* errorMsg = "The rolesSource is not a QAbstractItemModel."; |
1321 | + if (!sourceModel.isValid() || |
1322 | + (sourceModel.type() == QMetaType::VoidStar && sourceModel.toInt() == 0)) { |
1323 | + // if undefined or null is passed in, clear the roles source |
1324 | + if (m_rolesSource != 0) { |
1325 | + beginResetModel(); |
1326 | + m_rolesSource = 0; |
1327 | + endResetModel(); |
1328 | + Q_EMIT rolesSourceChanged(); |
1329 | + } |
1330 | + return; |
1331 | + } |
1332 | + QObject* object = qvariant_cast<QObject*>(sourceModel); |
1333 | + if (object == NULL) { |
1334 | + qWarning() << errorMsg << "It is:" << sourceModel.typeName(); |
1335 | + return; |
1336 | + } |
1337 | + QAbstractItemModel* list = qobject_cast<QAbstractItemModel*>(object); |
1338 | + if (list == NULL) { |
1339 | + qWarning() << errorMsg << "It is:" << object->metaObject()->className(); |
1340 | + return; |
1341 | + } |
1342 | + |
1343 | + if (list != m_rolesSource) { |
1344 | + beginResetModel(); |
1345 | + m_rolesSource = list; |
1346 | + endResetModel(); |
1347 | + Q_EMIT rolesSourceChanged(); |
1348 | + } |
1349 | +} |
1350 | |
1351 | === added file 'src/app/webbrowser/roles-adapter-model.h' |
1352 | --- src/app/webbrowser/roles-adapter-model.h 1970-01-01 00:00:00 +0000 |
1353 | +++ src/app/webbrowser/roles-adapter-model.h 2015-11-16 09:11:43 +0000 |
1354 | @@ -0,0 +1,50 @@ |
1355 | +/* |
1356 | + * Copyright 2015 Canonical Ltd. |
1357 | + * |
1358 | + * This file is part of webbrowser-app. |
1359 | + * |
1360 | + * webbrowser-app is free software; you can redistribute it and/or modify |
1361 | + * it under the terms of the GNU General Public License as published by |
1362 | + * the Free Software Foundation; version 3. |
1363 | + * |
1364 | + * webbrowser-app is distributed in the hope that it will be useful, |
1365 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1366 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1367 | + * GNU General Public License for more details. |
1368 | + * |
1369 | + * You should have received a copy of the GNU General Public License |
1370 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1371 | + */ |
1372 | + |
1373 | +#ifndef __ROLES_ADAPTER_MODEL_H__ |
1374 | +#define __ROLES_ADAPTER_MODEL_H__ |
1375 | + |
1376 | +// Qt |
1377 | +#include <QtCore/QIdentityProxyModel> |
1378 | +#include <QtCore/QMap> |
1379 | +#include <QtCore/QString> |
1380 | + |
1381 | +class RolesAdapterModel : public QIdentityProxyModel |
1382 | +{ |
1383 | + Q_OBJECT |
1384 | + Q_PROPERTY(QVariant rolesSource READ rolesSource WRITE setRolesSource NOTIFY rolesSourceChanged) |
1385 | + |
1386 | +public: |
1387 | + RolesAdapterModel(QObject* parent=0); |
1388 | + ~RolesAdapterModel(); |
1389 | + |
1390 | + // reimplemented from QAbstractListModel |
1391 | + QHash<int, QByteArray> roleNames() const; |
1392 | + QVariant data(const QModelIndex& index, int role) const; |
1393 | + |
1394 | + QVariant rolesSource() const; |
1395 | + void setRolesSource(QVariant rolesSource); |
1396 | + |
1397 | +Q_SIGNALS: |
1398 | + void rolesSourceChanged() const; |
1399 | + |
1400 | +private: |
1401 | + QAbstractItemModel* m_rolesSource; |
1402 | +}; |
1403 | + |
1404 | +#endif // __ROLES_ADAPTER_MODEL_H__ |
1405 | |
1406 | === modified file 'src/app/webbrowser/webbrowser-app.cpp' |
1407 | --- src/app/webbrowser/webbrowser-app.cpp 2015-10-22 15:07:26 +0000 |
1408 | +++ src/app/webbrowser/webbrowser-app.cpp 2015-11-16 09:11:43 +0000 |
1409 | @@ -28,6 +28,8 @@ |
1410 | #include "history-model.h" |
1411 | #include "history-timeframe-model.h" |
1412 | #include "limit-proxy-model.h" |
1413 | +#include "list-aggregator-model.h" |
1414 | +#include "roles-adapter-model.h" |
1415 | #include "searchengine.h" |
1416 | #include "text-search-filter-model.h" |
1417 | #include "tabs-model.h" |
1418 | @@ -89,10 +91,12 @@ |
1419 | qmlRegisterType<HistoryLastVisitDateListModel>(uri, 0, 1, "HistoryLastVisitDateListModel"); |
1420 | qmlRegisterType<HistoryLastVisitDateModel>(uri, 0, 1, "HistoryLastVisitDateModel"); |
1421 | qmlRegisterType<LimitProxyModel>(uri, 0 , 1, "LimitProxyModel"); |
1422 | + qmlRegisterType<ListAggregatorModel>(uri, 0, 1, "ListAggregatorModel"); |
1423 | qmlRegisterType<TabsModel>(uri, 0, 1, "TabsModel"); |
1424 | qmlRegisterSingletonType<BookmarksModel>(uri, 0, 1, "BookmarksModel", BookmarksModel_singleton_factory); |
1425 | qmlRegisterType<BookmarksFolderListModel>(uri, 0, 1, "BookmarksFolderListModel"); |
1426 | qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory); |
1427 | + qmlRegisterType<RolesAdapterModel>(uri, 0, 1, "RolesAdapterModel"); |
1428 | qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine"); |
1429 | qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory); |
1430 | qmlRegisterType<TextSearchFilterModel>(uri, 0, 1, "TextSearchFilterModel"); |
1431 | |
1432 | === modified file 'tests/autopilot/webbrowser_app/emulators/browser.py' |
1433 | --- tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-22 15:07:18 +0000 |
1434 | +++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-11-16 09:11:43 +0000 |
1435 | @@ -550,11 +550,20 @@ |
1436 | |
1437 | class NewTabView(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
1438 | |
1439 | + def get_collapsed_contents(self): |
1440 | + return self.select_single("QQuickFlickable", |
1441 | + objectName="collapsedContents") |
1442 | + |
1443 | def get_bookmarks_more_button(self): |
1444 | - return self.select_single("Button", objectName="bookmarks.moreButton") |
1445 | + collapsed = self.get_collapsed_contents() |
1446 | + if collapsed.visible: |
1447 | + return collapsed.select_single("Button", |
1448 | + objectName="bookmarks.moreButton") |
1449 | + else: |
1450 | + return self.get_bookmarks_folder_list_view().get_more_button() |
1451 | |
1452 | def get_homepage_bookmark(self): |
1453 | - return self.select_single(UrlDelegate, objectName="homepageBookmark") |
1454 | + return self.get_bookmarks_list().get_delegates()[0] |
1455 | |
1456 | def get_bookmarks_list(self): |
1457 | return self.select_single(UrlsList, objectName="bookmarksList") |
1458 | @@ -574,11 +583,10 @@ |
1459 | def get_bookmarks(self, folder_name): |
1460 | # assumes that the "more" button has been clicked |
1461 | folders = self.get_bookmarks_folder_list_view() |
1462 | - folder_delegate = folders.get_folder_delegate(folder_name) |
1463 | - return folders.get_urls_from_folder(folder_delegate) |
1464 | + return folders.get_urls_from_folder(folder_name) |
1465 | |
1466 | def get_folder_names(self): |
1467 | - folders = self.get_bookmarks_folder_list_view().get_delegates() |
1468 | + folders = self.get_bookmarks_folder_list_view().get_folder_delegates() |
1469 | return [folder.folderName for folder in folders] |
1470 | |
1471 | |
1472 | @@ -698,23 +706,46 @@ |
1473 | |
1474 | class BookmarksFoldersView(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
1475 | |
1476 | - def get_delegates(self): |
1477 | - return sorted(self.select_many("QQuickItem", |
1478 | - objectName="bookmarkFolderDelegate"), |
1479 | + # We need to check for visible=True because the ListView sometimes seems |
1480 | + # to not destroy section delegates immediately when a section is removed |
1481 | + # and re-created, but it just hides the old instance instead. |
1482 | + def get_folder_delegates(self): |
1483 | + return sorted(self.select_many("BookmarksSection", visible=True), |
1484 | key=lambda delegate: delegate.globalRect.y) |
1485 | |
1486 | def get_folder_delegate(self, folder): |
1487 | - return self.select_single("QQuickItem", |
1488 | - objectName="bookmarkFolderDelegate", |
1489 | + return self.select_single("BookmarksSection", |
1490 | + visible=True, |
1491 | folderName=folder) |
1492 | |
1493 | - def get_urls_from_folder(self, folder): |
1494 | - return sorted(folder.select_many(UrlDelegate), |
1495 | - key=lambda delegate: delegate.globalRect.y) |
1496 | - |
1497 | - def get_header_from_folder(self, folder): |
1498 | - return folder.wait_select_single("QQuickItem", |
1499 | - objectName="bookmarkFolderHeader") |
1500 | + @autopilot.logging.log_action(logger.info) |
1501 | + def toggle_folder_expanded(self, folder): |
1502 | + # We need to wait for all the delegates that existed before the click |
1503 | + # to be destroyed. If we don't, then any following calls to |
1504 | + # get_urls_from_folder may also return the delegates that are being |
1505 | + # removed, which will then be destroyed while the test is using them, |
1506 | + # causing the test to fail. |
1507 | + items = self.get_urls_from_folder(folder, no_placeholders=False) |
1508 | + self.pointing_device.click_object(self.get_folder_delegate(folder)) |
1509 | + for item in items: |
1510 | + # This is safe to call if the item has been destroyed already |
1511 | + item.wait_until_destroyed() |
1512 | + |
1513 | + def get_urls_from_folder(self, folder, no_placeholders=True): |
1514 | + # This seemed cleaner than adding a folderName property to the delegate |
1515 | + # only for testing purposes, though it is still a bit hackish |
1516 | + delegateName = "bookmarkDelegate_in_" + folder |
1517 | + folder_delegates = sorted(self.select_many(UrlDelegate, |
1518 | + objectName=delegateName), |
1519 | + key=lambda delegate: delegate.globalRect.y) |
1520 | + if no_placeholders: |
1521 | + # placeholder delegates are not visible due to zero height |
1522 | + return [item for item in folder_delegates if item.height > 0] |
1523 | + else: |
1524 | + return folder_delegates |
1525 | + |
1526 | + def get_more_button(self): |
1527 | + return self.select_single("Button", objectName="bookmarks.moreButton") |
1528 | |
1529 | |
1530 | class ContextMenuBase(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
1531 | |
1532 | === modified file 'tests/autopilot/webbrowser_app/tests/test_bookmark_options.py' |
1533 | --- tests/autopilot/webbrowser_app/tests/test_bookmark_options.py 2015-10-13 13:40:29 +0000 |
1534 | +++ tests/autopilot/webbrowser_app/tests/test_bookmark_options.py 2015-11-16 09:11:43 +0000 |
1535 | @@ -29,6 +29,7 @@ |
1536 | class TestBookmarkOptions(StartOpenRemotePageTestCaseBase): |
1537 | |
1538 | def setUp(self): |
1539 | + self.rootName = "__" |
1540 | self.create_temporary_profile() |
1541 | self.populate_bookmarks() |
1542 | super(TestBookmarkOptions, self).setUp() |
1543 | @@ -95,15 +96,18 @@ |
1544 | urls = tab.get_bookmarks(folder_name) |
1545 | self.assertThat(lambda: len(urls), Eventually(Equals(count))) |
1546 | |
1547 | + def _assert_folder_count(self, tab, count): |
1548 | + folders = tab.get_bookmarks_folder_list_view() |
1549 | + self.assertThat(lambda: len(folders.get_folder_delegates()), |
1550 | + Eventually(Equals(count))) |
1551 | + |
1552 | def _toggle_bookmark_folder(self, tab, folder_name): |
1553 | folders = tab.get_bookmarks_folder_list_view() |
1554 | - folder_delegate = folders.get_folder_delegate(folder_name) |
1555 | - self.pointing_device.click_object( |
1556 | - folders.get_header_from_folder(folder_delegate)) |
1557 | + folders.toggle_folder_expanded(folder_name) |
1558 | |
1559 | def test_save_bookmarked_url_in_default_folder(self): |
1560 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1561 | - self._assert_bookmark_count_in_folder(new_tab, "", 5) |
1562 | + self._assert_bookmark_count_in_folder(new_tab, self.rootName, 5) |
1563 | |
1564 | url = self.base_url + "/test2" |
1565 | self.main_window.go_to_url(url) |
1566 | @@ -119,12 +123,11 @@ |
1567 | self.assertThat(chrome.bookmarked, Eventually(Equals(True))) |
1568 | |
1569 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1570 | - self._assert_bookmark_count_in_folder(new_tab, "", 6) |
1571 | + self._assert_bookmark_count_in_folder(new_tab, self.rootName, 6) |
1572 | |
1573 | def test_save_bookmarked_url_in_existing_folder(self): |
1574 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1575 | - self.assertThat(lambda: len(new_tab.get_folder_names()), |
1576 | - Eventually(Equals(3))) |
1577 | + self._assert_folder_count(new_tab, 3) |
1578 | if not self.main_window.wide: |
1579 | self._toggle_bookmark_folder(new_tab, "Actinide") |
1580 | self._assert_bookmark_count_in_folder(new_tab, "Actinide", 1) |
1581 | @@ -155,16 +158,14 @@ |
1582 | self.assertThat(chrome.bookmarked, Eventually(Equals(True))) |
1583 | |
1584 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1585 | - self.assertThat(lambda: len(new_tab.get_folder_names()), |
1586 | - Eventually(Equals(3))) |
1587 | + self._assert_folder_count(new_tab, 3) |
1588 | if not self.main_window.wide: |
1589 | self._toggle_bookmark_folder(new_tab, "Actinide") |
1590 | self._assert_bookmark_count_in_folder(new_tab, "Actinide", 2) |
1591 | |
1592 | def test_save_bookmarked_url_in_new_folder(self): |
1593 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1594 | - self.assertThat(lambda: len(new_tab.get_folder_names()), |
1595 | - Eventually(Equals(3))) |
1596 | + self._assert_folder_count(new_tab, 3) |
1597 | |
1598 | url = self.base_url + "/test2" |
1599 | self.main_window.go_to_url(url) |
1600 | @@ -201,8 +202,7 @@ |
1601 | self.assertThat(chrome.bookmarked, Eventually(Equals(True))) |
1602 | |
1603 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1604 | - self.assertThat(lambda: len(new_tab.get_folder_names()), |
1605 | - Eventually(Equals(4))) |
1606 | + self._assert_folder_count(new_tab, 4) |
1607 | if not self.main_window.wide: |
1608 | self._toggle_bookmark_folder(new_tab, "NewFolder") |
1609 | self._assert_bookmark_count_in_folder(new_tab, "NewFolder", 1) |
1610 | @@ -230,9 +230,9 @@ |
1611 | self.assertThat(chrome.bookmarked, Eventually(Equals(True))) |
1612 | |
1613 | new_tab = self.open_new_tab(open_tabs_view=True, expand_view=True) |
1614 | - self._assert_bookmark_count_in_folder(new_tab, "", 6) |
1615 | + self._assert_bookmark_count_in_folder(new_tab, self.rootName, 6) |
1616 | |
1617 | - bookmark = new_tab.get_bookmarks("")[1] |
1618 | + bookmark = new_tab.get_bookmarks(self.rootName)[1] |
1619 | self.assertThat(bookmark.title, Equals("NewTitle")) |
1620 | |
1621 | def test_bookmark_options_from_contextual_menu(self): |
1622 | |
1623 | === modified file 'tests/autopilot/webbrowser_app/tests/test_new_tab_view.py' |
1624 | --- tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-10-15 19:09:59 +0000 |
1625 | +++ tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-11-16 09:11:43 +0000 |
1626 | @@ -20,6 +20,7 @@ |
1627 | |
1628 | import testtools |
1629 | |
1630 | +from autopilot.exceptions import StateNotFoundError |
1631 | from autopilot.platform import model |
1632 | from autopilot.matchers import Eventually |
1633 | from testtools.matchers import Equals, NotEquals |
1634 | @@ -218,6 +219,7 @@ |
1635 | class TestNewTabViewContentsNarrow(TestNewTabViewContentsBase): |
1636 | |
1637 | def setUp(self): |
1638 | + self.rootName = "__" |
1639 | super(TestNewTabViewContentsNarrow, self).setUp() |
1640 | if self.main_window.wide: |
1641 | self.skipTest("Only on narrow form factors") |
1642 | @@ -252,11 +254,10 @@ |
1643 | self.assertThat(more_button.visible, Equals(True)) |
1644 | self.pointing_device.click_object(more_button) |
1645 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1646 | - folder_delegate = folders.get_folder_delegate("") |
1647 | self.assertThat(lambda: len(folders.get_urls_from_folder( |
1648 | - folder_delegate)), |
1649 | + self.rootName)), |
1650 | Eventually(Equals(5))) |
1651 | - bookmark = folders.get_urls_from_folder(folder_delegate)[0] |
1652 | + bookmark = folders.get_urls_from_folder(self.rootName)[0] |
1653 | url = bookmark.url |
1654 | self.pointing_device.click_object(bookmark) |
1655 | self.new_tab_view.wait_until_destroyed() |
1656 | @@ -264,105 +265,127 @@ |
1657 | |
1658 | def test_bookmarks_section_expands_and_collapses(self): |
1659 | bookmarks = self.new_tab_view.get_bookmarks_list() |
1660 | - top_sites = self.new_tab_view.get_top_sites_list() |
1661 | - self.assertThat(top_sites.visible, Equals(True)) |
1662 | + collapsed = self.new_tab_view.get_collapsed_contents() |
1663 | + self.assertThat(collapsed.visible, Equals(True)) |
1664 | # When the bookmarks list is collapsed, it shows a maximum of 4 entries |
1665 | + # plus the home bookmark |
1666 | self.assertThat(lambda: len(bookmarks.get_delegates()), |
1667 | - Eventually(Equals(4))) |
1668 | + Eventually(Equals(5))) |
1669 | + |
1670 | # When expanded, it shows all entries |
1671 | more_button = self.new_tab_view.get_bookmarks_more_button() |
1672 | - self.assertThat(more_button.visible, Equals(True)) |
1673 | + self.assertThat(more_button.opacity, Equals(1.0)) |
1674 | self.pointing_device.click_object(more_button) |
1675 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1676 | - folder_delegate = folders.get_folder_delegate("") |
1677 | self.assertThat(lambda: len(folders.get_urls_from_folder( |
1678 | - folder_delegate)), |
1679 | + self.rootName)), |
1680 | Eventually(Equals(5))) |
1681 | - self.assertThat(top_sites.visible, Eventually(Equals(False))) |
1682 | + self.assertThat(collapsed.visible, Eventually(Equals(False))) |
1683 | + |
1684 | # Collapse again |
1685 | - self.assertThat(more_button.visible, Equals(True)) |
1686 | + more_button = self.new_tab_view.get_bookmarks_more_button() |
1687 | + self.assertThat(more_button.opacity, Equals(1.0)) |
1688 | self.pointing_device.click_object(more_button) |
1689 | self.assertThat(lambda: len(bookmarks.get_delegates()), |
1690 | - Eventually(Equals(4))) |
1691 | - self.assertThat(top_sites.visible, Eventually(Equals(True))) |
1692 | + Eventually(Equals(5))) |
1693 | + self.assertThat(collapsed.visible, Eventually(Equals(True))) |
1694 | |
1695 | def _remove_first_bookmark(self): |
1696 | - bookmarks = self.new_tab_view.get_bookmarks_list() |
1697 | - delegate = bookmarks.get_delegates()[0] |
1698 | + list = self.new_tab_view.get_bookmarks_list() |
1699 | + delegates = list.get_delegates() |
1700 | + count = len(delegates) |
1701 | + delegate = delegates[1] |
1702 | url = delegate.url |
1703 | - delegate.trigger_leading_action("leadingAction.delete", |
1704 | - delegate.wait_until_destroyed) |
1705 | - self.assertThat(lambda: bookmarks.get_urls()[0], |
1706 | - Eventually(NotEquals(url))) |
1707 | + |
1708 | + # Since we are using a LimitProxyModel, when removing the bookmark |
1709 | + # two things can happen: |
1710 | + try: |
1711 | + # if there are enough bookmarks in the source model to keep the |
1712 | + # LimitProxyModel full, then dataChanged events are sent to the |
1713 | + # delegates, but no actual item is removed, so we can't wait for |
1714 | + # the delegate to be destroyed, as we would normally do. |
1715 | + delegate.trigger_leading_action("leadingAction.delete", |
1716 | + lambda: list.get_urls()[1] != url) |
1717 | + except StateNotFoundError: |
1718 | + # otherwise the LimitProxyModel actually removes the item from its |
1719 | + # list and the delegate gets destroyed. this causes the above code |
1720 | + # to fail because the delegate disappears half way into the call. |
1721 | + # we catch the error here and verify that the count has decreased |
1722 | + # instead. |
1723 | + self.assertThat(len(list.get_delegates()), Equals(count - 1)) |
1724 | |
1725 | def _remove_first_bookmark_from_folder(self, folder): |
1726 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1727 | - folder_delegate = folders.get_folder_delegate(folder) |
1728 | - delegate = folders.get_urls_from_folder(folder_delegate)[0] |
1729 | + delegate = folders.get_urls_from_folder(folder)[0] |
1730 | url = delegate.url |
1731 | - count = len(folders.get_urls_from_folder(folder_delegate)) |
1732 | + count = len(folders.get_urls_from_folder(folder)) |
1733 | delegate.trigger_leading_action("leadingAction.delete", |
1734 | delegate.wait_until_destroyed) |
1735 | if ((count - 1) > 4): |
1736 | self.assertThat( |
1737 | - lambda: folders.get_urls_from_folder(folder_delegate)[0], |
1738 | + lambda: folders.get_urls_from_folder(folder)[0], |
1739 | Eventually(NotEquals(url))) |
1740 | |
1741 | - def _toggle_bookmark_folder(self, folder): |
1742 | - folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1743 | - folder_delegate = folders.get_folder_delegate(folder) |
1744 | - self.pointing_device.click_object( |
1745 | - folders.get_header_from_folder(folder_delegate)) |
1746 | - |
1747 | def test_remove_bookmarks_when_collapsed(self): |
1748 | bookmarks = self.new_tab_view.get_bookmarks_list() |
1749 | self.assertThat(lambda: len(bookmarks.get_delegates()), |
1750 | + Eventually(Equals(5))) |
1751 | + more_button = self.new_tab_view.get_bookmarks_more_button() |
1752 | + |
1753 | + # There are still bookmarks in the model so the list remains full |
1754 | + # and the more button visible |
1755 | + self._remove_first_bookmark() |
1756 | + self.assertThat(lambda: len(bookmarks.get_delegates()), |
1757 | + Eventually(Equals(5))) |
1758 | + print(more_button) |
1759 | + self.assertThat(more_button.opacity, Eventually(Equals(1.0))) |
1760 | + |
1761 | + # The number of bookmarks in the list is now equal to the number of |
1762 | + # bookmarks in the model, so the "more" button is gone |
1763 | + self._remove_first_bookmark() |
1764 | + self.assertThat(lambda: len(bookmarks.get_delegates()), |
1765 | + Eventually(Equals(5))) |
1766 | + self.assertThat(more_button.opacity, Eventually(Equals(0.0))) |
1767 | + |
1768 | + # The number of bookmarks is now less than the length of the list |
1769 | + self._remove_first_bookmark() |
1770 | + self.assertThat(lambda: len(bookmarks.get_delegates()), |
1771 | Eventually(Equals(4))) |
1772 | - more_button = self.new_tab_view.get_bookmarks_more_button() |
1773 | - for i in range(3): |
1774 | - self._remove_first_bookmark() |
1775 | - self.assertThat(more_button.visible, Eventually(Equals(i < 1))) |
1776 | - self.assertThat(len(bookmarks.get_delegates()), |
1777 | - Equals(4 if (i < 2) else 3)) |
1778 | + self.assertThat(more_button.opacity, Eventually(Equals(0.0))) |
1779 | |
1780 | def test_remove_bookmarks_when_expanded(self): |
1781 | more_button = self.new_tab_view.get_bookmarks_more_button() |
1782 | self.assertThat(more_button.visible, Equals(True)) |
1783 | self.pointing_device.click_object(more_button) |
1784 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1785 | - folder_delegate = folders.get_folder_delegate("") |
1786 | self.assertThat(lambda: len(folders.get_urls_from_folder( |
1787 | - folder_delegate)), |
1788 | + self.rootName)), |
1789 | Eventually(Equals(5))) |
1790 | - more_button = self.new_tab_view.get_bookmarks_more_button() |
1791 | - top_sites = self.new_tab_view.get_top_sites_list() |
1792 | - self._toggle_bookmark_folder("Actinide") |
1793 | + |
1794 | + collapsed_content = self.new_tab_view.get_collapsed_contents() |
1795 | + folders.toggle_folder_expanded("Actinide") |
1796 | self._remove_first_bookmark_from_folder("Actinide") |
1797 | - self._toggle_bookmark_folder("NobleGas") |
1798 | + folders.toggle_folder_expanded("NobleGas") |
1799 | self._remove_first_bookmark_from_folder("NobleGas") |
1800 | - self.assertThat(more_button.visible, Eventually(Equals(False))) |
1801 | - self.assertThat(top_sites.visible, Eventually(Equals(True))) |
1802 | + self.assertThat(collapsed_content.visible, Eventually(Equals(True))) |
1803 | + self.assertThat(self.new_tab_view.get_bookmarks_more_button().opacity, |
1804 | + Eventually(Equals(0.0))) |
1805 | |
1806 | def test_show_bookmarks_folders_when_expanded(self): |
1807 | more_button = self.new_tab_view.get_bookmarks_more_button() |
1808 | - self.assertThat(more_button.visible, Equals(True)) |
1809 | + self.assertThat(more_button.opacity, Equals(1.0)) |
1810 | self.pointing_device.click_object(more_button) |
1811 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1812 | - self.assertThat(lambda: len(folders.get_delegates()), |
1813 | + self.assertThat(lambda: len(folders.get_folder_delegates()), |
1814 | Eventually(Equals(3))) |
1815 | - folder_delegate = folders.get_folder_delegate("") |
1816 | self.assertThat(lambda: len(folders.get_urls_from_folder( |
1817 | - folder_delegate)), |
1818 | + self.rootName)), |
1819 | Eventually(Equals(5))) |
1820 | - self._toggle_bookmark_folder("Actinide") |
1821 | - folder_delegate = folders.get_folder_delegate("Actinide") |
1822 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1823 | - folder_delegate)), |
1824 | + folders.toggle_folder_expanded("Actinide") |
1825 | + self.assertThat(lambda: len(folders.get_urls_from_folder("Actinide")), |
1826 | Eventually(Equals(1))) |
1827 | - self._toggle_bookmark_folder("NobleGas") |
1828 | - folder_delegate = folders.get_folder_delegate("NobleGas") |
1829 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1830 | - folder_delegate)), |
1831 | + folders.toggle_folder_expanded("NobleGas") |
1832 | + self.assertThat(lambda: len(folders.get_urls_from_folder("NobleGas")), |
1833 | Eventually(Equals(1))) |
1834 | |
1835 | def test_collapsed_bookmarks_folders_when_expanded(self): |
1836 | @@ -370,19 +393,14 @@ |
1837 | self.assertThat(more_button.visible, Equals(True)) |
1838 | self.pointing_device.click_object(more_button) |
1839 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1840 | - self.assertThat(lambda: len(folders.get_delegates()), |
1841 | + self.assertThat(lambda: len(folders.get_folder_delegates()), |
1842 | Eventually(Equals(3))) |
1843 | - folder_delegate = folders.get_folder_delegate("") |
1844 | self.assertThat(lambda: len(folders.get_urls_from_folder( |
1845 | - folder_delegate)), |
1846 | + self.rootName)), |
1847 | Eventually(Equals(5))) |
1848 | - folder_delegate = folders.get_folder_delegate("Actinide") |
1849 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1850 | - folder_delegate)), |
1851 | + self.assertThat(lambda: len(folders.get_urls_from_folder("Actinide")), |
1852 | Eventually(Equals(0))) |
1853 | - folder_delegate = folders.get_folder_delegate("NobleGas") |
1854 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1855 | - folder_delegate)), |
1856 | + self.assertThat(lambda: len(folders.get_urls_from_folder("NobleGas")), |
1857 | Eventually(Equals(0))) |
1858 | |
1859 | def test_hide_empty_bookmarks_folders_when_expanded(self): |
1860 | @@ -390,46 +408,36 @@ |
1861 | self.assertThat(more_button.visible, Equals(True)) |
1862 | self.pointing_device.click_object(more_button) |
1863 | folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1864 | - self.assertThat(lambda: len(folders.get_delegates()), |
1865 | + self.assertThat(lambda: len(folders.get_folder_delegates()), |
1866 | Eventually(Equals(3))) |
1867 | - self._toggle_bookmark_folder("Actinide") |
1868 | - folder_delegate = folders.get_folder_delegate("Actinide") |
1869 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1870 | - folder_delegate)), |
1871 | + folders.toggle_folder_expanded("Actinide") |
1872 | + self.assertThat(lambda: len(folders.get_urls_from_folder("Actinide")), |
1873 | Eventually(Equals(1))) |
1874 | self._remove_first_bookmark_from_folder("Actinide") |
1875 | - self.assertThat(lambda: len(folders.get_delegates()), |
1876 | + self.assertThat(lambda: len(folders.get_folder_delegates()), |
1877 | Eventually(Equals(2))) |
1878 | - folder_delegate = folders.get_folder_delegate("") |
1879 | self.assertThat(lambda: len(folders.get_urls_from_folder( |
1880 | - folder_delegate)), |
1881 | + self.rootName)), |
1882 | Eventually(Equals(5))) |
1883 | - self._toggle_bookmark_folder("NobleGas") |
1884 | - folder_delegate = folders.get_folder_delegate("NobleGas") |
1885 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1886 | - folder_delegate)), |
1887 | + folders.toggle_folder_expanded("NobleGas") |
1888 | + self.assertThat(lambda: len(folders.get_urls_from_folder("NobleGas")), |
1889 | Eventually(Equals(1))) |
1890 | |
1891 | def test_bookmarks_folder_expands_and_collapses(self): |
1892 | more_button = self.new_tab_view.get_bookmarks_more_button() |
1893 | self.assertThat(more_button.visible, Equals(True)) |
1894 | self.pointing_device.click_object(more_button) |
1895 | - folders = self.new_tab_view.get_bookmarks_folder_list_view() |
1896 | - self.assertThat(lambda: len(folders.get_delegates()), |
1897 | + list = self.new_tab_view.get_bookmarks_folder_list_view() |
1898 | + self.assertThat(lambda: len(list.get_folder_delegates()), |
1899 | Eventually(Equals(3))) |
1900 | - folder_delegate = folders.get_folder_delegate("") |
1901 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1902 | - folder_delegate)), |
1903 | + self.assertThat(lambda: len(list.get_urls_from_folder(self.rootName)), |
1904 | Eventually(Equals(5))) |
1905 | - self.pointing_device.click_object( |
1906 | - folders.get_header_from_folder(folder_delegate)) |
1907 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1908 | - folder_delegate)), |
1909 | + |
1910 | + list.toggle_folder_expanded(self.rootName) |
1911 | + self.assertThat(lambda: len(list.get_urls_from_folder(self.rootName)), |
1912 | Eventually(Equals(0))) |
1913 | - self.pointing_device.click_object( |
1914 | - folders.get_header_from_folder(folder_delegate)) |
1915 | - self.assertThat(lambda: len(folders.get_urls_from_folder( |
1916 | - folder_delegate)), |
1917 | + list.toggle_folder_expanded(self.rootName) |
1918 | + self.assertThat(lambda: len(list.get_urls_from_folder(self.rootName)), |
1919 | Eventually(Equals(5))) |
1920 | |
1921 | def test_remove_top_sites(self): |
1922 | |
1923 | === modified file 'tests/unittests/CMakeLists.txt' |
1924 | --- tests/unittests/CMakeLists.txt 2015-10-06 09:48:38 +0000 |
1925 | +++ tests/unittests/CMakeLists.txt 2015-11-16 09:11:43 +0000 |
1926 | @@ -15,9 +15,11 @@ |
1927 | add_subdirectory(bookmarks-folder-model) |
1928 | add_subdirectory(bookmarks-folderlist-model) |
1929 | add_subdirectory(limit-proxy-model) |
1930 | +add_subdirectory(list-aggregator-model) |
1931 | add_subdirectory(container-url-patterns) |
1932 | add_subdirectory(cookie-store) |
1933 | add_subdirectory(oxide-cookie-helper) |
1934 | +add_subdirectory(roles-adapter-model) |
1935 | add_subdirectory(session-storage) |
1936 | add_subdirectory(favicon-fetcher) |
1937 | add_subdirectory(webapp-container-hook) |
1938 | |
1939 | === modified file 'tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp' |
1940 | --- tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp 2015-07-01 00:27:48 +0000 |
1941 | +++ tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp 2015-11-16 09:11:43 +0000 |
1942 | @@ -170,7 +170,12 @@ |
1943 | QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Icon).toUrl(), QUrl("image://webicon/123")); |
1944 | QVERIFY(model->data(model->index(0, 0), BookmarksModel::Created).toDateTime() <= QDateTime::currentDateTime()); |
1945 | QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Folder).toString(), QString("SampleFolder")); |
1946 | - QVERIFY(!model->data(model->index(0, 0), BookmarksModel::Folder + 1).isValid()); |
1947 | + QCOMPARE(model->data(model->index(0, 0), BookmarksModel::FolderName).toString(), QString("SampleFolder")); |
1948 | + QVERIFY(!model->data(model->index(0, 0), BookmarksModel::FolderName + 1).isValid()); |
1949 | + |
1950 | + model->add(QUrl("http://ubuntu.com/2"), "Ubuntu", QUrl("image://webicon/123"), ""); |
1951 | + QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Folder).toString(), QString("")); |
1952 | + QCOMPARE(model->data(model->index(0, 0), BookmarksModel::FolderName).toString(), BookmarksModel::RootFolderDisplayName); |
1953 | } |
1954 | |
1955 | void shouldReturnDatabasePath() |
1956 | |
1957 | === added directory 'tests/unittests/list-aggregator-model' |
1958 | === added file 'tests/unittests/list-aggregator-model/CMakeLists.txt' |
1959 | --- tests/unittests/list-aggregator-model/CMakeLists.txt 1970-01-01 00:00:00 +0000 |
1960 | +++ tests/unittests/list-aggregator-model/CMakeLists.txt 2015-11-16 09:11:43 +0000 |
1961 | @@ -0,0 +1,13 @@ |
1962 | +find_package(Qt5Core REQUIRED) |
1963 | +find_package(Qt5Sql REQUIRED) |
1964 | +find_package(Qt5Test REQUIRED) |
1965 | +set(TEST tst_ListAggregatorModelTests) |
1966 | +add_executable(${TEST} tst_ListAggregatorModelTests.cpp) |
1967 | +include_directories(${webbrowser-app_SOURCE_DIR}) |
1968 | +target_link_libraries(${TEST} |
1969 | + Qt5::Core |
1970 | + Qt5::Sql |
1971 | + Qt5::Test |
1972 | + webbrowser-app-models |
1973 | +) |
1974 | +add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml) |
1975 | |
1976 | === added file 'tests/unittests/list-aggregator-model/tst_ListAggregatorModelTests.cpp' |
1977 | --- tests/unittests/list-aggregator-model/tst_ListAggregatorModelTests.cpp 1970-01-01 00:00:00 +0000 |
1978 | +++ tests/unittests/list-aggregator-model/tst_ListAggregatorModelTests.cpp 2015-11-16 09:11:43 +0000 |
1979 | @@ -0,0 +1,361 @@ |
1980 | +/* |
1981 | + * Copyright 2011-2015 Canonical Ltd. |
1982 | + * |
1983 | + * This file is part of webbrowser-app. |
1984 | + * |
1985 | + * webbrowser-app is free software; you can redistribute it and/or modify |
1986 | + * it under the terms of the GNU General Public License as published by |
1987 | + * the Free Software Foundation; version 3. |
1988 | + * |
1989 | + * webbrowser-app is distributed in the hope that it will be useful, |
1990 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
1991 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1992 | + * GNU General Public License for more details. |
1993 | + * |
1994 | + * You should have received a copy of the GNU General Public License |
1995 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
1996 | + */ |
1997 | + |
1998 | +// Qt |
1999 | +#include <QtTest/QSignalSpy> |
2000 | +#include <QtTest/QtTest> |
2001 | + |
2002 | +// local |
2003 | +#include "list-aggregator-model.h" |
2004 | + |
2005 | +class ListAggregatorModelTest : public QObject |
2006 | +{ |
2007 | + Q_OBJECT |
2008 | + |
2009 | +private Q_SLOTS: |
2010 | + void testRoleNames() |
2011 | + { |
2012 | + // an aggregator with nothing in it also has no roles |
2013 | + ListAggregatorModel model; |
2014 | + QHash<int, QByteArray> roleNames = model.roleNames(); |
2015 | + QCOMPARE(roleNames.size(), 0); |
2016 | + |
2017 | + // the first model that gets aggregated will reset the model and roles |
2018 | + // will be taken from it |
2019 | + QSignalSpy spyOnModelReset(&model, SIGNAL(modelReset())); |
2020 | + QStringListModel list(QStringList() << "aa"); |
2021 | + QHash<int, QByteArray> listRoles = list.roleNames(); |
2022 | + model.appendModel(QVariant::fromValue(&list)); |
2023 | + QCOMPARE(spyOnModelReset.count(), 1); |
2024 | + roleNames = model.roleNames(); |
2025 | + QCOMPARE(roleNames.size(), listRoles.size()); |
2026 | + Q_FOREACH(int key, roleNames.keys()) { |
2027 | + QVERIFY(listRoles.contains(key)); |
2028 | + QCOMPARE(roleNames[key], listRoles[key]); |
2029 | + } |
2030 | + } |
2031 | + |
2032 | + void testAppendModelWrongType() |
2033 | + { |
2034 | + ListAggregatorModel model; |
2035 | + QVERIFY(model.m_models.isEmpty()); |
2036 | + QTest::ignoreMessage(QtWarningMsg, "Unable to append a model that is not of type QAbstractListModel. Invalid model."); |
2037 | + model.appendModel(QVariant()); |
2038 | + QVERIFY(model.m_models.isEmpty()); |
2039 | + QTest::ignoreMessage(QtWarningMsg, "Unable to append a model that is not of type QAbstractListModel. QVariant(bool, true) is of type bool"); |
2040 | + model.appendModel(QVariant(true)); |
2041 | + QVERIFY(model.m_models.isEmpty()); |
2042 | + QTest::ignoreMessage(QtWarningMsg, "Unable to append a model that is not of type QAbstractListModel. \"\" is of type QObject"); |
2043 | + model.appendModel(QVariant::fromValue(new QObject)); |
2044 | + QVERIFY(model.m_models.isEmpty()); |
2045 | + } |
2046 | + |
2047 | + void testAppendModel() |
2048 | + { |
2049 | + ListAggregatorModel model; |
2050 | + QVERIFY(model.m_models.isEmpty()); |
2051 | + QCOMPARE(model.rowCount(), 0); |
2052 | + |
2053 | + qRegisterMetaType<QModelIndex>("QModelIndex"); |
2054 | + QSignalSpy spyOnRowsAboutToBeInserted(&model, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int))); |
2055 | + QSignalSpy spyOnRowsInserted(&model, SIGNAL(rowsInserted(const QModelIndex&, int, int))); |
2056 | + QList<QVariant> signal; |
2057 | + |
2058 | + QStringListModel list1(QStringList() << "aa" << "ab" << "ac"); |
2059 | + model.appendModel(QVariant::fromValue(&list1)); |
2060 | + QCOMPARE(model.m_models.size(), 1); |
2061 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[0]), &list1); |
2062 | + QCOMPARE(model.rowCount(), 3); |
2063 | + QCOMPARE(spyOnRowsAboutToBeInserted.count(), 1); |
2064 | + signal = spyOnRowsAboutToBeInserted.takeFirst(); |
2065 | + QCOMPARE(signal[1].toInt(), 0); |
2066 | + QCOMPARE(signal[2].toInt(), 2); |
2067 | + QCOMPARE(spyOnRowsInserted.count(), 1); |
2068 | + signal = spyOnRowsInserted.takeFirst(); |
2069 | + QCOMPARE(signal[1].toInt(), 0); |
2070 | + QCOMPARE(signal[2].toInt(), 2); |
2071 | + |
2072 | + QStringListModel list2(QStringList() << "ba" << "bb" << "bc" << "bd"); |
2073 | + model.appendModel(QVariant::fromValue(&list2)); |
2074 | + QCOMPARE(model.m_models.size(), 2); |
2075 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[1]), &list2); |
2076 | + QCOMPARE(model.rowCount(), 7); |
2077 | + QCOMPARE(spyOnRowsAboutToBeInserted.count(), 1); |
2078 | + signal = spyOnRowsAboutToBeInserted.takeFirst(); |
2079 | + QCOMPARE(signal[1].toInt(), 3); |
2080 | + QCOMPARE(signal[2].toInt(), 6); |
2081 | + QCOMPARE(spyOnRowsInserted.count(), 1); |
2082 | + signal = spyOnRowsInserted.takeFirst(); |
2083 | + QCOMPARE(signal[1].toInt(), 3); |
2084 | + QCOMPARE(signal[2].toInt(), 6); |
2085 | + } |
2086 | + |
2087 | + void testInsertModel() |
2088 | + { |
2089 | + ListAggregatorModel model; |
2090 | + QVERIFY(model.m_models.isEmpty()); |
2091 | + QCOMPARE(model.rowCount(), 0); |
2092 | + |
2093 | + qRegisterMetaType<QModelIndex>("QModelIndex"); |
2094 | + QSignalSpy spyOnRowsAboutToBeInserted(&model, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int))); |
2095 | + QSignalSpy spyOnRowsInserted(&model, SIGNAL(rowsInserted(const QModelIndex&, int, int))); |
2096 | + QList<QVariant> signal; |
2097 | + |
2098 | + QStringListModel list1(QStringList() << "aa" << "ab" << "ac"); |
2099 | + model.appendModel(QVariant::fromValue(&list1)); |
2100 | + QCOMPARE(model.m_models.size(), 1); |
2101 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[0]), &list1); |
2102 | + QCOMPARE(model.rowCount(), 3); |
2103 | + QCOMPARE(spyOnRowsAboutToBeInserted.count(), 1); |
2104 | + signal = spyOnRowsAboutToBeInserted.takeFirst(); |
2105 | + QCOMPARE(signal[1].toInt(), 0); |
2106 | + QCOMPARE(signal[2].toInt(), 2); |
2107 | + QCOMPARE(spyOnRowsInserted.count(), 1); |
2108 | + signal = spyOnRowsInserted.takeFirst(); |
2109 | + QCOMPARE(signal[1].toInt(), 0); |
2110 | + QCOMPARE(signal[2].toInt(), 2); |
2111 | + |
2112 | + QStringListModel list2(QStringList() << "ba" << "bb" << "bc" << "bd"); |
2113 | + model.insertModel(QVariant::fromValue(&list2), 0); |
2114 | + QCOMPARE(model.m_models.size(), 2); |
2115 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[0]), &list2); |
2116 | + QCOMPARE(model.rowCount(), 7); |
2117 | + QCOMPARE(spyOnRowsAboutToBeInserted.count(), 1); |
2118 | + signal = spyOnRowsAboutToBeInserted.takeFirst(); |
2119 | + QCOMPARE(signal[1].toInt(), 0); |
2120 | + QCOMPARE(signal[2].toInt(), 3); |
2121 | + QCOMPARE(spyOnRowsInserted.count(), 1); |
2122 | + signal = spyOnRowsInserted.takeFirst(); |
2123 | + QCOMPARE(signal[1].toInt(), 0); |
2124 | + QCOMPARE(signal[2].toInt(), 3); |
2125 | + |
2126 | + // test that insert index is clamped correctly to existing list boundaries |
2127 | + QStringListModel list3(QStringList() << "xx"); |
2128 | + model.insertModel(QVariant::fromValue(&list3), -1); |
2129 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[0]), &list3); |
2130 | + |
2131 | + QStringListModel list4(QStringList() << "yy"); |
2132 | + model.insertModel(QVariant::fromValue(&list4), model.m_models.size() + 1); |
2133 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[model.m_models.size() - 1]), &list4); |
2134 | + } |
2135 | + |
2136 | + void testRemoveModel() |
2137 | + { |
2138 | + ListAggregatorModel model; |
2139 | + |
2140 | + qRegisterMetaType<QModelIndex>("QModelIndex"); |
2141 | + QSignalSpy spyOnRowsAboutToBeRemoved(&model, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int))); |
2142 | + QSignalSpy spyOnRowsRemoved(&model, SIGNAL(rowsRemoved(const QModelIndex&, int, int))); |
2143 | + QList<QVariant> signal; |
2144 | + |
2145 | + QStringListModel list1(QStringList() << "aa" << "ab" << "ac"); |
2146 | + model.appendModel(QVariant::fromValue(&list1)); |
2147 | + QStringListModel list2(QStringList() << "ba" << "bb" << "bc" << "bd"); |
2148 | + model.appendModel(QVariant::fromValue(&list2)); |
2149 | + QStringListModel list3(QStringList() << "ca" << "cb"); |
2150 | + model.appendModel(QVariant::fromValue(&list3)); |
2151 | + |
2152 | + model.removeModel(1); |
2153 | + QCOMPARE(model.m_models.size(), 2); |
2154 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[0]), &list1); |
2155 | + QCOMPARE(qobject_cast<QStringListModel*>(model.m_models[1]), &list3); |
2156 | + QCOMPARE(model.rowCount(), 5); |
2157 | + QCOMPARE(spyOnRowsAboutToBeRemoved.count(), 1); |
2158 | + signal = spyOnRowsAboutToBeRemoved.takeFirst(); |
2159 | + QCOMPARE(signal[1].toInt(), 3); |
2160 | + QCOMPARE(signal[2].toInt(), 6); |
2161 | + QCOMPARE(spyOnRowsRemoved.count(), 1); |
2162 | + signal = spyOnRowsRemoved.takeFirst(); |
2163 | + QCOMPARE(signal[1].toInt(), 3); |
2164 | + QCOMPARE(signal[2].toInt(), 6); |
2165 | + |
2166 | + // test that nothing happens if we remove out of bounds |
2167 | + model.removeModel(-1); |
2168 | + QCOMPARE(model.m_models.size(), 2); |
2169 | + QCOMPARE(spyOnRowsRemoved.count(), 0); |
2170 | + model.removeModel(3); |
2171 | + QCOMPARE(model.m_models.size(), 2); |
2172 | + QCOMPARE(spyOnRowsRemoved.count(), 0); |
2173 | + } |
2174 | + |
2175 | + void testData() |
2176 | + { |
2177 | + ListAggregatorModel model; |
2178 | + model.appendModel(QVariant::fromValue(new QStringListModel(QStringList() << "aa" << "ab" << "ac", &model))); |
2179 | + model.appendModel(QVariant::fromValue(new QStringListModel(QStringList() << "ba" << "bb" << "bc" << "bd", &model))); |
2180 | + model.appendModel(QVariant::fromValue(new QStringListModel(QStringList() << "ca" << "cb", &model))); |
2181 | + QStringList data = QStringList() << "aa" << "ab" << "ac" << "ba" << "bb" << "bc" << "bd" << "ca" << "cb"; |
2182 | + for (int i = 0; i < model.rowCount(); ++i) { |
2183 | + QCOMPARE(model.data(model.index(i)).toString(), data[i]); |
2184 | + } |
2185 | + } |
2186 | + |
2187 | + void testGet() |
2188 | + { |
2189 | + ListAggregatorModel model; |
2190 | + model.appendModel(QVariant::fromValue(new QStringListModel(QStringList() << "aa" << "ab" << "ac", &model))); |
2191 | + model.appendModel(QVariant::fromValue(new QStringListModel(QStringList() << "ba" << "bb" << "bc" << "bd", &model))); |
2192 | + model.appendModel(QVariant::fromValue(new QStringListModel(QStringList() << "ca" << "cb", &model))); |
2193 | + QStringList data = QStringList() << "aa" << "ab" << "ac" << "ba" << "bb" << "bc" << "bd" << "ca" << "cb"; |
2194 | + for (int i = 0; i < model.rowCount(); ++i) { |
2195 | + QCOMPARE(model.get(i).toString(), data[i]); |
2196 | + } |
2197 | + } |
2198 | + |
2199 | + void testComputeOffset() |
2200 | + { |
2201 | + ListAggregatorModel model; |
2202 | + QStringListModel list1(QStringList() << "aa" << "ab" << "ac"); |
2203 | + model.appendModel(QVariant::fromValue(&list1)); |
2204 | + QStringListModel list2(QStringList() << "ba" << "bb" << "bc" << "bd"); |
2205 | + model.appendModel(QVariant::fromValue(&list2)); |
2206 | + QStringListModel list3(QStringList() << "ca" << "cb"); |
2207 | + model.appendModel(QVariant::fromValue(&list3)); |
2208 | + |
2209 | + QCOMPARE(model.computeOffset(&list1), 0); |
2210 | + QCOMPARE(model.computeOffset(&list2), 3); |
2211 | + QCOMPARE(model.computeOffset(&list3), 7); |
2212 | + } |
2213 | + |
2214 | + void testModelAtIndex() |
2215 | + { |
2216 | + ListAggregatorModel model; |
2217 | + QStringListModel list1(QStringList() << "aa" << "ab" << "ac"); |
2218 | + model.appendModel(QVariant::fromValue(&list1)); |
2219 | + QStringListModel list2(QStringList() << "ba" << "bb" << "bc" << "bd"); |
2220 | + model.appendModel(QVariant::fromValue(&list2)); |
2221 | + QStringListModel list3(QStringList() << "ca" << "cb"); |
2222 | + model.appendModel(QVariant::fromValue(&list3)); |
2223 | + |
2224 | + QCOMPARE(model.modelAtIndex(0), &list1); |
2225 | + QCOMPARE(model.modelAtIndex(1), &list1); |
2226 | + QCOMPARE(model.modelAtIndex(2), &list1); |
2227 | + QCOMPARE(model.modelAtIndex(3), &list2); |
2228 | + QCOMPARE(model.modelAtIndex(4), &list2); |
2229 | + QCOMPARE(model.modelAtIndex(5), &list2); |
2230 | + QCOMPARE(model.modelAtIndex(6), &list2); |
2231 | + QCOMPARE(model.modelAtIndex(7), &list3); |
2232 | + QCOMPARE(model.modelAtIndex(8), &list3); |
2233 | + } |
2234 | + |
2235 | + void testRemoveFromAggregatedModel() |
2236 | + { |
2237 | + // Test that removing from any of the aggregate models will also remove |
2238 | + // from the aggregator at the right position |
2239 | + |
2240 | + ListAggregatorModel model; |
2241 | + QStringListModel list1(QStringList() << "aa" << "ab"); |
2242 | + model.appendModel(QVariant::fromValue(&list1)); |
2243 | + QStringListModel list2(QStringList() << "ba" << "bb"); |
2244 | + model.appendModel(QVariant::fromValue(&list2)); |
2245 | + |
2246 | + qRegisterMetaType<QModelIndex>("QModelIndex"); |
2247 | + QSignalSpy spyOnRowsRemoved(&model, SIGNAL(rowsRemoved(const QModelIndex&, int, int))); |
2248 | + QList<QVariant> signal; |
2249 | + |
2250 | + list1.removeRow(1); |
2251 | + QCOMPARE(spyOnRowsRemoved.count(), 1); |
2252 | + signal = spyOnRowsRemoved.takeFirst(); |
2253 | + QCOMPARE(signal[1].toInt(), 1); |
2254 | + QCOMPARE(signal[2].toInt(), 1); |
2255 | + QCOMPARE(model.rowCount(), 3); |
2256 | + QStringList data = QStringList() << "aa" << "ba" << "bb"; |
2257 | + for (int i = 0; i < model.rowCount(); ++i) { |
2258 | + QCOMPARE(model.data(model.index(i)).toString(), data[i]); |
2259 | + } |
2260 | + |
2261 | + list2.removeRows(0, 2); |
2262 | + QCOMPARE(spyOnRowsRemoved.count(), 1); |
2263 | + signal = spyOnRowsRemoved.takeFirst(); |
2264 | + QCOMPARE(signal[1].toInt(), 1); |
2265 | + QCOMPARE(signal[2].toInt(), 2); |
2266 | + QCOMPARE(model.rowCount(), 1); |
2267 | + QCOMPARE(model.data(model.index(0)).toString(), QString("aa")); |
2268 | + } |
2269 | + |
2270 | + void testInsertAndChangeDataInAggregatedModel() |
2271 | + { |
2272 | + // Test that inserting in any of the aggregate models will also insert |
2273 | + // into the aggregator at the right position. |
2274 | + // Due to the nature of the QStringListModel used to do these tests, |
2275 | + // which first requires inserting empty rows then updating their data, |
2276 | + // we also test here that data change is reported correctly, instead of |
2277 | + // doing so in a separate test. |
2278 | + |
2279 | + ListAggregatorModel model; |
2280 | + QStringListModel list1(QStringList() << "ab"); |
2281 | + model.appendModel(QVariant::fromValue(&list1)); |
2282 | + QStringListModel list2; |
2283 | + model.appendModel(QVariant::fromValue(&list2)); |
2284 | + |
2285 | + qRegisterMetaType<QModelIndex>("QModelIndex"); |
2286 | + QSignalSpy spyOnRowsInserted(&model, SIGNAL(rowsInserted(QModelIndex,int,int))); |
2287 | + QSignalSpy spyOnDataChanged(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); |
2288 | + QList<QVariant> signal; |
2289 | + |
2290 | + list1.insertRow(0); |
2291 | + QCOMPARE(spyOnRowsInserted.count(), 1); |
2292 | + signal = spyOnRowsInserted.takeFirst(); |
2293 | + QCOMPARE(signal[1].toInt(), 0); |
2294 | + QCOMPARE(signal[2].toInt(), 0); |
2295 | + QCOMPARE(model.rowCount(), 2); |
2296 | + QStringList data = QStringList() << "" << "ab"; |
2297 | + for (int i = 0; i < model.rowCount(); ++i) { |
2298 | + QCOMPARE(model.data(model.index(i)).toString(), data[i]); |
2299 | + } |
2300 | + |
2301 | + list1.setData(list1.index(0), QVariant("aa")); |
2302 | + QCOMPARE(spyOnDataChanged.count(), 1); |
2303 | + signal = spyOnDataChanged.takeFirst(); |
2304 | + QCOMPARE(qvariant_cast<QModelIndex>(signal[0]).row(), 0); |
2305 | + QCOMPARE(qvariant_cast<QModelIndex>(signal[1]).row(), 0); |
2306 | + QCOMPARE(model.rowCount(), 2); |
2307 | + data = QStringList() << "aa" << "ab"; |
2308 | + for (int i = 0; i < model.rowCount(); ++i) { |
2309 | + QCOMPARE(model.data(model.index(i)).toString(), data[i]); |
2310 | + } |
2311 | + |
2312 | + list2.insertRows(0, 2); |
2313 | + QCOMPARE(spyOnRowsInserted.count(), 1); |
2314 | + signal = spyOnRowsInserted.takeFirst(); |
2315 | + QCOMPARE(signal[1].toInt(), 2); |
2316 | + QCOMPARE(signal[2].toInt(), 3); |
2317 | + QCOMPARE(model.rowCount(), 4); |
2318 | + data = QStringList() << "aa" << "ab" << "" << ""; |
2319 | + for (int i = 0; i < model.rowCount(); ++i) { |
2320 | + QCOMPARE(model.data(model.index(i)).toString(), data[i]); |
2321 | + } |
2322 | + |
2323 | + list2.setData(list2.index(0), QVariant("ba")); |
2324 | + list2.setData(list2.index(1), QVariant("bb")); |
2325 | + QCOMPARE(spyOnDataChanged.count(), 2); |
2326 | + signal = spyOnDataChanged.takeFirst(); |
2327 | + QCOMPARE(qvariant_cast<QModelIndex>(signal[0]).row(), 2); |
2328 | + QCOMPARE(qvariant_cast<QModelIndex>(signal[1]).row(), 2); |
2329 | + signal = spyOnDataChanged.takeFirst(); |
2330 | + QCOMPARE(qvariant_cast<QModelIndex>(signal[0]).row(), 3); |
2331 | + QCOMPARE(qvariant_cast<QModelIndex>(signal[1]).row(), 3); |
2332 | + data = QStringList() << "aa" << "ab" << "ba" << "bb"; |
2333 | + for (int i = 0; i < model.rowCount(); ++i) { |
2334 | + QCOMPARE(model.data(model.index(i)).toString(), data[i]); |
2335 | + } |
2336 | + } |
2337 | +}; |
2338 | + |
2339 | +QTEST_MAIN(ListAggregatorModelTest) |
2340 | +#include "tst_ListAggregatorModelTests.moc" |
2341 | |
2342 | === added directory 'tests/unittests/roles-adapter-model' |
2343 | === added file 'tests/unittests/roles-adapter-model/CMakeLists.txt' |
2344 | --- tests/unittests/roles-adapter-model/CMakeLists.txt 1970-01-01 00:00:00 +0000 |
2345 | +++ tests/unittests/roles-adapter-model/CMakeLists.txt 2015-11-16 09:11:43 +0000 |
2346 | @@ -0,0 +1,13 @@ |
2347 | +find_package(Qt5Core REQUIRED) |
2348 | +find_package(Qt5Sql REQUIRED) |
2349 | +find_package(Qt5Test REQUIRED) |
2350 | +set(TEST tst_RolesAdapterModelTests) |
2351 | +add_executable(${TEST} tst_RolesAdapterModelTests.cpp) |
2352 | +include_directories(${webbrowser-app_SOURCE_DIR}) |
2353 | +target_link_libraries(${TEST} |
2354 | + Qt5::Core |
2355 | + Qt5::Sql |
2356 | + Qt5::Test |
2357 | + webbrowser-app-models |
2358 | +) |
2359 | +add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml) |
2360 | |
2361 | === added file 'tests/unittests/roles-adapter-model/tst_RolesAdapterModelTests.cpp' |
2362 | --- tests/unittests/roles-adapter-model/tst_RolesAdapterModelTests.cpp 1970-01-01 00:00:00 +0000 |
2363 | +++ tests/unittests/roles-adapter-model/tst_RolesAdapterModelTests.cpp 2015-11-16 09:11:43 +0000 |
2364 | @@ -0,0 +1,97 @@ |
2365 | +/* |
2366 | + * Copyright 2011-2015 Canonical Ltd. |
2367 | + * |
2368 | + * This file is part of webbrowser-app. |
2369 | + * |
2370 | + * webbrowser-app is free software; you can redistribute it and/or modify |
2371 | + * it under the terms of the GNU General Public License as published by |
2372 | + * the Free Software Foundation; version 3. |
2373 | + * |
2374 | + * webbrowser-app is distributed in the hope that it will be useful, |
2375 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
2376 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2377 | + * GNU General Public License for more details. |
2378 | + * |
2379 | + * You should have received a copy of the GNU General Public License |
2380 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
2381 | + */ |
2382 | + |
2383 | +// Qt |
2384 | +#include <QtCore/QStringListModel> |
2385 | +#include <QtTest/QSignalSpy> |
2386 | +#include <QtTest/QtTest> |
2387 | + |
2388 | +// local |
2389 | +#include "roles-adapter-model.h" |
2390 | + |
2391 | +class CustomRolesModel : public QStringListModel |
2392 | +{ |
2393 | + Q_OBJECT |
2394 | + |
2395 | +public: |
2396 | + QHash<int, QByteArray> roleNames() const { |
2397 | + return roles; |
2398 | + } |
2399 | + |
2400 | + QVariant data(const QModelIndex& index, int role) const { |
2401 | + if (roles.contains(role)) { |
2402 | + QString key = roles.value(role); |
2403 | + if (item.contains(key)) { |
2404 | + return item.value(key); |
2405 | + } |
2406 | + } |
2407 | + return QVariant(); |
2408 | + } |
2409 | + |
2410 | + QHash<int, QByteArray> roles; |
2411 | + QMap<QString, QString> item; |
2412 | +}; |
2413 | + |
2414 | +class RolesAdapterModelTest : public QObject |
2415 | +{ |
2416 | + Q_OBJECT |
2417 | + |
2418 | +private Q_SLOTS: |
2419 | + |
2420 | + void testAdapter() |
2421 | + { |
2422 | + RolesAdapterModel adapter; |
2423 | + QVERIFY(!adapter.data(adapter.index(0, 0), 0).isValid()); |
2424 | + |
2425 | + QSignalSpy spyOnRolesSourceChanged(&adapter, SIGNAL(rolesSourceChanged())); |
2426 | + QSignalSpy spyOnModelReset(&adapter, SIGNAL(modelReset())); |
2427 | + |
2428 | + CustomRolesModel rolesSource; |
2429 | + rolesSource.roles.insert(Qt::UserRole + 1, "foo"); |
2430 | + |
2431 | + CustomRolesModel dataSource; |
2432 | + dataSource.roles.insert(0, "foo"); |
2433 | + dataSource.item.insert("foo", "something"); |
2434 | + adapter.setSourceModel(&dataSource); |
2435 | + QCOMPARE(spyOnModelReset.count(), 1); |
2436 | + |
2437 | + // verify that without a roles source we respond with the roles from |
2438 | + // the source model. |
2439 | + QVERIFY(!adapter.data(adapter.index(0, 0), Qt::UserRole + 1).isValid()); |
2440 | + QCOMPARE(adapter.data(adapter.index(0, 0), 0).toString(), QString("something")); |
2441 | + |
2442 | + // once we set the roles source, we will respond to roles from it and |
2443 | + // not from the source model. |
2444 | + adapter.setRolesSource(QVariant::fromValue(&rolesSource)); |
2445 | + QVERIFY(!adapter.data(adapter.index(0, 0), 0).isValid()); |
2446 | + QCOMPARE(adapter.data(adapter.index(0, 0), Qt::UserRole + 1).toString(), QString("something")); |
2447 | + QCOMPARE(spyOnRolesSourceChanged.count(), 1); |
2448 | + QCOMPARE(spyOnModelReset.count(), 2); |
2449 | + |
2450 | + // verify that we can reset the roles rouce by setting roles to undefined |
2451 | + adapter.setRolesSource(QVariant()); |
2452 | + QVERIFY(!adapter.data(adapter.index(0, 0), Qt::UserRole + 1).isValid()); |
2453 | + QCOMPARE(adapter.data(adapter.index(0, 0), 0).toString(), QString("something")); |
2454 | + QCOMPARE(spyOnRolesSourceChanged.count(), 2); |
2455 | + QCOMPARE(spyOnModelReset.count(), 3); |
2456 | + } |
2457 | +}; |
2458 | + |
2459 | +QTEST_MAIN(RolesAdapterModelTest) |
2460 | +#include "tst_RolesAdapterModelTests.moc" |
2461 | + |
FAILED: Continuous integration, rev:1249 jenkins. qa.ubuntu. com/job/ webbrowser- app-ci/ 2377/ jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- vivid-touch/ 4717 jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- amd64-ci/ 1131 jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- armhf-ci/ 1131 jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- armhf-ci/ 1131/artifact/ work/output/ *zip*/output. zip jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- i386-ci/ 1131 jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- runner- vivid-mako/ 3810 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 4714 jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 4714/artifact/ work/output/ *zip*/output. zip s-jenkins. ubuntu- ci:8080/ job/touch- flash-device/ 24382
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/webbrowser- app-ci/ 2377/rebuild
http://