Merge lp:~kissiel/checkbox/converged-keyboard-support into lp:checkbox
- converged-keyboard-support
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Pierre Equoy | Approve | ||
Sylvain Pineau (community) | Needs Fixing | ||
Review via email: mp+281205@code.launchpad.net |
Commit message
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:/
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
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)
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.
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
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
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?
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 ?
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.
Pierre Equoy (pieq) wrote : | # |
Yes, good idea for Ctrl+A!
Thanks Maciek :)
Preview Diff
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 | + |
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.