-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Verify client shortcuts are unique #2209
Conversation
Nerd sniped by @cfm's suggestion at https://github.com/freedomofpress/securedrop-client/wiki/Keyboard-Shortcuts This test is a bit fragile, e.g. if you write Two other options:
Cory's part two was to have us auto-generate a SHORTCUTS.md file, with documentation for each one. I think this is doable, we'd just want some convention of requiring a documentation line right before the setShortcut() invocation. |
0f3b98f
to
aaf3230
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No good deed goes unpunished. Forgive me, @legoktm.
In all seriousness: The test plan checks out, and this adds a useful dimension to our test coverage, which we can refine as we expand our use and understanding of keyboard shortcuts in Qt. The discussion I've started inline should be resolvable with a comment, and then I say let's keep it!
client/tests/gui/test_shortcuts.py
Outdated
def test_shortcuts_are_unique(mocker): | ||
""" | ||
Look through the codebase for setShortcut() calls, | ||
verify the argument passed is unique |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't go into this level of detail in https://github.com/freedomofpress/securedrop-client/wiki/Keyboard-Shortcuts, but it's worth noting that this is not a strict requirement, just a policy we could adopt (and use this test to help enforce). Here's what I've realized after sleeping on it:
Shortcuts don't have to be unique globally always; they have to be unique per context per point in time. So this is a valid counterexample across times:
Time | Context | Shortcut | Action |
---|---|---|---|
1 | WindowShortcut (default) |
Ctrl + Q | Quit |
2 | WindowShortcut (default) |
Ctrl + Q | File bug report |
Now, this would be a terrible UX, which this test helps prevent. But here's another valid (and more realistic) counterexample across contexts and widgets:
Time | Context | Shortcut | Action |
---|---|---|---|
1 | WidgetShortcut for ReplyBox 1 |
Ctrl + Enter | Send from ReplyBox 1 |
1 | WidgetShortcut for ReplyBox 2 |
Ctrl + Enter | Send from ReplyBox 21 |
Again, this is valid! The shortcuts don't collide in the UI because they're scoped to the right context. But they also don't collide in the code, and wouldn't be caught by this test, because they're only defined once, on the same widget class before it's instantiated multiple times. With the WindowShortcut
context, they would collide, and Qt would complain about them, but they still wouldn't be caught by this test.
So a rigorous test would have to search for duplicate (shortcut, context)
pairs2 for all combinations of widgets actually displayed in a program run. That's at best a state-space explosion and at worst the halting problem: either way, infeasible.
In conclusion: I still think this test is a good linting measure; it's just both (a) not a complete check and (b) quite nuanced in what it does check. So let's just document that:
verify the argument passed is unique | |
verify the argument passed is unique. This won't catch collisions between instances of a class that calls `setShortcut()` without also adjusting `setShortcutContext()`. It also enforces a policy that we don't want to reuse the same global shortcut for different purposes (or even with different handlers) at different times. Either of these limitations may be worth revisiting for UX reasons in the future. |
Footnotes
-
In some application that displays multiple instances of the same widget at one time. Which we do, just not
ReplyBox
es. ↩ -
Actually duplicate shortcuts down the
ShortcutContext
hierarchy, but I digress.... ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So a rigorous test would have to search for duplicate
(shortcut, context)
pairs2 for all combinations of widgets actually displayed in a program run. That's at best a state-space explosion and at worst the halting problem: either way, infeasible.
Yeah. I wonder if we can add a hook to QAction::event: Ambiguous shortcut overload
that causes tests to fail, but from what I can tell, it only actual triggers when you press the shortcut, so we'd need tests that actually test the shortcuts...
client/securedrop_client/gui/main.py
Outdated
@@ -93,7 +93,7 @@ def __init__( | |||
# Actions | |||
quit = QAction(_("Quit"), self) | |||
quit.setIcon(QIcon.fromTheme("application-exit")) | |||
quit.setShortcut(QKeySequence.Quit) | |||
quit.setShortcut(Qt.CTRL + Qt.Key_Q) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means giving up QKeySequence
's cross-platform defaults, although QKeySequence.Quit
seems to resolve to Ctrl + Q everywhere in practice....
I wonder if the simpler method for this is: class Shortcuts:
QUIT = QKeySequence.Quit
DOWNLOAD_ALL = Qt.CTRL + Qt.Key_D
... Then it is trivial to verify there are no overlaps both by humans visually and a test that enumerates through them. And then a semgrep rule that verifies all setShortcut class use the Shortcuts class. And then our documentation generator just needs to look at one file in a very specific format instead of scanning through the entire codebase. |
I like that approach for checking that our intentions don't collide, and I'm happy to do a pass on that tomorrow just to close out the nerd-swipe. :-) Per #2209 (comment), I still don't think we can do much to check that our instance-level assignments don't collide at runtime, but let's not block on that. |
aaf3230
to
03ec2ec
Compare
I need to rebase, etc. this but a first pass at a |
03ec2ec
to
63228b3
Compare
If you have two shortcuts that point to the same key combo, you'll get "QAction::event: Ambiguous shortcut overload: <sequence>" in the logs and neither action will be taken. Centralize them in one place so it's easy for humans to spot duplicates. A test verifies uniqueness as well. A semgrep rule enforces that people use the new Shortcuts enum instead of just writing the sequence in the function call directly.
63228b3
to
28eb8b1
Compare
OK, should be ready for a re-review now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. Thanks, @legoktm.
Status
Ready for review
Description
If you have two shortcuts that point to the same key combo, you'll get "QAction::event: Ambiguous shortcut overload: " in the logs and neither action will be taken.
Centralize them in one place so it's easy for humans to spot duplicates. A test verifies uniqueness as well.
A semgrep rule enforces that people use the new Shortcuts enum instead of just writing the sequence in the function call directly.
Test Plan
Checklist