Merge lp:~artmello/webbrowser-app/webbrowser-app-bookmark_folders into lp:webbrowser-app

Proposed by Arthur Mello on 2015-05-28
Status: Merged
Approved by: Olivier Tilloy on 2015-07-03
Approved revision: 1125
Merged at revision: 1077
Proposed branch: lp:~artmello/webbrowser-app/webbrowser-app-bookmark_folders
Merge into: lp:webbrowser-app
Diff against target: 2658 lines (+1979/-70)
27 files modified
src/app/webbrowser/AddressBar.qml (+7/-0)
src/app/webbrowser/BookmarkOptions.qml (+166/-0)
src/app/webbrowser/BookmarksFolderListView.qml (+157/-0)
src/app/webbrowser/Browser.qml (+64/-4)
src/app/webbrowser/CMakeLists.txt (+2/-0)
src/app/webbrowser/Chrome.qml (+2/-0)
src/app/webbrowser/NewTabView.qml (+27/-9)
src/app/webbrowser/UrlsList.qml (+2/-2)
src/app/webbrowser/bookmarks-folder-model.cpp (+84/-0)
src/app/webbrowser/bookmarks-folder-model.h (+60/-0)
src/app/webbrowser/bookmarks-folderlist-model.cpp (+217/-0)
src/app/webbrowser/bookmarks-folderlist-model.h (+79/-0)
src/app/webbrowser/bookmarks-model.cpp (+164/-8)
src/app/webbrowser/bookmarks-model.h (+15/-3)
src/app/webbrowser/webbrowser-app.cpp (+2/-0)
tests/autopilot/webbrowser_app/emulators/browser.py (+52/-0)
tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py (+3/-0)
tests/autopilot/webbrowser_app/tests/test_bookmark_options.py (+243/-0)
tests/autopilot/webbrowser_app/tests/test_keyboard.py (+6/-2)
tests/autopilot/webbrowser_app/tests/test_new_tab_view.py (+150/-22)
tests/autopilot/webbrowser_app/tests/test_suggestions.py (+2/-2)
tests/unittests/CMakeLists.txt (+2/-0)
tests/unittests/bookmarks-folder-model/CMakeLists.txt (+6/-0)
tests/unittests/bookmarks-folder-model/tst_BookmarksFolderModelTests.cpp (+123/-0)
tests/unittests/bookmarks-folderlist-model/CMakeLists.txt (+6/-0)
tests/unittests/bookmarks-folderlist-model/tst_BookmarksFolderListModelTests.cpp (+269/-0)
tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp (+69/-18)
To merge this branch: bzr merge lp:~artmello/webbrowser-app/webbrowser-app-bookmark_folders
Reviewer Review Type Date Requested Status
Olivier Tilloy 2015-05-28 Approve on 2015-07-03
PS Jenkins bot continuous-integration Approve on 2015-07-02
Bill Filler (community) Needs Fixing on 2015-06-17
Review via email: mp+260410@code.launchpad.net

Commit Message

Implement bookmark folders

Description of the Change

Implement bookmark folders

To post a comment you must log in.
1032. By Arthur Mello on 2015-05-28

Remove empty line

1033. By Arthur Mello on 2015-06-02

Merge with trunk

1034. By Arthur Mello on 2015-06-04

Move the Folder data to a specific table in Database

1035. By Arthur Mello on 2015-06-04

Update models to handle the new bookmarks folders table

1036. By Arthur Mello on 2015-06-04

Add support to the default empty folder

1037. By Arthur Mello on 2015-06-04

Fix QML issues

1038. By Arthur Mello on 2015-06-04

Revert chnages in NewTabView

1039. By Arthur Mello on 2015-06-04

Merge with trunk

Olivier Tilloy (osomon) wrote :

Merged in the latest trunk, and tst_BookmarksModelTests.cpp fails to build.

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

After resolving the trivial build issue pointed out above, I’m doing some quick functional testing, and creating a new bookmark folder doesn’t seem to work: I’m bookmarking a page that wasn’t previously bookmarked, the popover appears, I click on "New Folder", enter a new name (btw pressing the Enter key should validate), click "Choose Folder", but the dropdown list still has only "All Bookmarks".

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

"CREATE TABLE IF NOT EXISTS bookmarks_folders" …

Can the table be named "folders", please? The "bookmarks_" prefix is redundant, given that the database name is "bookmarks" already.

Olivier Tilloy (osomon) wrote :

33 + if (state == "exisitngFolder") {

typo

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

A few remarks/suggestions, from a very quick read through the diff, in no particular order (and not a complete review yet):

189 + function show() {
190 + sourceComponent = bookmarkOptionsDialog
191 + }

This is unnecessary: instead of a custom show() function, where you would call it just do "bookmarkOptionsLoader.active = true", and have sourceComponent always set to bookmarkOptionsDialog and active set to false by default.

For BookmarkOptions.qml, should we use a Popover component from the UITK? It already handles the automatic dismissal when clicking outside of it, IIRC.

376 + Q_PROPERTY(QDateTime lastAddition READ lastAddition NOTIFY lastAdditionChanged)

Do we really need this "lastAddition" property (and role)? I was under the impression that we would always display the list of bookmarks sorted alphabetically, not by recency. BookmarksFolderListChronologicalModel would become unnecessary too.

790 + Empty

I’m not sure this role is very useful. Since there is an "Entries" role already, it’s easy to check whether the list of entries is empty. I’d remove it.

903 + emit folderInserted("");

Can you please use Q_EMIT everywhere instead of emit?
Can you rename the "folderInserted" signal to "folderAdded"? Similarly, "insertNewFolder" should be renamed to "addFolder".

The SQL "bookmarks" table should probably have a foreign key constraint on folderId. See https://www.sqlite.org/foreignkeys.html.

1040. By Arthur Mello on 2015-06-04

Merge with trunk

1041. By Arthur Mello on 2015-06-04

Fix build issue

1042. By Arthur Mello on 2015-06-04

Change the bookmarks folders table name

1043. By Arthur Mello on 2015-06-04

Fix typo

1044. By Arthur Mello on 2015-06-04

Remove unnecessary show function

1045. By Arthur Mello on 2015-06-04

Remove both the lastAddition property and the folder list chronological model

1046. By Arthur Mello on 2015-06-04

Remove empty property

1047. By Arthur Mello on 2015-06-04

Use Q_EMIT instead of emit
Rename methods names

1048. By Arthur Mello on 2015-06-04

Use the Popover component from the SDK for the BookmarkOptions

1049. By Arthur Mello on 2015-06-04

Add a new Dialog to add a new folder in BookmarkOptions

1050. By Arthur Mello on 2015-06-04

Create API in the foldersListModel to create a new folder

1051. By Arthur Mello on 2015-06-04

Add new unittest for the createNewFolder function

Arthur Mello (artmello) wrote :

> The SQL "bookmarks" table should probably have a foreign key constraint on
> folderId. See https://www.sqlite.org/foreignkeys.html.

It seems that Foreign Key support is disabled by default and need to be enabled setting the PRAGMA foreign_keys. But it is not possible to enable it in a multi-statement transaction so we would need to set it before each relevant transaction in the bookmarks-model. I think that could be error prone but can be done for sure. What do you think? Or maybe there is another way to do it that I am missing.

Arthur Mello (artmello) wrote :

> After resolving the trivial build issue pointed out above, I’m doing some
> quick functional testing, and creating a new bookmark folder doesn’t seem to
> work: I’m bookmarking a page that wasn’t previously bookmarked, the popover
> appears, I click on "New Folder", enter a new name (btw pressing the Enter key
> should validate), click "Choose Folder", but the dropdown list still has only
> "All Bookmarks".

The BookmarkOptions component had 2 states: choosing an existing folder and creating a new folder. We would only create a new folder if the user dismiss the component in the "creating a new folder" state. So when you clicked "Choose Folder" you cancelled the creation of the new folder. But, as we can see, that is not user friendly. I changed to show a dialog when you click "New Folder" and when it is confirmed we will create the folder and it will show up in the dropdown. Please, let me know what you think about it.

Olivier Tilloy (osomon) wrote :

> > The SQL "bookmarks" table should probably have a foreign key constraint on
> > folderId. See https://www.sqlite.org/foreignkeys.html.
>
> It seems that Foreign Key support is disabled by default and need to be
> enabled setting the PRAGMA foreign_keys. But it is not possible to enable it
> in a multi-statement transaction so we would need to set it before each
> relevant transaction in the bookmarks-model. I think that could be error prone
> but can be done for sure. What do you think? Or maybe there is another way to
> do it that I am missing.

Looks like too much hassle for little benefit. Let’s forget about it.

Olivier Tilloy (osomon) wrote :

> > After resolving the trivial build issue pointed out above, I’m doing some
> > quick functional testing, and creating a new bookmark folder doesn’t seem to
> > work: I’m bookmarking a page that wasn’t previously bookmarked, the popover
> > appears, I click on "New Folder", enter a new name (btw pressing the Enter
> key
> > should validate), click "Choose Folder", but the dropdown list still has
> only
> > "All Bookmarks".
>
> The BookmarkOptions component had 2 states: choosing an existing folder and
> creating a new folder. We would only create a new folder if the user dismiss
> the component in the "creating a new folder" state. So when you clicked
> "Choose Folder" you cancelled the creation of the new folder. But, as we can
> see, that is not user friendly. I changed to show a dialog when you click "New
> Folder" and when it is confirmed we will create the folder and it will show up
> in the dropdown. Please, let me know what you think about it.

This looks better, although I would prefer not to have an extra button. Maybe a special entry in the dropdown to create a new folder? In any case design will have to comment on this, so let’s leave it as is for now, it works.

Olivier Tilloy (osomon) wrote :

A few more comments/remarks from a functional review and partial code review:

 - When adding a bookmark, creating a new folder should auto-select it (supposedly when creating a new folder the user wants to save the current bookmark in it).

 - The arrow of the bookmark dialog popover should point at the star, not at the center of the chrome.

 - The dialog title says "bookmark added", however the bookmark is not actually added until after the dialog is closed. If the app crashes or is killed for whatever reason while the dialog is shown, the bookmark will be lost, which is unexpected. Can we add the bookmark right away (by default without a folder, and when changing the folder from the dropdown update it)?

 - The font size of all elements in the bookmark dialog should probably decreased, except maybe that of the title.

 - There should probably be top and bottom margins for the contents of the bookmark dialog.

 - The inclusion of QDateTime in bookmarks-folder-model.h is useless.

 - In BookmarksFolderListModel::setSourceModel(), why do you need to listen to layoutChanged()?

review: Needs Fixing
1052. By Arthur Mello on 2015-06-08

Set the optionSelector to the new created folder

1053. By Arthur Mello on 2015-06-08

Point the arrow of the bookmark dialog popover should at the star

1054. By Arthur Mello on 2015-06-08

Add the bookmark as soon as the toggle is clicked and update the data after

1055. By Arthur Mello on 2015-06-08

Add a top and a bottom margin to the bookmarkOptions

1056. By Arthur Mello on 2015-06-08

Remove unnecessary include

1057. By Arthur Mello on 2015-06-08

Stop connecting to unnecessary signal

1058. By Arthur Mello on 2015-06-08

Merge with trunk

Arthur Mello (artmello) wrote :

> - The font size of all elements in the bookmark dialog should probably
> decreased, except maybe that of the title.

The "Save in" text is inside the OptionSelector component and it seems it does not provide anyway to set the fontSize for the text.

Olivier Tilloy (osomon) wrote :

> The "Save in" text is inside the OptionSelector component and it seems
> it does not provide anyway to set the fontSize for the text.

Have you asked the UITK folks about that? What about the other widgets in the popover?

When creating a new folder from the popover, the new folder is selected as expected, but the dropdown remains expanded. I think it should be collapsed.

The __bookmarkToggle property is not a good idea, it breaks encapsulation. Instead, can you add an empty item that is anchored to the bookmark toggle, and have that item exposed as a top-level property? Properties whose names start with a double underscore "__" are not meant to be used in QML, they exist only for testing purposes.

Why does BookmarksModel::update() move the bookmark in the model? The "created" timestamp doesn’t need to be updated (it represents the bookmark creation time, not its last modification time), so the bookmark doesn’t need to be moved.

If BookmarksModel::update() is used only to change the folder, then it should be renamed to updateFolder() or setFolder(), and it should accept only a URL and folder name as parameter (it shouldn’t change the title and icon, unless there is a concrete use case for that).

Can you please rename BookmarksFolderListModel::getIndex() to BookmarksFolderListModel::indexOf() ?

review: Needs Fixing
1059. By Arthur Mello on 2015-06-09

Do not change the icon and the created roles when updating bookmark

1060. By Arthur Mello on 2015-06-09

Rename method

1061. By Arthur Mello on 2015-06-09

Expose an item to work as a place holder for the bookmark toggle

1062. By Arthur Mello on 2015-06-09

Colapse the OptionSelector after we add a new folder

1063. By Arthur Mello on 2015-06-09

Reduce font size of tex

1064. By Arthur Mello on 2015-06-11

Merge with trunk

1065. By Arthur Mello on 2015-06-11

Limit the size of the OptionSelector when expanded

1066. By Arthur Mello on 2015-06-12

Show the bookmarks folders when the user click in see more bookmarks

1067. By Arthur Mello on 2015-06-12

Merge with trunk

Arthur Mello (artmello) wrote :

Following some Bill's suggestions, I am showing the bookmark folders when the user click in the "see more" bookmarks button. Now, when the user click in the "see more" we show all the bookmarks separated by folder. All the folders are expanded and we can collapse them if we click in the folder name. Since that was not specified by the design team any feedback about the UX will be really appreciated

Olivier Tilloy (osomon) wrote :

Revision 1066 breaks autopilot tests. Please run the tests locally before pushing new revisions.

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

When expanding the list of favorites to display the bookmarks grouped by folder, empty folders should probably not be displayed (including "All Bookmarks" if it’s empty).

Olivier Tilloy (osomon) wrote :

238 + text: {
239 + var ret
240 + if (bookmarksFolderColumn.expanded) {
241 + ret = "- "
242 + } else {
243 + ret = "+ "
244 + }
245 +
246 + if (folder) {
247 + return ret + folder
248 + } else {
249 + return ret + i18n.tr("All Bookmarks")
250 + }
251 + }

This should be properly internationalized: exotic languages might have different ways of signifying that a section is expanded/collapsed than a minus/plus sign (not to mention RTL languages where the sign would probably be incorrectly placed).

review: Needs Fixing
Bill Filler (bfiller) wrote :

Some comments that need to be fixed (some dupes of Olivier's). You should run on the device to see some of these.

1) turn off predictive text hint for entry field in Save dialog
2) pressing the Save button in the dialog doesn't close it on the device, it only dismisses the keyboard but doesn't close the dialog
3) I think we really need a "Dismiss" button in the initial popover. Not clear how to close it, especially on device
4) On device, if the list of bookmark folders is too big it pushes the popover above the url field
5) On the bookmarks page, the "More" button is too big. I thought this was supposed to be a "See more.." link at the bottom of the bookmarks list, not a button to the right? If we leave it a button, please reduce the size. Same with less button.
6) Hide folders that are empty in the more view
7) Instead of using the + -, use right arrow and down arrow icons if we have them, if not don't show anything. Just allow clicking the section to expand/collapse.

review: Needs Fixing
1068. By Arthur Mello on 2015-06-15

Merge with trunk

1069. By Arthur Mello on 2015-06-16

Merge with trunk

1070. By Arthur Mello on 2015-06-17

Do not display empty folders
Show icons instead of +/- as symbols for folders names

1071. By Arthur Mello on 2015-06-17

Turn off predictive text hint for entry field in Save dialog

1072. By Arthur Mello on 2015-06-17

Add a dismiss button to the BookmarkOptions dialog

1073. By Arthur Mello on 2015-06-17

Change how the top and the bottom margin is handled in the BookmarkOptions so it will not push above the url field

1074. By Arthur Mello on 2015-06-17

Reduce the overall size of the BookmarkOptions component so it will not override url bar

1075. By Arthur Mello on 2015-06-17

Dismiss BookmarkOptions during AP tests

1076. By Arthur Mello on 2015-06-17

Get delegates from the "All Bookmarks" folder to fix AP test

1077. By Arthur Mello on 2015-06-17

Fix AP test to work with new bookmarks folder list view

Arthur Mello (artmello) wrote :

> 5) On the bookmarks page, the "More" button is too big. I thought this was
> supposed to be a "See more.." link at the bottom of the bookmarks list, not a
> button to the right? If we leave it a button, please reduce the size. Same
> with less button.

Thanks a lot for reviewing this. If you agree, I think it would be better handle this in a different MR. The button was not changed by this, but was applied during the changes for the new tab view. I will talk with the design team about it.

Bill Filler (bfiller) wrote :

Good fixes, still seeing 2 problems:
1) pressing the Save button in the dialog doesn't close it on the device, it only dismisses the keyboard but doesn't close the dialog. You have to press it twice to make it go away

2) after pressing it a second time, the popover is shown but you see the selection in the list switching to the new folder name and it takes a while. I would expect the new folder name to already be selected in the list before the popover becomes visible

review: Needs Fixing
1078. By Arthur Mello on 2015-06-17

Make sure that the clicked signal is triggered in the new folder dialog

1079. By Arthur Mello on 2015-06-17

Reduce BookmarkOptions space

1080. By Arthur Mello on 2015-06-19

Merge with trunk

Olivier Tilloy (osomon) wrote :

124 + text: i18n.tr("Ok")

Spelling is incorrect, this should be "OK" (see https://code.launchpad.net/~mterry/webbrowser-app/Ok/+merge/260630).

review: Needs Fixing
1081. By Arthur Mello on 2015-06-22

Fix text

1082. By Arthur Mello on 2015-06-22

Merge with trunk

1083. By Arthur Mello on 2015-06-23

Merge with trunk

1084. By Arthur Mello on 2015-06-23

Fix AP test and Ctrl-D call to bookmark URL

1085. By Arthur Mello on 2015-06-23

Fix creation of bookmark folder in suggestions AP test

1086. By Arthur Mello on 2015-06-23

Add AP tests to the NewTabView

1087. By Arthur Mello on 2015-06-24

Add Bookmark Options AP tests

1088. By Arthur Mello on 2015-06-24

Merge with trunk

Olivier Tilloy (osomon) wrote :
Download full text (3.6 KiB)

Test failure:

 - webbrowser_app.tests.test_bookmark_options.TestBookmarkOptions.test_set_bookmark_title reliably fails when run on krillin (with the latest vivid image). In fact the this is a functional issue, as the popover is partially scrolled out of view when the OSK slides in.

Functional issues:

 - Pressing Ctrl+D to bookmark a page should bring up the popover too.

 - When the popover is visible, pressing ESC should dismiss it and remove the bookmark, Return should dismiss it and save the bookmark (similar to desktop chromium’s behaviour).

UX issue, which I discussed with James earlier today:

 - Clicking the star or pressing Ctrl+D for a page that is already bookmarked should allow editing the bookmark, instead of removing it. This requires further design work though, so let’s keep it as it is for now.

Tests coverage:

 - bookmarks-folderlist-model.cpp has only 70.6% coverage, this should be improved.
 - bookmarks-model.cpp has 93.8% coverage, which is ok, but we’re not testing initially populating a model with existing folders. This should be tested.

Code remarks:

 - In BookmarkOptions.qml:
   * It doesn’t look like selectorDelegate is used in several places, perhaps it doesn’t need to be enclosed in a separate component, but rather be set directly as folderOptionSelector.delegate?
   * Can you add translator comments for the "Name" and "Save in" strings? Without context it’s not necessarily obvious how to translate those.
   * In titleTextField (and other places), please define anchors instead of setting width to parent.width.
   * Not sure why newFolderButton and okButton are inside a Row, but if you want to ensure that one is anchored to the left and the other one to the right, that’s not the right solution.
   * In newFolderDialog.saveButton, is there a bug report we could link to for the issue with onClicked not being triggered?

 - In BookmarksFolderDelegate.qml:
   * objectName doesn’t need to include the folderName. Instead, in autopilot tests, you can filter both on 'objectName' ("bookmarkFolderDelegate") and on the 'folderName' property.
   * There are two references to bookmarksFolderListViewItem, which isn’t defined in the scope of this component, this breaks encapsulation. In fact it doesn’t look like BookmarksFolderDelegate really needs to be a separate QML file (it’s not re-used anywhere else, is it?), so I think it would be easier to just inline it in BookmarksFolderListView.

 - In BookmarksFolderListView.qml:
   * The BookmarksFolderDelegate instance that serves as a delegate doesn’t need to be explicitly enclosed in a Component, QML does that automatically.

 - In Browser.qml:
   * In the BookmarkOptions instance, the binding of bookmarkTitle to browser.currentWebview.title has undesired side effects if the page updates the title periodically (test e.g. with http://htmlmarquee.com/title.html). Instead, the title needs to be retrieved on the current page and set in Component.onCompleted.

 - In bookmarks-folderlist-model.cpp:
   * The documentation says that each item has three roles, but they have two.
   * BookmarksFolderListModel::data() should probably call checkValidFolderIndex(index.row()) to ensure ...

Read more...

review: Needs Fixing
Arthur Mello (artmello) wrote :

> * In newFolderDialog.saveButton, is there a bug report we could link to for
> the issue with onClicked not being triggered?

I was not able to find one. I will talk with Sheldon and check if there is one, since he was the one that pointed out the issue. If there is not I will create one

1089. By Arthur Mello on 2015-06-30

Merge with trunk

1090. By Arthur Mello on 2015-06-30

Show the Bookmark Options popup when using Ctrl + D shortcut

1091. By Arthur Mello on 2015-06-30

Make the ESC key to close the BookmarkOptions popup

1092. By Arthur Mello on 2015-06-30

Improve unit test coverage for BookmarksFolderListModel

1093. By Arthur Mello on 2015-07-01

Add unittest to verify that bookmark model is populated with existing folders

1094. By Arthur Mello on 2015-07-01

Do not make OptionSelectorDelegate be inside another component

1095. By Arthur Mello on 2015-07-01

Add translator comments

1096. By Arthur Mello on 2015-07-01

Use anchors instead of setting width

1097. By Arthur Mello on 2015-07-01

Remove unnecessary Row

1098. By Arthur Mello on 2015-07-01

Does not set the folderName to the objectName

1099. By Arthur Mello on 2015-07-01

Remove BookmarksFolderDelegate and move code to inside BookmarksFolderListView

1100. By Arthur Mello on 2015-07-01

Does not enclose the delegate inside a Component

1101. By Arthur Mello on 2015-07-01

Do not bind the Bookmark Title in Bookmark Options to the currentWebView.title

1102. By Arthur Mello on 2015-07-01

Update doc for BookmarksFolderListModel

1103. By Arthur Mello on 2015-07-01

Call checkValidFolderIndex to ensure that the row is valid in BookmarksFolderListModel

1104. By Arthur Mello on 2015-07-01

Stop querying the value of populateFolderQuery more than once

1105. By Arthur Mello on 2015-07-01

Make the if statement more explicit

1106. By Arthur Mello on 2015-07-01

Make the if statement more explicit

1107. By Arthur Mello on 2015-07-01

Fix AP tests

1108. By Arthur Mello on 2015-07-01

Add AP test to check if "ESC" will unbookmark the URL

1109. By Arthur Mello on 2015-07-01

Skip test based in bug opened against SDK

Olivier Tilloy (osomon) wrote :

There are two remaining places in Browser.qml where a bookmark is created without the popover being displayed: HUD actions (which is defunct code, but until we remove it it should be kept up-to-date) and contextual actions.

The BookmarkOptions component should gain an additional 'url' property, whose value would be set to browser.currentWebview.url at construction time. This would ensure that the url doesn’t change while it’s shown. If the webview’s current URL changes while the popover is shown, the user can potentially try to unbookmark a different URL than the one that was bookmarked.

It’s wrong to skip test_set_bookmark_title. It’s not just the test that fails, it’s the actual functionality that’s broken. So we can’t land this branch until the UITK fix lands. By the way, have you tested the UITK branch (the fix appears to be merged in staging) to confirm that it fixes our issue?

If I press Ctrl+D to create a bookmark, then Ctrl+D again to remove it, the BookmarkOptions popover doesn’t disappear. If I then press Ctrl+D again to re-create the bookmark, I end up with two instances of the same popover.

review: Needs Fixing
1110. By Arthur Mello on 2015-07-01

Add bug report for the not emitted clicked signal issue

1111. By Arthur Mello on 2015-07-01

Call the BookmarkOptions popover for all the actions that add a bookmark

1112. By Arthur Mello on 2015-07-01

Add a bookmarkUrl property to BookmarkOptions set to the bookmarked url in construction time

1113. By Arthur Mello on 2015-07-01

Hide the BookmarkOptions if the Ctrl+D is pressed again

1114. By Arthur Mello on 2015-07-01

Remove unecessary code

1115. By Arthur Mello on 2015-07-01

Use a single function to add bookmark and show popover

1116. By Arthur Mello on 2015-07-01

Bookmark name field should have predictive text turned off

1117. By Arthur Mello on 2015-07-01

Do not show BookmarkOptions for the contextual action

1118. By Arthur Mello on 2015-07-01

Improve skip message for AP test

1119. By Arthur Mello on 2015-07-01

Fix add to bookmark call

Olivier Tilloy (osomon) wrote :

I tested again modifying a bookmark’s title on krillin, and while the experience is poor with the OSK moving the popover out of the way, I think it’s acceptable (as long as it’s only very temporary, and because the title field remains partly visible). No need to disable the title field.

A handful of minor remarks, and we’ll be good to land the feature I think:

 - When the bookmark options popover is displayed, pressing Return should dismiss it and save the bookmark (so that Ctrl+D followed by Return allows bookmarking quickly with the keyboard only).

 - In BookmarkOptions.qml, the color of the "OK" and "Save" buttons should be [UbuntuColors.green] (not exactly #3fb24f, but this will be consistent with all other OK buttons across the app).

 - In TestBookmarkOptions, is populate_config() really useful? The tests operates only on bookmarks, so I don’t think explicitly writing a config file is needed. Or are the tests relying on the fact that the homepage is always shown as a bookmark in the new tab view (if so, a comment in populate_config would be welcome to make that explicit) ?

 - Looking at BookmarkOptions.get_dismiss_button(), it seems this emulator method is only used to retrieve the button to then click it. So maybe a better method would be click_dismiss_button (with the appropriate @autopilot.logging.log_action decorator, see other click_*_button methods in emulators). The same goes for BookmarkOptions.get_new_folder_button().

 - In all new autopilot tests, after clicking the dismiss button, you should call bookmark_options.wait_until_destroyed().

 - tst_BookmarksFolderModelTests.cpp should #include <QtCore/QObject>, and the corresponding CMakeLists.txt should qt5_use_modules(Core) too. Same for tst_BookmarksFolderListModelTests.cpp.

review: Needs Fixing
1120. By Arthur Mello on 2015-07-02

Add missing include to unittests

1121. By Arthur Mello on 2015-07-02

Add check to make sure that bookmark options is closed

1122. By Arthur Mello on 2015-07-02

Change get buttons methods click buttons

1123. By Arthur Mello on 2015-07-02

Remove populate_config call

1124. By Arthur Mello on 2015-07-02

Change icon color

1125. By Arthur Mello on 2015-07-02

Accept return key to close bookmark options

Olivier Tilloy (osomon) wrote :

This looks good now.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/webbrowser/AddressBar.qml'
2--- src/app/webbrowser/AddressBar.qml 2015-06-11 17:00:59 +0000
3+++ src/app/webbrowser/AddressBar.qml 2015-07-02 13:49:44 +0000
4@@ -41,6 +41,8 @@
5
6 property var securityStatus: null
7
8+ readonly property Item bookmarkTogglePlaceHolder: bookmarkTogglePlaceHolderItem
9+
10 // XXX: for testing purposes only, do not use to modify the
11 // contents/behaviour of the internals of the component.
12 readonly property Item __textField: textField
13@@ -193,6 +195,11 @@
14 anchors.fill: parent
15 onClicked: addressbar.bookmarked = !addressbar.bookmarked
16 }
17+
18+ Item {
19+ id: bookmarkTogglePlaceHolderItem
20+ anchors.fill: parent
21+ }
22 }
23
24 font.pixelSize: FontUtils.sizeToPixels("small")
25
26=== added file 'src/app/webbrowser/BookmarkOptions.qml'
27--- src/app/webbrowser/BookmarkOptions.qml 1970-01-01 00:00:00 +0000
28+++ src/app/webbrowser/BookmarkOptions.qml 2015-07-02 13:49:44 +0000
29@@ -0,0 +1,166 @@
30+/*
31+ * Copyright 2015 Canonical Ltd.
32+ *
33+ * This file is part of webbrowser-app.
34+ *
35+ * webbrowser-app is free software; you can redistribute it and/or modify
36+ * it under the terms of the GNU General Public License as published by
37+ * the Free Software Foundation; version 3.
38+ *
39+ * webbrowser-app is distributed in the hope that it will be useful,
40+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
41+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
42+ * GNU General Public License for more details.
43+ *
44+ * You should have received a copy of the GNU General Public License
45+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
46+ */
47+
48+import QtQuick 2.0
49+import Ubuntu.Components 1.1
50+import Ubuntu.Components.Popups 1.0
51+
52+Popover {
53+ id: bookmarkOptions
54+
55+ property url bookmarkUrl
56+ property alias bookmarkTitle: titleTextField.text
57+ property alias folderModel: folderOptionSelector.model
58+
59+ readonly property string bookmarkFolder: folderModel.get(folderOptionSelector.selectedIndex).folder
60+
61+ contentHeight: bookmarkOptionsColumn.childrenRect.height + units.gu(2)
62+
63+ Column {
64+ id: bookmarkOptionsColumn
65+
66+ anchors {
67+ top: parent.top
68+ left: parent.left
69+ right: parent.right
70+ margins: units.gu(1)
71+ }
72+
73+ spacing: units.gu(1)
74+
75+ Label {
76+ font.bold: true
77+ text: i18n.tr("Bookmark Added")
78+ }
79+
80+ Label {
81+ // TRANSLATORS: Field where the title of bookmarked URL can be changed
82+ text: i18n.tr("Name")
83+ fontSize: "small"
84+ }
85+
86+ TextField {
87+ id: titleTextField
88+ objectName: "titleTextField"
89+
90+ anchors {
91+ left: parent.left
92+ right: parent.right
93+ }
94+
95+ inputMethodHints: Qt.ImhNoPredictiveText
96+ }
97+
98+ Label {
99+ // TRANSLATORS: Field to choose the folder where bookmarked URL will be saved in
100+ text: i18n.tr("Save in")
101+ fontSize: "small"
102+ }
103+
104+ OptionSelector {
105+ id: folderOptionSelector
106+
107+ delegate: OptionSelectorDelegate { text: folder === "" ? i18n.tr("All Bookmarks") : folder }
108+ containerHeight: itemHeight * 3
109+ }
110+
111+ Item {
112+ anchors {
113+ left: parent.left
114+ right: parent.right
115+ }
116+
117+ height: newFolderButton.height
118+
119+ Button {
120+ id: newFolderButton
121+ objectName: "bookmarkOptions.newButton"
122+ text: i18n.tr("New Folder")
123+ onClicked: PopupUtils.open(newFolderDialog)
124+ }
125+
126+ Button {
127+ id: okButton
128+ objectName: "bookmarkOptions.okButton"
129+ anchors.right: parent.right
130+ text: i18n.tr("OK")
131+ color: UbuntuColors.green
132+ onClicked: hide()
133+ }
134+ }
135+ }
136+
137+ Component {
138+ id: newFolderDialog
139+
140+ Dialog {
141+ id: dialogue
142+ objectName: "newFolderDialog"
143+
144+ title: i18n.tr("Create new folder")
145+
146+ Component.onCompleted: {
147+ folderTextField.forceActiveFocus()
148+ }
149+
150+ function createNewFolder(folder) {
151+ Qt.inputMethod.hide()
152+ folderModel.createNewFolder(folder)
153+ folderOptionSelector.selectedIndex = folderModel.indexOf(folder)
154+ folderOptionSelector.currentlyExpanded = false
155+ PopupUtils.close(dialogue)
156+ }
157+
158+ TextField {
159+ id: folderTextField
160+ objectName: "newFolderDialog.text"
161+ inputMethodHints: Qt.ImhNoPredictiveText
162+ placeholderText: i18n.tr("New Folder")
163+ onAccepted: createNewFolder(text)
164+ }
165+
166+ Button {
167+ objectName: "newFolderDialog.cancelButton"
168+ anchors {
169+ left: parent.left
170+ right: parent.right
171+ }
172+ text: i18n.tr("Cancel")
173+ onClicked: PopupUtils.close(dialogue)
174+ }
175+
176+ Button {
177+ objectName: "newFolderDialog.saveButton"
178+ anchors {
179+ left: parent.left
180+ right: parent.right
181+ }
182+ text: i18n.tr("Save")
183+ enabled: folderTextField.text
184+ color: UbuntuColors.green
185+ // Button took focus on press what makes the keyboard be
186+ // dismissed and that could make the Button moves between the
187+ // press and the release. Button onClicked is not triggered
188+ // if the release event happens outside of the button.
189+ // See: http://pad.lv/1415023
190+ activeFocusOnPress: false
191+ onClicked: createNewFolder(folderTextField.text)
192+ }
193+ }
194+ }
195+}
196
197=== added file 'src/app/webbrowser/BookmarksFolderListView.qml'
198--- src/app/webbrowser/BookmarksFolderListView.qml 1970-01-01 00:00:00 +0000
199+++ src/app/webbrowser/BookmarksFolderListView.qml 2015-07-02 13:49:44 +0000
200@@ -0,0 +1,157 @@
201+/*
202+ * Copyright 2015 Canonical Ltd.
203+ *
204+ * This file is part of webbrowser-app.
205+ *
206+ * webbrowser-app is free software; you can redistribute it and/or modify
207+ * it under the terms of the GNU General Public License as published by
208+ * the Free Software Foundation; version 3.
209+ *
210+ * webbrowser-app is distributed in the hope that it will be useful,
211+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
212+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
213+ * GNU General Public License for more details.
214+ *
215+ * You should have received a copy of the GNU General Public License
216+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
217+ */
218+
219+import QtQuick 2.0
220+import Ubuntu.Components 1.1
221+import Ubuntu.Components.ListItems 1.0 as ListItem
222+import webbrowserapp.private 0.1
223+
224+Item {
225+ id: bookmarksFolderListViewItem
226+
227+ property alias model: bookmarksFolderListModel.sourceModel
228+
229+ signal bookmarkClicked(url url)
230+ signal bookmarkRemoved(url url)
231+
232+ height: bookmarksFolderListView.contentHeight
233+
234+ BookmarksFolderListModel {
235+ id: bookmarksFolderListModel
236+ }
237+
238+ ListView {
239+ id: bookmarksFolderListView
240+ anchors.fill: parent
241+ interactive: false
242+
243+ model: bookmarksFolderListModel
244+ delegate: Loader {
245+ anchors {
246+ left: parent.left
247+ right: parent.right
248+ }
249+
250+ height: active ? item.height : 0
251+ active: entries.count > 0
252+
253+ sourceComponent: Item {
254+ objectName: "bookmarkFolderDelegate"
255+
256+ property string folderName: folder
257+
258+ anchors {
259+ left: parent ? parent.left : undefined
260+ right: parent ? parent.right : undefined
261+ }
262+
263+ height: delegateColumn.height
264+
265+ Column {
266+ id: delegateColumn
267+
268+ property bool expanded: true
269+
270+ anchors {
271+ left: parent.left
272+ right: parent.right
273+ }
274+
275+ Item {
276+ objectName: "bookmarkFolderHeader"
277+
278+ anchors {
279+ left: parent.left
280+ right: parent.right
281+ leftMargin: units.gu(2)
282+ rightMargin: units.gu(2)
283+ }
284+
285+ height: units.gu(6.5)
286+
287+ Row {
288+ anchors {
289+ left: parent.left
290+ leftMargin: units.gu(1.5)
291+ right: parent.right
292+ }
293+
294+ height: units.gu(6)
295+ spacing: units.gu(1.5)
296+
297+ Icon {
298+ id: expandedIcon
299+ name: delegateColumn.expanded ? "go-down" : "go-next"
300+
301+ height: units.gu(2)
302+ width: height
303+
304+ anchors {
305+ leftMargin: units.gu(1)
306+ topMargin: units.gu(2)
307+ top: parent.top
308+ }
309+ }
310+
311+ Label {
312+ width: parent.width - expandedIcon.width - units.gu(3)
313+ anchors.verticalCenter: expandedIcon.verticalCenter
314+
315+ text: folderName ? folderName : i18n.tr("All Bookmarks")
316+ fontSize: "small"
317+ }
318+ }
319+
320+ ListItem.ThinDivider {
321+ anchors {
322+ left: parent.left
323+ right: parent.right
324+ bottom: parent.bottom
325+ bottomMargin: units.gu(1)
326+ }
327+ }
328+
329+ MouseArea {
330+ anchors.fill: parent
331+ onClicked: delegateColumn.expanded = !delegateColumn.expanded
332+ }
333+ }
334+
335+ Loader {
336+ anchors {
337+ left: parent.left
338+ right: parent.right
339+ }
340+
341+ visible: status == Loader.Ready
342+
343+ active: delegateColumn.expanded
344+ sourceComponent: UrlsList {
345+ spacing: 0
346+
347+ model: entries
348+
349+ onUrlClicked: bookmarksFolderListViewItem.bookmarkClicked(url)
350+ onUrlRemoved: bookmarksFolderListViewItem.bookmarkRemoved(url)
351+ }
352+ }
353+ }
354+ }
355+ }
356+ }
357+}
358
359=== modified file 'src/app/webbrowser/Browser.qml'
360--- src/app/webbrowser/Browser.qml 2015-06-18 08:12:33 +0000
361+++ src/app/webbrowser/Browser.qml 2015-07-02 13:49:44 +0000
362@@ -80,7 +80,7 @@
363 },
364 Actions.Bookmark {
365 enabled: currentWebview && browser.bookmarksModel
366- onTriggered: browser.bookmarksModel.add(currentWebview.url, currentWebview.title, currentWebview.icon)
367+ onTriggered: internal.addBookmark(currentWebview.url, currentWebview.title, currentWebview.icon)
368 },
369 Actions.NewTab {
370 onTriggered: browser.openUrlInNewTab("", true)
371@@ -264,7 +264,7 @@
372 bookmarked: isCurrentUrlBookmarked()
373 onBookmarkedChanged: {
374 if (bookmarked && !isCurrentUrlBookmarked()) {
375- browser.bookmarksModel.add(webview.url, webview.title, webview.icon)
376+ internal.addBookmark(webview.url, webview.title, webview.icon)
377 } else if (!bookmarked && isCurrentUrlBookmarked()) {
378 browser.bookmarksModel.remove(webview.url)
379 }
380@@ -432,6 +432,58 @@
381 chrome.requestedUrl = url
382 }
383 }
384+
385+ Component {
386+ id: bookmarkOptionsComponent
387+ BookmarkOptions {
388+ folderModel: BookmarksFolderListModel {
389+ sourceModel: bookmarksModel
390+ }
391+
392+ Component.onCompleted: {
393+ forceActiveFocus()
394+ }
395+
396+ Component.onDestruction: {
397+ if (browser.bookmarksModel.contains(bookmarkUrl)) {
398+ browser.bookmarksModel.update(bookmarkUrl,
399+ bookmarkTitle,
400+ bookmarkFolder)
401+ }
402+ }
403+
404+ Keys.onPressed: {
405+ if (bookmarkOptionsShortcuts.processKey(event.key, event.modifiers)) {
406+ event.accepted = true
407+ }
408+ }
409+
410+ KeyboardShortcuts {
411+ id: bookmarkOptionsShortcuts
412+ KeyboardShortcut {
413+ key: Qt.Key_Return
414+ onTriggered: hide()
415+ }
416+
417+ KeyboardShortcut {
418+ key: Qt.Key_Escape
419+ onTriggered: {
420+ browser.bookmarksModel.remove(bookmarkUrl)
421+ hide()
422+ }
423+ }
424+
425+ KeyboardShortcut {
426+ modifiers: Qt.ControlModifier
427+ key: Qt.Key_D
428+ onTriggered: {
429+ browser.bookmarksModel.remove(bookmarkUrl)
430+ hide()
431+ }
432+ }
433+ }
434+ }
435+ }
436 }
437
438 FocusScope {
439@@ -764,7 +816,7 @@
440 }
441 Actions.BookmarkLink {
442 enabled: contextualData.href.toString() && browser.bookmarksModel
443- onTriggered: browser.bookmarksModel.add(contextualData.href, contextualData.title, "")
444+ onTriggered: bookmarksModel.add(contextualData.href, contextualData.title, "", "")
445 }
446 Actions.CopyLink {
447 enabled: contextualData.href.toString()
448@@ -1016,6 +1068,14 @@
449 currentWebview.goForward()
450 }
451 }
452+
453+ function addBookmark(url, title, icon) {
454+ bookmarksModel.add(url, title, icon, "")
455+ PopupUtils.open(bookmarkOptionsComponent,
456+ chrome.bookmarkTogglePlaceHolder,
457+ {"bookmarkUrl": url,
458+ "bookmarkTitle": title})
459+ }
460 }
461
462 function openUrlInNewTab(url, setCurrent, load) {
463@@ -1257,7 +1317,7 @@
464 if (bookmarksModel.contains(currentWebview.url)) {
465 bookmarksModel.remove(currentWebview.url)
466 } else {
467- bookmarksModel.add(currentWebview.url, currentWebview.title, currentWebview.icon)
468+ internal.addBookmark(currentWebview.url, currentWebview.title, currentWebview.icon)
469 }
470 }
471 }
472
473=== modified file 'src/app/webbrowser/CMakeLists.txt'
474--- src/app/webbrowser/CMakeLists.txt 2015-05-28 11:26:05 +0000
475+++ src/app/webbrowser/CMakeLists.txt 2015-07-02 13:49:44 +0000
476@@ -8,6 +8,8 @@
477
478 set(WEBBROWSER_APP_MODELS_SRC
479 bookmarks-model.cpp
480+ bookmarks-folder-model.cpp
481+ bookmarks-folderlist-model.cpp
482 history-domain-model.cpp
483 history-domainlist-chronological-model.cpp
484 history-domainlist-model.cpp
485
486=== modified file 'src/app/webbrowser/Chrome.qml'
487--- src/app/webbrowser/Chrome.qml 2015-06-11 16:47:26 +0000
488+++ src/app/webbrowser/Chrome.qml 2015-07-02 13:49:44 +0000
489@@ -32,6 +32,8 @@
490 property alias addressBarCanSimplifyText: addressbar.canSimplifyText
491 property alias incognito: addressbar.incognito
492
493+ readonly property alias bookmarkTogglePlaceHolder: addressbar.bookmarkTogglePlaceHolder
494+
495 backgroundColor: incognito ? UbuntuColors.darkGrey : Theme.palette.normal.background
496
497 function addressBarSelectAll() { addressbar.selectAll() }
498
499=== modified file 'src/app/webbrowser/NewTabView.qml'
500--- src/app/webbrowser/NewTabView.qml 2015-05-28 13:56:52 +0000
501+++ src/app/webbrowser/NewTabView.qml 2015-07-02 13:49:44 +0000
502@@ -43,7 +43,7 @@
503 QtObject {
504 id: internal
505
506- property bool seeMoreBookmarksView: bookmarksCountLimit > 4
507+ property bool seeMoreBookmarksView: false
508 property int bookmarksCountLimit: Math.min(4, numberOfBookmarks)
509 property int numberOfBookmarks: bookmarksModel ? bookmarksModel.count : 0
510
511@@ -65,7 +65,7 @@
512 Flickable {
513 anchors.fill: parent
514 contentHeight: internal.seeMoreBookmarksView ?
515- bookmarksColumn.height + units.gu(6) :
516+ bookmarksFolderListViewLoader.height + units.gu(6) :
517 contentColumn.height
518
519 Column {
520@@ -120,14 +120,9 @@
521
522 visible: internal.numberOfBookmarks > 4
523
524- text: internal.bookmarksCountLimit >= internal.numberOfBookmarks
525- ? i18n.tr("Less") : i18n.tr("More")
526+ text: internal.seeMoreBookmarksView ? i18n.tr("Less") : i18n.tr("More")
527
528- onClicked: {
529- internal.numberOfBookmarks > internal.bookmarksCountLimit ?
530- internal.bookmarksCountLimit += 5:
531- internal.bookmarksCountLimit = 4
532- }
533+ onClicked: internal.seeMoreBookmarksView = !internal.seeMoreBookmarksView
534 }
535 }
536
537@@ -141,6 +136,25 @@
538 color: "#d3d3d3"
539 }
540
541+ Loader {
542+ id: bookmarksFolderListViewLoader
543+
544+ anchors {
545+ left: parent.left
546+ right: parent.right
547+ }
548+
549+ height: status == Loader.Ready ? item.height : 0
550+
551+ active: internal.seeMoreBookmarksView
552+ sourceComponent: BookmarksFolderListView {
553+ model: newTabView.bookmarksModel
554+
555+ onBookmarkClicked: newTabView.bookmarkClicked(url)
556+ onBookmarkRemoved: newTabView.bookmarkRemoved(url)
557+ }
558+ }
559+
560 Column {
561 id: bookmarksColumn
562 anchors {
563@@ -148,6 +162,10 @@
564 right: parent.right
565 }
566
567+ opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0
568+ Behavior on opacity { UbuntuNumberAnimation {} }
569+ visible: opacity > 0
570+
571 // Force the height to be updated when bookmarks are removed
572 // in another new tab
573 height: units.gu(5) * (Math.min(internal.bookmarksCountLimit, internal.numberOfBookmarks) + 1)
574
575=== modified file 'src/app/webbrowser/UrlsList.qml'
576--- src/app/webbrowser/UrlsList.qml 2015-05-28 11:26:05 +0000
577+++ src/app/webbrowser/UrlsList.qml 2015-07-02 13:49:44 +0000
578@@ -23,7 +23,7 @@
579 id: urlsList
580
581 property alias model: urlsListRepeater.model
582- property int limit
583+ property int limit: -1
584
585 signal urlClicked(url url)
586 signal urlRemoved(url url)
587@@ -34,7 +34,7 @@
588 id: urlsListRepeater
589
590 delegate: Loader {
591- active: index < limit
592+ active: limit < 0 || index < limit
593 sourceComponent: UrlDelegate{
594 id: urlDelegate
595 width: urlsList.width
596
597=== added file 'src/app/webbrowser/bookmarks-folder-model.cpp'
598--- src/app/webbrowser/bookmarks-folder-model.cpp 1970-01-01 00:00:00 +0000
599+++ src/app/webbrowser/bookmarks-folder-model.cpp 2015-07-02 13:49:44 +0000
600@@ -0,0 +1,84 @@
601+/*
602+ * Copyright 2015 Canonical Ltd.
603+ *
604+ * This file is part of webbrowser-app.
605+ *
606+ * webbrowser-app is free software; you can redistribute it and/or modify
607+ * it under the terms of the GNU General Public License as published by
608+ * the Free Software Foundation; version 3.
609+ *
610+ * webbrowser-app is distributed in the hope that it will be useful,
611+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
612+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
613+ * GNU General Public License for more details.
614+ *
615+ * You should have received a copy of the GNU General Public License
616+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
617+ */
618+
619+#include "bookmarks-folder-model.h"
620+#include "bookmarks-model.h"
621+
622+// Qt
623+#include <QtCore/QUrl>
624+
625+/*!
626+ \class BookmarksFolderModel
627+ \brief Proxy model that filters the contents of a bookmarks model
628+ based on a folder name
629+
630+ BookmarksFolderModel is a proxy model that filters the contents of a
631+ bookmarks model based on a folder name.
632+
633+ An entry in the bookmarks model matches if it is stored in a folder
634+ with the same name that the filter folder name (case-sensitive
635+ comparison).
636+
637+ When no folder name is set (null or empty string), all entries that
638+ are not stored in any folder match.
639+*/
640+BookmarksFolderModel::BookmarksFolderModel(QObject* parent)
641+ : QSortFilterProxyModel(parent)
642+{
643+}
644+
645+BookmarksModel* BookmarksFolderModel::sourceModel() const
646+{
647+ return qobject_cast<BookmarksModel*>(QSortFilterProxyModel::sourceModel());
648+}
649+
650+void BookmarksFolderModel::setSourceModel(BookmarksModel* sourceModel)
651+{
652+ if (sourceModel != this->sourceModel()) {
653+ QSortFilterProxyModel::setSourceModel(sourceModel);
654+ Q_EMIT sourceModelChanged();
655+ Q_EMIT countChanged();
656+ }
657+}
658+
659+const QString& BookmarksFolderModel::folder() const
660+{
661+ return m_folder;
662+}
663+
664+void BookmarksFolderModel::setFolder(const QString& folder)
665+{
666+ if (folder != m_folder) {
667+ m_folder = folder;
668+ invalidate();
669+ Q_EMIT folderChanged();
670+ Q_EMIT countChanged();
671+ }
672+}
673+
674+int BookmarksFolderModel::count() const
675+{
676+ return rowCount();
677+}
678+
679+bool BookmarksFolderModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
680+{
681+ QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
682+ QString folder = sourceModel()->data(index, BookmarksModel::Folder).toString();
683+ return (folder.compare(m_folder, Qt::CaseSensitive) == 0);
684+}
685
686=== added file 'src/app/webbrowser/bookmarks-folder-model.h'
687--- src/app/webbrowser/bookmarks-folder-model.h 1970-01-01 00:00:00 +0000
688+++ src/app/webbrowser/bookmarks-folder-model.h 2015-07-02 13:49:44 +0000
689@@ -0,0 +1,60 @@
690+/*
691+ * Copyright 2015 Canonical Ltd.
692+ *
693+ * This file is part of webbrowser-app.
694+ *
695+ * webbrowser-app is free software; you can redistribute it and/or modify
696+ * it under the terms of the GNU General Public License as published by
697+ * the Free Software Foundation; version 3.
698+ *
699+ * webbrowser-app is distributed in the hope that it will be useful,
700+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
701+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
702+ * GNU General Public License for more details.
703+ *
704+ * You should have received a copy of the GNU General Public License
705+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
706+ */
707+
708+#ifndef __BOOKMARKS_FOLDER_MODEL_H__
709+#define __BOOKMARKS_FOLDER_MODEL_H__
710+
711+// Qt
712+#include <QtCore/QSortFilterProxyModel>
713+#include <QtCore/QString>
714+
715+class BookmarksModel;
716+
717+class BookmarksFolderModel : public QSortFilterProxyModel
718+{
719+ Q_OBJECT
720+
721+ Q_PROPERTY(BookmarksModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
722+ Q_PROPERTY(QString folder READ folder WRITE setFolder NOTIFY folderChanged)
723+ Q_PROPERTY(int count READ count NOTIFY countChanged)
724+
725+public:
726+ BookmarksFolderModel(QObject* parent=0);
727+
728+ BookmarksModel* sourceModel() const;
729+ void setSourceModel(BookmarksModel* sourceModel);
730+
731+ const QString& folder() const;
732+ void setFolder(const QString& domain);
733+
734+ int count() const;
735+
736+Q_SIGNALS:
737+ void sourceModelChanged() const;
738+ void folderChanged() const;
739+ void countChanged() const;
740+
741+protected:
742+ // reimplemented from QSortFilterProxyModel
743+ bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
744+
745+private:
746+ QString m_folder;
747+};
748+
749+#endif // __BOOKMARKS_FOLDER_MODEL_H__
750
751=== added file 'src/app/webbrowser/bookmarks-folderlist-model.cpp'
752--- src/app/webbrowser/bookmarks-folderlist-model.cpp 1970-01-01 00:00:00 +0000
753+++ src/app/webbrowser/bookmarks-folderlist-model.cpp 2015-07-02 13:49:44 +0000
754@@ -0,0 +1,217 @@
755+/*
756+ * Copyright 2015 Canonical Ltd.
757+ *
758+ * This file is part of webbrowser-app.
759+ *
760+ * webbrowser-app is free software; you can redistribute it and/or modify
761+ * it under the terms of the GNU General Public License as published by
762+ * the Free Software Foundation; version 3.
763+ *
764+ * webbrowser-app is distributed in the hope that it will be useful,
765+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
766+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
767+ * GNU General Public License for more details.
768+ *
769+ * You should have received a copy of the GNU General Public License
770+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
771+ */
772+
773+#include "bookmarks-folder-model.h"
774+#include "bookmarks-folderlist-model.h"
775+#include "bookmarks-model.h"
776+
777+// Qt
778+#include <QtCore/QDebug>
779+#include <QtCore/QStringList>
780+
781+/*!
782+ \class BookmarksFolderListModel
783+ \brief List model that exposes bookmarks entries grouped by folder name
784+
785+ BookmarksFolderListModel is a list model that exposes bookmarks entries
786+ from a BookmarksModel grouped by folder name. Each item in the list has
787+ two roles: 'folder' for the folder name and 'entries' for the corresponding
788+ BookmarksFolderModel that contains all entries in this group.
789+*/
790+BookmarksFolderListModel::BookmarksFolderListModel(QObject* parent)
791+ : QAbstractListModel(parent)
792+ , m_sourceModel(0)
793+{
794+}
795+
796+BookmarksFolderListModel::~BookmarksFolderListModel()
797+{
798+ clearFolders();
799+}
800+
801+QHash<int, QByteArray> BookmarksFolderListModel::roleNames() const
802+{
803+ static QHash<int, QByteArray> roles;
804+ if (roles.isEmpty()) {
805+ roles[Folder] = "folder";
806+ roles[Entries] = "entries";
807+ }
808+ return roles;
809+}
810+
811+int BookmarksFolderListModel::rowCount(const QModelIndex& parent) const
812+{
813+ Q_UNUSED(parent);
814+ return m_folders.count();
815+}
816+
817+QVariant BookmarksFolderListModel::data(const QModelIndex& index, int role) const
818+{
819+ if (!index.isValid() || !checkValidFolderIndex(index.row())) {
820+ return QVariant();
821+ }
822+ const QString folder = m_folders.keys().at(index.row());
823+ BookmarksFolderModel* entries = m_folders.value(folder);
824+
825+ switch (role) {
826+ case Folder:
827+ return folder;
828+ case Entries:
829+ return QVariant::fromValue(entries);
830+ default:
831+ return QVariant();
832+ }
833+}
834+
835+BookmarksModel* BookmarksFolderListModel::sourceModel() const
836+{
837+ return m_sourceModel;
838+}
839+
840+void BookmarksFolderListModel::setSourceModel(BookmarksModel* sourceModel)
841+{
842+ if (sourceModel != m_sourceModel) {
843+ beginResetModel();
844+ if (m_sourceModel != 0) {
845+ m_sourceModel->disconnect(this);
846+ }
847+ clearFolders();
848+ m_sourceModel = sourceModel;
849+ populateModel();
850+ if (m_sourceModel != 0) {
851+ connect(m_sourceModel, SIGNAL(folderAdded(const QString&)), SLOT(onFolderAdded(const QString&)));
852+ connect(m_sourceModel, SIGNAL(modelReset()), SLOT(onModelReset()));
853+ }
854+ endResetModel();
855+ Q_EMIT sourceModelChanged();
856+ }
857+}
858+
859+QVariantMap BookmarksFolderListModel::get(int row) const
860+{
861+ if (!checkValidFolderIndex(row)) {
862+ return QVariantMap();
863+ }
864+
865+ QVariantMap res;
866+ QHash<int,QByteArray> names = roleNames();
867+ QHashIterator<int, QByteArray> i(names);
868+
869+ while (i.hasNext()) {
870+ i.next();
871+ QModelIndex idx = index(row, 0);
872+ QVariant data = idx.data(i.key());
873+ res[i.value()] = data;
874+ }
875+
876+ return res;
877+}
878+
879+int BookmarksFolderListModel::indexOf(const QString& folder) const
880+{
881+ QStringList folders = m_folders.keys();
882+ return folders.indexOf(folder);
883+}
884+
885+void BookmarksFolderListModel::createNewFolder(const QString& folder)
886+{
887+ m_sourceModel->addFolder(folder);
888+}
889+
890+bool BookmarksFolderListModel::checkValidFolderIndex(int index) const
891+{
892+ if ((index < 0) || (index >= m_folders.count())) {
893+ qWarning() << "Invalid folder index:" << index;
894+ return false;
895+ }
896+ return true;
897+}
898+
899+void BookmarksFolderListModel::clearFolders()
900+{
901+ Q_FOREACH(const QString& folder, m_folders.keys()) {
902+ delete m_folders.take(folder);
903+ }
904+}
905+
906+void BookmarksFolderListModel::populateModel()
907+{
908+ if (m_sourceModel != 0) {
909+ Q_FOREACH(const QString& folder, m_sourceModel->folders()) {
910+ if (!m_folders.contains(folder)) {
911+ addFolder(folder);
912+ }
913+ }
914+ }
915+}
916+
917+void BookmarksFolderListModel::onFolderAdded(const QString& folder)
918+{
919+ if (!m_folders.contains(folder)) {
920+ QStringList folders = m_folders.keys();
921+ int insertAt = 0;
922+ while (insertAt < folders.count()) {
923+ if (folder.compare(folders.at(insertAt)) < 0) {
924+ break;
925+ }
926+ ++insertAt;
927+ }
928+ beginInsertRows(QModelIndex(), insertAt, insertAt);
929+ addFolder(folder);
930+ endInsertRows();
931+ }
932+}
933+
934+void BookmarksFolderListModel::onModelReset()
935+{
936+ beginResetModel();
937+ clearFolders();
938+ populateModel();
939+ endResetModel();
940+}
941+
942+void BookmarksFolderListModel::addFolder(const QString& folder)
943+{
944+ BookmarksFolderModel* model = new BookmarksFolderModel(this);
945+ model->setSourceModel(m_sourceModel);
946+ model->setFolder(folder);
947+ connect(model, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(onFolderDataChanged()));
948+ connect(model, SIGNAL(rowsRemoved(QModelIndex, int, int)), SLOT(onFolderDataChanged()));
949+ connect(model, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), SLOT(onFolderDataChanged()));
950+ connect(model, SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)), SLOT(onFolderDataChanged()));
951+ connect(model, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(onFolderDataChanged()));
952+ connect(model, SIGNAL(modelReset()), SLOT(onFolderDataChanged()));
953+ m_folders.insert(folder, model);
954+}
955+
956+void BookmarksFolderListModel::onFolderDataChanged()
957+{
958+ BookmarksFolderModel* model = qobject_cast<BookmarksFolderModel*>(sender());
959+ if (model != 0) {
960+ emitDataChanged(model->folder());
961+ }
962+}
963+
964+void BookmarksFolderListModel::emitDataChanged(const QString& folder)
965+{
966+ int i = m_folders.keys().indexOf(folder);
967+ if (i != -1) {
968+ QModelIndex index = this->index(i, 0);
969+ Q_EMIT dataChanged(index, index, QVector<int>() << Entries);
970+ }
971+}
972
973=== added file 'src/app/webbrowser/bookmarks-folderlist-model.h'
974--- src/app/webbrowser/bookmarks-folderlist-model.h 1970-01-01 00:00:00 +0000
975+++ src/app/webbrowser/bookmarks-folderlist-model.h 2015-07-02 13:49:44 +0000
976@@ -0,0 +1,79 @@
977+/*
978+ * Copyright 2015 Canonical Ltd.
979+ *
980+ * This file is part of webbrowser-app.
981+ *
982+ * webbrowser-app is free software; you can redistribute it and/or modify
983+ * it under the terms of the GNU General Public License as published by
984+ * the Free Software Foundation; version 3.
985+ *
986+ * webbrowser-app is distributed in the hope that it will be useful,
987+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
988+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
989+ * GNU General Public License for more details.
990+ *
991+ * You should have received a copy of the GNU General Public License
992+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
993+ */
994+
995+#ifndef __BOOKMARKS_FOLDERLIST_MODEL_H__
996+#define __BOOKMARKS_FOLDERLIST_MODEL_H__
997+
998+// Qt
999+#include <QtCore/QAbstractListModel>
1000+#include <QtCore/QMap>
1001+#include <QtCore/QString>
1002+
1003+class BookmarksFolderModel;
1004+class BookmarksModel;
1005+
1006+class BookmarksFolderListModel : public QAbstractListModel
1007+{
1008+ Q_OBJECT
1009+
1010+ Q_PROPERTY(BookmarksModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
1011+
1012+ Q_ENUMS(Roles)
1013+
1014+public:
1015+ BookmarksFolderListModel(QObject* parent=0);
1016+ ~BookmarksFolderListModel();
1017+
1018+ enum Roles {
1019+ Folder = Qt::UserRole + 1,
1020+ Entries
1021+ };
1022+
1023+ // reimplemented from QAbstractListModel
1024+ QHash<int, QByteArray> roleNames() const;
1025+ int rowCount(const QModelIndex& parent=QModelIndex()) const;
1026+ QVariant data(const QModelIndex& index, int role) const;
1027+
1028+ BookmarksModel* sourceModel() const;
1029+ void setSourceModel(BookmarksModel* sourceModel);
1030+
1031+ Q_INVOKABLE QVariantMap get(int row) const;
1032+ Q_INVOKABLE int indexOf(const QString& folder) const;
1033+ Q_INVOKABLE void createNewFolder(const QString& folder);
1034+
1035+Q_SIGNALS:
1036+ void sourceModelChanged() const;
1037+
1038+private Q_SLOTS:
1039+ void onFolderAdded(const QString& folder);
1040+ void onModelReset();
1041+
1042+ void onFolderDataChanged();
1043+
1044+private:
1045+ BookmarksModel* m_sourceModel;
1046+ QMap<QString, BookmarksFolderModel*> m_folders;
1047+
1048+ bool checkValidFolderIndex(int row) const;
1049+ void clearFolders();
1050+ void populateModel();
1051+ void addFolder(const QString& folder);
1052+ void emitDataChanged(const QString& folder);
1053+};
1054+
1055+#endif // __BOOKMARKS_FOLDERLIST_MODEL_H__
1056
1057=== modified file 'src/app/webbrowser/bookmarks-model.cpp'
1058--- src/app/webbrowser/bookmarks-model.cpp 2015-04-30 09:45:29 +0000
1059+++ src/app/webbrowser/bookmarks-model.cpp 2015-07-02 13:49:44 +0000
1060@@ -1,5 +1,5 @@
1061 /*
1062- * Copyright 2013-2014 Canonical Ltd.
1063+ * Copyright 2013-2015 Canonical Ltd.
1064 *
1065 * This file is part of webbrowser-app.
1066 *
1067@@ -55,6 +55,7 @@
1068 void BookmarksModel::resetDatabase(const QString& databaseName)
1069 {
1070 beginResetModel();
1071+ m_folders.clear();
1072 m_urls.clear();
1073 m_orderedEntries.clear();
1074 m_database.close();
1075@@ -70,21 +71,39 @@
1076 {
1077 QSqlQuery createQuery(m_database);
1078 QString query = QLatin1String("CREATE TABLE IF NOT EXISTS bookmarks "
1079- "(url VARCHAR, title VARCHAR, icon VARCHAR, created INTEGER);");
1080+ "(url VARCHAR, title VARCHAR, icon VARCHAR, "
1081+ "created INTEGER, folderId INTEGER);");
1082 createQuery.prepare(query);
1083 createQuery.exec();
1084
1085- // The first version of the database schema didn’t have a 'created' column
1086+ QSqlQuery createFolderQuery(m_database);
1087+ query = QLatin1String("CREATE TABLE IF NOT EXISTS folders "
1088+ "(folderId INTEGER PRIMARY KEY, folder VARCHAR);");
1089+ createFolderQuery.prepare(query);
1090+ createFolderQuery.exec();
1091+
1092+ // Older version of the database schema didn’t have 'created' and/or
1093+ // 'folderId' columns
1094 QSqlQuery tableInfoQuery(m_database);
1095 query = QLatin1String("PRAGMA TABLE_INFO(bookmarks);");
1096 tableInfoQuery.prepare(query);
1097 tableInfoQuery.exec();
1098+
1099+ bool missingCreatedColumn = true;
1100+ bool missingFolderIdColumn = true;
1101+
1102 while (tableInfoQuery.next()) {
1103 if (tableInfoQuery.value("name").toString() == "created") {
1104+ missingCreatedColumn = false;
1105+ }
1106+ if (tableInfoQuery.value("name").toString() == "folderId") {
1107+ missingFolderIdColumn = false;
1108+ }
1109+ if (!missingCreatedColumn && !missingFolderIdColumn) {
1110 break;
1111 }
1112 }
1113- if (!tableInfoQuery.isValid()) {
1114+ if (missingCreatedColumn) {
1115 QSqlQuery addCreatedColumnQuery(m_database);
1116 query = QLatin1String("ALTER TABLE bookmarks ADD COLUMN created INTEGER;");
1117 addCreatedColumnQuery.prepare(query);
1118@@ -93,13 +112,33 @@
1119 // when converted to a number. Zero represents a date far in the past, so
1120 // any newly created bookmark will correctly be represented as more recent than any other
1121 }
1122+ if (missingFolderIdColumn) {
1123+ QSqlQuery addFolderColumnQuery(m_database);
1124+ query = QLatin1String("ALTER TABLE bookmarks ADD COLUMN folderId INTEGER;");
1125+ addFolderColumnQuery.prepare(query);
1126+ addFolderColumnQuery.exec();
1127+ }
1128 }
1129
1130 void BookmarksModel::populateFromDatabase()
1131 {
1132+ //Add default empty folder
1133+ m_folders.insert(0, "");
1134+ Q_EMIT folderAdded("");
1135+
1136+ QSqlQuery populateFolderQuery(m_database);
1137+ QString query = QLatin1String("SELECT folderId, folder FROM folders;");
1138+ populateFolderQuery.prepare(query);
1139+ populateFolderQuery.exec();
1140+ while (populateFolderQuery.next()) {
1141+ QString folder = populateFolderQuery.value(1).toString();
1142+ m_folders.insert(populateFolderQuery.value(0).toInt(), folder);
1143+ Q_EMIT folderAdded(folder);
1144+ }
1145+
1146 QSqlQuery populateQuery(m_database);
1147- QString query = QLatin1String("SELECT url, title, icon, created "
1148- "FROM bookmarks ORDER BY created DESC;");
1149+ query = QLatin1String("SELECT url, title, icon, created, folderId "
1150+ "FROM bookmarks ORDER BY created DESC;");
1151 populateQuery.prepare(query);
1152 populateQuery.exec();
1153 int count = 0;
1154@@ -109,6 +148,15 @@
1155 entry.title = populateQuery.value(1).toString();
1156 entry.icon = populateQuery.value(2).toUrl();
1157 entry.created = QDateTime::fromMSecsSinceEpoch(populateQuery.value(3).toULongLong());
1158+ entry.folderId = populateQuery.value(4).toInt();
1159+
1160+ if (m_folders.contains(entry.folderId)) {
1161+ entry.folder = m_folders.value(entry.folderId);
1162+ } else {
1163+ entry.folderId = 0;
1164+ updateExistingEntryInDatabase(entry);
1165+ }
1166+
1167 beginInsertRows(QModelIndex(), count, count);
1168 m_urls.insert(entry.url);
1169 m_orderedEntries.append(entry);
1170@@ -125,6 +173,7 @@
1171 roles[Title] = "title";
1172 roles[Icon] = "icon";
1173 roles[Created] = "created";
1174+ roles[Folder] = "folder";
1175 }
1176 return roles;
1177 }
1178@@ -150,6 +199,8 @@
1179 return entry.icon;
1180 case Created:
1181 return entry.created;
1182+ case Folder:
1183+ return entry.folder;
1184 default:
1185 return QVariant();
1186 }
1187@@ -172,6 +223,21 @@
1188 }
1189 }
1190
1191+QStringList BookmarksModel::folders() const
1192+{
1193+ return m_folders.values();
1194+}
1195+
1196+int BookmarksModel::addFolder(const QString& folder)
1197+{
1198+ int newFolderId = insertNewFolderInDatabase(folder);
1199+ if (newFolderId != 0) {
1200+ m_folders.insert(newFolderId, folder);
1201+ Q_EMIT folderAdded(folder);
1202+ return newFolderId;
1203+ }
1204+}
1205+
1206 /*!
1207 Test if a given URL is already bookmarked.
1208
1209@@ -188,7 +254,7 @@
1210
1211 If the URL was previously bookmarked, do nothing.
1212 */
1213-void BookmarksModel::add(const QUrl& url, const QString& title, const QUrl& icon)
1214+void BookmarksModel::add(const QUrl& url, const QString& title, const QUrl& icon, const QString& folder)
1215 {
1216 if (m_urls.contains(url)) {
1217 qWarning() << "URL already bookmarked:" << url;
1218@@ -199,6 +265,8 @@
1219 entry.title = title;
1220 entry.icon = icon;
1221 entry.created = QDateTime::currentDateTime();
1222+ entry.folder = folder;
1223+ entry.folderId = getFolderId(entry.folder);
1224 m_urls.insert(url);
1225 m_orderedEntries.prepend(entry);
1226 endInsertRows();
1227@@ -212,12 +280,18 @@
1228 {
1229 QSqlQuery query(m_database);
1230 static QString insertStatement = QLatin1String("INSERT INTO bookmarks (url, "
1231- "title, icon, created) VALUES (?, ?, ?, ?);");
1232+ "title, icon, created, folderId) "
1233+ "VALUES (?, ?, ?, ?, ?);");
1234 query.prepare(insertStatement);
1235 query.addBindValue(entry.url.toString());
1236 query.addBindValue(entry.title);
1237 query.addBindValue(entry.icon.toString());
1238 query.addBindValue(entry.created.toMSecsSinceEpoch());
1239+ if (!entry.folderId) {
1240+ query.addBindValue(QVariant());
1241+ } else {
1242+ query.addBindValue(entry.folderId);
1243+ }
1244 query.exec();
1245 }
1246
1247@@ -257,3 +331,85 @@
1248 query.addBindValue(url.toString());
1249 query.exec();
1250 }
1251+
1252+void BookmarksModel::update(const QUrl& url, const QString& title, const QString& folder)
1253+{
1254+ if (m_urls.contains(url)) {
1255+ int index = 0;
1256+ Q_FOREACH(BookmarkEntry entry, m_orderedEntries) {
1257+ if (entry.url == url) {
1258+ BookmarkEntry& updatedEntry = m_orderedEntries[index];
1259+ QVector<int> roles;
1260+ if (title != updatedEntry.title) {
1261+ updatedEntry.title = title;
1262+ roles << Title;
1263+ }
1264+ if (folder != updatedEntry.folder) {
1265+ updatedEntry.folder = folder;
1266+ updatedEntry.folderId = getFolderId(updatedEntry.folder);
1267+ roles << Folder;
1268+ }
1269+ if (!roles.isEmpty()) {
1270+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), roles);
1271+ updateExistingEntryInDatabase(updatedEntry);
1272+ }
1273+ return;
1274+ } else {
1275+ index++;
1276+ }
1277+ };
1278+ } else {
1279+ qWarning() << "Invalid bookmark:" << url;
1280+ }
1281+}
1282+
1283+void BookmarksModel::updateExistingEntryInDatabase(const BookmarkEntry& entry)
1284+{
1285+ QSqlQuery query(m_database);
1286+ static QString updateStatement = QLatin1String("UPDATE bookmarks SET title=?, "
1287+ "icon=?, created=?, folderId=? "
1288+ "WHERE url=?;");
1289+ query.prepare(updateStatement);
1290+ query.addBindValue(entry.title);
1291+ query.addBindValue(entry.icon.toString());
1292+ query.addBindValue(entry.created.toMSecsSinceEpoch());
1293+ if (entry.folderId == 0) {
1294+ query.addBindValue(QVariant());
1295+ } else {
1296+ query.addBindValue(entry.folderId);
1297+ }
1298+ query.addBindValue(entry.url.toString());
1299+ query.exec();
1300+}
1301+
1302+int BookmarksModel::getFolderId(const QString& folder) {
1303+ QHashIterator<int, QString> i(m_folders);
1304+ while (i.hasNext()) {
1305+ i.next();
1306+ if (i.value() == folder) {
1307+ return i.key();
1308+ }
1309+ }
1310+
1311+ return addFolder(folder);
1312+}
1313+
1314+int BookmarksModel::insertNewFolderInDatabase(const QString& folder)
1315+{
1316+ QSqlQuery insertQuery(m_database);
1317+ QString query = QLatin1String("INSERT INTO folders (folder) VALUES (?);");
1318+ insertQuery.prepare(query);
1319+ insertQuery.addBindValue(folder);
1320+ insertQuery.exec();
1321+
1322+ QSqlQuery selectQuery(m_database);
1323+ query = QLatin1String("SELECT folderId FROM folders WHERE folder=?;");
1324+ selectQuery.prepare(query);
1325+ selectQuery.addBindValue(folder);
1326+ selectQuery.exec();
1327+ if (selectQuery.next()) {
1328+ return selectQuery.value(0).toInt();
1329+ }
1330+
1331+ return 0;
1332+}
1333
1334=== modified file 'src/app/webbrowser/bookmarks-model.h'
1335--- src/app/webbrowser/bookmarks-model.h 2015-05-21 16:20:32 +0000
1336+++ src/app/webbrowser/bookmarks-model.h 2015-07-02 13:49:44 +0000
1337@@ -1,5 +1,5 @@
1338 /*
1339- * Copyright 2013-2014 Canonical Ltd.
1340+ * Copyright 2013-2015 Canonical Ltd.
1341 *
1342 * This file is part of webbrowser-app.
1343 *
1344@@ -45,7 +45,8 @@
1345 Url = Qt::UserRole + 1,
1346 Title,
1347 Icon,
1348- Created
1349+ Created,
1350+ Folder
1351 };
1352
1353 // reimplemented from QAbstractListModel
1354@@ -56,12 +57,17 @@
1355 const QString databasePath() const;
1356 void setDatabasePath(const QString& path);
1357
1358+ QStringList folders() const;
1359+ int addFolder(const QString& folder);
1360+
1361 Q_INVOKABLE bool contains(const QUrl& url) const;
1362- Q_INVOKABLE void add(const QUrl& url, const QString& title, const QUrl& icon);
1363+ Q_INVOKABLE void add(const QUrl& url, const QString& title, const QUrl& icon, const QString& folder);
1364 Q_INVOKABLE void remove(const QUrl& url);
1365+ Q_INVOKABLE void update(const QUrl& url, const QString& title, const QString& folder);
1366
1367 Q_SIGNALS:
1368 void databasePathChanged() const;
1369+ void folderAdded(const QString& folder) const;
1370 void added(const QUrl& url) const;
1371 void removed(const QUrl& url) const;
1372 void rowCountChanged();
1373@@ -74,7 +80,10 @@
1374 QString title;
1375 QUrl icon;
1376 QDateTime created;
1377+ int folderId;
1378+ QString folder;
1379 };
1380+ QHash<int, QString> m_folders;
1381 QSet<QUrl> m_urls;
1382 QList<BookmarkEntry> m_orderedEntries;
1383
1384@@ -83,6 +92,9 @@
1385 void populateFromDatabase();
1386 void insertNewEntryInDatabase(const BookmarkEntry& entry);
1387 void removeExistingEntryFromDatabase(const QUrl& url);
1388+ void updateExistingEntryInDatabase(const BookmarkEntry& entry);
1389+ int getFolderId(const QString& folder);
1390+ int insertNewFolderInDatabase(const QString& folder);
1391 };
1392
1393 #endif // __BOOKMARKS_MODEL_H__
1394
1395=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
1396--- src/app/webbrowser/webbrowser-app.cpp 2015-05-14 05:57:01 +0000
1397+++ src/app/webbrowser/webbrowser-app.cpp 2015-07-02 13:49:44 +0000
1398@@ -17,6 +17,7 @@
1399 */
1400
1401 #include "bookmarks-model.h"
1402+#include "bookmarks-folderlist-model.h"
1403 #include "cache-deleter.h"
1404 #include "config.h"
1405 #include "file-operations.h"
1406@@ -72,6 +73,7 @@
1407 qmlRegisterType<LimitProxyModel>(uri, 0 , 1, "LimitProxyModel");
1408 qmlRegisterType<TabsModel>(uri, 0, 1, "TabsModel");
1409 qmlRegisterType<BookmarksModel>(uri, 0, 1, "BookmarksModel");
1410+ qmlRegisterType<BookmarksFolderListModel>(uri, 0, 1, "BookmarksFolderListModel");
1411 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
1412 qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine");
1413 qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory);
1414
1415=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
1416--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-06-10 11:19:10 +0000
1417+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-07-02 13:49:44 +0000
1418@@ -151,6 +151,13 @@
1419 def get_bottom_edge_hint(self):
1420 return self.select_single("QQuickImage", objectName="bottomEdgeHint")
1421
1422+ def get_bookmark_options(self):
1423+ return self.select_single(BookmarkOptions)
1424+
1425+ def get_new_bookmarks_folder_dialog(self):
1426+ return self.wait_select_single("Dialog",
1427+ objectName="newFolderDialog")
1428+
1429 # The history view is dynamically created, so it might or might not be
1430 # available
1431 def get_history_view(self):
1432@@ -159,6 +166,9 @@
1433 except exceptions.StateNotFoundError:
1434 return None
1435
1436+ def get_bookmarks_folder_list_view(self):
1437+ return self.select_single(BookmarksFolderListView)
1438+
1439 def press_key(self, key):
1440 self.keyboard.press_and_release(key)
1441
1442@@ -405,3 +415,45 @@
1443 class UrlDelegate(uitk.UCListItem):
1444
1445 pass
1446+
1447+
1448+class BookmarkOptions(uitk.UbuntuUIToolkitCustomProxyObjectBase):
1449+
1450+ def get_title_text_field(self):
1451+ return self.select_single(uitk.TextField, objectName="titleTextField")
1452+
1453+ def get_save_in_option_selector(self):
1454+ return self.select_single("OptionSelector", currentlyExpanded=False)
1455+
1456+ @autopilot.logging.log_action(logger.info)
1457+ def click_new_folder_button(self):
1458+ button = self.select_single("Button",
1459+ objectName="bookmarkOptions.newButton")
1460+ self.pointing_device.click_object(button)
1461+
1462+ @autopilot.logging.log_action(logger.info)
1463+ def click_dismiss_button(self):
1464+ button = self.select_single("Button",
1465+ objectName="bookmarkOptions.okButton")
1466+ self.pointing_device.click_object(button)
1467+
1468+
1469+class BookmarksFolderListView(uitk.UbuntuUIToolkitCustomProxyObjectBase):
1470+
1471+ def get_delegates(self):
1472+ return sorted(self.select_many("QQuickItem",
1473+ objectName="bookmarkFolderDelegate"),
1474+ key=lambda delegate: delegate.globalRect.y)
1475+
1476+ def get_folder_delegate(self, folder):
1477+ return self.select_single("QQuickItem",
1478+ objectName="bookmarkFolderDelegate",
1479+ folderName=folder)
1480+
1481+ def get_urls_from_folder(self, folder):
1482+ return sorted(folder.select_many(UrlDelegate),
1483+ key=lambda delegate: delegate.globalRect.y)
1484+
1485+ def get_header_from_folder(self, folder):
1486+ return folder.wait_select_single("QQuickItem",
1487+ objectName="bookmarkFolderHeader")
1488
1489=== modified file 'tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py'
1490--- tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-06-16 08:29:57 +0000
1491+++ tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-07-02 13:49:44 +0000
1492@@ -27,6 +27,9 @@
1493 address_bar = self.main_window.address_bar
1494 bookmark_toggle = address_bar.get_bookmark_toggle()
1495 self.pointing_device.click_object(bookmark_toggle)
1496+ bookmark_options = self.main_window.get_bookmark_options()
1497+ bookmark_options.click_dismiss_button()
1498+ bookmark_options.wait_until_destroyed()
1499 self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1500
1501 self.open_tabs_view()
1502
1503=== added file 'tests/autopilot/webbrowser_app/tests/test_bookmark_options.py'
1504--- tests/autopilot/webbrowser_app/tests/test_bookmark_options.py 1970-01-01 00:00:00 +0000
1505+++ tests/autopilot/webbrowser_app/tests/test_bookmark_options.py 2015-07-02 13:49:44 +0000
1506@@ -0,0 +1,243 @@
1507+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
1508+#
1509+# Copyright 2015 Canonical
1510+#
1511+# This program is free software: you can redistribute it and/or modify it
1512+# under the terms of the GNU General Public License version 3, as published
1513+# by the Free Software Foundation.
1514+#
1515+# This program is distributed in the hope that it will be useful,
1516+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1517+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1518+# GNU General Public License for more details.
1519+#
1520+# You should have received a copy of the GNU General Public License
1521+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1522+
1523+import os.path
1524+import sqlite3
1525+import time
1526+import testtools
1527+
1528+from autopilot.matchers import Eventually
1529+from testtools.matchers import Equals
1530+
1531+import ubuntuuitoolkit as uitk
1532+
1533+from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
1534+
1535+
1536+class TestBookmarkOptions(StartOpenRemotePageTestCaseBase):
1537+
1538+ def setUp(self):
1539+ self.create_temporary_profile()
1540+ self.populate_bookmarks()
1541+ super(TestBookmarkOptions, self).setUp()
1542+
1543+ def populate_bookmarks(self):
1544+ db_path = os.path.join(self.data_location, "bookmarks.sqlite")
1545+ connection = sqlite3.connect(db_path)
1546+
1547+ connection.execute("""CREATE TABLE IF NOT EXISTS folders
1548+ (folderId INTEGER PRIMARY KEY,
1549+ folder VARCHAR);""")
1550+ rows = [
1551+ "Actinide",
1552+ "NobleGas",
1553+ ]
1554+
1555+ for row in rows:
1556+ query = "INSERT INTO folders (folder) VALUES ('{}');"
1557+ query = query.format(row)
1558+ connection.execute(query)
1559+
1560+ foldersId = dict(connection.execute("""SELECT folder, folderId
1561+ FROM folders;"""))
1562+
1563+ connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks
1564+ (url VARCHAR, title VARCHAR, icon VARCHAR,
1565+ created INTEGER, folderId INTEGER);""")
1566+ rows = [
1567+ ("http://test/periodic-table/element/24/chromium",
1568+ "Chromium - Element Information",
1569+ 0),
1570+ ("http://test/periodic-table/element/77/iridium",
1571+ "Iridium - Element Information",
1572+ 0),
1573+ ("http://test/periodic-table/element/31/gallium",
1574+ "Gallium - Element Information",
1575+ 0),
1576+ ("http://test/periodic-table/element/116/livermorium",
1577+ "Livermorium - Element Information",
1578+ 0),
1579+ ("http://test/periodic-table/element/89/actinium",
1580+ "Actinium - Element Information",
1581+ foldersId['Actinide']),
1582+ ("http://test/periodic-table/element/2/helium",
1583+ "Helium - Element Information",
1584+ foldersId['NobleGas']),
1585+ ]
1586+ for i, row in enumerate(rows):
1587+ timestamp = int(time.time()) - i * 10
1588+ query = "INSERT INTO bookmarks \
1589+ VALUES ('{}', '{}', '', {}, {});"
1590+ query = query.format(row[0], row[1], timestamp, row[2])
1591+ connection.execute(query)
1592+ connection.commit()
1593+ connection.close()
1594+
1595+ def _get_bookmarks_folders_list_view(self):
1596+ self.open_tabs_view()
1597+ new_tab_view = self.open_new_tab()
1598+ more_button = new_tab_view.get_bookmarks_more_button()
1599+ self.assertThat(more_button.visible, Equals(True))
1600+ self.pointing_device.click_object(more_button)
1601+ return self.main_window.get_bookmarks_folder_list_view()
1602+
1603+ def _get_bookmark_options(self):
1604+ address_bar = self.main_window.address_bar
1605+ bookmark_toggle = address_bar.get_bookmark_toggle()
1606+ self.pointing_device.click_object(bookmark_toggle)
1607+ return self.main_window.get_bookmark_options()
1608+
1609+ def test_save_bookmarked_url_in_default_folder(self):
1610+ folders = self._get_bookmarks_folders_list_view()
1611+ folder_delegate = folders.get_folder_delegate("")
1612+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1613+ folder_delegate)),
1614+ Eventually(Equals(4)))
1615+
1616+ url = self.base_url + "/test2"
1617+ self.main_window.go_to_url(url)
1618+ self.main_window.wait_until_page_loaded(url)
1619+
1620+ chrome = self.main_window.chrome
1621+ self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1622+
1623+ bookmark_options = self._get_bookmark_options()
1624+ bookmark_options.click_dismiss_button()
1625+ bookmark_options.wait_until_destroyed()
1626+
1627+ self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1628+
1629+ folders = self._get_bookmarks_folders_list_view()
1630+ folder_delegate = folders.get_folder_delegate("")
1631+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1632+ folder_delegate)),
1633+ Eventually(Equals(5)))
1634+
1635+ def test_save_bookmarked_url_in_existing_folder(self):
1636+ folders = self._get_bookmarks_folders_list_view()
1637+ self.assertThat(lambda: len(folders.get_delegates()),
1638+ Eventually(Equals(3)))
1639+ folder_delegate = folders.get_folder_delegate("Actinide")
1640+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1641+ folder_delegate)),
1642+ Eventually(Equals(1)))
1643+
1644+ url = self.base_url + "/test2"
1645+ self.main_window.go_to_url(url)
1646+ self.main_window.wait_until_page_loaded(url)
1647+
1648+ chrome = self.main_window.chrome
1649+ self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1650+
1651+ bookmark_options = self._get_bookmark_options()
1652+
1653+ option_selector = bookmark_options.get_save_in_option_selector()
1654+ self.pointing_device.click_object(option_selector)
1655+ option_selector.currentlyExpanded.wait_for(True)
1656+ option_selector_delegate = option_selector.select_single(
1657+ "OptionSelectorDelegate", text="Actinide")
1658+ self.pointing_device.click_object(option_selector_delegate)
1659+ option_selector.currentlyExpanded.wait_for(False)
1660+
1661+ bookmark_options.click_dismiss_button()
1662+ bookmark_options.wait_until_destroyed()
1663+
1664+ self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1665+
1666+ folders = self._get_bookmarks_folders_list_view()
1667+ self.assertThat(lambda: len(folders.get_delegates()),
1668+ Eventually(Equals(3)))
1669+ folder_delegate = folders.get_folder_delegate("Actinide")
1670+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1671+ folder_delegate)),
1672+ Eventually(Equals(2)))
1673+
1674+ def test_save_bookmarked_url_in_new_folder(self):
1675+ folders = self._get_bookmarks_folders_list_view()
1676+ self.assertThat(lambda: len(folders.get_delegates()),
1677+ Eventually(Equals(3)))
1678+
1679+ url = self.base_url + "/test2"
1680+ self.main_window.go_to_url(url)
1681+ self.main_window.wait_until_page_loaded(url)
1682+
1683+ chrome = self.main_window.chrome
1684+ self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1685+
1686+ bookmark_options = self._get_bookmark_options()
1687+
1688+ # First test cancelling the creation of a new folder
1689+ bookmark_options.click_new_folder_button()
1690+ dialog = self.main_window.get_new_bookmarks_folder_dialog()
1691+ cancel_button = dialog.select_single(
1692+ "Button", objectName="newFolderDialog.cancelButton")
1693+ self.pointing_device.click_object(cancel_button)
1694+ dialog.wait_until_destroyed()
1695+
1696+ # Then test actually creating a new folder
1697+ bookmark_options.click_new_folder_button()
1698+ dialog = self.main_window.get_new_bookmarks_folder_dialog()
1699+ text_field = dialog.select_single(uitk.TextField,
1700+ objectName="newFolderDialog.text")
1701+ text_field.activeFocus.wait_for(True)
1702+ text_field.write("NewFolder", True)
1703+ save_button = dialog.select_single(
1704+ "Button", objectName="newFolderDialog.saveButton")
1705+ self.pointing_device.click_object(save_button)
1706+ dialog.wait_until_destroyed()
1707+
1708+ bookmark_options.click_dismiss_button()
1709+ bookmark_options.wait_until_destroyed()
1710+
1711+ self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1712+
1713+ folders = self._get_bookmarks_folders_list_view()
1714+ self.assertThat(lambda: len(folders.get_delegates()),
1715+ Eventually(Equals(4)))
1716+ folder_delegate = folders.get_folder_delegate("NewFolder")
1717+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1718+ folder_delegate)),
1719+ Eventually(Equals(1)))
1720+
1721+ @testtools.skip("Temporarily skipped until popover going out of view with"
1722+ " OSK is fixed http://pad.lv/1466222")
1723+ def test_set_bookmark_title(self):
1724+ url = self.base_url + "/test2"
1725+ self.main_window.go_to_url(url)
1726+ self.main_window.wait_until_page_loaded(url)
1727+
1728+ chrome = self.main_window.chrome
1729+ self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1730+
1731+ bookmark_options = self._get_bookmark_options()
1732+
1733+ title_text_field = bookmark_options.get_title_text_field()
1734+ self.pointing_device.click_object(title_text_field)
1735+ title_text_field.activeFocus.wait_for(True)
1736+ title_text_field.write("NewTitle", True)
1737+
1738+ bookmark_options.click_dismiss_button()
1739+ bookmark_options.wait_until_destroyed()
1740+
1741+ self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1742+
1743+ folders = self._get_bookmarks_folders_list_view()
1744+ folder_delegate = folders.get_folder_delegate("")
1745+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1746+ folder_delegate)),
1747+ Eventually(Equals(5)))
1748+ delegate = folders.get_urls_from_folder(folder_delegate)[0]
1749+ self.assertThat(delegate.title, Equals("NewTitle"))
1750
1751=== modified file 'tests/autopilot/webbrowser_app/tests/test_keyboard.py'
1752--- tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-06-16 16:15:35 +0000
1753+++ tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-07-02 13:49:44 +0000
1754@@ -40,7 +40,7 @@
1755 connection = sqlite3.connect(db_path)
1756 connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks
1757 (url VARCHAR, title VARCHAR, icon VARCHAR,
1758- created INTEGER);""")
1759+ created INTEGER, folderId INTEGER);""")
1760 rows = [
1761 ("http://www.rsc.org/periodic-table/element/77/iridium",
1762 "Iridium - Element Information")
1763@@ -49,7 +49,7 @@
1764 for i, row in enumerate(rows):
1765 timestamp = int(time.time()) - i * 10
1766 query = "INSERT INTO bookmarks \
1767- VALUES ('{}', '{}', '', {});"
1768+ VALUES ('{}', '{}', '', {}, '');"
1769 query = query.format(row[0], row[1], timestamp)
1770 connection.execute(query)
1771
1772@@ -216,6 +216,10 @@
1773 self.assertThat(chrome.bookmarked, Equals(False))
1774 self.main_window.press_key('Ctrl+D')
1775 self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1776+ self.main_window.press_key('Escape')
1777+ self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1778+ self.main_window.press_key('Ctrl+D')
1779+ self.assertThat(chrome.bookmarked, Eventually(Equals(True)))
1780 self.main_window.press_key('Ctrl+D')
1781 self.assertThat(chrome.bookmarked, Eventually(Equals(False)))
1782
1783
1784=== modified file 'tests/autopilot/webbrowser_app/tests/test_new_tab_view.py'
1785--- tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-05-28 13:56:52 +0000
1786+++ tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-07-02 13:49:44 +0000
1787@@ -137,28 +137,51 @@
1788 def populate_bookmarks(self):
1789 db_path = os.path.join(self.data_location, "bookmarks.sqlite")
1790 connection = sqlite3.connect(db_path)
1791+
1792+ connection.execute("""CREATE TABLE IF NOT EXISTS folders
1793+ (folderId INTEGER PRIMARY KEY,
1794+ folder VARCHAR);""")
1795+ rows = [
1796+ "Actinide",
1797+ "NobleGas",
1798+ ]
1799+
1800+ for row in rows:
1801+ query = "INSERT INTO folders (folder) VALUES ('{}');"
1802+ query = query.format(row)
1803+ connection.execute(query)
1804+
1805+ foldersId = dict(connection.execute("""SELECT folder, folderId
1806+ FROM folders;"""))
1807+
1808 connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks
1809 (url VARCHAR, title VARCHAR, icon VARCHAR,
1810- created INTEGER);""")
1811+ created INTEGER, folderId INTEGER);""")
1812 rows = [
1813 ("http://test/periodic-table/element/24/chromium",
1814- "Chromium - Element Information"),
1815+ "Chromium - Element Information",
1816+ 0),
1817 ("http://test/periodic-table/element/77/iridium",
1818- "Iridium - Element Information"),
1819+ "Iridium - Element Information",
1820+ 0),
1821 ("http://test/periodic-table/element/31/gallium",
1822- "Gallium - Element Information"),
1823+ "Gallium - Element Information",
1824+ 0),
1825 ("http://test/periodic-table/element/116/livermorium",
1826- "Livermorium - Element Information"),
1827- ("http://test/periodic-table/element/62/samarium",
1828- "Samarium - Element Information"),
1829- ("http://test/periodic-table/element/63/europium",
1830- "Europium - Element Information"),
1831+ "Livermorium - Element Information",
1832+ 0),
1833+ ("http://test/periodic-table/element/89/actinium",
1834+ "Actinium - Element Information",
1835+ foldersId['Actinide']),
1836+ ("http://test/periodic-table/element/2/helium",
1837+ "Helium - Element Information",
1838+ foldersId['NobleGas']),
1839 ]
1840 for i, row in enumerate(rows):
1841 timestamp = int(time.time()) - i * 10
1842 query = "INSERT INTO bookmarks \
1843- VALUES ('{}', '{}', '', {});"
1844- query = query.format(row[0], row[1], timestamp)
1845+ VALUES ('{}', '{}', '', {}, {});"
1846+ query = query.format(row[0], row[1], timestamp, row[2])
1847 connection.execute(query)
1848 connection.commit()
1849 connection.close()
1850@@ -172,7 +195,7 @@
1851 new_tab_view.wait_until_destroyed()
1852 self.main_window.wait_until_page_loaded(self.homepage)
1853
1854- def test_open_bookmark(self):
1855+ def test_open_bookmark_when_collapsed(self):
1856 self.open_tabs_view()
1857 new_tab_view = self.open_new_tab()
1858 bookmarks = new_tab_view.get_bookmarks_list()
1859@@ -184,6 +207,23 @@
1860 new_tab_view.wait_until_destroyed()
1861 self.main_window.wait_until_page_loaded(url)
1862
1863+ def test_open_bookmark_when_expanded(self):
1864+ self.open_tabs_view()
1865+ new_tab_view = self.open_new_tab()
1866+ more_button = new_tab_view.get_bookmarks_more_button()
1867+ self.assertThat(more_button.visible, Equals(True))
1868+ self.pointing_device.click_object(more_button)
1869+ folders = self.main_window.get_bookmarks_folder_list_view()
1870+ folder_delegate = folders.get_folder_delegate("")
1871+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1872+ folder_delegate)),
1873+ Eventually(Equals(4)))
1874+ bookmark = folders.get_urls_from_folder(folder_delegate)[0]
1875+ url = bookmark.url
1876+ self.pointing_device.click_object(bookmark)
1877+ new_tab_view.wait_until_destroyed()
1878+ self.main_window.wait_until_page_loaded(url)
1879+
1880 def test_bookmarks_section_expands_and_collapses(self):
1881 self.open_tabs_view()
1882 new_tab_view = self.open_new_tab()
1883@@ -197,8 +237,11 @@
1884 more_button = new_tab_view.get_bookmarks_more_button()
1885 self.assertThat(more_button.visible, Equals(True))
1886 self.pointing_device.click_object(more_button)
1887- self.assertThat(lambda: len(bookmarks.get_delegates()),
1888- Eventually(Equals(6)))
1889+ folders = self.main_window.get_bookmarks_folder_list_view()
1890+ folder_delegate = folders.get_folder_delegate("")
1891+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1892+ folder_delegate)),
1893+ Eventually(Equals(4)))
1894 self.assertThat(top_sites.visible, Eventually(Equals(False)))
1895 # Collapse again
1896 self.assertThat(more_button.visible, Equals(True))
1897@@ -216,6 +259,19 @@
1898 self.assertThat(lambda: bookmarks.get_urls()[0],
1899 Eventually(NotEquals(url)))
1900
1901+ def _remove_first_bookmark_from_folder(self, folder):
1902+ folders = self.main_window.get_bookmarks_folder_list_view()
1903+ folder_delegate = folders.get_folder_delegate(folder)
1904+ delegate = folders.get_urls_from_folder(folder_delegate)[0]
1905+ url = delegate.url
1906+ count = len(folders.get_urls_from_folder(folder_delegate))
1907+ delegate.trigger_leading_action("leadingAction.delete",
1908+ delegate.wait_until_destroyed)
1909+ if ((count - 1) > 4):
1910+ self.assertThat(
1911+ lambda: folders.get_urls_from_folder(folder_delegate)[0],
1912+ Eventually(NotEquals(url)))
1913+
1914 def test_remove_bookmarks_when_collapsed(self):
1915 self.open_tabs_view()
1916 new_tab_view = self.open_new_tab()
1917@@ -232,19 +288,91 @@
1918 def test_remove_bookmarks_when_expanded(self):
1919 self.open_tabs_view()
1920 new_tab_view = self.open_new_tab()
1921- bookmarks = new_tab_view.get_bookmarks_list()
1922 more_button = new_tab_view.get_bookmarks_more_button()
1923 self.assertThat(more_button.visible, Equals(True))
1924 self.pointing_device.click_object(more_button)
1925- self.assertThat(lambda: len(bookmarks.get_delegates()),
1926- Eventually(Equals(6)))
1927+ folders = self.main_window.get_bookmarks_folder_list_view()
1928+ folder_delegate = folders.get_folder_delegate("")
1929+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1930+ folder_delegate)),
1931+ Eventually(Equals(4)))
1932 more_button = new_tab_view.get_bookmarks_more_button()
1933 top_sites = new_tab_view.get_top_sites_list()
1934- for i in range(3):
1935- self._remove_first_bookmark()
1936- self.assertThat(len(bookmarks.get_delegates()), Equals(5 - i))
1937- self.assertThat(more_button.visible, Eventually(Equals(i < 1)))
1938- self.assertThat(top_sites.visible, Eventually(Equals(i > 0)))
1939+ self._remove_first_bookmark_from_folder("Actinide")
1940+ self._remove_first_bookmark_from_folder("NobleGas")
1941+ self.assertThat(more_button.visible, Eventually(Equals(False)))
1942+ self.assertThat(top_sites.visible, Eventually(Equals(True)))
1943+
1944+ def test_show_bookmarks_folders_when_expanded(self):
1945+ self.open_tabs_view()
1946+ new_tab_view = self.open_new_tab()
1947+ more_button = new_tab_view.get_bookmarks_more_button()
1948+ self.assertThat(more_button.visible, Equals(True))
1949+ self.pointing_device.click_object(more_button)
1950+ folders = self.main_window.get_bookmarks_folder_list_view()
1951+ self.assertThat(lambda: len(folders.get_delegates()),
1952+ Eventually(Equals(3)))
1953+ folder_delegate = folders.get_folder_delegate("")
1954+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1955+ folder_delegate)),
1956+ Eventually(Equals(4)))
1957+ folder_delegate = folders.get_folder_delegate("Actinide")
1958+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1959+ folder_delegate)),
1960+ Eventually(Equals(1)))
1961+ folder_delegate = folders.get_folder_delegate("NobleGas")
1962+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1963+ folder_delegate)),
1964+ Eventually(Equals(1)))
1965+
1966+ def test_hide_empty_bookmarks_folders_when_expanded(self):
1967+ self.open_tabs_view()
1968+ new_tab_view = self.open_new_tab()
1969+ more_button = new_tab_view.get_bookmarks_more_button()
1970+ self.assertThat(more_button.visible, Equals(True))
1971+ self.pointing_device.click_object(more_button)
1972+ folders = self.main_window.get_bookmarks_folder_list_view()
1973+ self.assertThat(lambda: len(folders.get_delegates()),
1974+ Eventually(Equals(3)))
1975+ folder_delegate = folders.get_folder_delegate("Actinide")
1976+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1977+ folder_delegate)),
1978+ Eventually(Equals(1)))
1979+ self._remove_first_bookmark_from_folder("Actinide")
1980+ self.assertThat(lambda: len(folders.get_delegates()),
1981+ Eventually(Equals(2)))
1982+ folder_delegate = folders.get_folder_delegate("")
1983+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1984+ folder_delegate)),
1985+ Eventually(Equals(4)))
1986+ folder_delegate = folders.get_folder_delegate("NobleGas")
1987+ self.assertThat(lambda: len(folders.get_urls_from_folder(
1988+ folder_delegate)),
1989+ Eventually(Equals(1)))
1990+
1991+ def test_bookmarks_folder_expands_and_collapses(self):
1992+ self.open_tabs_view()
1993+ new_tab_view = self.open_new_tab()
1994+ more_button = new_tab_view.get_bookmarks_more_button()
1995+ self.assertThat(more_button.visible, Equals(True))
1996+ self.pointing_device.click_object(more_button)
1997+ folders = self.main_window.get_bookmarks_folder_list_view()
1998+ self.assertThat(lambda: len(folders.get_delegates()),
1999+ Eventually(Equals(3)))
2000+ folder_delegate = folders.get_folder_delegate("")
2001+ self.assertThat(lambda: len(folders.get_urls_from_folder(
2002+ folder_delegate)),
2003+ Eventually(Equals(4)))
2004+ self.pointing_device.click_object(
2005+ folders.get_header_from_folder(folder_delegate))
2006+ self.assertThat(lambda: len(folders.get_urls_from_folder(
2007+ folder_delegate)),
2008+ Eventually(Equals(0)))
2009+ self.pointing_device.click_object(
2010+ folders.get_header_from_folder(folder_delegate))
2011+ self.assertThat(lambda: len(folders.get_urls_from_folder(
2012+ folder_delegate)),
2013+ Eventually(Equals(4)))
2014
2015 def test_open_top_site(self):
2016 self.open_tabs_view()
2017
2018=== modified file 'tests/autopilot/webbrowser_app/tests/test_suggestions.py'
2019--- tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-06-11 18:40:30 +0000
2020+++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-07-02 13:49:44 +0000
2021@@ -80,7 +80,7 @@
2022 connection = sqlite3.connect(db_path)
2023 connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks
2024 (url VARCHAR, title VARCHAR, icon VARCHAR,
2025- created INTEGER);""")
2026+ created INTEGER, folderId INTEGER);""")
2027 rows = [
2028 ("http://www.rsc.org/periodic-table/element/24/chromium",
2029 "Chromium - Element Information"),
2030@@ -101,7 +101,7 @@
2031 for i, row in enumerate(rows):
2032 timestamp = int(time.time()) - i * 10
2033 query = "INSERT INTO bookmarks \
2034- VALUES ('{}', '{}', '', {});"
2035+ VALUES ('{}', '{}', '', {}, '');"
2036 query = query.format(row[0], row[1], timestamp)
2037 connection.execute(query)
2038
2039
2040=== modified file 'tests/unittests/CMakeLists.txt'
2041--- tests/unittests/CMakeLists.txt 2015-04-29 20:51:24 +0000
2042+++ tests/unittests/CMakeLists.txt 2015-07-02 13:49:44 +0000
2043@@ -11,6 +11,8 @@
2044 add_subdirectory(suggestions-filter-model)
2045 add_subdirectory(tabs-model)
2046 add_subdirectory(bookmarks-model)
2047+add_subdirectory(bookmarks-folder-model)
2048+add_subdirectory(bookmarks-folderlist-model)
2049 add_subdirectory(limit-proxy-model)
2050 add_subdirectory(container-url-patterns)
2051 add_subdirectory(cookie-store)
2052
2053=== added directory 'tests/unittests/bookmarks-folder-model'
2054=== added file 'tests/unittests/bookmarks-folder-model/CMakeLists.txt'
2055--- tests/unittests/bookmarks-folder-model/CMakeLists.txt 1970-01-01 00:00:00 +0000
2056+++ tests/unittests/bookmarks-folder-model/CMakeLists.txt 2015-07-02 13:49:44 +0000
2057@@ -0,0 +1,6 @@
2058+set(TEST tst_BookmarksFolderModelTests)
2059+add_executable(${TEST} tst_BookmarksFolderModelTests.cpp)
2060+include_directories(${webbrowser-app_SOURCE_DIR})
2061+qt5_use_modules(${TEST} Core Test)
2062+target_link_libraries(${TEST} webbrowser-app-models)
2063+add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
2064
2065=== added file 'tests/unittests/bookmarks-folder-model/tst_BookmarksFolderModelTests.cpp'
2066--- tests/unittests/bookmarks-folder-model/tst_BookmarksFolderModelTests.cpp 1970-01-01 00:00:00 +0000
2067+++ tests/unittests/bookmarks-folder-model/tst_BookmarksFolderModelTests.cpp 2015-07-02 13:49:44 +0000
2068@@ -0,0 +1,123 @@
2069+/*
2070+ * Copyright 2015 Canonical Ltd.
2071+ *
2072+ * This file is part of webbrowser-app.
2073+ *
2074+ * webbrowser-app is free software; you can redistribute it and/or modify
2075+ * it under the terms of the GNU General Public License as published by
2076+ * the Free Software Foundation; version 3.
2077+ *
2078+ * webbrowser-app is distributed in the hope that it will be useful,
2079+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2080+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2081+ * GNU General Public License for more details.
2082+ *
2083+ * You should have received a copy of the GNU General Public License
2084+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2085+ */
2086+
2087+// Qt
2088+#include <QtCore/QObject>
2089+#include <QtTest/QSignalSpy>
2090+#include <QtTest/QtTest>
2091+
2092+// local
2093+#include "bookmarks-model.h"
2094+#include "bookmarks-folder-model.h"
2095+
2096+
2097+class BookmarksFolderModelTests : public QObject
2098+{
2099+ Q_OBJECT
2100+
2101+private:
2102+ BookmarksModel* bookmarks;
2103+ BookmarksFolderModel* model;
2104+
2105+private Q_SLOTS:
2106+ void init()
2107+ {
2108+ bookmarks = new BookmarksModel;
2109+ bookmarks->setDatabasePath(":memory:");
2110+ model = new BookmarksFolderModel;
2111+ model->setSourceModel(bookmarks);
2112+ }
2113+
2114+ void cleanup()
2115+ {
2116+ delete model;
2117+ delete bookmarks;
2118+ }
2119+
2120+ void shouldBeInitiallyEmpty()
2121+ {
2122+ QCOMPARE(model->rowCount(), 0);
2123+ }
2124+
2125+ void shouldNotifyWhenChangingSourceModel()
2126+ {
2127+ QSignalSpy spy(model, SIGNAL(sourceModelChanged()));
2128+ model->setSourceModel(bookmarks);
2129+ QVERIFY(spy.isEmpty());
2130+ BookmarksModel* bookmarks2 = new BookmarksModel;
2131+ model->setSourceModel(bookmarks2);
2132+ QCOMPARE(spy.count(), 1);
2133+ QCOMPARE(model->sourceModel(), bookmarks2);
2134+ model->setSourceModel(0);
2135+ QCOMPARE(spy.count(), 2);
2136+ QCOMPARE(model->sourceModel(), (BookmarksModel*) 0);
2137+ }
2138+
2139+ void shouldNotifyWhenChangingFolder()
2140+ {
2141+ QSignalSpy spy(model, SIGNAL(folderChanged()));
2142+ model->setFolder(QString());
2143+ QVERIFY(spy.isEmpty());
2144+ model->setFolder(QString(""));
2145+ QVERIFY(spy.isEmpty());
2146+ model->setFolder("SampleFolder");
2147+ QCOMPARE(spy.count(), 1);
2148+ }
2149+
2150+ void shouldMatchAllNotInFoldersWhenNoFolderSet()
2151+ {
2152+ bookmarks->add(QUrl("http://example.org"), "Example Domain", QUrl(), "");
2153+ bookmarks->add(QUrl("http://example.com"), "Example Domain", QUrl(), "SampleFolder");
2154+ QCOMPARE(model->rowCount(), 1);
2155+ }
2156+
2157+ void shouldFilterOutNonMatchingFolders()
2158+ {
2159+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2160+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "SampleFolder01");
2161+ bookmarks->add(QUrl("http://example.net/"), "Example Domain", QUrl(), "SampleFolder02");
2162+ model->setFolder("");
2163+ QCOMPARE(model->rowCount(), 1);
2164+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/"));
2165+ model->setFolder("SampleFolder01");
2166+ QCOMPARE(model->rowCount(), 1);
2167+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.com/"));
2168+ model->setFolder("SampleFolder02");
2169+ QCOMPARE(model->rowCount(), 1);
2170+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.net/"));
2171+ model->setFolder("AnotherFolder");
2172+ QCOMPARE(model->rowCount(), 0);
2173+ }
2174+
2175+ void shouldMatchCaseSensitiveFolderName()
2176+ {
2177+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "SAMPLE");
2178+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "sample");
2179+ model->setFolder("SAMPLE");
2180+ QCOMPARE(model->rowCount(), 1);
2181+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/"));
2182+ model->setFolder("sample");
2183+ QCOMPARE(model->rowCount(), 1);
2184+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.com/"));
2185+ model->setFolder("SaMpLe");
2186+ QCOMPARE(model->rowCount(), 0);
2187+ }
2188+};
2189+
2190+QTEST_MAIN(BookmarksFolderModelTests)
2191+#include "tst_BookmarksFolderModelTests.moc"
2192
2193=== added directory 'tests/unittests/bookmarks-folderlist-model'
2194=== added file 'tests/unittests/bookmarks-folderlist-model/CMakeLists.txt'
2195--- tests/unittests/bookmarks-folderlist-model/CMakeLists.txt 1970-01-01 00:00:00 +0000
2196+++ tests/unittests/bookmarks-folderlist-model/CMakeLists.txt 2015-07-02 13:49:44 +0000
2197@@ -0,0 +1,6 @@
2198+set(TEST tst_BookmarksFolderListModelTests)
2199+add_executable(${TEST} tst_BookmarksFolderListModelTests.cpp)
2200+include_directories(${webbrowser-app_SOURCE_DIR})
2201+qt5_use_modules(${TEST} Core Test)
2202+target_link_libraries(${TEST} webbrowser-app-models)
2203+add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
2204
2205=== added file 'tests/unittests/bookmarks-folderlist-model/tst_BookmarksFolderListModelTests.cpp'
2206--- tests/unittests/bookmarks-folderlist-model/tst_BookmarksFolderListModelTests.cpp 1970-01-01 00:00:00 +0000
2207+++ tests/unittests/bookmarks-folderlist-model/tst_BookmarksFolderListModelTests.cpp 2015-07-02 13:49:44 +0000
2208@@ -0,0 +1,269 @@
2209+/*
2210+ * Copyright 2015 Canonical Ltd.
2211+ *
2212+ * This file is part of webbrowser-app.
2213+ *
2214+ * webbrowser-app is free software; you can redistribute it and/or modify
2215+ * it under the terms of the GNU General Public License as published by
2216+ * the Free Software Foundation; version 3.
2217+ *
2218+ * webbrowser-app is distributed in the hope that it will be useful,
2219+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2220+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2221+ * GNU General Public License for more details.
2222+ *
2223+ * You should have received a copy of the GNU General Public License
2224+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2225+ */
2226+
2227+// Qt
2228+#include <QtCore/QObject>
2229+#include <QtTest/QSignalSpy>
2230+#include <QtTest/QtTest>
2231+
2232+// local
2233+#include "bookmarks-model.h"
2234+#include "bookmarks-folder-model.h"
2235+#include "bookmarks-folderlist-model.h"
2236+
2237+class BookmarksFolderListModelTests : public QObject
2238+{
2239+ Q_OBJECT
2240+
2241+private:
2242+ BookmarksModel* bookmarks;
2243+ BookmarksFolderListModel* model;
2244+
2245+ void verifyDataChanged(QSignalSpy& spy, int row)
2246+ {
2247+ QList<QVariant> args;
2248+ bool changed = false;
2249+ while(!changed && !spy.isEmpty()) {
2250+ args = spy.takeFirst();
2251+ int start = args.at(0).toModelIndex().row();
2252+ int end = args.at(1).toModelIndex().row();
2253+ changed = (start <= row) && (row <= end);
2254+ }
2255+ QVERIFY(changed);
2256+ }
2257+
2258+private Q_SLOTS:
2259+ void init()
2260+ {
2261+ bookmarks = new BookmarksModel;
2262+ bookmarks->setDatabasePath(":memory:");
2263+ model = new BookmarksFolderListModel;
2264+ model->setSourceModel(bookmarks);
2265+ }
2266+
2267+ void cleanup()
2268+ {
2269+ delete model;
2270+ delete bookmarks;
2271+ }
2272+
2273+ void shouldHaveInitiallyOnlyDefaultFolder()
2274+ {
2275+ QCOMPARE(model->rowCount(), 1);
2276+ QCOMPARE(model->data(model->index(0, 0), BookmarksFolderListModel::Folder).toString(), QString(""));
2277+ }
2278+
2279+ void shouldUpdateFolderListWhenInsertingEntries()
2280+ {
2281+ QSignalSpy spyRowsInserted(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)));
2282+ qRegisterMetaType<QVector<int> >();
2283+ QSignalSpy spyDataChanged(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
2284+
2285+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "SampleFolder");
2286+ QVERIFY(!spyDataChanged.isEmpty());
2287+ QCOMPARE(spyRowsInserted.count(), 1);
2288+ QList<QVariant> args = spyRowsInserted.takeFirst();
2289+ QCOMPARE(args.at(1).toInt(), 1);
2290+ QCOMPARE(args.at(2).toInt(), 1);
2291+ QCOMPARE(model->rowCount(), 2);
2292+ QCOMPARE(model->data(model->index(1, 0), BookmarksFolderListModel::Folder).toString(), QString("SampleFolder"));
2293+
2294+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "AnotherFolder");
2295+ QVERIFY(!spyDataChanged.isEmpty());
2296+ QCOMPARE(spyRowsInserted.count(), 1);
2297+ args = spyRowsInserted.takeFirst();
2298+ QCOMPARE(args.at(1).toInt(), 1);
2299+ QCOMPARE(args.at(2).toInt(), 1);
2300+ QCOMPARE(model->rowCount(), 3);
2301+ QCOMPARE(model->data(model->index(1, 0), BookmarksFolderListModel::Folder).toString(), QString("AnotherFolder"));
2302+
2303+ bookmarks->add(QUrl("http://example.org/test.html"), "Test page", QUrl(), "SampleFolder");
2304+ QVERIFY(spyRowsInserted.isEmpty());
2305+ QVERIFY(!spyDataChanged.isEmpty());
2306+ QCOMPARE(model->rowCount(), 3);
2307+ }
2308+
2309+ void shouldCreateNewEmptyFolder()
2310+ {
2311+ model->createNewFolder("SampleFolder");
2312+ QCOMPARE(model->rowCount(), 2);
2313+ QModelIndex index = model->index(1, 0);
2314+ QCOMPARE(model->data(index, BookmarksFolderListModel::Folder).toString(), QString("SampleFolder"));
2315+ BookmarksFolderModel* entries = model->data(index, BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2316+ QCOMPARE(entries->rowCount(), 0);
2317+ }
2318+
2319+ void shouldNotUpdateFolderListWhenRemovingEntries()
2320+ {
2321+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "SampleFolder");
2322+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "AnotherFolder");
2323+ bookmarks->add(QUrl("http://example.org/test"), "Example Domain", QUrl(), "SampleFolder");
2324+ QCOMPARE(model->rowCount(), 3);
2325+
2326+ bookmarks->remove(QUrl("http://example.org/test"));
2327+ QCOMPARE(model->rowCount(), 3);
2328+
2329+ bookmarks->remove(QUrl("http://example.org/"));
2330+ QCOMPARE(model->rowCount(), 3);
2331+ QModelIndex index = model->index(2, 0);
2332+ QString folder = model->data(index, BookmarksFolderListModel::Folder).toString();
2333+ QCOMPARE(folder, QString("SampleFolder"));
2334+ BookmarksFolderModel* entries = model->data(index, BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2335+ QVERIFY(entries != 0);
2336+ QCOMPARE(entries->rowCount(), 0);
2337+ }
2338+
2339+ void shouldUpdateDataWhenMovingEntries()
2340+ {
2341+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "SampleFolder");
2342+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "AnotherFolder");
2343+ QTest::qWait(100);
2344+
2345+ QSignalSpy spyRowsMoved(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)));
2346+ qRegisterMetaType<QVector<int> >();
2347+ QSignalSpy spyDataChanged(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
2348+
2349+ bookmarks->add(QUrl("http://example.org/test"), "Example Domain", QUrl(), "SampleFolder");
2350+ QVERIFY(spyRowsMoved.isEmpty());
2351+ }
2352+
2353+ void shouldUpdateDataWhenDataChanges()
2354+ {
2355+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "SampleFolder");
2356+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "AnotherFolder");
2357+
2358+ QSignalSpy spyRowsMoved(model, SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)));
2359+ qRegisterMetaType<QVector<int> >();
2360+ QSignalSpy spyDataChanged(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
2361+
2362+ bookmarks->add(QUrl("http://example.org/foobar"), "Example Domain", QUrl(), "SampleFolder");
2363+ QVERIFY(spyRowsMoved.isEmpty());
2364+ QVERIFY(!spyDataChanged.isEmpty());
2365+ verifyDataChanged(spyDataChanged, 2);
2366+ }
2367+
2368+ void shouldUpdateWhenChangingSourceModel()
2369+ {
2370+ QSignalSpy spy(model, SIGNAL(sourceModelChanged()));
2371+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "SampleFolder");
2372+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "AnotherFolder");
2373+ bookmarks->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2374+ QCOMPARE(model->rowCount(), 3);
2375+
2376+ model->setSourceModel(bookmarks);
2377+ QVERIFY(spy.isEmpty());
2378+ QCOMPARE(model->rowCount(), 3);
2379+
2380+ model->setSourceModel(0);
2381+ QCOMPARE(spy.count(), 1);
2382+ QCOMPARE(model->sourceModel(), (BookmarksModel*) 0);
2383+ QCOMPARE(model->rowCount(), 0);
2384+
2385+ BookmarksModel* bookmarks2 = new BookmarksModel();
2386+ model->setSourceModel(bookmarks2);
2387+ QCOMPARE(spy.count(), 2);
2388+ QCOMPARE(model->sourceModel(), bookmarks2);
2389+ QCOMPARE(model->rowCount(), 0);
2390+ }
2391+
2392+ void shouldKeepFolderSorted()
2393+ {
2394+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "Folder02");
2395+ bookmarks->add(QUrl("http://www.gogle.com/lawnmower"), "Gogle Lawn Mower", QUrl(), "Folder03");
2396+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "Folder01");
2397+ bookmarks->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "Folder04");
2398+ bookmarks->add(QUrl("http://www.gogle.com/mail"), "Gogle Mail", QUrl(), "Folder03");
2399+ bookmarks->add(QUrl("https://mail.gogle.com/"), "Gogle Mail", QUrl(), "Folder03");
2400+ QCOMPARE(model->rowCount(), 5);
2401+ QStringList folders;
2402+ folders << "" << "Folder01" << "Folder02" << "Folder03" << "Folder04";
2403+ for (int i = 0; i < folders.count(); ++i) {
2404+ QModelIndex index = model->index(i, 0);
2405+ QString folder = model->data(index, BookmarksFolderListModel::Folder).toString();
2406+ BookmarksFolderModel* entries = model->data(index, BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2407+ QVERIFY(!folder.isNull());
2408+ QCOMPARE(folder, folders.at(i));
2409+ QCOMPARE(entries->folder(), folder);
2410+ }
2411+ }
2412+
2413+ void shouldExposeFolderModels()
2414+ {
2415+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "SampleFolder");
2416+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "AnotherFolder");
2417+ QTest::qWait(100);
2418+ bookmarks->add(QUrl("http://example.org/test.html"), "Test Page", QUrl(), "AnotherFolder");
2419+ bookmarks->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2420+ QCOMPARE(model->rowCount(), 3);
2421+
2422+ QModelIndex index = model->index(0, 0);
2423+ QString folder = model->data(index, BookmarksFolderListModel::Folder).toString();
2424+ QCOMPARE(folder, QString(""));
2425+ BookmarksFolderModel* entries = model->data(index, BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2426+ QCOMPARE(entries->rowCount(), 1);
2427+ QCOMPARE(entries->data(entries->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://ubuntu.com/"));
2428+
2429+ index = model->index(1, 0);
2430+ folder = model->data(index, BookmarksFolderListModel::Folder).toString();
2431+ QCOMPARE(folder, QString("AnotherFolder"));
2432+ entries = model->data(index, BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2433+ QCOMPARE(entries->rowCount(), 2);
2434+ QCOMPARE(entries->data(entries->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/test.html"));
2435+ QCOMPARE(entries->data(entries->index(1, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/"));
2436+
2437+ index = model->index(2, 0);
2438+ folder = model->data(index, BookmarksFolderListModel::Folder).toString();
2439+ QCOMPARE(folder, QString("SampleFolder"));
2440+ entries = model->data(index, BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2441+ QCOMPARE(entries->rowCount(), 1);
2442+ QCOMPARE(entries->data(entries->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.com/"));
2443+ }
2444+
2445+ void shouldReturnData()
2446+ {
2447+ QDateTime now = QDateTime::currentDateTimeUtc();
2448+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "SampleFolder");
2449+ QVERIFY(!model->data(QModelIndex(), BookmarksFolderListModel::Folder).isValid());
2450+ QVERIFY(!model->data(model->index(-1, 0), BookmarksFolderListModel::Folder).isValid());
2451+ QVERIFY(!model->data(model->index(3, 0), BookmarksFolderListModel::Folder).isValid());
2452+ QCOMPARE(model->data(model->index(1, 0), BookmarksFolderListModel::Folder).toString(), QString("SampleFolder"));
2453+ BookmarksFolderModel* entries = model->data(model->index(1, 0), BookmarksFolderListModel::Entries).value<BookmarksFolderModel*>();
2454+ QVERIFY(entries != 0);
2455+ QCOMPARE(entries->rowCount(), 1);
2456+ QVERIFY(!model->data(model->index(1, 0), BookmarksFolderListModel::Entries + 1).isValid());
2457+ }
2458+
2459+ void shouldReturnDataByIndex()
2460+ {
2461+ bookmarks->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "SampleFolder");
2462+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "AnotherFolder");
2463+ bookmarks->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2464+ QCOMPARE(model->rowCount(), 3);
2465+ QCOMPARE(model->indexOf("AnotherFolder"), 1);
2466+ QVariantMap folderMap = model->get(3);
2467+ QVERIFY(folderMap.isEmpty());
2468+ folderMap = model->get(1);
2469+ QCOMPARE(folderMap.value("folder").toString(), QString("AnotherFolder"));
2470+ BookmarksFolderModel* entries = folderMap.value("entries").value<BookmarksFolderModel*>();
2471+ QCOMPARE(entries->rowCount(), 1);
2472+ QCOMPARE(entries->data(entries->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/"));
2473+ }
2474+};
2475+
2476+QTEST_MAIN(BookmarksFolderListModelTests)
2477+#include "tst_BookmarksFolderListModelTests.moc"
2478
2479=== modified file 'tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp'
2480--- tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp 2015-05-16 11:29:15 +0000
2481+++ tests/unittests/bookmarks-model/tst_BookmarksModelTests.cpp 2015-07-02 13:49:44 +0000
2482@@ -56,34 +56,35 @@
2483 QVERIFY(roleNames.contains("title"));
2484 QVERIFY(roleNames.contains("icon"));
2485 QVERIFY(roleNames.contains("created"));
2486+ QVERIFY(roleNames.contains("folder"));
2487 }
2488
2489 void shouldAddNewEntries()
2490 {
2491 QSignalSpy spy(model, SIGNAL(rowsInserted(QModelIndex, int, int)));
2492
2493- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2494+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2495 QCOMPARE(model->rowCount(), 1);
2496 QCOMPARE(spy.count(), 1);
2497 QVariantList args = spy.takeFirst();
2498 QCOMPARE(args.at(1).toInt(), 0);
2499 QCOMPARE(args.at(2).toInt(), 0);
2500
2501- model->add(QUrl("http://wikipedia.org/"), "Wikipedia", QUrl());
2502+ model->add(QUrl("http://wikipedia.org/"), "Wikipedia", QUrl(), "");
2503 QCOMPARE(model->rowCount(), 2);
2504 QCOMPARE(spy.count(), 1);
2505 args = spy.takeFirst();
2506 QCOMPARE(args.at(1).toInt(), 0);
2507 QCOMPARE(args.at(2).toInt(), 0);
2508
2509- model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl());
2510+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2511 QCOMPARE(model->rowCount(), 3);
2512 QCOMPARE(spy.count(), 1);
2513 args = spy.takeFirst();
2514 QCOMPARE(args.at(1).toInt(), 0);
2515 QCOMPARE(args.at(2).toInt(), 0);
2516
2517- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2518+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2519 QCOMPARE(model->rowCount(), 3);
2520 QVERIFY(spy.isEmpty());
2521 }
2522@@ -91,9 +92,9 @@
2523 void shouldRemoveEntries()
2524 {
2525 QSignalSpy spy(model, SIGNAL(rowsRemoved(QModelIndex, int, int)));
2526- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2527- model->add(QUrl("http://wikipedia.org/"), "Wikipedia", QUrl());
2528- model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl());
2529+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2530+ model->add(QUrl("http://wikipedia.org/"), "Wikipedia", QUrl(), "");
2531+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2532 QCOMPARE(model->rowCount(), 3);
2533 QVERIFY(spy.isEmpty());
2534
2535@@ -111,10 +112,37 @@
2536 QVERIFY(spy.isEmpty());
2537 }
2538
2539+ void shouldUpdateEntries()
2540+ {
2541+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
2542+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2543+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2544+ QCOMPARE(model->rowCount(), 2);
2545+ QVERIFY(spy.isEmpty());
2546+
2547+ model->update(QUrl("http://example.org/"), "New Domain Title", "SampleFolder");
2548+ QCOMPARE(model->rowCount(), 2);
2549+ QCOMPARE(spy.count(), 1);
2550+ QList<QVariant> args = spy.takeFirst();
2551+ QCOMPARE(args.at(0).toModelIndex().row(), 1);
2552+ QCOMPARE(args.at(1).toModelIndex().row(), 1);
2553+ QVector<int> roles = args.at(2).value<QVector<int> >();
2554+ QVERIFY(roles.size() >= 2);
2555+ QVERIFY(roles.contains(BookmarksModel::Title));
2556+ QVERIFY(roles.contains(BookmarksModel::Folder));
2557+
2558+ QCOMPARE(model->data(model->index(1, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/"));
2559+ QCOMPARE(model->data(model->index(1, 0), BookmarksModel::Title).toString(), QString("New Domain Title"));
2560+ QCOMPARE(model->data(model->index(1, 0), BookmarksModel::Icon).toUrl(), QUrl(""));
2561+ QCOMPARE(model->data(model->index(1, 0), BookmarksModel::Folder).toString(), QString("SampleFolder"));
2562+
2563+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://ubuntu.com/"));
2564+ }
2565+
2566 void shouldContainEntries()
2567 {
2568- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2569- model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl());
2570+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2571+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2572
2573 QVERIFY(model->contains(QUrl("http://ubuntu.com/")));
2574 QVERIFY(!model->contains(QUrl("http://wikipedia.org/")));
2575@@ -122,9 +150,9 @@
2576
2577 void shouldKeepEntriesSortedChronologically()
2578 {
2579- model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl());
2580- model->add(QUrl("http://wikipedia.org/"), "Wikipedia", QUrl());
2581- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2582+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2583+ model->add(QUrl("http://wikipedia.org/"), "Wikipedia", QUrl(), "");
2584+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2585
2586 QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Url).toUrl(), QUrl("http://example.org/"));
2587 QCOMPARE(model->data(model->index(1, 0), BookmarksModel::Url).toUrl(), QUrl("http://wikipedia.org/"));
2588@@ -133,7 +161,7 @@
2589
2590 void shouldReturnData()
2591 {
2592- model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl("image://webicon/123"));
2593+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl("image://webicon/123"), "SampleFolder");
2594 QVERIFY(!model->data(QModelIndex(), BookmarksModel::Url).isValid());
2595 QVERIFY(!model->data(model->index(-1, 0), BookmarksModel::Url).isValid());
2596 QVERIFY(!model->data(model->index(3, 0), BookmarksModel::Url).isValid());
2597@@ -141,7 +169,8 @@
2598 QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Title).toString(), QString("Ubuntu"));
2599 QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Icon).toUrl(), QUrl("image://webicon/123"));
2600 QVERIFY(model->data(model->index(0, 0), BookmarksModel::Created).toDateTime() <= QDateTime::currentDateTime());
2601- QVERIFY(!model->data(model->index(0, 0), BookmarksModel::Created + 1).isValid());
2602+ QCOMPARE(model->data(model->index(0, 0), BookmarksModel::Folder).toString(), QString("SampleFolder"));
2603+ QVERIFY(!model->data(model->index(0, 0), BookmarksModel::Folder + 1).isValid());
2604 }
2605
2606 void shouldReturnDatabasePath()
2607@@ -172,8 +201,8 @@
2608 delete model;
2609 model = new BookmarksModel;
2610 model->setDatabasePath(fileName);
2611- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2612- model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl());
2613+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2614+ model->add(QUrl("http://ubuntu.com/"), "Ubuntu", QUrl(), "");
2615 delete model;
2616 model = new BookmarksModel;
2617 model->setDatabasePath(fileName);
2618@@ -184,16 +213,38 @@
2619 {
2620 QSignalSpy spyCount(model, SIGNAL(rowCountChanged()));
2621 QCOMPARE(model->property("count").toInt(), 0);
2622- model->add(QUrl("http://example.org/"), "Example Domain", QUrl());
2623+ model->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
2624 QCOMPARE(model->property("count").toInt(), 1);
2625 QCOMPARE(spyCount.count(), 1);
2626- model->add(QUrl("http://example.com/"), "Example Domain", QUrl());
2627+ model->add(QUrl("http://example.com/"), "Example Domain", QUrl(), "");
2628 QCOMPARE(model->property("count").toInt(), 2);
2629 QCOMPARE(spyCount.count(), 2);
2630 model->remove(QUrl("http://example.com/"));
2631 QCOMPARE(model->property("count").toInt(), 1);
2632 QCOMPARE(spyCount.count(), 3);
2633 }
2634+
2635+ void shouldPopulateModelWithExistingFolders()
2636+ {
2637+ QTemporaryFile tempFile;
2638+ tempFile.open();
2639+ QString fileName = tempFile.fileName();
2640+ delete model;
2641+ model = new BookmarksModel;
2642+ QSignalSpy spy(model, SIGNAL(folderAdded(QString)));
2643+ model->setDatabasePath(fileName);
2644+ model->addFolder("SampleFolder");
2645+ model->addFolder("AnotherFolder");
2646+ // The empty folder is added by default
2647+ QCOMPARE(spy.count(), 3);
2648+ QCOMPARE(model->folders().count(), 3);
2649+ delete model;
2650+ model = new BookmarksModel;
2651+ QSignalSpy spyPopulate(model, SIGNAL(folderAdded(QString)));
2652+ model->setDatabasePath(fileName);
2653+ QCOMPARE(spyPopulate.count(), 3);
2654+ QCOMPARE(model->folders().count(), 3);
2655+ }
2656 };
2657
2658 QTEST_MAIN(BookmarksModelTests)

Subscribers

People subscribed via source and target branches

to status/vote changes: