Merge lp:~kissiel/checkbox/converged-keyboard-support into lp:checkbox

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Sylvain Pineau
Approved revision: 4441
Merged at revision: 4431
Proposed branch: lp:~kissiel/checkbox/converged-keyboard-support
Merge into: lp:checkbox
Diff against target: 1081 lines (+718/-41)
13 files modified
checkbox-touch/checkbox-touch.qml (+35/-0)
checkbox-touch/components/ConfirmationDialog.qml (+15/-4)
checkbox-touch/components/ConfirmationLogic.js (+10/-1)
checkbox-touch/components/InteractIntroPage.qml (+11/-2)
checkbox-touch/components/KeysDelegator.qml (+226/-0)
checkbox-touch/components/ManualIntroPage.qml (+12/-2)
checkbox-touch/components/QmlConfinedPage.qml (+12/-2)
checkbox-touch/components/QmlNativePage.qml (+12/-2)
checkbox-touch/components/SelectionPage.qml (+112/-24)
checkbox-touch/components/TestVerificationPage.qml (+15/-2)
checkbox-touch/components/UserInteractSummaryPage.qml (+12/-2)
checkbox-touch/components/WelcomePage.qml (+4/-0)
checkbox-touch/tests/unit/tst_KeysDelegator.qml (+242/-0)
To merge this branch: bzr merge lp:~kissiel/checkbox/converged-keyboard-support
Reviewer Review Type Date Requested Status
Pierre Equoy Approve
Sylvain Pineau (community) Needs Fixing
Review via email: mp+281205@code.launchpad.net

Description of the change

This MR brings keyboard support to Checkbox-Converged.

There are two distinct new features:

--- Filtering on selection screens ---

    The goals for filtering were as follows:
     * case insensitive, i.e. 'foo' in 'Foo' matches
     * any position in the string, i.e. 'oo' in 'Foo' matches
     * support for regex, i.e. 'F..b..' in 'Foobar' matches

    Additional goals for UX were to keep toggle selection action, but to make it
    apply on filtered list. Example scenarios:
     original list = [foo, bar, baz]
     none selected
     query for 'ba'
     -> bar and baz visible on the list
     toggle selection
     -> both - bar and baz are selected
     clear the query string
     -> foo is not selected, bar and bas are
    Deselection should mirrors this behaviour

--- Enable key shortcuts in the app ---
As requested here: https://bugs.launchpad.net/checkbox-converged/+bug/1318466, currently supported keystrokes are:

for selection screens:
    * alt + c - same as hitting continue (if anything is selected)
    * ctrl + a - select all / deselect all (toggle selection - same as the header action)

'in test' screens:
    * alt + t - to start the test (if in intro) or continue (if outcome is automatically resolved)
    * alt + p - to pass the test
    * alt + f - to fail the test
    * alt + s - to skip the test
    * alt + c - to enter comment

To post a comment you must log in.
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Two issues with the first screens:

1. In addition to the continue button, you could also add a keyboard shortcut for the first screen "start testing"

2. The selection screens Alt+C does the right job but does not grey out the continue button.

review: Needs Fixing
Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

@kissiel

Could you please rebase this branch on trunk. I'd like to give it a second look since we're now using converged launchers (you know some of us are fond of keyboard shortcuts)

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

> Two issues with the first screens:
>
> 1. In addition to the continue button, you could also add a keyboard shortcut
> for the first screen "start testing"
>
> 2. The selection screens Alt+C does the right job but does not grey out the
> continue button.

Added both things.
Rebased on current trunk.

Revision history for this message
Pierre Equoy (pieq) wrote :

The search filter is very handy, thanks!

Regarding the shortcuts, given there is no visual clues, it's a bit weird to have one (alt+C) that is "continue" in some screens and "comment" in others.

I'm not sure I understood how to use "alt+T". It unselects the test that is highlighted, but since there is no way to move the selection using the keyboard in the first place, its usefulness is limited (unless I missed something).

for the in test screens, I would suggest to use something similar to the cli:

    - alt + T ("Test") to start the test
    - alt + P ("Passed") to pass the test
    - alt + F ("Failed") to fail the test
    - alt + S ("Skipped") to skip the test
    - alt + C ("Comment") to comment the test

for selection screens:
    * alt + c - same as hitting continue (if anything is selected)
    * alt + t - toggle selection (same as the header action)

'in test' screens:
    * alt + t - to start the test (if in intro) or continue (if outcome is automatically resolved)
    * alt + y - to pass the test
    * alt + n - to fail the test
    * alt + x - to skip the test
    * alt + c - to enter comment

Revision history for this message
Pierre Equoy (pieq) wrote :

The search filter is very handy, thanks!

Regarding the shortcuts, given there is no visual clues, it's a bit weird to have one (alt+C) that is "continue" in some screens and "comment" in others.

I'm not sure I understood how to use "alt+T". It unselects the test that is highlighted, but since there is no way to move the selection using the keyboard in the first place, its usefulness is limited (unless I missed something).

for the in test screens, I would suggest to use something similar to the cli:

    - alt + T ("Test") to start the test
    - alt + P ("Passed") to pass the test
    - alt + F ("Failed") to fail the test
    - alt + S ("Skipped") to skip the test
    - alt + C ("Comment") to comment the test

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

> The search filter is very handy, thanks!
>
> Regarding the shortcuts, given there is no visual clues, it's a bit weird to
> have one (alt+C) that is "continue" in some screens and "comment" in others.
>
> I'm not sure I understood how to use "alt+T". It unselects the test that is
> highlighted, but since there is no way to move the selection using the
> keyboard in the first place, its usefulness is limited (unless I missed
> something).
>
> for the in test screens, I would suggest to use something similar to the cli:
>
> - alt + T ("Test") to start the test
> - alt + P ("Passed") to pass the test
> - alt + F ("Failed") to fail the test
> - alt + S ("Skipped") to skip the test
> - alt + C ("Comment") to comment the test

I like those shortcuts more.
What would you like to have for 'toggle selection' action and 'continue' on selection screens?

Revision history for this message
Pierre Equoy (pieq) wrote :

I think alt + T for "continue", just to keep the consistency with the later screens (where we press alt + T to start a test).

For the selection, since it's an all or nothing type of selection, maybe alt + A ?

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

> I think alt + T for "continue", just to keep the consistency with the later
> screens (where we press alt + T to start a test).
>
> For the selection, since it's an all or nothing type of selection, maybe alt +
> A ?

Instead of 'alt+a' I went with more natural 'ctrl+a'.
I made the rest as you suggested.

Revision history for this message
Pierre Equoy (pieq) wrote :

Yes, good idea for Ctrl+A!

Thanks Maciek :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'checkbox-touch/checkbox-touch.qml'
2--- checkbox-touch/checkbox-touch.qml 2016-07-08 10:21:16 +0000
3+++ checkbox-touch/checkbox-touch.qml 2016-07-11 12:39:56 +0000
4@@ -87,6 +87,13 @@
5 }
6 }
7
8+ KeysDelegator {
9+ id: rootKeysDelegator
10+ }
11+
12+ // forward all keypresses to the delegator
13+ Keys.onPressed: rootKeysDelegator.keyPress(event)
14+
15 Component.onCompleted: {
16 i18n.domain = "com.ubuntu.checkbox";
17 if (args.values["autopilot"]) {
18@@ -198,6 +205,13 @@
19 PageStack {
20 id: pageStack
21 Component.onCompleted: push(welcomePage)
22+ onCurrentPageChanged: {
23+ if (pageStack.depth > 1) {
24+ // there was something before, we need to pop it from the kd's activeStack
25+ rootKeysDelegator.activeStack.pop();
26+ }
27+ rootKeysDelegator.activeStack.push(pageStack.currentPage);
28+ }
29 }
30
31 WelcomePage {
32@@ -310,6 +324,13 @@
33 title: i18n.tr("Select test plan")
34 onlyOneAllowed: true
35 largeBuffer: args.values["autopilot"]
36+ onVisibleChanged: {
37+ if (visible) {
38+ rootKeysDelegator.onKeyPressed.connect(keys.keyPress)
39+ } else {
40+ rootKeysDelegator.onKeyPressed.disconnect(keys.keyPress)
41+ }
42+ }
43
44 function setup(testplan_info_list) {
45 if (testplan_info_list.length<1) {
46@@ -336,6 +357,13 @@
47 objectName: "categorySelectionPage"
48 title: i18n.tr("Select categories")
49 largeBuffer: args.values["autopilot"]
50+ onVisibleChanged: {
51+ if (visible) {
52+ rootKeysDelegator.onKeyPressed.connect(keys.keyPress)
53+ } else {
54+ rootKeysDelegator.onKeyPressed.disconnect(keys.keyPress)
55+ }
56+ }
57
58 function setup(continuation) {
59 app.getCategories(function(response) {
60@@ -372,6 +400,13 @@
61 title: i18n.tr("Select tests")
62 continueText: i18n.tr("Start testing")
63 largeBuffer: args.values["autopilot"]
64+ onVisibleChanged: {
65+ if (visible) {
66+ rootKeysDelegator.onKeyPressed.connect(keys.keyPress)
67+ } else {
68+ rootKeysDelegator.onKeyPressed.disconnect(keys.keyPress)
69+ }
70+ }
71
72 function setup(continuation) {
73 app.getTests(function(response) {
74
75=== modified file 'checkbox-touch/components/ConfirmationDialog.qml'
76--- checkbox-touch/components/ConfirmationDialog.qml 2016-06-30 12:19:17 +0000
77+++ checkbox-touch/components/ConfirmationDialog.qml 2016-07-11 12:39:56 +0000
78@@ -66,21 +66,21 @@
79 title: question
80
81 Button {
82+ id: yesButton
83 text: i18n.tr("YES")
84 objectName: "yesButton"
85 color: UbuntuColors.green
86 onClicked: {
87- answer(true, checkBox.checked);
88- PopupUtils.close(dlg);
89+ _giveAnswer(true);
90 }
91 }
92 Button {
93+ id: noButton
94 text: i18n.tr("NO")
95 objectName: "noButton"
96 color: UbuntuColors.red
97 onClicked: {
98- answer(false, checkBox.checked);
99- PopupUtils.close(dlg);
100+ _giveAnswer(false);
101 }
102 }
103
104@@ -102,6 +102,17 @@
105 }
106 }
107 }
108+ Component.onCompleted: {
109+ rootKeysDelegator.setHandler('alt+y', confirmationDialog, yesButton.clicked);
110+ rootKeysDelegator.setHandler('alt+n', confirmationDialog, noButton.clicked);
111+ rootKeysDelegator.activeStack.push(confirmationDialog);
112+ }
113+ function _giveAnswer(confirmation) {
114+ // ensures that dialog is closed
115+ answer(confirmation, checkBox.checked);
116+ PopupUtils.close(dlg);
117+ rootKeysDelegator.activeStack.pop();
118+ }
119 }
120 }
121 }
122
123=== modified file 'checkbox-touch/components/ConfirmationLogic.js'
124--- checkbox-touch/components/ConfirmationLogic.js 2014-10-28 15:03:19 +0000
125+++ checkbox-touch/components/ConfirmationLogic.js 2016-07-11 12:39:56 +0000
126@@ -1,5 +1,7 @@
127 .import Ubuntu.Components.Popups 1.0 as Popups
128
129+.import QtQuick 2.0 as QtQuick
130+
131 function confirmRequest(caller, options, continuation) {
132 // if the question was answered before and user selected to
133 // remember their selection - 'returning' true
134@@ -7,7 +9,14 @@
135 continuation(true);
136 return;
137 }
138- var popup = Qt.createComponent(Qt.resolvedUrl("ConfirmationDialog.qml")).createObject(caller);
139+ var component = Qt.createComponent(Qt.resolvedUrl("ConfirmationDialog.qml"));
140+ if (component.status == QtQuick.Component.Error) {
141+ var msg = i18n.tr("could not create ConfirmationDialog component\n'") + component.errorString();
142+ console.error(msg);
143+ ErrorLogic.showError(mainView, msg, Qt.quit, i18n.tr("Quit"));
144+ }
145+ var popup = component.createObject(caller);
146+
147 popup.withRemember = options.remember;
148 popup.question = options.question;
149 popup.answer.connect(function(result, remember) {
150
151=== modified file 'checkbox-touch/components/InteractIntroPage.qml'
152--- checkbox-touch/components/InteractIntroPage.qml 2016-06-30 12:19:17 +0000
153+++ checkbox-touch/components/InteractIntroPage.qml 2016-07-11 12:39:56 +0000
154@@ -57,8 +57,12 @@
155 trailingActionBar {
156 objectName: 'trailingActionBar'
157 actions: [
158- AddCommentAction {},
159- SkipAction {}
160+ AddCommentAction {
161+ id: addCommentAction
162+ },
163+ SkipAction {
164+ id: skipAction
165+ }
166 ]
167 }
168 }
169@@ -111,4 +115,9 @@
170 }
171 }
172 }
173+ Component.onCompleted: {
174+ rootKeysDelegator.setHandler('alt+s', userInteractVerifyIntroPage, skipAction.trigger);
175+ rootKeysDelegator.setHandler('alt+c', userInteractVerifyIntroPage, addCommentAction.trigger);
176+ rootKeysDelegator.setHandler('alt+t', userInteractVerifyIntroPage, startTestButton.clicked);
177+ }
178 }
179
180=== added file 'checkbox-touch/components/KeysDelegator.qml'
181--- checkbox-touch/components/KeysDelegator.qml 1970-01-01 00:00:00 +0000
182+++ checkbox-touch/components/KeysDelegator.qml 2016-07-11 12:39:56 +0000
183@@ -0,0 +1,226 @@
184+/*
185+ * This file is part of Checkbox
186+ *
187+ * Copyright 2015 Canonical Ltd.
188+ *
189+ * Authors:
190+ * - Maciej Kisielewski <maciej.kisielewski@canonical.com>
191+ *
192+ * This program is free software; you can redistribute it and/or modify
193+ * it under the terms of the GNU General Public License as published by
194+ * the Free Software Foundation; version 3.
195+ *
196+ * This program is distributed in the hope that it will be useful,
197+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
198+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
199+ * GNU General Public License for more details.
200+ *
201+ * You should have received a copy of the GNU General Public License
202+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
203+ */
204+import QtQuick 2.0
205+import Ubuntu.Components 1.3
206+
207+/*! \brief Helper for delegating keypresses
208+ \inherits Item
209+
210+ This component helps to register keystroke handlers in a human-friendly
211+ manner. Once handler is assigned to a given keystroke, the next time
212+ keyPress will be called with event matching the key combination expressed
213+ as registered keystroke, the supplied handler will be called.
214+ There are two kinds of handlers: owned (normal), and 'global'.
215+ Owned are registered with 'owner' parameter, that owner must match the top
216+ of the 'activeStack' in order for the handler to be called.
217+ Global handlers are handler regardless of what's on activeStack.
218+ Note that owned handlers take precedence over global ones. I.e. if you have
219+ registered two handlers for the same keystroke and the owner of the owned
220+ one is on activeStack, ONLY that one will be called.
221+ Example usage:
222+ KeysDelegator {
223+ id: keysDelegator
224+ }
225+ Keys.onPressed: keysDelegator.keyPress(event)
226+ (...)
227+ kd.setGlobalHandler('ctrl+q', Qt.quit);
228+ kd.setHandler('ctrl+y', yesButton.clicked);
229+*/
230+
231+Item {
232+
233+ /*!
234+ Gets signalled when no handler was found for given keystroke.
235+ */
236+ signal keyPressed(var event);
237+
238+ /*!
239+ Handle a keyboard event.
240+ This is intended to be called as a handler for Keys.onPressed in your MainView.
241+ */
242+ function keyPress(event) {
243+ // build the keystroke string
244+ var keystroke ='';
245+ if (event.modifiers & Qt.AltModifier) keystroke += 'alt+';
246+ if (event.modifiers & Qt.ControlModifier) keystroke += 'ctrl+';
247+ if (event.modifiers & Qt.ShiftModifier) keystroke += 'shift+';
248+ keystroke += String.fromCharCode(event.key).toLowerCase();
249+
250+ var ownedHandlerHit = false;
251+ // get the object this keystroke should be processed for
252+ if (activeStack.length > 0) {
253+ var candidate = activeStack[activeStack.length - 1];
254+ if (candidate in _handlers) {
255+ if (keystroke in _handlers[candidate]) {
256+ _handlers[candidate][keystroke]();
257+ ownedHandlerHit = true;
258+ }
259+ }
260+ }
261+ if (!ownedHandlerHit) {
262+ if (keystroke in _globalHandlers) {
263+ _globalHandlers[keystroke]();
264+ } else {
265+ // no handlers set, forwarding as keyPressed signal
266+ keyPressed(event);
267+ }
268+ }
269+ }
270+
271+ /*!
272+ Make given page receive all (yet) unhandled keystrokes while it's visible.
273+ Note, page must have its own KeysDelegator exposed as 'keys'.
274+ */
275+ function forwardPressesWhileVisible(page) {
276+ var handler = function() {
277+ if (page.visible == true) {
278+ root.onKeyPressed.connect(page.keys.keyPress);
279+ } else {
280+ root.onKeyPressed.disconnect(page.keys.keyPress);
281+ }
282+ }
283+ page.onVisibleChanged.connect(handler);
284+ page.Component.onDestruction.connect(function() {
285+ page.onVisibleChanged.disconnect(handler);
286+ });
287+ }
288+
289+ /*!
290+ Set handler for given keystroke.
291+ This function stores `handler` as the function that is to be called if
292+ `keystroke` is processed and owner is on the top of the activeStack.
293+ `keystroke` is in form of [<mod_key>+...]<key>, E.g. 'ctrl+x'
294+
295+ If called with the keystroke that's already in the registry, it will
296+ overwrite the previous handler.
297+ */
298+ function setHandler(keystroke, owner, handler) {
299+ if (!(owner in _handlers)) {
300+ _handlers[owner] = [];
301+ }
302+
303+ _handlers[owner][_normalizeKeystroke(keystroke)] = handler;
304+ }
305+
306+ /*!
307+ Unset handler for a given keystroke for a given owner.
308+
309+ It silently ignores keystroke entries that are not in the registry
310+ */
311+ function unsetHandler(keystroke, owner) {
312+ if (!(owner in _handlers)) {
313+ return;
314+ }
315+ delete _handlers[owner][_normalizeKeystroke(keystroke)];
316+ }
317+
318+ /*!
319+ Unset handlers owned by the given owner.
320+ */
321+ function unsetHandlersByOwner(owner) {
322+ _handlers[owner] = [];
323+ }
324+
325+ /*!
326+ Unset all handlers.
327+ */
328+ function unsetAllHandlers() {
329+ _handlers = [];
330+ }
331+
332+ /*!
333+ Set global handler for a keystroke.
334+ */
335+ function setGlobalHandler(keystroke, handler) {
336+ _globalHandlers[_normalizeKeystroke(keystroke)] = handler;
337+ }
338+
339+ /*!
340+ Unset global handler for a given keystroke.
341+ NOTE: owned handlers will not be affected
342+ */
343+ function unsetGlobalHandler(keystroke) {
344+ delete _globalHandlers[_normalizeKeystroke(keystroke)];
345+ }
346+
347+ /*!
348+ Unset all global handlers.
349+ NOTE: owned handlers will not be affected
350+ */
351+ function unsetAllGlobalHandlers() {
352+ _globalHandlers = [];
353+ }
354+
355+ property var activeStack: []
356+
357+ property var _handlers : []
358+
359+ property var _globalHandlers : []
360+
361+ /*!
362+ Validate and normalize keystroke description.
363+
364+ This function 'repairs' strings that depict keystroke to match following template:
365+ [<mod_key>+...]<key>
366+ where:
367+ * `mod_key`is one of the modifier keys defined as `allowedModifiers` below
368+ * given `mod_key` is supplied at most once
369+ * `key` is an lowercase alphanumeric character - [a-z0-9]
370+ * supplied `mod_key`s occur in alphabetic order
371+
372+ Examples:
373+ _normalizeKeystroke('ctrl+alt+K') -> 'alt+ctrl+k'
374+ _normalizeKeystroke('Q') -> 'q'
375+ _normalizeKeystroke('shift+W') -> 'shift+w'
376+
377+ Returns normalized keystroke
378+ Throws an `Error` when parsing of keystroke string failed
379+ */
380+ function _normalizeKeystroke(keystroke) {
381+ var allowedModifiers = ['alt', 'ctrl', 'shift'];
382+ var parts = keystroke.toLowerCase().split('+');
383+ var k = parts.pop();
384+ if (!k) {
385+ throw new Error('Missing key in the keystroke. Got: "%1"'.arg(keystroke));
386+ }
387+ if (!k.match(/^[a-z0-9]$/)) {
388+ throw new Error('Picked key "%1" is not an alphanumeric. Got: "%2"'.arg(k).arg(keystroke));
389+ }
390+
391+ var modifiers = {}
392+
393+ while (parts.length > 0) {
394+ var modifier = parts.shift();
395+ if (allowedModifiers.indexOf(modifier) < 0) {
396+ throw new Error('Unknown modifier key "%1". Allowed modifiers: %2'.arg(modifier).arg(allowedModifiers));
397+ }
398+ modifiers[modifier] = true;
399+ }
400+ var normalizedKeystroke = '';
401+ for (var i in allowedModifiers) {
402+ if (allowedModifiers[i] in modifiers) {
403+ normalizedKeystroke += allowedModifiers[i] + '+';
404+ }
405+ }
406+ normalizedKeystroke += k;
407+ return normalizedKeystroke;
408+ }
409+}
410
411=== modified file 'checkbox-touch/components/ManualIntroPage.qml'
412--- checkbox-touch/components/ManualIntroPage.qml 2016-06-30 12:19:17 +0000
413+++ checkbox-touch/components/ManualIntroPage.qml 2016-07-11 12:39:56 +0000
414@@ -51,8 +51,12 @@
415 trailingActionBar {
416 objectName: 'trailingActionBar'
417 actions: [
418- AddCommentAction {},
419- SkipAction {}
420+ AddCommentAction {
421+ id: addCommentAction
422+ },
423+ SkipAction {
424+ id: skipAction
425+ }
426 ]
427 }
428 }
429@@ -61,6 +65,7 @@
430 header: test["name"]
431 body: test["description"]
432 Button {
433+ id: continueButton
434 objectName: "continueButton"
435 color: UbuntuColors.green
436 Layout.fillWidth: true
437@@ -70,4 +75,9 @@
438 }
439 }
440 }
441+ Component.onCompleted: {
442+ rootKeysDelegator.setHandler('alt+s', manualIntroPage, skipAction.trigger);
443+ rootKeysDelegator.setHandler('alt+c', manualIntroPage, addCommentAction.trigger);
444+ rootKeysDelegator.setHandler('alt+t', manualIntroPage, continueButton.clicked);
445+ }
446 }
447
448=== modified file 'checkbox-touch/components/QmlConfinedPage.qml'
449--- checkbox-touch/components/QmlConfinedPage.qml 2016-06-30 12:19:17 +0000
450+++ checkbox-touch/components/QmlConfinedPage.qml 2016-07-11 12:39:56 +0000
451@@ -86,8 +86,12 @@
452 trailingActionBar {
453 objectName: 'trailingActionBar'
454 actions: [
455- AddCommentAction {},
456- SkipAction {}
457+ AddCommentAction {
458+ id: addCommentAction
459+ },
460+ SkipAction {
461+ id: skipAction
462+ }
463 ]
464 }
465 }
466@@ -112,6 +116,7 @@
467 }
468
469 LatchButton {
470+ id: continueButton
471 objectName: "continueButton"
472 color: UbuntuColors.green
473 Layout.fillWidth: true
474@@ -148,4 +153,9 @@
475 visible: false
476 }
477 }
478+ Component.onCompleted: {
479+ rootKeysDelegator.setHandler('alt+s', qmlNativePage, skipAction.trigger);
480+ rootKeysDelegator.setHandler('alt+c', qmlNativePage, addCommentAction.trigger);
481+ rootKeysDelegator.setHandler('alt+t', qmlNativePage, continueButton.clicked);
482+ }
483 }
484
485=== modified file 'checkbox-touch/components/QmlNativePage.qml'
486--- checkbox-touch/components/QmlNativePage.qml 2016-06-30 12:19:17 +0000
487+++ checkbox-touch/components/QmlNativePage.qml 2016-07-11 12:39:56 +0000
488@@ -59,8 +59,12 @@
489 trailingActionBar {
490 objectName: 'trailingActionBar'
491 actions: [
492- AddCommentAction {},
493- SkipAction {}
494+ AddCommentAction {
495+ id: addCommentAction
496+ },
497+ SkipAction {
498+ id: skipAction
499+ }
500 ]
501 }
502 }
503@@ -70,6 +74,7 @@
504 body: test["description"]
505
506 LatchButton {
507+ id: continueButton
508 objectName: "continueButton"
509 color: UbuntuColors.green
510 Layout.fillWidth: true
511@@ -107,4 +112,9 @@
512 }
513 }
514 }
515+ Component.onCompleted: {
516+ rootKeysDelegator.setHandler('alt+s', qmlNativePage, skipAction.trigger);
517+ rootKeysDelegator.setHandler('alt+c', qmlNativePage, addCommentAction.trigger);
518+ rootKeysDelegator.setHandler('alt+t', qmlNativePage, continueButton.clicked);
519+ }
520 }
521
522=== modified file 'checkbox-touch/components/SelectionPage.qml'
523--- checkbox-touch/components/SelectionPage.qml 2016-06-30 12:19:17 +0000
524+++ checkbox-touch/components/SelectionPage.qml 2016-07-11 12:39:56 +0000
525@@ -36,6 +36,7 @@
526 signal selectionDone(var selected_id_list)
527 property string continueText: i18n.tr("Continue")
528 readonly property alias model: selectionModel
529+ property alias keys: keysDelegator
530 property bool onlyOneAllowed: false
531 property bool emptyAllowed: false
532 property bool largeBuffer: false
533@@ -44,9 +45,15 @@
534 visible: false
535 flickable: null
536 property var selectedCount : 0
537+ property var filteredSelectedCount: 0
538 property var disabledSelectedCount: 0
539+ property var filter: new RegExp('.*');
540 state : selectedCount > 0 ? "nonempty selection" :
541 (disabledSelectedCount > 0 ? "disabled only selection" : "empty selection")
542+ ListModel {
543+ // This model holds all items that can be selected, even when filtered-out
544+ id: selectionModel
545+ }
546
547 // A function that needs to be called after changes are done to the model
548 // to re-count number of selected items on the list
549@@ -54,14 +61,30 @@
550 selectedCount = 0;
551 disabledSelectedCount = 0;
552 for (var i=0; i < selectionModel.count; i++) {
553- if (selectionModel.get(i).mod_selected) {
554- if (selectionModel.get(i).mod_disabled) {
555+ var modelItem = selectionModel.get(i);
556+ if (modelItem.mod_selected) {
557+ if (modelItem.mod_disabled) {
558 disabledSelectedCount++;
559 } else {
560 selectedCount++;
561 }
562 }
563 }
564+ updateFilteredModel();
565+ }
566+ function updateFilteredModel() {
567+ filteredSelectionModel.clear();
568+ filteredSelectedCount = 0;
569+ for (var i=0; i < selectionModel.count; i++) {
570+ var modelItem = selectionModel.get(i);
571+ modelItem['fullListIndex'] = i;
572+ if (modelItem.mod_name.search(filter) > -1) {
573+ filteredSelectionModel.append(modelItem);
574+ if (modelItem.mod_selected) {
575+ filteredSelectedCount++;
576+ }
577+ }
578+ }
579 }
580 function gatherSelection() {
581 var selected_id_list = [];
582@@ -77,28 +100,27 @@
583 continueButton.unlatch();
584 }
585 function deselectAll() {
586- selectedCount = 0;
587- disabledSelectedCount = 0;
588- for (var i=0; i<selectionModel.count; i++) {
589- if (!selectionModel.get(i)["mod_disabled"]) {
590- selectionModel.setProperty(i, "mod_selected", false);
591- } else {
592- if (selectionModel.get(i).mod_selected) {
593- disabledSelectedCount++;
594+ for (var i=0; i<filteredSelectionModel.count; i++) {
595+ var modelItem = filteredSelectionModel.get(i);
596+ if (!modelItem.mod_disabled) {
597+ if (modelItem.mod_selected) {
598+ filteredSelectionModel.setProperty(i, "mod_selected", false);
599+ selectionModel.setProperty(modelItem.fullListIndex, "mod_selected", false);
600+ selectedCount--;
601+ filteredSelectedCount--;
602 }
603 }
604 }
605 }
606 function selectAll() {
607- selectedCount = 0;
608- disabledSelectedCount = 0;
609- for (var i=0; i<selectionModel.count; i++) {
610- if (!selectionModel.get(i)["mod_disabled"]) {
611- selectionModel.setProperty(i, "mod_selected", true);
612- selectedCount++;
613- } else {
614- if (selectionModel.get(i).mod_selected) {
615- disabledSelectedCount++;
616+ for (var i=0; i<filteredSelectionModel.count; i++) {
617+ var modelItem = filteredSelectionModel.get(i);
618+ if (!modelItem.mod_disabled) {
619+ if (!modelItem.mod_selected) {
620+ filteredSelectionModel.setProperty(i, "mod_selected", true);
621+ selectionModel.setProperty(modelItem.fullListIndex, "mod_selected", true);
622+ selectedCount++;
623+ filteredSelectedCount++;
624 }
625 }
626 }
627@@ -117,12 +139,25 @@
628 visible: !onlyOneAllowed
629 onTriggered: {
630 if (state === "empty selection" || state == "disabled only selection") {
631- selectAll();
632+ if (!onlyOneAllowed) // still reachable via key shortcut
633+ selectAll();
634 }
635 else if (state === "nonempty selection") {
636 deselectAll();
637 }
638-
639+ }
640+ },
641+ Action {
642+ id: findAction
643+ text: i18n.tr("Find")
644+ iconName: 'find'
645+ onTriggered: {
646+ if (!searchBox.visible) {
647+ searchBox.visible = true;
648+ searchBox.forceActiveFocus();
649+ } else {
650+ searchBox.visible = false;
651+ }
652 }
653 }
654 ]
655@@ -145,6 +180,32 @@
656 }
657 ]
658
659+ KeysDelegator {
660+ id: keysDelegator
661+ onKeyPressed: {
662+ var c = event.text
663+ if (event.modifiers == 0 && c.search(/[a-z]/, 'i') > -1) {
664+ searchBox.insert(searchBox.cursorPosition, c)
665+ searchBox.forceActiveFocus();
666+ searchBox.visible = true
667+ searchBox.focus = true
668+ }
669+ if (event.key == Qt.Key_Escape) {
670+ searchBox.text = '';
671+ searchBox.focus = false;
672+ searchBox.visible = false;
673+ }
674+ }
675+ Component.onCompleted: {
676+ rootKeysDelegator.setHandler('alt+t', root, function() {
677+ if (selectedCount > 0 || disabledSelectedCount > 0) {
678+ continueButton.clicked();
679+ }
680+ });
681+ rootKeysDelegator.setHandler('ctrl+a', root, toggleSelection.trigger);
682+ }
683+ }
684+
685 ColumnLayout {
686 spacing: units.gu(3)
687 anchors {
688@@ -158,6 +219,21 @@
689 rightMargin: units.gu(1)
690 }
691
692+ TextField {
693+ id: searchBox
694+ Layout.fillWidth: true
695+ visible: false
696+ onTextChanged: {
697+ filter = new RegExp('.*' + text, 'i');
698+ updateFilteredModel();
699+ }
700+ onFocusChanged: {
701+ if (text == '' && focus == false) {
702+ visible = false;
703+ }
704+ }
705+ }
706+
707 Component {
708 id: sectionHeading
709 Item {
710@@ -191,7 +267,7 @@
711
712 UbuntuListView {
713 model: ListModel {
714- id: selectionModel
715+ id: filteredSelectionModel
716 }
717 objectName: "listView"
718 Layout.fillWidth: true
719@@ -218,11 +294,23 @@
720 // Toggle the mod_selected property
721 onClicked: {
722 if (onlyOneAllowed && !checked && selectedCount > 0) {
723- // clear other selections
724 deselectAll();
725+ // clear other selections on the original list
726+ for (var i=0; i < selectionModel.count; i++) {
727+ var modelItem = selectionModel.get(i);
728+ if (!modelItem.mod_disabled) {
729+ if (modelItem.mod_selected) {
730+ selectionModel.setProperty(i, "mod_selected", false);
731+ selectedCount--;
732+ }
733+ }
734+ }
735 }
736- selectionModel.setProperty(index, 'mod_selected', !checked);
737+ filteredSelectionModel.setProperty(index, 'mod_selected', !checked);
738 selectedCount += checked ? 1 : -1;
739+ // propagate selection to the original list
740+ selectionModel.setProperty(fullListIndex, 'mod_selected', checked);
741+
742 }
743 }
744 onClicked: checkBox.clicked()
745
746=== modified file 'checkbox-touch/components/TestVerificationPage.qml'
747--- checkbox-touch/components/TestVerificationPage.qml 2016-06-30 12:19:17 +0000
748+++ checkbox-touch/components/TestVerificationPage.qml 2016-07-11 12:39:56 +0000
749@@ -50,13 +50,19 @@
750 trailingActionBar {
751 objectName: 'trailingActionBar'
752 actions: [
753- AddCommentAction {},
754- SkipAction {}
755+ AddCommentAction {
756+ id: addCommentAction
757+ },
758+ SkipAction {
759+ id: skipAction
760+ }
761 ]
762 }
763 }
764
765 TestPageBody {
766+ id: body
767+
768 header: test["name"]
769 body: test["verificationDescription"]
770
771@@ -99,6 +105,13 @@
772 }
773 }
774 }
775+ Component.onCompleted: {
776+ rootKeysDelegator.setHandler('alt+s', testVerification, skipAction.trigger);
777+ rootKeysDelegator.setHandler('alt+c', testVerification, addCommentAction.trigger);
778+ rootKeysDelegator.setHandler('alt+p', testVerification, passButton.clicked);
779+ rootKeysDelegator.setHandler('alt+f', testVerification, failButton.clicked);
780+ }
781+
782
783 function latchingTestDone() {
784 passButton.state = "latched";
785
786=== modified file 'checkbox-touch/components/UserInteractSummaryPage.qml'
787--- checkbox-touch/components/UserInteractSummaryPage.qml 2016-06-30 12:19:17 +0000
788+++ checkbox-touch/components/UserInteractSummaryPage.qml 2016-07-11 12:39:56 +0000
789@@ -49,8 +49,12 @@
790 trailingActionBar {
791 objectName: 'trailingActionBar'
792 actions: [
793- AddCommentAction {},
794- SkipAction {}
795+ AddCommentAction {
796+ id: addCommentAction
797+ },
798+ SkipAction {
799+ id: skipAction
800+ }
801 ]
802 }
803 }
804@@ -97,6 +101,7 @@
805 }
806
807 Button {
808+ id: continueButton
809 color: UbuntuColors.green
810 objectName: "continueButton"
811 Layout.fillWidth: true
812@@ -106,4 +111,9 @@
813 }
814 }
815 }
816+ Component.onCompleted: {
817+ rootKeysDelegator.setHandler('alt+s', userInteractSummary, skipAction.trigger);
818+ rootKeysDelegator.setHandler('alt+c', userInteractSummary, addCommentAction.trigger);
819+ rootKeysDelegator.setHandler('alt+t', userInteractSummary, continueButton.clicked);
820+ }
821 }
822
823=== modified file 'checkbox-touch/components/WelcomePage.qml'
824--- checkbox-touch/components/WelcomePage.qml 2016-06-30 12:19:17 +0000
825+++ checkbox-touch/components/WelcomePage.qml 2016-07-11 12:39:56 +0000
826@@ -124,4 +124,8 @@
827 text: i18n.tr("Start testing")
828 onLatchedClicked: startTestingTriggered();
829 }
830+
831+ Component.onCompleted: {
832+ rootKeysDelegator.setHandler('alt+t', welcomePage, startTestButton.clicked);
833+ }
834 }
835
836=== added file 'checkbox-touch/tests/unit/tst_KeysDelegator.qml'
837--- checkbox-touch/tests/unit/tst_KeysDelegator.qml 1970-01-01 00:00:00 +0000
838+++ checkbox-touch/tests/unit/tst_KeysDelegator.qml 2016-07-11 12:39:56 +0000
839@@ -0,0 +1,242 @@
840+/*
841+ * This file is part of Checkbox
842+ *
843+ * Copyright 2015 Canonical Ltd.
844+ *
845+ * Authors:
846+ * - Maciej Kisielewski <maciej.kisielewski@canonical.com>
847+ *
848+ * This program is free software; you can redistribute it and/or modify
849+ * it under the terms of the GNU General Public License as published by
850+ * the Free Software Foundation; version 3.
851+ *
852+ * This program is distributed in the hope that it will be useful,
853+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
854+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
855+ * GNU General Public License for more details.
856+ *
857+ * You should have received a copy of the GNU General Public License
858+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
859+ */
860+
861+import QtQuick 2.0
862+import QtTest 1.0
863+import Ubuntu.Test 1.0
864+import "../../components"
865+
866+Item {
867+ KeysDelegator {
868+ id: kd
869+ }
870+ UbuntuTestCase {
871+ name: "KeysDelegatorTestCase"
872+ when: windowShown
873+
874+ function cleanup() {
875+ kd.unsetAllHandlers();
876+ }
877+
878+ function test_normalizeKeyStroke_LowerCaseKeyOnly() {
879+ var result = kd._normalizeKeystroke('a');
880+ compare(result, 'a');
881+ }
882+
883+ function test_normalizeKeystroke_UpperCaseKeyOnly() {
884+ var result = kd._normalizeKeystroke('A');
885+ compare(result, 'a');
886+ }
887+
888+ function test_normalize_UnorderedModifiers() {
889+ var result = kd._normalizeKeystroke('shift+alt+ctrl+X');
890+ compare(result, 'alt+ctrl+shift+x');
891+ }
892+
893+ function test_normalizeKeystroke_ctrlOnly() {
894+ var result = kd._normalizeKeystroke('ctrl+5');
895+ compare(result, 'ctrl+5');
896+ }
897+
898+ function test_normalizeKeystroke_modifierRepeated() {
899+ var result = kd._normalizeKeystroke('ctrl+alt+alt+ctrl+q');
900+ compare(result, 'alt+ctrl+q');
901+ }
902+
903+ function test_normalizeKeystroke_throws_noKey() {
904+ try {
905+ var result = kd._normalizeKeystroke('ctrl+');
906+ fail("Didn't fail with missing key");
907+ } catch (e) {
908+ verify(e.message.match(/Missing key in the keystroke.*/), '"Missing key" error message matches');
909+ }
910+
911+ }
912+
913+ function test_normalizeKeystroke_throws_unknownModifier() {
914+ try {
915+ var result = kd._normalizeKeystroke('select+X');
916+ fail("Didn't fail with unknown modifier");
917+ } catch (e) {
918+ verify(e.message.match(/Unknown modifier key.*/), '"Unknown modifier key" error message matches');
919+ }
920+ }
921+
922+ function test_normalizeKeystroke_thows_badKey() {
923+ try {
924+ var result = kd._normalizeKeystroke('ctrl+[');
925+ fail("Didn't fail with '[' as the key");
926+ } catch (e) {
927+ verify(e.message.match(/Picked key "\[" is not an alphanumeric.*/), '"Picked key is not an alphanumeric" error message matches');
928+ }
929+ }
930+
931+ function test_normalizeKeystroke_thows_reverseOrder() {
932+ try {
933+ var result = kd._normalizeKeystroke('b+ctrl');
934+ fail("Didn't fail with 'ctrl' as the key");
935+ } catch (e) {
936+ verify(e.message.match(/Picked key "ctrl" is not an alphanumeric.*/), '"Picked key is not an alphanumeric" error message matches');
937+ }
938+ }
939+
940+ function test_endToEnd_smoke() {
941+ var result = 0;
942+ var handler = function() {
943+ result++;
944+ }
945+ kd.setHandler('alt+r', 'mock', handler)
946+ var altR = {"objectName":"","key":82,"text":"r","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":27,"accepted":false}
947+ kd.activeStack.push('mock');
948+ kd.keyPress(altR);
949+ kd.activeStack.pop();
950+ compare(result, 1);
951+ }
952+
953+ function test_endToEnd_unsetting() {
954+ var result = 0;
955+ var handler = function() {
956+ result++;
957+ }
958+ kd.setHandler('alt+e', 'mock', handler)
959+ kd.unsetHandler('alt+e', 'mock')
960+ var altE = {"objectName":"","key":69,"text":"e","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":26,"accepted":false}
961+ kd.activeStack.push('mock');
962+ kd.keyPress(altE);
963+ kd.activeStack.pop();
964+ compare(result, 0);
965+ }
966+ function test_unsetHandler_nonExisting() {
967+ kd.unsetHandler('shift+m');
968+ }
969+
970+ function test_endToEnd_overwriteHandler() {
971+ var result = '';
972+ var handlerFoo = function() {
973+ result += 'foo'
974+ }
975+ var handlerBar = function() {
976+ result += 'bar'
977+ }
978+ kd.setHandler('alt+w', 'mock', handlerFoo)
979+ kd.setHandler('alt+w', 'mock', handlerBar) // this should overwrite the handler, making handlerBar the one in force
980+ var altW = {"objectName":"","key":87,"text":"w","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":25,"accepted":false}
981+ kd.activeStack.push('mock');
982+ kd.keyPress(altW);
983+ kd.activeStack.pop();
984+ compare(result, 'bar');
985+ }
986+
987+ function test_endToEnd_global_smoke() {
988+ var result = 0;
989+ var handler = function() {
990+ result++;
991+ }
992+ kd.setGlobalHandler('ctrl+g', handler);
993+ var ctrlG = {"objectName":"","key":71,"text":"\u0007","modifiers":67108864,"isAutoRepeat":false,"count":1,"nativeScanCode":42,"accepted":false}
994+ kd.keyPress(ctrlG);
995+ compare(result, 1);
996+ }
997+
998+ function test_endToEnd_global_unsetting() {
999+ var result = 0;
1000+ var handler = function() {
1001+ result++;
1002+ }
1003+ kd.setGlobalHandler('alt+e', handler)
1004+ kd.unsetGlobalHandler('alt+e')
1005+ var altE = {"objectName":"","key":69,"text":"e","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":26,"accepted":false}
1006+ kd.keyPress(altE);
1007+ compare(result, 0);
1008+ }
1009+
1010+ function test_endToEnd_ownedOverGlobal() {
1011+ var result = '';
1012+ var ownedHandler = function() {
1013+ result += 'owned';
1014+ }
1015+ var globalHandler = function() {
1016+ result += 'global';
1017+ }
1018+ kd.setHandler('ctrl+h', 'mock', ownedHandler);
1019+ kd.setGlobalHandler('ctrl+h', globalHandler);
1020+ var ctrlH = {"objectName":"","key":72,"text":"\b","modifiers":67108864,"isAutoRepeat":false,"count":1,"nativeScanCode":43,"accepted":false};
1021+ kd.activeStack.push('mock');
1022+ kd.keyPress(ctrlH);
1023+ kd.activeStack.pop();
1024+ compare(result, 'owned');
1025+ }
1026+
1027+ function test_keyPress_capturesHandled() {
1028+ var result = '';
1029+ var handler = function() {
1030+ result += 'from handler'
1031+ }
1032+ var signalHandler = function() {
1033+ result += 'from signal'
1034+ }
1035+ kd.onKeyPressed.connect(signalHandler);
1036+
1037+ kd.setHandler('alt+r', 'mock', handler)
1038+ var altR = {"objectName":"","key":82,"text":"r","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":27,"accepted":false}
1039+ kd.activeStack.push('mock');
1040+ kd.keyPress(altR);
1041+ kd.activeStack.pop();
1042+ compare(result, 'from handler');
1043+ }
1044+
1045+ function test_keyPress_forwardsUnhandled() {
1046+ var result = '';
1047+ var handler = function() {
1048+ result += 'from handler'
1049+ }
1050+ var signalHandler = function() {
1051+ result += 'from signal'
1052+ }
1053+ kd.onKeyPressed.connect(signalHandler);
1054+
1055+ kd.setHandler('alt+x', 'mock', handler)
1056+ var altR = {"objectName":"","key":82,"text":"r","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":27,"accepted":false}
1057+ kd.activeStack.push('mock');
1058+ kd.keyPress(altR);
1059+ kd.activeStack.pop();
1060+ compare(result, 'from signal');
1061+ }
1062+
1063+ function test_keyPress_forwardsUnhandledAfterUnsetting() {
1064+ var result = '';
1065+ var handler = function() {
1066+ result += 'from handler'
1067+ }
1068+ var signalHandler = function() {
1069+ result += 'from signal'
1070+ }
1071+ kd.onKeyPressed.connect(signalHandler);
1072+
1073+ kd.setHandler('alt+r', 'mock', handler)
1074+ kd.unsetHandler('alt+r', 'mock', handler)
1075+ var altR = {"objectName":"","key":82,"text":"r","modifiers":134217728,"isAutoRepeat":false,"count":1,"nativeScanCode":27,"accepted":false}
1076+ kd.keyPress(altR);
1077+ compare(result, 'from signal');
1078+ }
1079+ }
1080+}
1081+

Subscribers

People subscribed via source and target branches