Merge lp:~artmello/ubuntu-system-settings/keyboard_navigation into lp:ubuntu-system-settings

Proposed by Arthur Mello
Status: Needs review
Proposed branch: lp:~artmello/ubuntu-system-settings/keyboard_navigation
Merge into: lp:ubuntu-system-settings
Diff against target: 445 lines (+246/-14)
7 files modified
src/SystemSettings/ItemPage.qml (+1/-0)
src/item-model.cpp (+78/-0)
src/item-model.h (+7/-0)
src/qml/CategorySection.qml (+21/-2)
src/qml/MainWindow.qml (+116/-9)
src/qml/UncategorizedItemsView.qml (+22/-2)
tests/plugins/main/tst_MainWindow.qml (+1/-1)
To merge this branch: bzr merge lp:~artmello/ubuntu-system-settings/keyboard_navigation
Reviewer Review Type Date Requested Status
system-apps-ci-bot continuous-integration Approve
Ubuntu Touch System Settings Pending
Review via email: mp+320046@code.launchpad.net

Commit message

Add support for keyboard navigation:
- Up/Down to navigate between categories
- Left/Right to change focus from first and second column
- Escape to close panel and return to categories when in single column mode

Description of the change

Add support for keyboard navigation:
- Up/Down to navigate between categories
- Left/Right to change focus from first and second column
- Escape to close panel and return to categories when in single column mode

To post a comment you must log in.
Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
1768. By Arthur Mello

Fix AP tests

Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :

PASSED: Continuous integration, rev:1768
https://jenkins.canonical.com/system-apps/job/lp-ubuntu-system-settings-ci/8/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build/2320
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-0-fetch/2319
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/2141
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/2141/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=zesty/2141
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=zesty/2141/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/2141
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/2141/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=zesty/2141
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=zesty/2141/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/2141
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/2141/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=zesty/2141
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=zesty/2141/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/system-apps/job/lp-ubuntu-system-settings-ci/8/rebuild

review: Approve (continuous-integration)

Unmerged revisions

1768. By Arthur Mello

Fix AP tests

1767. By Arthur Mello

Fix behavior of Esc and Left keys on single column mode

1766. By Arthur Mello

Add new keyboard navigation

- Use left/right to change focus from first and second column in 2 column mode
- Use left to close current panel and return to categories in single column mode
- Use Escape to do the same as left

1765. By Arthur Mello

Initial support for keyboard navigation

- Up/Down navigate between categories

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/SystemSettings/ItemPage.qml'
2--- src/SystemSettings/ItemPage.qml 2016-12-06 15:38:02 +0000
3+++ src/SystemSettings/ItemPage.qml 2017-03-16 14:39:38 +0000
4@@ -23,6 +23,7 @@
5
6 Page {
7 id: root
8+ focus: true
9
10 property alias title: pageHeader.title
11 property alias flickable: pageHeader.flickable
12
13=== modified file 'src/item-model.cpp'
14--- src/item-model.cpp 2015-05-12 12:08:47 +0000
15+++ src/item-model.cpp 2017-03-16 14:39:38 +0000
16@@ -134,6 +134,17 @@
17 return d->m_roleNames;
18 }
19
20+int ItemModel::getIndexByName(const QString& name) const
21+{
22+ Q_D(const ItemModel);
23+ for (int i = 0; i < d->m_visibleItems.count(); ++i) {
24+ if (d->m_visibleItems.at(i)->baseName() == name) {
25+ return i;
26+ }
27+ }
28+ return -1;
29+}
30+
31 void ItemModel::onItemVisibilityChanged()
32 {
33 Q_D(ItemModel);
34@@ -162,6 +173,73 @@
35 {
36 }
37
38+int ItemModelSortProxy::getIndexByName(const QString& name) const
39+{
40+ ItemModel *source = (ItemModel*) sourceModel();
41+ int sourceRow = source->getIndexByName(name);
42+
43+ if (sourceRow < 0)
44+ return sourceRow;
45+
46+ return mapFromSource(source->index(sourceRow, 0)).row();
47+}
48+
49+QString ItemModelSortProxy::getNameByIndex(int row) const
50+{
51+ if (row < 0 || row >= rowCount()) {
52+ return QString();
53+ }
54+
55+ QVariant data = index(row, 0).data(ItemModel::ItemRole);
56+ Plugin *plugin = data.value<Plugin *>();
57+
58+ if (plugin != NULL) {
59+ return plugin->baseName();
60+ }
61+
62+ return QString();
63+}
64+
65+int ItemModelSortProxy::getPreviousVisibleIndex(int from) const
66+{
67+ int ret = rowCount() - 1;
68+
69+ if (from >= 0) {
70+ ret = from - 1;
71+ }
72+
73+ while (ret >= 0) {
74+ QVariant data = index(ret, 0).data(ItemModel::ItemRole);
75+ Plugin *plugin = data.value<Plugin *>();
76+ if (plugin && plugin->isVisible()) {
77+ return ret;
78+ }
79+ ret--;
80+ }
81+
82+ return -1;
83+}
84+
85+int ItemModelSortProxy::getNextVisibleIndex(int from) const
86+{
87+ int ret = 0;
88+
89+ if (from >= 0) {
90+ ret = from + 1;
91+ }
92+
93+ while (ret < rowCount()) {
94+ QVariant data = index(ret, 0).data(ItemModel::ItemRole);
95+ Plugin *plugin = data.value<Plugin *>();
96+ if (plugin->isVisible()) {
97+ return ret;
98+ }
99+ ret++;
100+ }
101+
102+ return -1;
103+}
104+
105 bool ItemModelSortProxy::lessThan(const QModelIndex &left,
106 const QModelIndex &right) const
107 {
108
109=== modified file 'src/item-model.h'
110--- src/item-model.h 2014-07-23 13:29:15 +0000
111+++ src/item-model.h 2017-03-16 14:39:38 +0000
112@@ -49,6 +49,8 @@
113 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
114 QHash<int, QByteArray> roleNames() const;
115
116+ int getIndexByName(const QString &name) const;
117+
118 private Q_SLOTS:
119 void onItemVisibilityChanged();
120
121@@ -64,6 +66,11 @@
122 public:
123 explicit ItemModelSortProxy(QObject *parent = 0);
124
125+ Q_INVOKABLE int getIndexByName(const QString &name) const;
126+ Q_INVOKABLE QString getNameByIndex(int row) const;
127+ Q_INVOKABLE int getPreviousVisibleIndex(int from) const;
128+ Q_INVOKABLE int getNextVisibleIndex(int from) const;
129+
130 protected:
131 virtual bool lessThan(const QModelIndex &left,
132 const QModelIndex &right) const;
133
134=== modified file 'src/qml/CategorySection.qml'
135--- src/qml/CategorySection.qml 2016-12-09 15:19:24 +0000
136+++ src/qml/CategorySection.qml 2017-03-16 14:39:38 +0000
137@@ -36,6 +36,22 @@
138
139 objectName: "categoryGrid-" + category
140
141+ function getPluginIndexByName(plugin) {
142+ return repeater.model.getIndexByName(plugin)
143+ }
144+
145+ function getPluginNameByIndex(index) {
146+ return repeater.model.getNameByIndex(index)
147+ }
148+
149+ function getPreviousPluginIndex(from) {
150+ return repeater.model.getPreviousVisibleIndex(from)
151+ }
152+
153+ function getNextPluginIndex(from) {
154+ return repeater.model.getNextVisibleIndex(from)
155+ }
156+
157 SettingsItemTitle {
158 id: header
159 text: categoryName
160@@ -70,6 +86,9 @@
161 if (pageComponent) {
162 Haptics.play();
163 loadPluginByName(model.item.baseName);
164+ if (apl.columns == 1) {
165+ currentPluginPage.forceActiveFocus();
166+ }
167 }
168 }
169 }
170@@ -77,13 +96,13 @@
171 target: loader.item
172 property: "color"
173 value: theme.palette.highlighted.background
174- when: currentPlugin == model.item.baseName && apl.columns > 1
175+ when: currentPlugin == model.item.baseName
176 }
177 Binding {
178 target: loader.item
179 property: "color"
180 value: "transparent"
181- when: currentPlugin != model.item.baseName || apl.columns == 1
182+ when: currentPlugin != model.item.baseName
183 }
184 }
185 }
186
187=== modified file 'src/qml/MainWindow.qml'
188--- src/qml/MainWindow.qml 2016-12-14 13:37:49 +0000
189+++ src/qml/MainWindow.qml 2017-03-16 14:39:38 +0000
190@@ -31,8 +31,10 @@
191 objectName: "systemSettingsMainView"
192 automaticOrientation: true
193 anchorToKeyboard: true
194+ focus: true
195 property var pluginManager: PluginManager {}
196 property string currentPlugin: ""
197+ property Page currentPluginPage: null
198
199 /* Workaround for lp:1648801, i.e. APL does not support a placeholder,
200 so we implement it here. */
201@@ -55,12 +57,30 @@
202 page = apl.addComponentToNextColumnSync(
203 apl.primaryPage, pageComponent, opts
204 );
205+ page.Component.destruction.connect(function () {
206+ if (mainPage) {
207+ mainPage.forceActiveFocus()
208+ }
209+ }.bind(plugin))
210+
211+ page.Keys.pressed.connect(function (event) {
212+ if (event.key == Qt.Key_Left) {
213+ if (apl.columns > 1) {
214+ mainPage.forceActiveFocus()
215+ event.accepted = true
216+ }
217+ } else if (event.key == Qt.Key_Escape) {
218+ if (apl.columns > 1) {
219+ mainPage.forceActiveFocus()
220+ } else {
221+ apl.removePages(apl.primaryPage);
222+ }
223+ event.accepted = true
224+ }
225+ }.bind(plugin))
226+
227 currentPlugin = pluginName;
228- page.Component.destruction.connect(function () {
229- if (currentPlugin == this.baseName) {
230- currentPlugin = "";
231- }
232- }.bind(plugin))
233+ currentPluginPage = page;
234 }
235 return true
236 } else {
237@@ -85,6 +105,8 @@
238 // when running in windowed mode, constrain width
239 view.minimumWidth = Qt.binding( function() { return units.gu(40) } )
240 view.maximumWidth = Qt.binding( function() { return units.gu(140) } )
241+
242+ mainPage.forceActiveFocus()
243 }
244
245 Connections {
246@@ -92,9 +114,13 @@
247 ignoreUnknownSignals: true
248 onColumnsChanged: {
249 var columns = target.columns;
250- if (columns > 1 && !currentPlugin) {
251- loadPluginByName(placeholderPlugin);
252- } else if (columns == 1 && currentPlugin == placeholderPlugin) {
253+ if (columns > 1) {
254+ if (!currentPlugin) {
255+ loadPluginByName(placeholderPlugin);
256+ } else {
257+ loadPluginByName(currentPlugin);
258+ }
259+ } else if (columns == 1) {
260 apl.removePages(apl.primaryPage);
261 }
262 }
263@@ -131,12 +157,13 @@
264 }
265 }
266 }
267-
268+
269 USSAdaptivePageLayout {
270 id: apl
271 objectName: "apl"
272 anchors.fill: parent
273 primaryPage: mainPage
274+ focus: true
275 layouts: [
276 PageColumnsLayout {
277 when: width >= units.gu(90)
278@@ -163,6 +190,85 @@
279 objectName: "systemSettingsPage"
280 visible: false
281 header: standardHeader
282+ focus: true
283+
284+ function selectPreviousPlugin() {
285+ for (var i = 0; i < mainColumn.children.length; ++i) {
286+ var curr = mainColumn.children[i].getPluginIndexByName(main.currentPlugin)
287+
288+ if (curr >= 0) {
289+ var prev = mainColumn.children[i].getPreviousPluginIndex(curr)
290+
291+ var prevPlugin
292+ if (prev >= 0) {
293+ prevPlugin = mainColumn.children[i].getPluginNameByIndex(prev)
294+ } else {
295+ var j = i - 1
296+ if (j <= 0) {
297+ j = mainColumn.children.length - 1
298+ }
299+ prev = mainColumn.children[j].getPreviousPluginIndex(-1)
300+ prevPlugin = mainColumn.children[j].getPluginNameByIndex(prev)
301+ }
302+
303+ if (apl.columns > 1) {
304+ loadPluginByName(prevPlugin)
305+ } else {
306+ main.currentPlugin = prevPlugin
307+ }
308+ break
309+ }
310+ }
311+ }
312+
313+ function selectNextPlugin() {
314+ for (var i = 0; i < mainColumn.children.length; ++i) {
315+ var curr = mainColumn.children[i].getPluginIndexByName(main.currentPlugin)
316+
317+ if (curr >= 0) {
318+ var next = mainColumn.children[i].getNextPluginIndex(curr)
319+
320+ var nextPlugin
321+ if (next >= 0) {
322+ nextPlugin = mainColumn.children[i].getPluginNameByIndex(next)
323+ } else {
324+ var j = i + 1
325+ if (j >= mainColumn.children.length) {
326+ j = 1
327+ }
328+ next = mainColumn.children[j].getNextPluginIndex(-1)
329+ nextPlugin = mainColumn.children[j].getPluginNameByIndex(next)
330+ }
331+ if (apl.columns > 1) {
332+ loadPluginByName(nextPlugin)
333+ } else {
334+ main.currentPlugin = nextPlugin
335+ }
336+ break
337+ }
338+ }
339+ }
340+
341+ Keys.onPressed: {
342+ if (event.key == Qt.Key_Up) {
343+ mainPage.selectPreviousPlugin()
344+ event.accepted = true
345+ } else if (event.key == Qt.Key_Down) {
346+ mainPage.selectNextPlugin()
347+ event.accepted = true
348+ } else if (event.key == Qt.Key_Right) {
349+ if (apl.columns > 1 && main.currentPluginPage) {
350+ main.currentPluginPage.forceActiveFocus()
351+ event.accepted = true
352+ }
353+ } else if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter) {
354+ if (apl.columns == 1 && main.currentPlugin) {
355+ loadPluginByName(main.currentPlugin)
356+ main.currentPluginPage.forceActiveFocus()
357+ event.accepted = true
358+ }
359+ }
360+ }
361
362 PageHeader {
363 id: standardHeader
364@@ -227,6 +333,7 @@
365 flickableDirection: Flickable.VerticalFlick
366
367 Column {
368+ id: mainColumn
369 anchors.left: parent.left
370 anchors.right: parent.right
371
372
373=== modified file 'src/qml/UncategorizedItemsView.qml'
374--- src/qml/UncategorizedItemsView.qml 2016-12-09 15:19:24 +0000
375+++ src/qml/UncategorizedItemsView.qml 2017-03-16 14:39:38 +0000
376@@ -24,6 +24,7 @@
377 import SystemSettings 1.0
378
379 Column {
380+ id: column
381 property alias model: repeater.model
382
383 visible: repeater.count > 0
384@@ -31,6 +32,22 @@
385 anchors.left: parent.left
386 anchors.right: parent.right
387
388+ function getPluginIndexByName(plugin) {
389+ return repeater.model.getIndexByName(plugin)
390+ }
391+
392+ function getPluginNameByIndex(index) {
393+ return repeater.model.getNameByIndex(index)
394+ }
395+
396+ function getPreviousPluginIndex(from) {
397+ return repeater.model.getPreviousVisibleIndex(from)
398+ }
399+
400+ function getNextPluginIndex(from) {
401+ return repeater.model.getNextVisibleIndex(from)
402+ }
403+
404 Repeater {
405 id: repeater
406 Column {
407@@ -51,6 +68,9 @@
408 if (pageComponent) {
409 Haptics.play();
410 loadPluginByName(model.item.baseName);
411+ if (apl.columns == 1) {
412+ currentPluginPage.forceActiveFocus();
413+ }
414 }
415 }
416 }
417@@ -58,13 +78,13 @@
418 target: loader.item
419 property: "color"
420 value: theme.palette.highlighted.background
421- when: currentPlugin == model.item.baseName && apl.columns > 1
422+ when: currentPlugin == model.item.baseName
423 }
424 Binding {
425 target: loader.item
426 property: "color"
427 value: "transparent"
428- when: currentPlugin != model.item.baseName || apl.columns == 1
429+ when: currentPlugin != model.item.baseName
430 }
431 }
432 }
433
434=== modified file 'tests/plugins/main/tst_MainWindow.qml'
435--- tests/plugins/main/tst_MainWindow.qml 2016-12-14 13:37:49 +0000
436+++ tests/plugins/main/tst_MainWindow.qml 2017-03-16 14:39:38 +0000
437@@ -249,7 +249,7 @@
438
439 // Pop a page
440 apl.removePages(apl.primaryPage);
441- tryCompare(instance, "currentPlugin", "");
442+ tryCompare(instance, "currentPlugin", "Test");
443 }
444
445 // Seems this is how Unity8 is resizing a window to its previous size.

Subscribers

People subscribed via source and target branches