Merge lp:~uriboni/webbrowser-app/keyboard-navigation into lp:webbrowser-app
- keyboard-navigation
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Olivier Tilloy | ||||
Approved revision: | 1097 | ||||
Merged at revision: | 1063 | ||||
Proposed branch: | lp:~uriboni/webbrowser-app/keyboard-navigation | ||||
Merge into: | lp:webbrowser-app | ||||
Prerequisite: | lp:~rpadovani/webbrowser-app/newTabRefactoring | ||||
Diff against target: |
1236 lines (+756/-78) 16 files modified
src/app/webbrowser/AddressBar.qml (+16/-8) src/app/webbrowser/Browser.qml (+213/-21) src/app/webbrowser/Chrome.qml (+3/-0) src/app/webbrowser/KeyboardShortcut.qml (+25/-0) src/app/webbrowser/KeyboardShortcuts.qml (+41/-0) src/app/webbrowser/Suggestion.qml (+3/-2) src/app/webbrowser/Suggestions.qml (+40/-38) src/app/webbrowser/limit-proxy-model.cpp (+16/-0) src/app/webbrowser/limit-proxy-model.h (+2/-0) tests/autopilot/webbrowser_app/emulators/browser.py (+17/-2) tests/autopilot/webbrowser_app/tests/__init__.py (+10/-2) tests/autopilot/webbrowser_app/tests/http_server.py (+4/-0) tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py (+0/-2) tests/autopilot/webbrowser_app/tests/test_keyboard.py (+276/-0) tests/autopilot/webbrowser_app/tests/test_suggestions.py (+68/-3) tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp (+22/-0) |
||||
To merge this branch: | bzr merge lp:~uriboni/webbrowser-app/keyboard-navigation | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Olivier Tilloy | Approve | ||
PS Jenkins bot | continuous-integration | Needs Fixing | |
Review via email: mp+260183@code.launchpad.net |
Commit message
Make the browser chrome usable on desktop by implementing common keyboard shortcuts and behaviors that users normally expect in such an app
Description of the change
The goal of this merge request is to make the browser chrome usable on desktop by implementing common keyboard shortcuts and behaviors that users normally expect in such an app.
Some come directly from the design document and some have been added based on what is obvious and intuitive (and already by other browsers).
This is based on, and supersedes, the following contributor branch: https:/
It is also based on this refactoring branch: https:/
Finally it also supersedes this branch adding keyboard support to the suggestions list, as it is essentially a superset of that work so they might as well go together: https:/
PS Jenkins bot (ps-jenkins) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1050
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
The flake8 unit test fails.
A few functional issues (I haven’t done a complete review yet):
- If the address bar is empty and focused (e.g. when opening a new blank tab), pressing the down arrow key removes focus from it, and pressing the up arrow key doesn’t restore it.
- If I type a word in the address bar and wait for search suggestions to appear, then press the down arrow key to navigate the suggestions list, the search suggestions disappear.
- If I focus the address bar, then open the drawer menu with the mouse and open the history view from there, it cannot be dismissed with ESC (it works as expected if the address bar wasn’t focused or if the view was opened with Ctrl+H).
Ugo Riboni (uriboni) wrote : | # |
> - If the address bar is empty and focused (e.g. when opening a new blank
> tab), pressing the down arrow key removes focus from it, and pressing the up
> arrow key doesn’t restore it.
This was fixed by making sure we can move to the suggestions list with Down only if there are actually items in it.
> - If I type a word in the address bar and wait for search suggestions to
> appear, then press the down arrow key to navigate the suggestions list, the
> search suggestions disappear.
This was more tricky than how you described it. It happened only if the only suggestions in the list were from the search engine. If there were history or bookmarks in the list it did not happen.
The cause was the fact that the search engine suggestions model would be disabled (therefore reducing the count of suggestion to zero and closing the list) when the chrome did not have the focus, but did not take into account the case when the suggestion list itself had focus.
> - If I focus the address bar, then open the drawer menu with the mouse and
> open the history view from there, it cannot be dismissed with ESC (it works as
> expected if the address bar wasn’t focused or if the view was opened with
> Ctrl+H).
Wasn't giving focus to the history page when activating from menu.
Note: I added a test case to prevent the first issue from re-occurring, but I have no idea how to test for the other two, as the tests would need to verify that *eventually* something does not happen, which is hard to do without using arbitrary waits that make the tests fragile.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1056
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1057
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
The following autopilot test is broken on desktop:
webbrowser_
Olivier Tilloy (osomon) wrote : | # |
A few more functional issues I found while testing:
- If I load a page, then focus the address bar, then type e.g. "ab", then press the down arrow to navigate the suggestions, then press the up arrow to re-focus the address bar, then press ESC, the current URL is not restored in the address bar (if I do it while navigating the suggestions it is restored)
- If I’m browsing a page that has a favicon and that’s loaded over a secure connection (i.e. the lock icon is displayed), and if I focus the address bar to start typing search terms (e.g. "ab"), the favicon and the lock icon are hidden, and a magnifier icon is displayed. But if I then press the down key to navigate the suggestions, the favicon and lock icon are displayed again. I think the magnifier icon should remain when navigating through the suggestions.
- On a page where I can navigate back/forward, if I focus the address bar with Ctrl+L, then press Alt+[Left/Right], the previous/next page is loaded, but the address bar is cleared. I would say that navigating back/forward should remove focus from the address bar (and that should solve the issue).
- If I open the history view with Ctrl+H, then press Ctrl+L, nothing visible happens (as expected because the address bar is not visible), however I can’t close the history view with ESC any longer. I think Ctrl+L should not focus the address bar if it’s not visible.
- Similarly, Ctrl+D to (un)bookmark also works when the history view is visible, but it shouldn’t.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1059
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1063
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1064
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
webbrowser_
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1065
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
ESC in the tabs view (recentView) and in the settings page should exit the view (I think it’s ok if more advanced keyboard navigation is not implemented in those views yet, but at the very least it should be possible to leave them using the keyboard).
It might be interesting to have Ctrl+T, Ctrl+W and Ctrl+Tab work while recentView is visible.
The current implementation of Ctrl+Tab doesn’t switch to the next tab, it switches to the last one, is that intended?
I’m not fond of the name of the 'textLocked' property: it kind of suggests that the text cannot be modified, which isn’t true, because keyboard input will modify the text. Can we think of a more appropriate name?
In internal.
In the definition of LimitProxyModel
In Browser.
Can PrepopulatedDat
According to https:/
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1069
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Ugo Riboni (uriboni) wrote : | # |
What does not have a reply was simply fixed.
> ESC in the tabs view (recentView) and in the settings page should exit the
> view (I think it’s ok if more advanced keyboard navigation is not implemented
> in those views yet, but at the very least it should be possible to leave them
> using the keyboard).
> It might be interesting to have Ctrl+T, Ctrl+W and Ctrl+Tab work while
> recentView is visible.
I did all of the above, and also unified the way we exit the tabs view (by switching to whatever tab is current, or by switching to the tab that the user clicked on). This helps fix the problem of sometimes not focusing the address bar when the new tab page is selected (and allows having less places where to put the conditional to focus it only when on desktop that you mention below).
> The current implementation of Ctrl+Tab doesn’t switch to the next tab, it
> switches to the last one, is that intended?
Yes, due to the way tabs are implemented the current tab is always brought to the top of the stack, so this is by design.
> I’m not fond of the name of the 'textLocked' property: it kind of suggests
> that the text cannot be modified, which isn’t true, because keyboard input
> will modify the text. Can we think of a more appropriate name?
I suck at naming variables. preventSimplifyText is the best I could come up with. Hope it works.
> In Browser.
> press a key? Shouldn’t the instance be created in Browser.__init__() instead?
> And why is there a try… except block? When can we get a RuntimeError for
> pressing a key? Silently swallowing exceptions is usually not a good practice,
> except in very specific cases.
This was from the autopilot guide. I think the create() method can give a RuntimeError if no keyboard is available, which is also why I was getting the keyboard only when in need to send a keystroke.
However I looked at the UI toolkit emulators and they get the keyboard at set up and without any error checking, so it must be safe. I did the same thing that they do now.
> Can PrepopulatedDat
> that all tests can re-use?
I see little point. Most of the class is populating the database with data, and the data is different for every test.
You could possibly generalize some of the database access in a base class and allow subclasses to provide only the list of data, but it would be much work for little advantage in my opinion.
I'd rather avoid this.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1074
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1074
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
When in the tabs view, pressing Ctrl+T to open a new tab should probably dismiss the view (just like clicking the "New Tab" button does).
> I suck at naming variables. preventSimplifyText is the best I could
> come up with. Hope it works.
While it works, I must say I don’t like the name very much either. I would prefer 'canSimplifyText' (changing the semantics of the property to its opposite).
> You could possibly generalize some of the database access in a base
> class and allow subclasses to provide only the list of data, but it
> would be much work for little advantage in my opinion.
Yeah, that’s what I was kind of suggesting. We can do that separately later on though.
In AddressBar.qml:
- updateUrlFromFo
- I’m not sure I understand why the test on 'preventSimplif
As pointed out by Bill in his e-mail review (adding it here for future reference), the following need implementing (per design spec):
- F11 to toggle fullscreen mode
- F6 should focus the address bar, just like Ctrl+L and Alt+D
- and F5 for page reload, as you suggested, would be a useful addition, too
Ugo Riboni (uriboni) wrote : | # |
All done except where commented below:
> - I’m not sure I understand why the test on 'preventSimplif
> inside updateUrlFromFo
> want the text to be expanded, right? So the check would be better placed in
> the 'else if' branch of the function, no?
Address bar and suggestion list should be logically considered one single unit in terms of what should happen when they have active focus. However they are two different elements in two different locations in the object tree.
The canSimplifyText exists for this reason: when it is true it means that neither the address bar nor the suggestions list have active focus.
I moved the check inside the updateUrlFromFocus anyway, as it makes the code simpler, but it should not be in the else branch as you suggest.
> As pointed out by Bill in his e-mail review (adding it here for future
> reference), the following need implementing (per design spec):
> - F11 to toggle fullscreen mode
This will be addressed later as we first need to create a proper design that takes into account the difference between application fullscreen and page fullscreen. Tracked by: https:/
Olivier Tilloy (osomon) wrote : | # |
> Address bar and suggestion list should be logically considered one
> single unit in terms of what should happen when they have active focus.
> However they are two different elements in two different locations in
> the object tree.
Right, and we should probably consider refactoring that code to include the suggestions list in the address bar. In a separate branch, of course.
Olivier Tilloy (osomon) wrote : | # |
We’re getting there nicely. A few more minor remarks:
288 + if (tabsModel.count >= 0) {
It should be (tabsModel.count > 0), there is no point in trying to close the current tab if there isn’t any.
680 + def setUp(self, url="/test1"):
That argument would better be named 'path', rather than 'url'.
986 + self.assertThat
Although functionally equivalent, GreaterThan would be more appropriate than NotEquals.
996 + def test_keyboard_
Can test_keyboard_
1085 + QCOMPARE(
Can I suggest QVERIFY(
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1081
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1082
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1085
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Riccardo Padovani (rpadovani) wrote : | # |
Thanks for working on this, now the desktop experience is definitely better!
I leave some impression, don't know if you want to fix in this branch or in a latter one
Some comments:
- I expect to navigate the history with arrows after I open it with CTRL+H
- If I navigate to a page I can scroll with arrows (good!), then I open history with CTRL+H, then I press ESC, arrows don't do anything anymore. I expect they still scroll the page
Some more keybinding I would like:
- CTRL+SHIFT+T to reopen the last closed tab
- ALT+F (as on Chromium) to open the menu and then arrows to navigate it
- F11 to go fullscreen (but this I think requires some other works)
I took a fast look to the code and, apart of a little typo I left a comment inline, looks good to me, except Python I didn't review due my lack of knowledge of Python.
Cool work!
Olivier Tilloy (osomon) wrote : | # |
I don’t think the fix in revision 1085 is correct, the tabs view is not being open any longer, so it breaks the test assumption, doesn’t it?
Olivier Tilloy (osomon) wrote : | # |
> I expect to navigate the history with arrows after I open it with CTRL+H
We agreed that this would be implemented later on
> If I navigate to a page I can scroll with arrows (good!), then I open history with CTRL+H, then I press ESC, arrows don't do anything anymore. I expect they still scroll the page
Good catch, active focus should be restored on the webview when exiting the history view, if the address bar wasn’t focused previously.
> CTRL+SHIFT+T to reopen the last closed tab
Would be nice to have, but will require more work to keep track of recently closed tab, so let’s do that separately.
> ALT+F (as on Chromium) to open the menu and then arrows to navigate it
Would be nice to have too, but not a must for this iteration. Again, let’s do that separately.
> F11 to go fullscreen (but this I think requires some other works)
Yes, this is bug #1464333.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1087
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1089
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
webbrowser_
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1093
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1093
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1094
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) : | # |
Olivier Tilloy (osomon) : | # |
Preview Diff
1 | === modified file 'src/app/webbrowser/AddressBar.qml' | |||
2 | --- src/app/webbrowser/AddressBar.qml 2015-05-26 21:42:54 +0000 | |||
3 | +++ src/app/webbrowser/AddressBar.qml 2015-06-16 16:15:45 +0000 | |||
4 | @@ -37,6 +37,7 @@ | |||
5 | 37 | signal requestReload() | 37 | signal requestReload() |
6 | 38 | signal requestStop() | 38 | signal requestStop() |
7 | 39 | property string searchUrl | 39 | property string searchUrl |
8 | 40 | property bool canSimplifyText: true | ||
9 | 40 | 41 | ||
10 | 41 | property var securityStatus: null | 42 | property var securityStatus: null |
11 | 42 | 43 | ||
12 | @@ -48,6 +49,8 @@ | |||
13 | 48 | 49 | ||
14 | 49 | height: textField.height | 50 | height: textField.height |
15 | 50 | 51 | ||
16 | 52 | function selectAll() { textField.selectAll() } | ||
17 | 53 | |||
18 | 51 | TextField { | 54 | TextField { |
19 | 52 | id: textField | 55 | id: textField |
20 | 53 | objectName: "addressBarTextField" | 56 | objectName: "addressBarTextField" |
21 | @@ -84,7 +87,7 @@ | |||
22 | 84 | height: parent.height | 87 | height: parent.height |
23 | 85 | width: height | 88 | width: height |
24 | 86 | 89 | ||
26 | 87 | visible: addressbar.activeFocus || addressbar.loading || !addressbar.text | 90 | visible: addressbar.activeFocus || addressbar.loading || !addressbar.text || !canSimplifyText |
27 | 88 | 91 | ||
28 | 89 | enabled: addressbar.text | 92 | enabled: addressbar.text |
29 | 90 | opacity: enabled ? 1.0 : 0.3 | 93 | opacity: enabled ? 1.0 : 0.3 |
30 | @@ -229,7 +232,7 @@ | |||
31 | 229 | QtObject { | 232 | QtObject { |
32 | 230 | id: internal | 233 | id: internal |
33 | 231 | 234 | ||
35 | 232 | readonly property bool idle: !addressbar.loading && !addressbar.activeFocus | 235 | readonly property bool idle: !addressbar.loading && !addressbar.activeFocus && addressbar.canSimplifyText |
36 | 233 | 236 | ||
37 | 234 | readonly property int securityLevel: addressbar.securityStatus ? addressbar.securityStatus.securityLevel : Oxide.SecurityStatus.SecurityLevelNone | 237 | readonly property int securityLevel: addressbar.securityStatus ? addressbar.securityStatus.securityLevel : Oxide.SecurityStatus.SecurityLevelNone |
38 | 235 | readonly property bool secureConnection: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelSecure || securityLevel == Oxide.SecurityStatus.SecurityLevelSecureEV || securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false | 238 | readonly property bool secureConnection: addressbar.securityStatus ? (securityLevel == Oxide.SecurityStatus.SecurityLevelSecure || securityLevel == Oxide.SecurityStatus.SecurityLevelSecureEV || securityLevel == Oxide.SecurityStatus.SecurityLevelWarning) : false |
39 | @@ -289,16 +292,21 @@ | |||
40 | 289 | return url | 292 | return url |
41 | 290 | } | 293 | } |
42 | 291 | } | 294 | } |
43 | 292 | } | ||
44 | 293 | 295 | ||
50 | 294 | onActiveFocusChanged: { | 296 | function updateUrlFromFocus() { |
51 | 295 | if (activeFocus) { | 297 | if (canSimplifyText) { |
52 | 296 | text = actualUrl | 298 | if (addressbar.activeFocus) { |
53 | 297 | } else if (!loading && actualUrl.toString()) { | 299 | text = actualUrl |
54 | 298 | text = internal.simplifyUrl(actualUrl) | 300 | } else if (!loading && actualUrl.toString()) { |
55 | 301 | text = internal.simplifyUrl(actualUrl) | ||
56 | 302 | } | ||
57 | 303 | } | ||
58 | 299 | } | 304 | } |
59 | 300 | } | 305 | } |
60 | 301 | 306 | ||
61 | 307 | onActiveFocusChanged: internal.updateUrlFromFocus() | ||
62 | 308 | onCanSimplifyTextChanged: internal.updateUrlFromFocus() | ||
63 | 309 | |||
64 | 302 | onActualUrlChanged: { | 310 | onActualUrlChanged: { |
65 | 303 | if (!activeFocus || !actualUrl.toString()) { | 311 | if (!activeFocus || !actualUrl.toString()) { |
66 | 304 | text = internal.simplifyUrl(actualUrl) | 312 | text = internal.simplifyUrl(actualUrl) |
67 | 305 | 313 | ||
68 | === modified file 'src/app/webbrowser/Browser.qml' | |||
69 | --- src/app/webbrowser/Browser.qml 2015-06-02 14:26:38 +0000 | |||
70 | +++ src/app/webbrowser/Browser.qml 2015-06-16 16:15:45 +0000 | |||
71 | @@ -303,7 +303,10 @@ | |||
72 | 303 | text: i18n.tr("History") | 303 | text: i18n.tr("History") |
73 | 304 | iconName: "history" | 304 | iconName: "history" |
74 | 305 | enabled: browser.historyModel | 305 | enabled: browser.historyModel |
76 | 306 | onTriggered: historyViewComponent.createObject(historyViewContainer) | 306 | onTriggered: { |
77 | 307 | historyViewComponent.createObject(historyViewContainer) | ||
78 | 308 | historyViewContainer.focus = true | ||
79 | 309 | } | ||
80 | 307 | }, | 310 | }, |
81 | 308 | Action { | 311 | Action { |
82 | 309 | objectName: "tabs" | 312 | objectName: "tabs" |
83 | @@ -313,6 +316,7 @@ | |||
84 | 313 | onTriggered: { | 316 | onTriggered: { |
85 | 314 | recentView.state = "shown" | 317 | recentView.state = "shown" |
86 | 315 | recentToolbar.state = "shown" | 318 | recentToolbar.state = "shown" |
87 | 319 | recentView.focus = true | ||
88 | 316 | } | 320 | } |
89 | 317 | }, | 321 | }, |
90 | 318 | Action { | 322 | Action { |
91 | @@ -326,7 +330,10 @@ | |||
92 | 326 | objectName: "settings" | 330 | objectName: "settings" |
93 | 327 | text: i18n.tr("Settings") | 331 | text: i18n.tr("Settings") |
94 | 328 | iconName: "settings" | 332 | iconName: "settings" |
96 | 329 | onTriggered: settingsComponent.createObject(settingsContainer) | 333 | onTriggered: { |
97 | 334 | settingsComponent.createObject(settingsContainer) | ||
98 | 335 | settingsContainer.focus = true | ||
99 | 336 | } | ||
100 | 330 | }, | 337 | }, |
101 | 331 | Action { | 338 | Action { |
102 | 332 | objectName: "privatemode" | 339 | objectName: "privatemode" |
103 | @@ -346,6 +353,10 @@ | |||
104 | 346 | } | 353 | } |
105 | 347 | } | 354 | } |
106 | 348 | ] | 355 | ] |
107 | 356 | |||
108 | 357 | addressBarCanSimplifyText: !(activeFocus || suggestionsList.activeFocus) | ||
109 | 358 | Keys.onDownPressed: if (suggestionsList.count) suggestionsList.focus = true | ||
110 | 359 | Keys.onEscapePressed: internal.resetFocus() | ||
111 | 349 | } | 360 | } |
112 | 350 | 361 | ||
113 | 351 | ChromeController { | 362 | ChromeController { |
114 | @@ -358,7 +369,7 @@ | |||
115 | 358 | 369 | ||
116 | 359 | Suggestions { | 370 | Suggestions { |
117 | 360 | id: suggestionsList | 371 | id: suggestionsList |
119 | 361 | opacity: ((chrome.state == "shown") && chrome.activeFocus && (count > 0) && !chrome.drawerOpen) ? 1.0 : 0.0 | 372 | opacity: ((chrome.state == "shown") && (activeFocus || chrome.activeFocus) && count > 0 && !chrome.drawerOpen) ? 1.0 : 0.0 |
120 | 362 | Behavior on opacity { | 373 | Behavior on opacity { |
121 | 363 | UbuntuNumberAnimation {} | 374 | UbuntuNumberAnimation {} |
122 | 364 | } | 375 | } |
123 | @@ -372,6 +383,9 @@ | |||
124 | 372 | 383 | ||
125 | 373 | searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 }) | 384 | searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 }) |
126 | 374 | 385 | ||
127 | 386 | Keys.onUpPressed: chrome.focus = true | ||
128 | 387 | Keys.onEscapePressed: internal.resetFocus() | ||
129 | 388 | |||
130 | 375 | models: [historySuggestions, | 389 | models: [historySuggestions, |
131 | 376 | bookmarksSuggestions, | 390 | bookmarksSuggestions, |
132 | 377 | searchSuggestions.limit(4)] | 391 | searchSuggestions.limit(4)] |
133 | @@ -404,7 +418,7 @@ | |||
134 | 404 | id: searchSuggestions | 418 | id: searchSuggestions |
135 | 405 | terms: suggestionsList.searchTerms | 419 | terms: suggestionsList.searchTerms |
136 | 406 | searchEngine: currentSearchEngine | 420 | searchEngine: currentSearchEngine |
138 | 407 | active: chrome.activeFocus && | 421 | active: (chrome.activeFocus || suggestionsList.activeFocus) && |
139 | 408 | !browser.incognito && | 422 | !browser.incognito && |
140 | 409 | !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+")) | 423 | !UrlManagement.looksLikeAUrl(chrome.text.replace(/ /g, "+")) |
141 | 410 | 424 | ||
142 | @@ -416,7 +430,7 @@ | |||
143 | 416 | } | 430 | } |
144 | 417 | } | 431 | } |
145 | 418 | 432 | ||
147 | 419 | onSelected: { | 433 | onActivated: { |
148 | 420 | browser.currentWebview.url = url | 434 | browser.currentWebview.url = url |
149 | 421 | browser.currentWebview.forceActiveFocus() | 435 | browser.currentWebview.forceActiveFocus() |
150 | 422 | chrome.requestedUrl = url | 436 | chrome.requestedUrl = url |
151 | @@ -426,6 +440,7 @@ | |||
152 | 426 | 440 | ||
153 | 427 | FocusScope { | 441 | FocusScope { |
154 | 428 | id: recentView | 442 | id: recentView |
155 | 443 | objectName: "recentView" | ||
156 | 429 | 444 | ||
157 | 430 | anchors.fill: parent | 445 | anchors.fill: parent |
158 | 431 | visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown") | 446 | visible: bottomEdgeHandle.dragging || tabslist.animating || (state == "shown") |
159 | @@ -434,6 +449,13 @@ | |||
160 | 434 | name: "shown" | 449 | name: "shown" |
161 | 435 | } | 450 | } |
162 | 436 | 451 | ||
163 | 452 | function closeAndSwitchToTab(index) { | ||
164 | 453 | recentView.reset() | ||
165 | 454 | internal.switchToTab(index) | ||
166 | 455 | } | ||
167 | 456 | |||
168 | 457 | Keys.onEscapePressed: closeAndSwitchToTab(0) | ||
169 | 458 | |||
170 | 437 | TabsList { | 459 | TabsList { |
171 | 438 | id: tabslist | 460 | id: tabslist |
172 | 439 | anchors.fill: parent | 461 | anchors.fill: parent |
173 | @@ -453,15 +475,7 @@ | |||
174 | 453 | } | 475 | } |
175 | 454 | } | 476 | } |
176 | 455 | chromeOffset: chrome.height - invisibleTabChrome.height | 477 | chromeOffset: chrome.height - invisibleTabChrome.height |
186 | 456 | onTabSelected: { | 478 | onTabSelected: recentView.closeAndSwitchToTab(index) |
178 | 457 | var tab = tabsModel.get(index) | ||
179 | 458 | if (tab) { | ||
180 | 459 | tab.load() | ||
181 | 460 | tab.forceActiveFocus() | ||
182 | 461 | tabslist.model.setCurrent(index) | ||
183 | 462 | } | ||
184 | 463 | recentView.reset() | ||
185 | 464 | } | ||
187 | 465 | onTabClosed: { | 479 | onTabClosed: { |
188 | 466 | var tab = tabsModel.remove(index) | 480 | var tab = tabsModel.remove(index) |
189 | 467 | if (tab) { | 481 | if (tab) { |
190 | @@ -497,10 +511,7 @@ | |||
191 | 497 | 511 | ||
192 | 498 | text: i18n.tr("Done") | 512 | text: i18n.tr("Done") |
193 | 499 | 513 | ||
198 | 500 | onClicked: { | 514 | onClicked: recentView.closeAndSwitchToTab(0) |
195 | 501 | recentView.reset() | ||
196 | 502 | tabsModel.currentTab.load() | ||
197 | 503 | } | ||
199 | 504 | } | 515 | } |
200 | 505 | 516 | ||
201 | 506 | ToolbarAction { | 517 | ToolbarAction { |
202 | @@ -600,8 +611,9 @@ | |||
203 | 600 | } | 611 | } |
204 | 601 | } | 612 | } |
205 | 602 | 613 | ||
207 | 603 | Item { | 614 | FocusScope { |
208 | 604 | id: historyViewContainer | 615 | id: historyViewContainer |
209 | 616 | objectName: "historyView" | ||
210 | 605 | 617 | ||
211 | 606 | visible: children.length > 0 | 618 | visible: children.length > 0 |
212 | 607 | anchors.fill: parent | 619 | anchors.fill: parent |
213 | @@ -612,6 +624,12 @@ | |||
214 | 612 | HistoryView { | 624 | HistoryView { |
215 | 613 | anchors.fill: parent | 625 | anchors.fill: parent |
216 | 614 | visible: historyViewContainer.children.length == 1 | 626 | visible: historyViewContainer.children.length == 1 |
217 | 627 | focus: true | ||
218 | 628 | |||
219 | 629 | Keys.onEscapePressed: { | ||
220 | 630 | destroy() | ||
221 | 631 | internal.resetFocus() | ||
222 | 632 | } | ||
223 | 615 | 633 | ||
224 | 616 | Timer { | 634 | Timer { |
225 | 617 | // Set the model asynchronously to ensure | 635 | // Set the model asynchronously to ensure |
226 | @@ -650,7 +668,7 @@ | |||
227 | 650 | } | 668 | } |
228 | 651 | } | 669 | } |
229 | 652 | 670 | ||
231 | 653 | Item { | 671 | FocusScope { |
232 | 654 | id: settingsContainer | 672 | id: settingsContainer |
233 | 655 | 673 | ||
234 | 656 | visible: children.length > 0 | 674 | visible: children.length > 0 |
235 | @@ -661,9 +679,14 @@ | |||
236 | 661 | 679 | ||
237 | 662 | SettingsPage { | 680 | SettingsPage { |
238 | 663 | anchors.fill: parent | 681 | anchors.fill: parent |
239 | 682 | focus: true | ||
240 | 664 | historyModel: browser.historyModel | 683 | historyModel: browser.historyModel |
241 | 665 | settingsObject: settings | 684 | settingsObject: settings |
242 | 666 | onDone: destroy() | 685 | onDone: destroy() |
243 | 686 | Keys.onEscapePressed: { | ||
244 | 687 | destroy() | ||
245 | 688 | internal.resetFocus() | ||
246 | 689 | } | ||
247 | 667 | } | 690 | } |
248 | 668 | } | 691 | } |
249 | 669 | } | 692 | } |
250 | @@ -920,9 +943,33 @@ | |||
251 | 920 | } | 943 | } |
252 | 921 | } | 944 | } |
253 | 922 | 945 | ||
255 | 923 | function focusAddressBar() { | 946 | function switchToTab(index) { |
256 | 947 | var tab = tabsModel.get(index) | ||
257 | 948 | if (tab) { | ||
258 | 949 | tab.load() | ||
259 | 950 | tabslist.model.setCurrent(index) | ||
260 | 951 | if (tab.initialUrl == "" && formFactor == "desktop") focusAddressBar() | ||
261 | 952 | else tab.forceActiveFocus() | ||
262 | 953 | } | ||
263 | 954 | } | ||
264 | 955 | |||
265 | 956 | function closeCurrentTab() { | ||
266 | 957 | if (tabsModel.count > 0) { | ||
267 | 958 | var tab = tabsModel.remove(0) | ||
268 | 959 | if (tab) tab.close() | ||
269 | 960 | |||
270 | 961 | if (tabsModel.count === 0) { | ||
271 | 962 | browser.openUrlInNewTab("", true) | ||
272 | 963 | } else { | ||
273 | 964 | internal.switchToTab(0) | ||
274 | 965 | } | ||
275 | 966 | } | ||
276 | 967 | } | ||
277 | 968 | |||
278 | 969 | function focusAddressBar(selectContent) { | ||
279 | 924 | chrome.forceActiveFocus() | 970 | chrome.forceActiveFocus() |
280 | 925 | Qt.inputMethod.show() // work around http://pad.lv/1316057 | 971 | Qt.inputMethod.show() // work around http://pad.lv/1316057 |
281 | 972 | if (selectContent) chrome.addressBarSelectAll() | ||
282 | 926 | } | 973 | } |
283 | 927 | 974 | ||
284 | 928 | function resetFocus() { | 975 | function resetFocus() { |
285 | @@ -959,6 +1006,20 @@ | |||
286 | 959 | } | 1006 | } |
287 | 960 | return false | 1007 | return false |
288 | 961 | } | 1008 | } |
289 | 1009 | |||
290 | 1010 | function historyGoBack() { | ||
291 | 1011 | if (currentWebview && currentWebview.canGoBack) { | ||
292 | 1012 | internal.resetFocus() | ||
293 | 1013 | currentWebview.goBack() | ||
294 | 1014 | } | ||
295 | 1015 | } | ||
296 | 1016 | |||
297 | 1017 | function historyGoForward() { | ||
298 | 1018 | if (currentWebview && currentWebview.canGoForward) { | ||
299 | 1019 | internal.resetFocus() | ||
300 | 1020 | currentWebview.goForward() | ||
301 | 1021 | } | ||
302 | 1022 | } | ||
303 | 962 | } | 1023 | } |
304 | 963 | 1024 | ||
305 | 964 | function openUrlInNewTab(url, setCurrent, load) { | 1025 | function openUrlInNewTab(url, setCurrent, load) { |
306 | @@ -1128,4 +1189,135 @@ | |||
307 | 1128 | } | 1189 | } |
308 | 1129 | } | 1190 | } |
309 | 1130 | } | 1191 | } |
310 | 1192 | |||
311 | 1193 | Keys.onPressed: if (shortcuts.processKey(event.key, event.modifiers)) event.accepted = true | ||
312 | 1194 | KeyboardShortcuts { | ||
313 | 1195 | id: shortcuts | ||
314 | 1196 | |||
315 | 1197 | // Ctrl + Tab: pull the tab from the bottom of the stack to the | ||
316 | 1198 | // top (i.e. make it current) | ||
317 | 1199 | KeyboardShortcut { | ||
318 | 1200 | modifiers: Qt.ControlModifier | ||
319 | 1201 | key: Qt.Key_Tab | ||
320 | 1202 | enabled: chrome.visible || recentView.visible | ||
321 | 1203 | onTriggered: { | ||
322 | 1204 | internal.switchToTab(tabsModel.count - 1) | ||
323 | 1205 | if (chrome.visible) recentView.reset() | ||
324 | 1206 | else if (recentView.visible) recentView.focus = true | ||
325 | 1207 | } | ||
326 | 1208 | } | ||
327 | 1209 | |||
328 | 1210 | // Ctrl + w or Ctrl+F4: Close the current tab | ||
329 | 1211 | KeyboardShortcut { | ||
330 | 1212 | modifiers: Qt.ControlModifier | ||
331 | 1213 | key: Qt.Key_W | ||
332 | 1214 | enabled: chrome.visible || recentView.visible | ||
333 | 1215 | onTriggered: internal.closeCurrentTab() | ||
334 | 1216 | } | ||
335 | 1217 | KeyboardShortcut { | ||
336 | 1218 | modifiers: Qt.ControlModifier | ||
337 | 1219 | key: Qt.Key_F4 | ||
338 | 1220 | enabled: chrome.visible || recentView.visible | ||
339 | 1221 | onTriggered: internal.closeCurrentTab() | ||
340 | 1222 | } | ||
341 | 1223 | |||
342 | 1224 | // Ctrl + t: Open a new Tab | ||
343 | 1225 | KeyboardShortcut { | ||
344 | 1226 | modifiers: Qt.ControlModifier | ||
345 | 1227 | key: Qt.Key_T | ||
346 | 1228 | enabled: chrome.visible || recentView.visible | ||
347 | 1229 | onTriggered: { | ||
348 | 1230 | openUrlInNewTab("", true) | ||
349 | 1231 | if (recentView.visible) recentView.reset() | ||
350 | 1232 | } | ||
351 | 1233 | } | ||
352 | 1234 | |||
353 | 1235 | // F6 or Ctrl + L or Alt + D: Select the content in the address bar | ||
354 | 1236 | KeyboardShortcut { | ||
355 | 1237 | modifiers: Qt.ControlModifier | ||
356 | 1238 | key: Qt.Key_L | ||
357 | 1239 | enabled: chrome.visible | ||
358 | 1240 | onTriggered: internal.focusAddressBar(true) | ||
359 | 1241 | } | ||
360 | 1242 | KeyboardShortcut { | ||
361 | 1243 | modifiers: Qt.AltModifier | ||
362 | 1244 | key: Qt.Key_D | ||
363 | 1245 | enabled: chrome.visible | ||
364 | 1246 | onTriggered: internal.focusAddressBar(true) | ||
365 | 1247 | } | ||
366 | 1248 | KeyboardShortcut { | ||
367 | 1249 | key: Qt.Key_F6 | ||
368 | 1250 | enabled: chrome.visible | ||
369 | 1251 | onTriggered: internal.focusAddressBar(true) | ||
370 | 1252 | } | ||
371 | 1253 | |||
372 | 1254 | // Ctrl + D: Toggle bookmarked state on current Tab | ||
373 | 1255 | KeyboardShortcut { | ||
374 | 1256 | modifiers: Qt.ControlModifier | ||
375 | 1257 | key: Qt.Key_D | ||
376 | 1258 | enabled: chrome.visible | ||
377 | 1259 | onTriggered: { | ||
378 | 1260 | if (currentWebview) { | ||
379 | 1261 | if (bookmarksModel.contains(currentWebview.url)) { | ||
380 | 1262 | bookmarksModel.remove(currentWebview.url) | ||
381 | 1263 | } else { | ||
382 | 1264 | bookmarksModel.add(currentWebview.url, currentWebview.title, currentWebview.icon) | ||
383 | 1265 | } | ||
384 | 1266 | } | ||
385 | 1267 | } | ||
386 | 1268 | } | ||
387 | 1269 | |||
388 | 1270 | // Ctrl + H: Show History | ||
389 | 1271 | KeyboardShortcut { | ||
390 | 1272 | modifiers: Qt.ControlModifier | ||
391 | 1273 | key: Qt.Key_H | ||
392 | 1274 | enabled: chrome.visible | ||
393 | 1275 | onTriggered: { | ||
394 | 1276 | if (historyViewContainer.children.length === 0) { | ||
395 | 1277 | historyViewComponent.createObject(historyViewContainer) | ||
396 | 1278 | historyViewContainer.focus = true | ||
397 | 1279 | } | ||
398 | 1280 | } | ||
399 | 1281 | } | ||
400 | 1282 | |||
401 | 1283 | // Alt + Left Arrow or Backspace: Goes to the previous page in history | ||
402 | 1284 | KeyboardShortcut { | ||
403 | 1285 | modifiers: Qt.AltModifier | ||
404 | 1286 | key: Qt.Key_Left | ||
405 | 1287 | enabled: chrome.visible | ||
406 | 1288 | onTriggered: internal.historyGoBack() | ||
407 | 1289 | } | ||
408 | 1290 | KeyboardShortcut { | ||
409 | 1291 | key: Qt.Key_Backspace | ||
410 | 1292 | enabled: chrome.visible | ||
411 | 1293 | onTriggered: internal.historyGoBack() | ||
412 | 1294 | } | ||
413 | 1295 | |||
414 | 1296 | // Alt + Right Arrow or Shift + Backspace: Goes to the next page in history | ||
415 | 1297 | KeyboardShortcut { | ||
416 | 1298 | modifiers: Qt.AltModifier | ||
417 | 1299 | key: Qt.Key_Right | ||
418 | 1300 | enabled: chrome.visible | ||
419 | 1301 | onTriggered: internal.historyGoForward() | ||
420 | 1302 | } | ||
421 | 1303 | KeyboardShortcut { | ||
422 | 1304 | modifiers: Qt.ShiftModifier | ||
423 | 1305 | key: Qt.Key_Backspace | ||
424 | 1306 | enabled: chrome.visible | ||
425 | 1307 | onTriggered: internal.historyGoForward() | ||
426 | 1308 | } | ||
427 | 1309 | |||
428 | 1310 | // F5 or Ctrl + R: Reload current Tab | ||
429 | 1311 | KeyboardShortcut { | ||
430 | 1312 | key: Qt.Key_F5 | ||
431 | 1313 | enabled: chrome.visible | ||
432 | 1314 | onTriggered: if (currentWebview) currentWebview.reload() | ||
433 | 1315 | } | ||
434 | 1316 | KeyboardShortcut { | ||
435 | 1317 | modifiers: Qt.ControlModifier | ||
436 | 1318 | key: Qt.Key_R | ||
437 | 1319 | enabled: chrome.visible | ||
438 | 1320 | onTriggered: if (currentWebview) currentWebview.reload() | ||
439 | 1321 | } | ||
440 | 1322 | } | ||
441 | 1131 | } | 1323 | } |
442 | 1132 | 1324 | ||
443 | === modified file 'src/app/webbrowser/Chrome.qml' | |||
444 | --- src/app/webbrowser/Chrome.qml 2015-05-26 21:42:54 +0000 | |||
445 | +++ src/app/webbrowser/Chrome.qml 2015-06-16 16:15:45 +0000 | |||
446 | @@ -29,10 +29,13 @@ | |||
447 | 29 | property list<Action> drawerActions | 29 | property list<Action> drawerActions |
448 | 30 | readonly property bool drawerOpen: internal.openDrawer | 30 | readonly property bool drawerOpen: internal.openDrawer |
449 | 31 | property alias requestedUrl: addressbar.requestedUrl | 31 | property alias requestedUrl: addressbar.requestedUrl |
450 | 32 | property alias addressBarCanSimplifyText: addressbar.canSimplifyText | ||
451 | 32 | property alias incognito: addressbar.incognito | 33 | property alias incognito: addressbar.incognito |
452 | 33 | 34 | ||
453 | 34 | backgroundColor: incognito ? UbuntuColors.darkGrey : Theme.palette.normal.background | 35 | backgroundColor: incognito ? UbuntuColors.darkGrey : Theme.palette.normal.background |
454 | 35 | 36 | ||
455 | 37 | function addressBarSelectAll() { addressbar.selectAll() } | ||
456 | 38 | |||
457 | 36 | FocusScope { | 39 | FocusScope { |
458 | 37 | anchors { | 40 | anchors { |
459 | 38 | fill: parent | 41 | fill: parent |
460 | 39 | 42 | ||
461 | === added file 'src/app/webbrowser/KeyboardShortcut.qml' | |||
462 | --- src/app/webbrowser/KeyboardShortcut.qml 1970-01-01 00:00:00 +0000 | |||
463 | +++ src/app/webbrowser/KeyboardShortcut.qml 2015-06-16 16:15:45 +0000 | |||
464 | @@ -0,0 +1,25 @@ | |||
465 | 1 | /* | ||
466 | 2 | * Copyright 2015 Canonical Ltd. | ||
467 | 3 | * | ||
468 | 4 | * This file is part of webbrowser-app. | ||
469 | 5 | * | ||
470 | 6 | * webbrowser-app is free software; you can redistribute it and/or modify | ||
471 | 7 | * it under the terms of the GNU General Public License as published by | ||
472 | 8 | * the Free Software Foundation; version 3. | ||
473 | 9 | * | ||
474 | 10 | * webbrowser-app is distributed in the hope that it will be useful, | ||
475 | 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
476 | 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
477 | 13 | * GNU General Public License for more details. | ||
478 | 14 | * | ||
479 | 15 | * You should have received a copy of the GNU General Public License | ||
480 | 16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
481 | 17 | */ | ||
482 | 18 | |||
483 | 19 | import QtQuick 2.0 | ||
484 | 20 | import Ubuntu.Components 1.1 | ||
485 | 21 | |||
486 | 22 | Action { | ||
487 | 23 | property int key | ||
488 | 24 | property int modifiers: Qt.NoModifier | ||
489 | 25 | } | ||
490 | 0 | 26 | ||
491 | === added file 'src/app/webbrowser/KeyboardShortcuts.qml' | |||
492 | --- src/app/webbrowser/KeyboardShortcuts.qml 1970-01-01 00:00:00 +0000 | |||
493 | +++ src/app/webbrowser/KeyboardShortcuts.qml 2015-06-16 16:15:45 +0000 | |||
494 | @@ -0,0 +1,41 @@ | |||
495 | 1 | /* | ||
496 | 2 | * Copyright 2015 Canonical Ltd. | ||
497 | 3 | * | ||
498 | 4 | * This file is part of webbrowser-app. | ||
499 | 5 | * | ||
500 | 6 | * webbrowser-app is free software; you can redistribute it and/or modify | ||
501 | 7 | * it under the terms of the GNU General Public License as published by | ||
502 | 8 | * the Free Software Foundation; version 3. | ||
503 | 9 | * | ||
504 | 10 | * webbrowser-app is distributed in the hope that it will be useful, | ||
505 | 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
506 | 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
507 | 13 | * GNU General Public License for more details. | ||
508 | 14 | * | ||
509 | 15 | * You should have received a copy of the GNU General Public License | ||
510 | 16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
511 | 17 | */ | ||
512 | 18 | |||
513 | 19 | import QtQuick 2.0 | ||
514 | 20 | |||
515 | 21 | Item { | ||
516 | 22 | function processKey(key, modifiers) { | ||
517 | 23 | for (var i = 0; i < data.length; i++) { | ||
518 | 24 | var shortcut = data[i]; | ||
519 | 25 | |||
520 | 26 | if (!shortcut.enabled) continue | ||
521 | 27 | if (key !== shortcut.key) continue | ||
522 | 28 | |||
523 | 29 | if (shortcut.modifiers === Qt.NoModifier) { | ||
524 | 30 | if (modifiers === Qt.NoModifier) { | ||
525 | 31 | shortcut.trigger() | ||
526 | 32 | return true | ||
527 | 33 | } | ||
528 | 34 | } else if ((modifiers & shortcut.modifiers) === shortcut.modifiers) { | ||
529 | 35 | shortcut.trigger() | ||
530 | 36 | return true | ||
531 | 37 | } | ||
532 | 38 | } | ||
533 | 39 | return false | ||
534 | 40 | } | ||
535 | 41 | } | ||
536 | 0 | 42 | ||
537 | === modified file 'src/app/webbrowser/Suggestion.qml' | |||
538 | --- src/app/webbrowser/Suggestion.qml 2015-04-22 11:00:02 +0000 | |||
539 | +++ src/app/webbrowser/Suggestion.qml 2015-06-16 16:15:45 +0000 | |||
540 | @@ -28,7 +28,7 @@ | |||
541 | 28 | property alias icon: icon.name | 28 | property alias icon: icon.name |
542 | 29 | property url url | 29 | property url url |
543 | 30 | 30 | ||
545 | 31 | signal selected(url url) | 31 | signal activated(url url) |
546 | 32 | 32 | ||
547 | 33 | __height: Math.max(middleVisuals.height, units.gu(6)) | 33 | __height: Math.max(middleVisuals.height, units.gu(6)) |
548 | 34 | // disable focus handling | 34 | // disable focus handling |
549 | @@ -77,8 +77,9 @@ | |||
550 | 77 | fontSize: "small" | 77 | fontSize: "small" |
551 | 78 | elide: Text.ElideRight | 78 | elide: Text.ElideRight |
552 | 79 | visible: text !== "" | 79 | visible: text !== "" |
553 | 80 | color: selected ? "#DB4923" : "black" | ||
554 | 80 | } | 81 | } |
555 | 81 | } | 82 | } |
556 | 82 | 83 | ||
558 | 83 | onClicked: selected(url) | 84 | onClicked: activated(url) |
559 | 84 | } | 85 | } |
560 | 85 | 86 | ||
561 | === modified file 'src/app/webbrowser/Suggestions.qml' | |||
562 | --- src/app/webbrowser/Suggestions.qml 2015-05-12 09:56:40 +0000 | |||
563 | +++ src/app/webbrowser/Suggestions.qml 2015-06-16 16:15:45 +0000 | |||
564 | @@ -19,20 +19,24 @@ | |||
565 | 19 | import QtQuick 2.0 | 19 | import QtQuick 2.0 |
566 | 20 | import Ubuntu.Components 1.1 | 20 | import Ubuntu.Components 1.1 |
567 | 21 | 21 | ||
569 | 22 | Rectangle { | 22 | FocusScope { |
570 | 23 | id: suggestions | 23 | id: suggestions |
571 | 24 | 24 | ||
572 | 25 | property var searchTerms | 25 | property var searchTerms |
573 | 26 | property var models | 26 | property var models |
574 | 27 | |||
575 | 27 | readonly property int count: models.reduce(countItems, 0) | 28 | readonly property int count: models.reduce(countItems, 0) |
576 | 28 | readonly property alias contentHeight: suggestionsList.contentHeight | 29 | readonly property alias contentHeight: suggestionsList.contentHeight |
577 | 29 | 30 | ||
579 | 30 | signal selected(url url) | 31 | signal activated(url url) |
580 | 31 | 32 | ||
585 | 32 | radius: units.gu(0.5) | 33 | Rectangle { |
586 | 33 | border { | 34 | anchors.fill: parent |
587 | 34 | color: "#dedede" | 35 | radius: units.gu(0.5) |
588 | 35 | width: 1 | 36 | border { |
589 | 37 | color: "#dedede" | ||
590 | 38 | width: 1 | ||
591 | 39 | } | ||
592 | 36 | } | 40 | } |
593 | 37 | 41 | ||
594 | 38 | clip: true | 42 | clip: true |
595 | @@ -40,39 +44,37 @@ | |||
596 | 40 | ListView { | 44 | ListView { |
597 | 41 | id: suggestionsList | 45 | id: suggestionsList |
598 | 42 | anchors.fill: parent | 46 | anchors.fill: parent |
603 | 43 | 47 | focus: true | |
604 | 44 | model: suggestions.models | 48 | |
605 | 45 | delegate: Column { | 49 | model: models.reduce(function(list, model) { |
606 | 46 | id: suggestionsSection | 50 | var modelItems = []; |
607 | 51 | |||
608 | 52 | // Models inheriting from QAbstractItemModel and JS arrays expose their | ||
609 | 53 | // data differently, so we need to collect their items differently | ||
610 | 54 | if (model.forEach) { | ||
611 | 55 | model.forEach(function(item) { modelItems.push(item) }) | ||
612 | 56 | } else { | ||
613 | 57 | for (var i = 0; i < model.count; i++) modelItems.push(model.get(i)) | ||
614 | 58 | } | ||
615 | 59 | |||
616 | 60 | modelItems.forEach(function(item) { | ||
617 | 61 | item["icon"] = model.icon | ||
618 | 62 | item["displayUrl"] = model.displayUrl | ||
619 | 63 | list.push(item); | ||
620 | 64 | }) | ||
621 | 65 | return list; | ||
622 | 66 | }, []) | ||
623 | 67 | |||
624 | 68 | delegate: Suggestion { | ||
625 | 47 | width: suggestionsList.width | 69 | width: suggestionsList.width |
654 | 48 | height: childrenRect.height | 70 | showDivider: index < model.length - 1 |
655 | 49 | 71 | ||
656 | 50 | property string icon: models[index].icon | 72 | title: highlightTerms(modelData.title) |
657 | 51 | property bool displayUrl: models[index].displayUrl | 73 | subtitle: modelData.displayUrl ? highlightTerms(modelData.url) : "" |
658 | 52 | property int firstItemIndex: models.slice(0, index).reduce(countItems, 0) | 74 | icon: modelData.icon |
659 | 53 | 75 | selected: suggestionsList.activeFocus && ListView.isCurrentItem | |
660 | 54 | Repeater { | 76 | |
661 | 55 | id: suggestionsSource | 77 | onActivated: suggestions.activated(modelData.url) |
634 | 56 | model: modelData | ||
635 | 57 | |||
636 | 58 | delegate: Suggestion { | ||
637 | 59 | id: suggestion | ||
638 | 60 | width: suggestionsList.width | ||
639 | 61 | showDivider: suggestionsSection.firstItemIndex + index < | ||
640 | 62 | suggestions.count - 1 | ||
641 | 63 | |||
642 | 64 | // Necessary to support both using objects inheriting from | ||
643 | 65 | // QAbstractItemModel and JS arrays as models, since they | ||
644 | 66 | // expose their data differently | ||
645 | 67 | property var item: (model.modelData) ? model.modelData : model | ||
646 | 68 | |||
647 | 69 | title: highlightTerms(item.title) | ||
648 | 70 | subtitle: suggestionsSection.displayUrl ? highlightTerms(item.url) : "" | ||
649 | 71 | icon: suggestionsSection.icon | ||
650 | 72 | |||
651 | 73 | onSelected: suggestions.selected(item.url) | ||
652 | 74 | } | ||
653 | 75 | } | ||
662 | 76 | } | 78 | } |
663 | 77 | } | 79 | } |
664 | 78 | 80 | ||
665 | 79 | 81 | ||
666 | === modified file 'src/app/webbrowser/limit-proxy-model.cpp' | |||
667 | --- src/app/webbrowser/limit-proxy-model.cpp 2014-07-04 12:30:09 +0000 | |||
668 | +++ src/app/webbrowser/limit-proxy-model.cpp 2015-06-16 16:15:45 +0000 | |||
669 | @@ -266,3 +266,19 @@ | |||
670 | 266 | m_dataChangedEnd = -1; | 266 | m_dataChangedEnd = -1; |
671 | 267 | } | 267 | } |
672 | 268 | } | 268 | } |
673 | 269 | |||
674 | 270 | QVariantMap LimitProxyModel::get(int i) const | ||
675 | 271 | { | ||
676 | 272 | QVariantMap item; | ||
677 | 273 | QHash<int, QByteArray> roles = roleNames(); | ||
678 | 274 | |||
679 | 275 | QModelIndex modelIndex = index(i, 0); | ||
680 | 276 | if (modelIndex.isValid()) { | ||
681 | 277 | Q_FOREACH(int role, roles.keys()) { | ||
682 | 278 | QString roleName = QString::fromUtf8(roles.value(role)); | ||
683 | 279 | item.insert(roleName, data(modelIndex, role)); | ||
684 | 280 | } | ||
685 | 281 | } | ||
686 | 282 | return item; | ||
687 | 283 | } | ||
688 | 284 | |||
689 | 269 | 285 | ||
690 | === modified file 'src/app/webbrowser/limit-proxy-model.h' | |||
691 | --- src/app/webbrowser/limit-proxy-model.h 2014-06-27 20:29:52 +0000 | |||
692 | +++ src/app/webbrowser/limit-proxy-model.h 2015-06-16 16:15:45 +0000 | |||
693 | @@ -45,6 +45,8 @@ | |||
694 | 45 | int rowCount(const QModelIndex &parent = QModelIndex()) const; | 45 | int rowCount(const QModelIndex &parent = QModelIndex()) const; |
695 | 46 | int unlimitedRowCount(const QModelIndex &parent = QModelIndex()) const; | 46 | int unlimitedRowCount(const QModelIndex &parent = QModelIndex()) const; |
696 | 47 | 47 | ||
697 | 48 | Q_INVOKABLE QVariantMap get(int index) const; | ||
698 | 49 | |||
699 | 48 | Q_SIGNALS: | 50 | Q_SIGNALS: |
700 | 49 | void sourceModelChanged() const; | 51 | void sourceModelChanged() const; |
701 | 50 | void limitChanged() const; | 52 | void limitChanged() const; |
702 | 51 | 53 | ||
703 | === modified file 'tests/autopilot/webbrowser_app/emulators/browser.py' | |||
704 | --- tests/autopilot/webbrowser_app/emulators/browser.py 2015-05-28 13:56:52 +0000 | |||
705 | +++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-06-16 16:15:45 +0000 | |||
706 | @@ -19,7 +19,7 @@ | |||
707 | 19 | import autopilot.logging | 19 | import autopilot.logging |
708 | 20 | import ubuntuuitoolkit as uitk | 20 | import ubuntuuitoolkit as uitk |
709 | 21 | from autopilot import exceptions | 21 | from autopilot import exceptions |
711 | 22 | 22 | from autopilot import input | |
712 | 23 | 23 | ||
713 | 24 | logger = logging.getLogger(__name__) | 24 | logger = logging.getLogger(__name__) |
714 | 25 | 25 | ||
715 | @@ -30,6 +30,7 @@ | |||
716 | 30 | super().__init__(*args) | 30 | super().__init__(*args) |
717 | 31 | self.chrome = self._get_chrome() | 31 | self.chrome = self._get_chrome() |
718 | 32 | self.address_bar = self.chrome.address_bar | 32 | self.address_bar = self.chrome.address_bar |
719 | 33 | self.keyboard = input.Keyboard.create() | ||
720 | 33 | 34 | ||
721 | 34 | def _get_chrome(self): | 35 | def _get_chrome(self): |
722 | 35 | return self.select_single(Chrome) | 36 | return self.select_single(Chrome) |
723 | @@ -150,6 +151,17 @@ | |||
724 | 150 | def get_bottom_edge_hint(self): | 151 | def get_bottom_edge_hint(self): |
725 | 151 | return self.select_single("QQuickImage", objectName="bottomEdgeHint") | 152 | return self.select_single("QQuickImage", objectName="bottomEdgeHint") |
726 | 152 | 153 | ||
727 | 154 | # The history view is dynamically created, so it might or might not be | ||
728 | 155 | # available | ||
729 | 156 | def get_history_view(self): | ||
730 | 157 | try: | ||
731 | 158 | return self.select_single("HistoryView") | ||
732 | 159 | except exceptions.StateNotFoundError: | ||
733 | 160 | return None | ||
734 | 161 | |||
735 | 162 | def press_key(self, key): | ||
736 | 163 | self.keyboard.press_and_release(key) | ||
737 | 164 | |||
738 | 153 | 165 | ||
739 | 154 | class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase): | 166 | class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
740 | 155 | 167 | ||
741 | @@ -224,11 +236,14 @@ | |||
742 | 224 | @autopilot.logging.log_action(logger.info) | 236 | @autopilot.logging.log_action(logger.info) |
743 | 225 | def go_to_url(self, url): | 237 | def go_to_url(self, url): |
744 | 226 | self.write(url) | 238 | self.write(url) |
746 | 227 | self.text_field.keyboard.press_and_release('Enter') | 239 | self.press_key('Enter') |
747 | 228 | 240 | ||
748 | 229 | def write(self, text, clear=True): | 241 | def write(self, text, clear=True): |
749 | 230 | self.text_field.write(text, clear) | 242 | self.text_field.write(text, clear) |
750 | 231 | 243 | ||
751 | 244 | def press_key(self, key): | ||
752 | 245 | self.text_field.keyboard.press_and_release(key) | ||
753 | 246 | |||
754 | 232 | @autopilot.logging.log_action(logger.info) | 247 | @autopilot.logging.log_action(logger.info) |
755 | 233 | def click_action_button(self): | 248 | def click_action_button(self): |
756 | 234 | button = self.select_single("QQuickMouseArea", | 249 | button = self.select_single("QQuickMouseArea", |
757 | 235 | 250 | ||
758 | === modified file 'tests/autopilot/webbrowser_app/tests/__init__.py' | |||
759 | --- tests/autopilot/webbrowser_app/tests/__init__.py 2015-05-25 19:17:25 +0000 | |||
760 | +++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-06-16 16:15:45 +0000 | |||
761 | @@ -169,6 +169,14 @@ | |||
762 | 169 | self.pointing_device.click_object(settings_action) | 169 | self.pointing_device.click_object(settings_action) |
763 | 170 | return self.main_window.get_settings_page() | 170 | return self.main_window.get_settings_page() |
764 | 171 | 171 | ||
765 | 172 | def open_history(self): | ||
766 | 173 | chrome = self.main_window.chrome | ||
767 | 174 | drawer_button = chrome.get_drawer_button() | ||
768 | 175 | self.pointing_device.click_object(drawer_button) | ||
769 | 176 | chrome.get_drawer() | ||
770 | 177 | settings_action = chrome.get_drawer_action("history") | ||
771 | 178 | self.pointing_device.click_object(settings_action) | ||
772 | 179 | |||
773 | 172 | def assert_number_webviews_eventually(self, count): | 180 | def assert_number_webviews_eventually(self, count): |
774 | 173 | self.assertThat(lambda: len(self.main_window.get_webviews()), | 181 | self.assertThat(lambda: len(self.main_window.get_webviews()), |
775 | 174 | Eventually(Equals(count))) | 182 | Eventually(Equals(count))) |
776 | @@ -195,7 +203,7 @@ | |||
777 | 195 | are executed, thus making them more robust. | 203 | are executed, thus making them more robust. |
778 | 196 | """ | 204 | """ |
779 | 197 | 205 | ||
781 | 198 | def setUp(self): | 206 | def setUp(self, path="/test1"): |
782 | 199 | self.http_server = http_server.HTTPServerInAThread() | 207 | self.http_server = http_server.HTTPServerInAThread() |
783 | 200 | self.ping_server(self.http_server) | 208 | self.ping_server(self.http_server) |
784 | 201 | self.addCleanup(self.http_server.cleanup) | 209 | self.addCleanup(self.http_server.cleanup) |
785 | @@ -203,7 +211,7 @@ | |||
786 | 203 | 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES', | 211 | 'UBUNTU_WEBVIEW_HOST_MAPPING_RULES', |
787 | 204 | "MAP test:80 localhost:{}".format(self.http_server.port))) | 212 | "MAP test:80 localhost:{}".format(self.http_server.port))) |
788 | 205 | self.base_url = "http://test" | 213 | self.base_url = "http://test" |
790 | 206 | self.url = self.base_url + "/test1" | 214 | self.url = self.base_url + path |
791 | 207 | self.ARGS = self.ARGS + [self.url] | 215 | self.ARGS = self.ARGS + [self.url] |
792 | 208 | super(StartOpenRemotePageTestCaseBase, self).setUp() | 216 | super(StartOpenRemotePageTestCaseBase, self).setUp() |
793 | 209 | self.assert_home_page_eventually_loaded() | 217 | self.assert_home_page_eventually_loaded() |
794 | 210 | 218 | ||
795 | === modified file 'tests/autopilot/webbrowser_app/tests/http_server.py' | |||
796 | --- tests/autopilot/webbrowser_app/tests/http_server.py 2015-04-23 15:34:16 +0000 | |||
797 | +++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-06-16 16:15:45 +0000 | |||
798 | @@ -140,6 +140,10 @@ | |||
799 | 140 | if query in self.suggestions_data: | 140 | if query in self.suggestions_data: |
800 | 141 | suggestions = self.suggestions_data[query] | 141 | suggestions = self.suggestions_data[query] |
801 | 142 | self.wfile.write(json.dumps(suggestions).encode()) | 142 | self.wfile.write(json.dumps(suggestions).encode()) |
802 | 143 | elif self.path.startswith("/tab/"): | ||
803 | 144 | self.send_response(200) | ||
804 | 145 | name = self.path[len("/tab/"):] | ||
805 | 146 | self.send_html('<html><body>' + name + '</body></html>') | ||
806 | 143 | else: | 147 | else: |
807 | 144 | self.send_error(404) | 148 | self.send_error(404) |
808 | 145 | 149 | ||
809 | 146 | 150 | ||
810 | === modified file 'tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py' | |||
811 | --- tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-04-27 03:23:00 +0000 | |||
812 | +++ tests/autopilot/webbrowser_app/tests/test_addressbar_bookmark.py 2015-06-16 16:15:45 +0000 | |||
813 | @@ -60,12 +60,10 @@ | |||
814 | 60 | self.pointing_device.click_object(webview) | 60 | self.pointing_device.click_object(webview) |
815 | 61 | address_bar = self.main_window.address_bar | 61 | address_bar = self.main_window.address_bar |
816 | 62 | bookmark_toggle = address_bar.get_bookmark_toggle() | 62 | bookmark_toggle = address_bar.get_bookmark_toggle() |
817 | 63 | self.assertThat(address_bar.activeFocus, Eventually(Equals(False))) | ||
818 | 64 | self.assertThat(bookmark_toggle.visible, Eventually(Equals(True))) | 63 | self.assertThat(bookmark_toggle.visible, Eventually(Equals(True))) |
819 | 65 | 64 | ||
820 | 66 | self.open_tabs_view() | 65 | self.open_tabs_view() |
821 | 67 | tabs_view = self.main_window.get_tabs_view() | 66 | tabs_view = self.main_window.get_tabs_view() |
822 | 68 | self.main_window.get_tabs_view().get_previews()[1].select() | 67 | self.main_window.get_tabs_view().get_previews()[1].select() |
823 | 69 | tabs_view.visible.wait_for(False) | 68 | tabs_view.visible.wait_for(False) |
824 | 70 | self.assertThat(address_bar.activeFocus, Equals(False)) | ||
825 | 71 | self.assertThat(bookmark_toggle.visible, Eventually(Equals(False))) | 69 | self.assertThat(bookmark_toggle.visible, Eventually(Equals(False))) |
826 | 72 | 70 | ||
827 | === added file 'tests/autopilot/webbrowser_app/tests/test_keyboard.py' | |||
828 | --- tests/autopilot/webbrowser_app/tests/test_keyboard.py 1970-01-01 00:00:00 +0000 | |||
829 | +++ tests/autopilot/webbrowser_app/tests/test_keyboard.py 2015-06-16 16:15:45 +0000 | |||
830 | @@ -0,0 +1,276 @@ | |||
831 | 1 | # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- | ||
832 | 2 | # | ||
833 | 3 | # Copyright 2015 Canonical | ||
834 | 4 | # | ||
835 | 5 | # This program is free software: you can redistribute it and/or modify it | ||
836 | 6 | # under the terms of the GNU General Public License version 3, as published | ||
837 | 7 | # by the Free Software Foundation. | ||
838 | 8 | # | ||
839 | 9 | # This program is distributed in the hope that it will be useful, | ||
840 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
841 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
842 | 12 | # GNU General Public License for more details. | ||
843 | 13 | # | ||
844 | 14 | # You should have received a copy of the GNU General Public License | ||
845 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
846 | 16 | |||
847 | 17 | import os | ||
848 | 18 | import sqlite3 | ||
849 | 19 | import time | ||
850 | 20 | import testtools | ||
851 | 21 | |||
852 | 22 | from testtools.matchers import Equals, NotEquals, GreaterThan | ||
853 | 23 | from autopilot.matchers import Eventually | ||
854 | 24 | from autopilot.platform import model | ||
855 | 25 | |||
856 | 26 | from webbrowser_app.tests import StartOpenRemotePageTestCaseBase | ||
857 | 27 | |||
858 | 28 | |||
859 | 29 | class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase): | ||
860 | 30 | |||
861 | 31 | """Helper test class that pre-populates history and bookmarks databases.""" | ||
862 | 32 | |||
863 | 33 | def setUp(self): | ||
864 | 34 | self.create_temporary_profile() | ||
865 | 35 | self.populate_bookmarks() | ||
866 | 36 | super(PrepopulatedDatabaseTestCaseBase, self).setUp("/tab/0") | ||
867 | 37 | |||
868 | 38 | def populate_bookmarks(self): | ||
869 | 39 | db_path = os.path.join(self.data_location, "bookmarks.sqlite") | ||
870 | 40 | connection = sqlite3.connect(db_path) | ||
871 | 41 | connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks | ||
872 | 42 | (url VARCHAR, title VARCHAR, icon VARCHAR, | ||
873 | 43 | created INTEGER);""") | ||
874 | 44 | rows = [ | ||
875 | 45 | ("http://www.rsc.org/periodic-table/element/77/iridium", | ||
876 | 46 | "Iridium - Element Information") | ||
877 | 47 | ] | ||
878 | 48 | |||
879 | 49 | for i, row in enumerate(rows): | ||
880 | 50 | timestamp = int(time.time()) - i * 10 | ||
881 | 51 | query = "INSERT INTO bookmarks \ | ||
882 | 52 | VALUES ('{}', '{}', '', {});" | ||
883 | 53 | query = query.format(row[0], row[1], timestamp) | ||
884 | 54 | connection.execute(query) | ||
885 | 55 | |||
886 | 56 | connection.commit() | ||
887 | 57 | connection.close() | ||
888 | 58 | |||
889 | 59 | |||
890 | 60 | @testtools.skipIf(model() != "Desktop", "on desktop only") | ||
891 | 61 | class TestKeyboard(PrepopulatedDatabaseTestCaseBase): | ||
892 | 62 | |||
893 | 63 | """Test keyboard interaction""" | ||
894 | 64 | |||
895 | 65 | def setUp(self): | ||
896 | 66 | super(TestKeyboard, self).setUp() | ||
897 | 67 | self.address_bar = self.main_window.address_bar | ||
898 | 68 | |||
899 | 69 | def open_tab(self, url): | ||
900 | 70 | self.main_window.press_key('Ctrl+T') | ||
901 | 71 | new_tab_view = self.main_window.get_new_tab_view() | ||
902 | 72 | self.address_bar.go_to_url(url) | ||
903 | 73 | new_tab_view.wait_until_destroyed() | ||
904 | 74 | self.assertThat(lambda: self.main_window.get_current_webview().url, | ||
905 | 75 | Eventually(Equals(url))) | ||
906 | 76 | |||
907 | 77 | # Name tabs starting from 1 by default because tab 0 has been opened | ||
908 | 78 | # already via StartOpenRemotePageTestCaseBase | ||
909 | 79 | def open_tabs(self, count, base=1): | ||
910 | 80 | for i in range(0, count): | ||
911 | 81 | self.open_tab(self.base_url + "/tab/" + str(i + base)) | ||
912 | 82 | |||
913 | 83 | def check_tab_number(self, number): | ||
914 | 84 | url = self.base_url + "/tab/" + str(number) | ||
915 | 85 | self.assertThat(lambda: self.main_window.get_current_webview().url, | ||
916 | 86 | Eventually(Equals(url))) | ||
917 | 87 | |||
918 | 88 | def test_new_tab(self): | ||
919 | 89 | self.main_window.press_key('Ctrl+T') | ||
920 | 90 | |||
921 | 91 | webview = self.main_window.get_current_webview() | ||
922 | 92 | self.assertThat(webview.url, Equals("")) | ||
923 | 93 | new_tab_view = self.main_window.get_new_tab_view() | ||
924 | 94 | self.assertThat(new_tab_view.visible, Eventually(Equals(True))) | ||
925 | 95 | |||
926 | 96 | def test_switch_tabs(self): | ||
927 | 97 | self.open_tabs(2) | ||
928 | 98 | self.check_tab_number(2) | ||
929 | 99 | self.main_window.press_key('Ctrl+Tab') | ||
930 | 100 | self.check_tab_number(0) | ||
931 | 101 | self.main_window.press_key('Ctrl+Tab') | ||
932 | 102 | self.check_tab_number(1) | ||
933 | 103 | self.main_window.press_key('Ctrl+Tab') | ||
934 | 104 | self.check_tab_number(2) | ||
935 | 105 | |||
936 | 106 | def test_can_switch_tabs_after_suggestions_escape(self): | ||
937 | 107 | self.open_tabs(1) | ||
938 | 108 | self.check_tab_number(1) | ||
939 | 109 | |||
940 | 110 | suggestions = self.main_window.get_suggestions() | ||
941 | 111 | self.address_bar.write('el') | ||
942 | 112 | self.assertThat(suggestions.opacity, Eventually(Equals(1))) | ||
943 | 113 | self.main_window.press_key('Down') | ||
944 | 114 | self.assertThat(suggestions.activeFocus, Eventually(Equals(True))) | ||
945 | 115 | |||
946 | 116 | self.main_window.press_key('Escape') | ||
947 | 117 | self.assertThat(suggestions.opacity, Eventually(Equals(0))) | ||
948 | 118 | |||
949 | 119 | self.main_window.press_key('Ctrl+Tab') | ||
950 | 120 | self.check_tab_number(0) | ||
951 | 121 | |||
952 | 122 | def test_switch_tabs_from_tabs_view(self): | ||
953 | 123 | self.open_tabs(1) | ||
954 | 124 | self.check_tab_number(1) | ||
955 | 125 | tabs = self.open_tabs_view() | ||
956 | 126 | self.main_window.press_key('Ctrl+Tab') | ||
957 | 127 | self.check_tab_number(0) | ||
958 | 128 | self.main_window.press_key('Ctrl+Tab') | ||
959 | 129 | self.check_tab_number(1) | ||
960 | 130 | self.main_window.press_key('Ctrl+Tab') | ||
961 | 131 | self.check_tab_number(0) | ||
962 | 132 | self.main_window.press_key('Escape') | ||
963 | 133 | self.assertThat(tabs.visible, Eventually(Equals(False))) | ||
964 | 134 | self.check_tab_number(0) | ||
965 | 135 | |||
966 | 136 | def test_close_tabs_ctrl_f4(self): | ||
967 | 137 | self.open_tabs(1) | ||
968 | 138 | self.check_tab_number(1) | ||
969 | 139 | self.main_window.press_key('Ctrl+F4') | ||
970 | 140 | self.check_tab_number(0) | ||
971 | 141 | self.main_window.press_key('Ctrl+F4') | ||
972 | 142 | webview = self.main_window.get_current_webview() | ||
973 | 143 | self.assertThat(webview.url, Equals("")) | ||
974 | 144 | |||
975 | 145 | def test_close_tabs_ctrl_w(self): | ||
976 | 146 | self.open_tabs(1) | ||
977 | 147 | self.check_tab_number(1) | ||
978 | 148 | self.main_window.press_key('Ctrl+w') | ||
979 | 149 | self.check_tab_number(0) | ||
980 | 150 | self.main_window.press_key('Ctrl+w') | ||
981 | 151 | webview = self.main_window.get_current_webview() | ||
982 | 152 | self.assertThat(webview.url, Equals("")) | ||
983 | 153 | |||
984 | 154 | def test_close_tabs_tabs_view(self): | ||
985 | 155 | self.open_tabs(1) | ||
986 | 156 | self.check_tab_number(1) | ||
987 | 157 | self.open_tabs_view() | ||
988 | 158 | self.main_window.press_key('Ctrl+w') | ||
989 | 159 | self.check_tab_number(0) | ||
990 | 160 | self.main_window.press_key('Ctrl+F4') | ||
991 | 161 | webview = self.main_window.get_current_webview() | ||
992 | 162 | self.assertThat(webview.url, Equals("")) | ||
993 | 163 | |||
994 | 164 | def test_select_address_bar_ctrl_l(self): | ||
995 | 165 | self.main_window.press_key('Ctrl+L') | ||
996 | 166 | self.assertThat(self.address_bar.text_field.selectedText, | ||
997 | 167 | Eventually(Equals(self.address_bar.text_field.text))) | ||
998 | 168 | |||
999 | 169 | def test_select_address_bar_alt_d(self): | ||
1000 | 170 | self.main_window.press_key('Alt+D') | ||
1001 | 171 | self.assertThat(self.address_bar.text_field.selectedText, | ||
1002 | 172 | Eventually(Equals(self.address_bar.text_field.text))) | ||
1003 | 173 | |||
1004 | 174 | def test_select_address_bar_f6(self): | ||
1005 | 175 | self.main_window.press_key('F6') | ||
1006 | 176 | self.assertThat(self.address_bar.text_field.selectedText, | ||
1007 | 177 | Eventually(Equals(self.address_bar.text_field.text))) | ||
1008 | 178 | |||
1009 | 179 | def test_escape_from_address_bar(self): | ||
1010 | 180 | self.main_window.press_key('Alt+D') | ||
1011 | 181 | self.assertThat(self.address_bar.text_field.selectedText, | ||
1012 | 182 | Eventually(Equals(self.address_bar.text_field.text))) | ||
1013 | 183 | self.main_window.press_key('Escape') | ||
1014 | 184 | self.assertThat(self.address_bar.text_field.selectedText, | ||
1015 | 185 | Eventually(Equals(""))) | ||
1016 | 186 | self.assertThat(self.address_bar.activeFocus, | ||
1017 | 187 | Eventually(Equals(False))) | ||
1018 | 188 | webview = self.main_window.get_current_webview() | ||
1019 | 189 | self.assertThat(webview.activeFocus, Eventually(Equals(True))) | ||
1020 | 190 | |||
1021 | 191 | def test_reload(self): | ||
1022 | 192 | webview = self.main_window.get_current_webview() | ||
1023 | 193 | self.assertThat(webview.loading, Eventually(Equals(False))) | ||
1024 | 194 | |||
1025 | 195 | watcher = webview.watch_signal('loadingStateChanged()') | ||
1026 | 196 | previous = watcher.num_emissions | ||
1027 | 197 | |||
1028 | 198 | self.main_window.press_key('Ctrl+R') | ||
1029 | 199 | self.assertThat( | ||
1030 | 200 | lambda: watcher.num_emissions, | ||
1031 | 201 | Eventually(GreaterThan(previous))) | ||
1032 | 202 | |||
1033 | 203 | self.assertThat(webview.loading, Eventually(Equals(False))) | ||
1034 | 204 | |||
1035 | 205 | previous = watcher.num_emissions | ||
1036 | 206 | |||
1037 | 207 | self.main_window.press_key('F5') | ||
1038 | 208 | self.assertThat( | ||
1039 | 209 | lambda: watcher.num_emissions, | ||
1040 | 210 | Eventually(GreaterThan(previous))) | ||
1041 | 211 | |||
1042 | 212 | self.assertThat(webview.loading, Eventually(Equals(False))) | ||
1043 | 213 | |||
1044 | 214 | def test_bookmark(self): | ||
1045 | 215 | chrome = self.main_window.chrome | ||
1046 | 216 | self.assertThat(chrome.bookmarked, Equals(False)) | ||
1047 | 217 | self.main_window.press_key('Ctrl+D') | ||
1048 | 218 | self.assertThat(chrome.bookmarked, Eventually(Equals(True))) | ||
1049 | 219 | self.main_window.press_key('Ctrl+D') | ||
1050 | 220 | self.assertThat(chrome.bookmarked, Eventually(Equals(False))) | ||
1051 | 221 | |||
1052 | 222 | def test_history_navigation_with_alt_arrows(self): | ||
1053 | 223 | previous = self.main_window.get_current_webview().url | ||
1054 | 224 | url = self.base_url + "/test2" | ||
1055 | 225 | self.main_window.go_to_url(url) | ||
1056 | 226 | self.main_window.wait_until_page_loaded(url) | ||
1057 | 227 | |||
1058 | 228 | self.main_window.press_key('Alt+Left') | ||
1059 | 229 | self.assertThat(lambda: self.main_window.get_current_webview().url, | ||
1060 | 230 | Eventually(Equals(previous))) | ||
1061 | 231 | |||
1062 | 232 | self.main_window.press_key('Alt+Right') | ||
1063 | 233 | self.assertThat(lambda: self.main_window.get_current_webview().url, | ||
1064 | 234 | Eventually(Equals(url))) | ||
1065 | 235 | |||
1066 | 236 | def test_history_navigation_with_backspace(self): | ||
1067 | 237 | previous = self.main_window.get_current_webview().url | ||
1068 | 238 | url = self.base_url + "/test2" | ||
1069 | 239 | self.main_window.go_to_url(url) | ||
1070 | 240 | self.main_window.wait_until_page_loaded(url) | ||
1071 | 241 | |||
1072 | 242 | self.main_window.press_key('Backspace') | ||
1073 | 243 | self.assertThat(lambda: self.main_window.get_current_webview().url, | ||
1074 | 244 | Eventually(Equals(previous))) | ||
1075 | 245 | |||
1076 | 246 | self.main_window.press_key('Shift+Backspace') | ||
1077 | 247 | self.assertThat(lambda: self.main_window.get_current_webview().url, | ||
1078 | 248 | Eventually(Equals(url))) | ||
1079 | 249 | |||
1080 | 250 | def test_toggle_history(self): | ||
1081 | 251 | self.assertThat(self.main_window.get_history_view(), Equals(None)) | ||
1082 | 252 | self.main_window.press_key('Ctrl+H') | ||
1083 | 253 | self.assertThat(lambda: self.main_window.get_history_view(), | ||
1084 | 254 | Eventually(NotEquals(None))) | ||
1085 | 255 | history_view = self.main_window.get_history_view() | ||
1086 | 256 | |||
1087 | 257 | self.main_window.press_key('Escape') | ||
1088 | 258 | history_view.wait_until_destroyed() | ||
1089 | 259 | webview = self.main_window.get_current_webview() | ||
1090 | 260 | self.assertThat(webview.activeFocus, Eventually(Equals(True))) | ||
1091 | 261 | |||
1092 | 262 | def test_toggle_history_from_menu(self): | ||
1093 | 263 | self.assertThat(self.main_window.get_history_view(), Equals(None)) | ||
1094 | 264 | self.open_history() | ||
1095 | 265 | history_view = self.main_window.get_history_view() | ||
1096 | 266 | self.assertThat(history_view.activeFocus, Eventually(Equals(True))) | ||
1097 | 267 | |||
1098 | 268 | self.main_window.press_key('Escape') | ||
1099 | 269 | history_view.wait_until_destroyed() | ||
1100 | 270 | |||
1101 | 271 | def test_escape_settings(self): | ||
1102 | 272 | settings = self.open_settings() | ||
1103 | 273 | self.main_window.press_key('Escape') | ||
1104 | 274 | settings.wait_until_destroyed() | ||
1105 | 275 | webview = self.main_window.get_current_webview() | ||
1106 | 276 | self.assertThat(webview.activeFocus, Eventually(Equals(True))) | ||
1107 | 0 | 277 | ||
1108 | === modified file 'tests/autopilot/webbrowser_app/tests/test_suggestions.py' | |||
1109 | --- tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-05-20 05:45:29 +0000 | |||
1110 | +++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-06-16 16:15:45 +0000 | |||
1111 | @@ -18,9 +18,11 @@ | |||
1112 | 18 | import random | 18 | import random |
1113 | 19 | import sqlite3 | 19 | import sqlite3 |
1114 | 20 | import time | 20 | import time |
1115 | 21 | import unittest | ||
1116 | 21 | 22 | ||
1118 | 22 | from testtools.matchers import Contains, Equals | 23 | from testtools.matchers import Contains, Equals, GreaterThan |
1119 | 23 | from autopilot.matchers import Eventually | 24 | from autopilot.matchers import Eventually |
1120 | 25 | from autopilot.platform import model | ||
1121 | 24 | 26 | ||
1122 | 25 | from webbrowser_app.tests import StartOpenRemotePageTestCaseBase | 27 | from webbrowser_app.tests import StartOpenRemotePageTestCaseBase |
1123 | 26 | from . import http_server | 28 | from . import http_server |
1124 | @@ -170,10 +172,9 @@ | |||
1125 | 170 | def test_show_list_of_suggestions(self): | 172 | def test_show_list_of_suggestions(self): |
1126 | 171 | suggestions = self.main_window.get_suggestions() | 173 | suggestions = self.main_window.get_suggestions() |
1127 | 172 | self.assert_suggestions_eventually_hidden() | 174 | self.assert_suggestions_eventually_hidden() |
1128 | 173 | self.assert_suggestions_eventually_hidden() | ||
1129 | 174 | self.address_bar.focus() | 175 | self.address_bar.focus() |
1130 | 175 | self.assert_suggestions_eventually_shown() | 176 | self.assert_suggestions_eventually_shown() |
1132 | 176 | self.assertThat(suggestions.count, Eventually(Equals(1))) | 177 | self.assertThat(suggestions.count, Eventually(GreaterThan(0))) |
1133 | 177 | self.address_bar.clear() | 178 | self.address_bar.clear() |
1134 | 178 | self.assert_suggestions_eventually_hidden() | 179 | self.assert_suggestions_eventually_hidden() |
1135 | 179 | 180 | ||
1136 | @@ -303,3 +304,67 @@ | |||
1137 | 303 | highlighted = self.highlight_term("highlight", "high") | 304 | highlighted = self.highlight_term("highlight", "high") |
1138 | 304 | self.assertThat(entries[0].title, Equals(highlighted)) | 305 | self.assertThat(entries[0].title, Equals(highlighted)) |
1139 | 305 | self.assertThat(entries[0].subtitle, Equals('')) | 306 | self.assertThat(entries[0].subtitle, Equals('')) |
1140 | 307 | |||
1141 | 308 | @unittest.skipIf(model() != "Desktop", "on desktop only") | ||
1142 | 309 | def test_keyboard_navigation(self): | ||
1143 | 310 | suggestions = self.main_window.get_suggestions() | ||
1144 | 311 | address_bar = self.address_bar | ||
1145 | 312 | address_bar.write('element') | ||
1146 | 313 | self.assert_suggestions_eventually_shown() | ||
1147 | 314 | self.assertThat(suggestions.count, Eventually(Equals(2))) | ||
1148 | 315 | entries = suggestions.get_ordered_entries() | ||
1149 | 316 | self.assertThat(entries[0].selected, Equals(False)) | ||
1150 | 317 | self.assertThat(entries[1].selected, Equals(False)) | ||
1151 | 318 | |||
1152 | 319 | address_bar.press_key('Down') | ||
1153 | 320 | self.assertThat(address_bar.activeFocus, Eventually(Equals(False))) | ||
1154 | 321 | self.assertThat(suggestions.activeFocus, Eventually(Equals(True))) | ||
1155 | 322 | self.assertThat(entries[0].selected, Equals(True)) | ||
1156 | 323 | |||
1157 | 324 | self.main_window.press_key('Down') | ||
1158 | 325 | self.assertThat(entries[0].selected, Equals(False)) | ||
1159 | 326 | self.assertThat(entries[1].selected, Equals(True)) | ||
1160 | 327 | |||
1161 | 328 | # verify that selection does not wrap around | ||
1162 | 329 | self.main_window.press_key('Down') | ||
1163 | 330 | self.assertThat(entries[0].selected, Equals(False)) | ||
1164 | 331 | self.assertThat(entries[1].selected, Equals(True)) | ||
1165 | 332 | |||
1166 | 333 | self.main_window.press_key('Up') | ||
1167 | 334 | self.assertThat(entries[0].selected, Equals(True)) | ||
1168 | 335 | self.assertThat(entries[1].selected, Equals(False)) | ||
1169 | 336 | |||
1170 | 337 | self.main_window.press_key('Up') | ||
1171 | 338 | self.assertThat(address_bar.activeFocus, Eventually(Equals(True))) | ||
1172 | 339 | self.assertThat(suggestions.activeFocus, Eventually(Equals(False))) | ||
1173 | 340 | self.assertThat(entries[0].selected, Equals(False)) | ||
1174 | 341 | self.assertThat(entries[1].selected, Equals(False)) | ||
1175 | 342 | |||
1176 | 343 | @unittest.skipIf(model() != "Desktop", "on desktop only") | ||
1177 | 344 | def test_suggestions_escape(self): | ||
1178 | 345 | suggestions = self.main_window.get_suggestions() | ||
1179 | 346 | previous_text = self.address_bar.text | ||
1180 | 347 | self.address_bar.write('element') | ||
1181 | 348 | self.assert_suggestions_eventually_shown() | ||
1182 | 349 | self.main_window.press_key('Down') | ||
1183 | 350 | self.assertThat(suggestions.activeFocus, Eventually(Equals(True))) | ||
1184 | 351 | self.assertThat(self.address_bar.text, Equals("element")) | ||
1185 | 352 | |||
1186 | 353 | self.main_window.press_key('Escape') | ||
1187 | 354 | self.assert_suggestions_eventually_hidden() | ||
1188 | 355 | self.assertThat(self.address_bar.text, Equals(previous_text)) | ||
1189 | 356 | |||
1190 | 357 | @unittest.skipIf(model() != "Desktop", "on desktop only") | ||
1191 | 358 | def test_suggestions_escape_on_addressbar(self): | ||
1192 | 359 | suggestions = self.main_window.get_suggestions() | ||
1193 | 360 | previous_text = self.address_bar.text | ||
1194 | 361 | self.address_bar.write('element') | ||
1195 | 362 | self.assert_suggestions_eventually_shown() | ||
1196 | 363 | self.main_window.press_key('Down') | ||
1197 | 364 | self.assertThat(suggestions.activeFocus, Eventually(Equals(True))) | ||
1198 | 365 | self.main_window.press_key('Up') | ||
1199 | 366 | self.assertThat(suggestions.activeFocus, Eventually(Equals(False))) | ||
1200 | 367 | |||
1201 | 368 | self.main_window.press_key('Escape') | ||
1202 | 369 | self.assert_suggestions_eventually_hidden() | ||
1203 | 370 | self.assertThat(self.address_bar.text, Equals(previous_text)) | ||
1204 | 306 | 371 | ||
1205 | === modified file 'tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp' | |||
1206 | --- tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp 2015-05-19 10:58:04 +0000 | |||
1207 | +++ tests/unittests/limit-proxy-model/tst_LimitProxyModelTests.cpp 2015-06-16 16:15:45 +0000 | |||
1208 | @@ -157,6 +157,28 @@ | |||
1209 | 157 | QCOMPARE(model->unlimitedRowCount(), 3); | 157 | QCOMPARE(model->unlimitedRowCount(), 3); |
1210 | 158 | QCOMPARE(model->rowCount(), 2); | 158 | QCOMPARE(model->rowCount(), 2); |
1211 | 159 | } | 159 | } |
1212 | 160 | |||
1213 | 161 | void shouldGetItemWithCorrectValues() | ||
1214 | 162 | { | ||
1215 | 163 | history->add(QUrl("http://example1.org/"), "Example 1 Domain", QUrl()); | ||
1216 | 164 | |||
1217 | 165 | QVariantMap item = model->get(0); | ||
1218 | 166 | QHash<int, QByteArray> roles = model->roleNames(); | ||
1219 | 167 | |||
1220 | 168 | QCOMPARE(roles.count(), item.count()); | ||
1221 | 169 | |||
1222 | 170 | Q_FOREACH(int role, roles.keys()) { | ||
1223 | 171 | QString roleName = QString::fromUtf8(roles.value(role)); | ||
1224 | 172 | QCOMPARE(model->data(model->index(0, 0), role), item.value(roleName)); | ||
1225 | 173 | } | ||
1226 | 174 | } | ||
1227 | 175 | |||
1228 | 176 | void shouldReturnEmptyItemIfGetOutOfBounds() | ||
1229 | 177 | { | ||
1230 | 178 | QVariantMap item = model->get(1); | ||
1231 | 179 | QVERIFY(item.isEmpty()); | ||
1232 | 180 | } | ||
1233 | 181 | |||
1234 | 160 | }; | 182 | }; |
1235 | 161 | 183 | ||
1236 | 162 | QTEST_MAIN(LimitProxyModelTests) | 184 | QTEST_MAIN(LimitProxyModelTests) |
FAILED: Continuous integration, rev:1049 jenkins. qa.ubuntu. com/job/ webbrowser- app-ci/ 1813/ jenkins. qa.ubuntu. com/job/ generic- deb-autopilot- vivid-touch/ 2950/console jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- amd64-ci/ 570/console jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- armhf-ci/ 570/console jenkins. qa.ubuntu. com/job/ webbrowser- app-vivid- i386-ci/ 570/console jenkins. qa.ubuntu. com/job/ generic- mediumtests- builder- vivid-armhf/ 2948/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/webbrowser- app-ci/ 1813/rebuild
http://