Merge ~osomon/oxide:updated-popup-menu into oxide:master
- Git
- lp:~osomon/oxide
- updated-popup-menu
- Merge into master
Proposed by
Olivier Tilloy
Status: | Needs review |
---|---|
Proposed branch: | ~osomon/oxide:updated-popup-menu |
Merge into: | oxide:master |
Diff against target: |
458 lines (+253/-50) 4 files modified
qt/tests/qmltests/ubuntu_ui/tst_WebViewPopupMenu.qml (+43/-15) qt/uitk/lib/resources/WebPopupMenu.qml (+162/-24) qt/uitk/lib/uitk_web_popup_menu.cc (+46/-9) qt/uitk/lib/uitk_web_popup_menu.h (+2/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Oxide Developers | Pending | ||
Review via email: mp+319566@code.launchpad.net |
Commit message
Improved Oxide.Ubuntu web popup menu UX.
Backported recent webbrowser-app update, by Florian Boucault.
Description of the change
To post a comment you must log in.
~osomon/oxide:updated-popup-menu
updated
- 3fbf137... by Olivier Tilloy
-
Add unit tests for keyboard navigation in popup menu.
Unmerged commits
- 3fbf137... by Olivier Tilloy
-
Add unit tests for keyboard navigation in popup menu.
- d0f2173... by Olivier Tilloy
-
Improved Oxide.Ubuntu web popup menu UX.
Backported recent webbrowser-app update, by Florian Boucault.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/qt/tests/qmltests/ubuntu_ui/tst_WebViewPopupMenu.qml b/qt/tests/qmltests/ubuntu_ui/tst_WebViewPopupMenu.qml |
2 | index ca36c27..2cc02f0 100644 |
3 | --- a/qt/tests/qmltests/ubuntu_ui/tst_WebViewPopupMenu.qml |
4 | +++ b/qt/tests/qmltests/ubuntu_ui/tst_WebViewPopupMenu.qml |
5 | @@ -52,7 +52,7 @@ UbuntuTestWebView { |
6 | } |
7 | |
8 | function getPopupMenuEntryAtIndex(index) { |
9 | - return TestSupport.findItemInScene(getPopupMenu(), "webView_WebPopupMenu_item" + index); |
10 | + return TestSupport.findItemInScene(getPopupMenu(), "itemDelegate_%1".arg(index)); |
11 | } |
12 | |
13 | function getSelectedIndex(selector) { |
14 | @@ -69,13 +69,13 @@ UbuntuTestWebView { |
15 | return [ |
16 | { selector: "#select1", |
17 | items: [ |
18 | - { type: "header", text: "Group A" }, |
19 | + { type: "header", text: "Group A", enabled: true }, |
20 | { type: "item", text: " Test 1", enabled: true }, |
21 | { type: "item", text: " Test 2", enabled: true }, |
22 | - { type: "header", text: "Group B" }, |
23 | + { type: "header", text: "Group B", enabled: true }, |
24 | { type: "item", text: " Test 3", enabled: true }, |
25 | { type: "item", text: " Test 4", enabled: false }, |
26 | - { type: "header", text: "Group C" }, |
27 | + { type: "header", text: "Group C", enabled: false }, |
28 | { type: "item", text: " Test 5", enabled: false }, |
29 | { type: "item", text: " Test 6", enabled: false } |
30 | ] |
31 | @@ -102,24 +102,21 @@ UbuntuTestWebView { |
32 | var items = []; |
33 | for (var i in listView.children[0].children) { |
34 | var item = listView.children[0].children[i]; |
35 | - if (item.z == 0) { |
36 | - // Ignore the highlight item. This isn't a great way to detect this |
37 | + if (item.objectName.indexOf("itemDelegate_") != 0 && |
38 | + item.objectName.indexOf("headerDelegate_") != 0) { |
39 | continue; |
40 | } |
41 | - items.push(listView.children[0].children[i]); |
42 | + items.push(item); |
43 | } |
44 | items.sort(function(a, b) { return a.y - b.y; }); |
45 | |
46 | compare(items.length, data.items.length); |
47 | for (var i in items) { |
48 | - if (data.items[i].type == "item") { |
49 | - compare(items[i].listItemData[0].title.text, data.items[i].text); |
50 | - compare(items[i].enabled, data.items[i].enabled); |
51 | - } else if (data.items[i].type == "header") { |
52 | - compare(items[i].text, data.items[i].text); |
53 | - } else { |
54 | - fail(); |
55 | - } |
56 | + var itemType = data.items[i].type; |
57 | + verify(itemType == "item" || itemType == "header"); |
58 | + compare(items[i].objectName.indexOf("%1Delegate_".arg(itemType)), 0); |
59 | + compare(items[i].text, data.items[i].text); |
60 | + compare(items[i].enabled, data.items[i].enabled); |
61 | } |
62 | } |
63 | |
64 | @@ -187,5 +184,36 @@ UbuntuTestWebView { |
65 | webView2.destroy(); |
66 | verify(TestUtils.waitFor(function() { return helper.destroyed; })); |
67 | } |
68 | + |
69 | + function test_WebViewPopupMenu6_arrow_key_navigation() { |
70 | + var r = webView.getTestApi().getBoundingClientRectForSelector("#select2"); |
71 | + mouseClick(webView, r.x + r.width / 2, r.y + r.height / 2); |
72 | + verify(waitForPopupMenu()); |
73 | + |
74 | + var listView = TestSupport.findItemInScene(getPopupMenu(), "webView_WebPopupMenu_ListView"); |
75 | + compare(listView.currentIndex, 2); |
76 | + |
77 | + keyPress(Qt.Key_Up); |
78 | + compare(listView.currentIndex, 1); |
79 | + keyPress(Qt.Key_Up); |
80 | + compare(listView.currentIndex, 0); |
81 | + keyPress(Qt.Key_Up); |
82 | + compare(listView.currentIndex, 0); |
83 | + |
84 | + keyPress(Qt.Key_Down); |
85 | + compare(listView.currentIndex, 1); |
86 | + keyPress(Qt.Key_Down); |
87 | + compare(listView.currentIndex, 2); |
88 | + keyPress(Qt.Key_Down); |
89 | + compare(listView.currentIndex, 4); |
90 | + keyPress(Qt.Key_Down); |
91 | + compare(listView.currentIndex, 5); |
92 | + keyPress(Qt.Key_Down); |
93 | + compare(listView.currentIndex, 5); |
94 | + |
95 | + keyPress(Qt.Key_Enter); |
96 | + verify(waitForPopupMenuToClose()); |
97 | + compare(getSelectedIndex("#select2"), 5); |
98 | + } |
99 | } |
100 | } |
101 | diff --git a/qt/uitk/lib/resources/WebPopupMenu.qml b/qt/uitk/lib/resources/WebPopupMenu.qml |
102 | index a2987d3..9fa68cf 100644 |
103 | --- a/qt/uitk/lib/resources/WebPopupMenu.qml |
104 | +++ b/qt/uitk/lib/resources/WebPopupMenu.qml |
105 | @@ -1,5 +1,5 @@ |
106 | // vim:expandtab:shiftwidth=2:tabstop=2: |
107 | -// Copyright (C) 2013-2016 Canonical Ltd. |
108 | +// Copyright (C) 2013-2017 Canonical Ltd. |
109 | |
110 | // This library is free software; you can redistribute it and/or |
111 | // modify it under the terms of the GNU Lesser General Public |
112 | @@ -17,54 +17,192 @@ |
113 | |
114 | import QtQuick 2.4 |
115 | import Ubuntu.Components 1.3 |
116 | -import Ubuntu.Components.ListItems 1.3 as ListItems |
117 | -import Ubuntu.Components.Popups 1.3 as Popups |
118 | - |
119 | -Popups.Popover { |
120 | - property var model |
121 | +import com.canonical.Oxide 1.5 |
122 | |
123 | +Item { |
124 | id: root |
125 | objectName: parent.objectName ? parent.objectName + "_WebPopupMenu" : "" |
126 | |
127 | signal selectedItem(int index) |
128 | + signal closed() |
129 | + |
130 | + property alias model: listView.model |
131 | + property rect elementBounds |
132 | + |
133 | + readonly property var webview: parent |
134 | |
135 | - caller: parent |
136 | + property real contentHeight |
137 | + width: elementBounds.width |
138 | + height: contentHeight |
139 | |
140 | - contentWidth: Math.min(parent.width - units.gu(10), units.gu(40)) |
141 | + property real listContentHeight: (listView.count + listView.sectionCount) * (listItemHeight + units.dp(1)) |
142 | + property real listItemHeight: units.gu(4) |
143 | + property real addressBarHeight: webview.locationBarController.height |
144 | |
145 | - property real listContentHeight: 0 // intermediate property to avoid binding loop |
146 | - contentHeight: Math.min(parent.height - units.gu(10), listContentHeight) |
147 | + // When the webview's size changes, dismiss the menu. |
148 | + // Ideally we would call updatePosition instead but because elementBounds |
149 | + // is not updated, it would result in incorrect positioning. |
150 | + Connections { |
151 | + target: webview |
152 | + onWidthChanged: closed() |
153 | + onHeightChanged: closed() |
154 | + } |
155 | + onListContentHeightChanged: updatePosition() |
156 | + onAddressBarHeightChanged: updatePosition() |
157 | + |
158 | + function updatePosition() { |
159 | + root.x = elementBounds.x; |
160 | + var availableAbove = elementBounds.y - addressBarHeight; |
161 | + var availableBelow = webview.height - elementBounds.y - elementBounds.height; |
162 | + |
163 | + if (availableBelow >= listContentHeight || availableBelow >= availableAbove) { |
164 | + // position popover below the box |
165 | + root.contentHeight = Math.min(availableBelow, listContentHeight); |
166 | + root.y = elementBounds.y + elementBounds.height; |
167 | + } else { |
168 | + // position popover above the box |
169 | + root.contentHeight = Math.min(availableAbove, listContentHeight); |
170 | + root.y = elementBounds.y - root.contentHeight; |
171 | + } |
172 | + } |
173 | + |
174 | + Keys.onEscapePressed: closed() |
175 | + Keys.onPressed: { |
176 | + // Swallow left and right key events |
177 | + if (event.key == Qt.Key_Left || event.key == Qt.Key_Right) { |
178 | + event.accepted = true; |
179 | + } |
180 | + } |
181 | + |
182 | + // Eat mouse events beneath the list so that they never reach the webview below |
183 | + MouseArea { |
184 | + anchors.fill: parent |
185 | + acceptedButtons: Qt.AllButtons |
186 | + onWheel: wheel.accepted = true |
187 | + } |
188 | + |
189 | + // Eat mouse events around the list so that they never reach the webview below |
190 | + InverseMouseArea { |
191 | + anchors.fill: parent |
192 | + acceptedButtons: Qt.AllButtons |
193 | + onPressed: selectedItem(listView.currentIndex) |
194 | + onWheel: wheel.accepted = true |
195 | + } |
196 | |
197 | Rectangle { |
198 | anchors.fill: parent |
199 | - color: "#ececec" |
200 | + color: theme.palette.normal.overlay |
201 | + function newColorWithAlpha(color, alpha) { |
202 | + return Qt.rgba(color.r, color.g, color.b, alpha); |
203 | + } |
204 | + border.color: newColorWithAlpha(theme.palette.normal.base, 0.4) |
205 | + border.width: units.dp(1) |
206 | } |
207 | |
208 | ListView { |
209 | + id: listView |
210 | + objectName: root.objectName ? root.objectName + "_ListView" : "" |
211 | clip: true |
212 | - width: root.contentWidth |
213 | - height: root.contentHeight |
214 | + anchors.fill: parent |
215 | + focus: true |
216 | + |
217 | + // Forces all delegates to be instantiated so that initialIndex is |
218 | + // set adequately to the index of the selected item |
219 | + cacheBuffer: 32767 |
220 | + property int initialIndex |
221 | + Component.onCompleted: { |
222 | + // calling forceLayout ensures that all delegates have been created and |
223 | + // that initialIndex is set adequately as a consequence |
224 | + forceLayout(); |
225 | + currentIndex = initialIndex; |
226 | + positionViewAtIndex(initialIndex, ListView.Contain); |
227 | + } |
228 | |
229 | - objectName: root.objectName ? root.objectName + "_ListView" : "" |
230 | + property int sectionCount: 0 |
231 | |
232 | - model: root.model |
233 | + Keys.onDownPressed: { |
234 | + var nextIndex = model.nextEnabledItem(currentIndex); |
235 | + if (nextIndex != -1) { |
236 | + currentIndex = nextIndex; |
237 | + } |
238 | + } |
239 | + Keys.onUpPressed: { |
240 | + var prevIndex = model.prevEnabledItem(currentIndex); |
241 | + if (prevIndex != -1) { |
242 | + currentIndex = prevIndex; |
243 | + } |
244 | + } |
245 | |
246 | delegate: ListItem { |
247 | - objectName: root.objectName ? root.objectName + "_item" + index : "" |
248 | - |
249 | + objectName: "itemDelegate_%1".arg(model.index) |
250 | + readonly property string text: listItemLayout.title.text |
251 | + enabled: model.enabled |
252 | + height: listItemLayout.height + (divider.visible ? divider.height : 0) |
253 | ListItemLayout { |
254 | - title.text: model.text |
255 | + id: listItemLayout |
256 | + padding { |
257 | + top: 0 |
258 | + bottom: 0 |
259 | + leading: 0 |
260 | + trailing: 0 |
261 | + } |
262 | + height: listItemHeight |
263 | + title { |
264 | + height: listItemHeight |
265 | + verticalAlignment: Text.AlignVCenter |
266 | + text: model.text |
267 | + textSize: Label.Small |
268 | + color: enabled ? theme.palette.normal.baseText : theme.palette.disabled.baseText |
269 | + } |
270 | + } |
271 | + |
272 | + color: (ListView.isCurrentItem || mouseArea.pressed) ? theme.palette.normal.focus : "transparent" |
273 | + Component.onCompleted: if (model.checked) listView.initialIndex = model.index |
274 | + |
275 | + Keys.onReturnPressed: selectedItem(model.index) |
276 | + Keys.onEnterPressed: selectedItem(model.index) |
277 | + Keys.onTabPressed: selectedItem(model.index) |
278 | + |
279 | + // Use a separate MouseArea because ListItem.onClicked |
280 | + // is invoked when opening the popup with the Enter key. |
281 | + MouseArea { |
282 | + id: mouseArea |
283 | + anchors.fill: parent |
284 | + enabled: model.enabled |
285 | + onClicked: selectedItem(model.index) |
286 | } |
287 | - enabled: model.enabled |
288 | - selected: model.checked |
289 | - onClicked: selectedItem(model.index) |
290 | } |
291 | |
292 | section.property: "group" |
293 | - section.delegate: ListItems.Header { |
294 | - text: section |
295 | + section.delegate: ListItem { |
296 | + objectName: "headerDelegate_%1".arg(model.index) |
297 | + readonly property string text: listItemLayout.title.text |
298 | + enabled: model.groupEnabled(section) |
299 | + height: listItemHeight + (divider.visible ? divider.height : 0) |
300 | + Component.onCompleted: listView.sectionCount += 1 |
301 | + ListItemLayout { |
302 | + id: listItemLayout |
303 | + padding { |
304 | + top: 0 |
305 | + bottom: 0 |
306 | + leading: 0 |
307 | + trailing: 0 |
308 | + } |
309 | + height: listItemHeight |
310 | + title { |
311 | + verticalAlignment: Text.AlignVCenter |
312 | + height: listItemHeight |
313 | + text: section |
314 | + textSize: Label.Small |
315 | + font.bold: true |
316 | + color: enabled ? theme.palette.normal.baseText : theme.palette.disabled.baseText |
317 | + } |
318 | + } |
319 | } |
320 | + } |
321 | |
322 | - onContentHeightChanged: root.listContentHeight = contentHeight |
323 | + Scrollbar { |
324 | + flickableItem: listView |
325 | + align: Qt.AlignTrailing |
326 | } |
327 | } |
328 | diff --git a/qt/uitk/lib/uitk_web_popup_menu.cc b/qt/uitk/lib/uitk_web_popup_menu.cc |
329 | index 7175169..ac18dc5 100644 |
330 | --- a/qt/uitk/lib/uitk_web_popup_menu.cc |
331 | +++ b/qt/uitk/lib/uitk_web_popup_menu.cc |
332 | @@ -1,5 +1,5 @@ |
333 | // vim:expandtab:shiftwidth=2:tabstop=2: |
334 | -// Copyright (C) 2016 Canonical Ltd. |
335 | +// Copyright (C) 2016-2017 Canonical Ltd. |
336 | |
337 | // This library is free software; you can redistribute it and/or |
338 | // modify it under the terms of the GNU Lesser General Public |
339 | @@ -36,6 +36,8 @@ namespace uitk { |
340 | using qt::MenuItem; |
341 | |
342 | class WebPopupMenu::Model : public QAbstractListModel { |
343 | + Q_OBJECT |
344 | + |
345 | public: |
346 | Model(const std::vector<MenuItem>& items); |
347 | ~Model() override; |
348 | @@ -55,6 +57,10 @@ class WebPopupMenu::Model : public QAbstractListModel { |
349 | int role = Qt::DisplayRole) const override; |
350 | QHash<int, QByteArray> roleNames() const override; |
351 | |
352 | + Q_INVOKABLE int nextEnabledItem(int currentIndex) const; |
353 | + Q_INVOKABLE int prevEnabledItem(int currentIndex) const; |
354 | + Q_INVOKABLE bool groupEnabled(const QString& group) const; |
355 | + |
356 | private: |
357 | struct Item { |
358 | QString label; |
359 | @@ -137,12 +143,43 @@ QHash<int, QByteArray> WebPopupMenu::Model::roleNames() const { |
360 | return roles; |
361 | } |
362 | |
363 | +int WebPopupMenu::Model::nextEnabledItem(int currentIndex) const { |
364 | + int index = currentIndex + 1; |
365 | + while (index < items_.count()) { |
366 | + if (items_.at(index).enabled) { |
367 | + return index; |
368 | + } |
369 | + ++index; |
370 | + } |
371 | + return -1; |
372 | +} |
373 | + |
374 | +int WebPopupMenu::Model::prevEnabledItem(int currentIndex) const { |
375 | + int index = currentIndex - 1; |
376 | + while (index >= 0) { |
377 | + if (items_.at(index).enabled) { |
378 | + return index; |
379 | + } |
380 | + --index; |
381 | + } |
382 | + return -1; |
383 | +} |
384 | + |
385 | +bool WebPopupMenu::Model::groupEnabled(const QString& group) const { |
386 | + for (const auto& item : items_) { |
387 | + if (item.group == group && item.enabled) { |
388 | + return true; |
389 | + } |
390 | + } |
391 | + return false; |
392 | +} |
393 | + |
394 | void WebPopupMenu::Show() { |
395 | - QMetaObject::invokeMethod(item_.get(), "show"); |
396 | + item_->setVisible(true); |
397 | } |
398 | |
399 | void WebPopupMenu::Hide() { |
400 | - QMetaObject::invokeMethod(item_.get(), "hide"); |
401 | + item_->setVisible(false); |
402 | } |
403 | |
404 | WebPopupMenu::WebPopupMenu(const std::vector<MenuItem>& items, |
405 | @@ -192,22 +229,20 @@ bool WebPopupMenu::Init(QQuickItem* parent) { |
406 | } |
407 | |
408 | item_->setProperty("model", QVariant::fromValue(model_.get())); |
409 | + item_->setProperty("elementBounds", bounds_); |
410 | item_->setParentItem(parent); |
411 | |
412 | component.completeCreate(); |
413 | |
414 | - connect(item_.get(), &QQuickItem::visibleChanged, |
415 | - this, &WebPopupMenu::OnVisibleChanged); |
416 | + connect(item_.get(), SIGNAL(closed()), this, SLOT(OnClosed())); |
417 | connect(item_.get(), SIGNAL(selectedItem(int)), |
418 | this, SLOT(OnItemSelected(int))); |
419 | |
420 | return true; |
421 | } |
422 | |
423 | -void WebPopupMenu::OnVisibleChanged() { |
424 | - if (!item_->isVisible()) { |
425 | - client_->cancel(); |
426 | - } |
427 | +void WebPopupMenu::OnClosed() { |
428 | + client_->cancel(); |
429 | } |
430 | |
431 | void WebPopupMenu::OnItemSelected(int index) { |
432 | @@ -235,3 +270,5 @@ WebPopupMenu::~WebPopupMenu() = default; |
433 | |
434 | } // namespace uitk |
435 | } // namespace oxide |
436 | + |
437 | +#include "uitk_web_popup_menu.moc" |
438 | diff --git a/qt/uitk/lib/uitk_web_popup_menu.h b/qt/uitk/lib/uitk_web_popup_menu.h |
439 | index d1553d2..5fa2772 100644 |
440 | --- a/qt/uitk/lib/uitk_web_popup_menu.h |
441 | +++ b/qt/uitk/lib/uitk_web_popup_menu.h |
442 | @@ -1,5 +1,5 @@ |
443 | // vim:expandtab:shiftwidth=2:tabstop=2: |
444 | -// Copyright (C) 2016 Canonical Ltd. |
445 | +// Copyright (C) 2016-2017 Canonical Ltd. |
446 | |
447 | // This library is free software; you can redistribute it and/or |
448 | // modify it under the terms of the GNU Lesser General Public |
449 | @@ -56,8 +56,8 @@ class WebPopupMenu : public QObject, |
450 | ~WebPopupMenu() override; |
451 | |
452 | private Q_SLOTS: |
453 | - void OnVisibleChanged(); |
454 | void OnItemSelected(int index); |
455 | + void OnClosed(); |
456 | |
457 | private: |
458 | WebPopupMenu(const std::vector<qt::MenuItem>& items, |