-
Notifications
You must be signed in to change notification settings - Fork 58
Improve reliability of catching modifier key-up events #156
Changes from 1 commit
94fd11b
53d9c89
1888c70
faca320
5695cc1
070c406
c3bcde2
f91f755
c9b32cc
b5d6d15
800dc79
8fde502
1bd09a5
d77ccbd
13375ee
4fc8165
2e38501
1ced703
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
KeyBinding = require '../src/key-binding' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great idea unit testing this class. |
||
|
||
describe "KeyBinding", -> | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: 🔥 this newline. |
||
describe "is_matched_modifer_keydown_keyup", -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method name is also quite a mouthful. I'm not sure I can come up with anything better though. |
||
|
||
describe "returns false when the binding...", -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this |
||
it "has no modifier keys", -> | ||
kb = new KeyBinding('test', 'whatever', 'a', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "has no keyups", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl-a', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "is a bare modifier", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "is a bare modifier keyup", -> | ||
kb = new KeyBinding('test', 'whatever', '^ctrl', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "has mismatched last_keystroke: ctrl ^shift", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl-a ^shift', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "has mismatched last_keystroke: cmd ^alt", -> | ||
kb = new KeyBinding('test', 'whatever', 'cmd-a ^alt', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "has partially mismatched last_keystroke: ctrl-cmd ^ctrl", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl-cmd-a ^ctrl', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "has partially mismatched last_keystroke: ctrl-cmd ^cmd", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl-cmd-a ^cmd', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
it "has partially mismatched last_keystroke: ctrl ^ctrl-shift", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl ^ctrl-shift', 'body', 0) | ||
assert(not kb.is_matched_modifer_keydown_keyup()) | ||
|
||
describe "returns true when the binding...", -> | ||
it "has a matched ctrl", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl-a ^ctrl', 'body', 0) | ||
assert(kb.is_matched_modifer_keydown_keyup()) | ||
it "has a matched shift", -> | ||
kb = new KeyBinding('test', 'whatever', 'shift-a ^shift', 'body', 0) | ||
assert(kb.is_matched_modifer_keydown_keyup()) | ||
it "has a matched alt", -> | ||
kb = new KeyBinding('test', 'whatever', 'alt-a ^alt', 'body', 0) | ||
assert(kb.is_matched_modifer_keydown_keyup()) | ||
it "has a matched cmd", -> | ||
kb = new KeyBinding('test', 'whatever', 'cmd-a ^cmd', 'body', 0) | ||
assert(kb.is_matched_modifer_keydown_keyup()) | ||
it "has a matched ctrl-shift", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl-shift-a ^ctrl-shift', 'body', 0) | ||
assert(kb.is_matched_modifer_keydown_keyup()) | ||
it "has matched bare last_keystroke", -> | ||
kb = new KeyBinding('test', 'whatever', 'ctrl ^ctrl', 'body', 0) | ||
assert(kb.is_matched_modifer_keydown_keyup()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -187,19 +187,31 @@ nonAltModifiedKeyForKeyboardEvent = (event) -> | |
else | ||
characters.unmodified | ||
|
||
exports.MODIFIERS = MODIFIERS | ||
|
||
exports.characterForKeyboardEvent = (event) -> | ||
event.key unless event.ctrlKey or event.metaKey | ||
|
||
exports.calculateSpecificity = calculateSpecificity | ||
|
||
exports.isBareModifier = (keystroke) -> ENDS_IN_MODIFIER_REGEX.test(keystroke) | ||
|
||
exports.isModifierKeyup = (keystroke) -> keystroke.startsWith('^') and ENDS_IN_MODIFIER_REGEX.test(keystroke) | ||
|
||
exports.keydownEvent = (key, options) -> | ||
return buildKeyboardEvent(key, 'keydown', options) | ||
|
||
exports.keyupEvent = (key, options) -> | ||
return buildKeyboardEvent(key, 'keyup', options) | ||
|
||
exports.getModKeys = (keystroke) -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We generally spell stuff out fully for maximal clarity throughout the Atom codebase. |
||
keys = keystroke.split('-') | ||
mod_keys = [] | ||
for key in keys when MODIFIERS.has(key) | ||
mod_keys.push(key) | ||
mod_keys | ||
|
||
|
||
buildKeyboardEvent = (key, eventType, {ctrl, shift, alt, cmd, keyCode, target, location}={}) -> | ||
ctrlKey = ctrl ? false | ||
altKey = alt ? false | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ path = require 'path' | |
{Emitter, Disposable, CompositeDisposable} = require 'event-kit' | ||
KeyBinding = require './key-binding' | ||
CommandEvent = require './command-event' | ||
{normalizeKeystrokes, keystrokeForKeyboardEvent, isBareModifier, keydownEvent, keyupEvent, characterForKeyboardEvent, keystrokesMatch} = require './helpers' | ||
{normalizeKeystrokes, keystrokeForKeyboardEvent, isBareModifier, keydownEvent, keyupEvent, characterForKeyboardEvent, keystrokesMatch, isModifierKeyup} = require './helpers' | ||
|
||
Platforms = ['darwin', 'freebsd', 'linux', 'sunos', 'win32'] | ||
OtherPlatforms = Platforms.filter (platform) -> platform isnt process.platform | ||
|
@@ -95,6 +95,10 @@ class KeymapManager | |
pendingStateTimeoutHandle: null | ||
dvorakQwertyWorkaroundEnabled: false | ||
|
||
# Pending matches to bindings that begin with a modifier keydown and and with | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
# the matching modifier keyup | ||
pendingPartialMatchedModifierKeystrokes: null | ||
|
||
### | ||
Section: Construction and Destruction | ||
### | ||
|
@@ -490,7 +494,9 @@ class KeymapManager | |
# | ||
# Godspeed. | ||
|
||
# keystroke is the atom keybind syntax, e.g. 'ctrl-a' | ||
keystroke = @keystrokeForKeyboardEvent(event) | ||
console.log(keystroke) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 |
||
|
||
# We dont care about bare modifier keys in the bindings. e.g. `ctrl y` isnt going to work. | ||
if event.type is 'keydown' and @queuedKeystrokes.length > 0 and isBareModifier(keystroke) | ||
|
@@ -513,6 +519,15 @@ class KeymapManager | |
dispatchedExactMatch = null | ||
partialMatches = @findPartialMatches(partialMatchCandidates, target) | ||
|
||
if @pendingPartialMatchedModifierKeystrokes? and isModifierKeyup(keystroke) | ||
for binding in @pendingPartialMatchedModifierKeystrokes | ||
binding_mod_keyups = getModKeys(binding.keystrokeArray[binding.keystrokeArray.length-1]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be encapsulated in a method in the binding that takes the keystroke string? |
||
keystroke_mod_keyups = getModKeys(keystroke) | ||
if keystroke_mod_keyups.length == 1 and binding_mod_keyups.has(keystroke_mod_keyups[0]) | ||
exactMatchCandidates.push(binding) | ||
# Ian TODO remove from @pendingPartialMatchedModifierKeystrokes | ||
# Ian TODO deal with all the other partial match possibilities | ||
|
||
# If any partial match *was* pending but has now failed to match, add it to | ||
# the list of bindings to disable so we don't attempt to match it again | ||
# during a subsequent event replay by `terminatePendingState`. | ||
|
@@ -525,7 +540,7 @@ class KeymapManager | |
shouldUsePartialMatches = hasPartialMatches | ||
|
||
# Determine if the current keystrokes match any bindings *exactly*. If we | ||
# do find and exact match, the next step depends on whether we have any | ||
# do find an exact match, the next step depends on whether we have any | ||
# partial matches. If we have no partial matches, we dispatch the command | ||
# immediately. Otherwise we break and allow ourselves to enter the pending | ||
# state with a timeout. | ||
|
@@ -552,7 +567,7 @@ class KeymapManager | |
if hasPartialMatches | ||
# When there is a set of bindings like `'ctrl-y', 'ctrl-y ^ctrl'`, | ||
# and a `ctrl-y` comes in, this will allow the `ctrl-y` command to be | ||
# dispatched without waiting for any other keystrokes | ||
# dispatched without waiting for any other keystrokes. | ||
allPartialMatchesContainKeyupRemainder = true | ||
for partialMatch in partialMatches | ||
if keydownExactMatchCandidates.indexOf(partialMatch) < 0 | ||
|
@@ -562,6 +577,7 @@ class KeymapManager | |
shouldUsePartialMatches = false | ||
|
||
if @dispatchCommandEvent(exactMatchCandidate.command, target, event) | ||
console.log('dispatched: ' + exactMatchCandidate.keystrokes) | ||
dispatchedExactMatch = exactMatchCandidate | ||
eventHandled = true | ||
break | ||
|
@@ -700,14 +716,29 @@ class KeymapManager | |
@bindingsToDisable = [] | ||
|
||
enterPendingState: (pendingPartialMatches, enableTimeout) -> | ||
@cancelPendingState() if @pendingStateTimeoutHandle? | ||
if @pendingStateTimeoutHandle? | ||
@cancelPendingState() | ||
else | ||
@buildPendingPartialMatchedModiferKeystrokes() | ||
|
||
@pendingPartialMatches = pendingPartialMatches | ||
if enableTimeout | ||
@pendingStateTimeoutHandle = setTimeout(@terminatePendingState.bind(this, true), @partialMatchTimeout) | ||
|
||
cancelPendingState: -> | ||
buildPendingPartialMatchedModiferKeystrokes: -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would make more sense to store the matched-keydown-keyup style bindings in a separate array all the time rather than filtering them out of the partial matches array later. You could potentially do this in the |
||
@pendingPartialMatchedModifierKeystrokes = null | ||
for match in @pendingPartialMatches? | ||
if match.is_matched_modifer_keydown_keyup() | ||
@pendingPartialMatchedModifierKeystrokes.push(match) | ||
|
||
cancelPendingState: (modifierKeyupMatched = false) -> | ||
clearTimeout(@pendingStateTimeoutHandle) | ||
@pendingStateTimeoutHandle = null | ||
|
||
# Ian TODO perf? | ||
# Preserve pending modifier keydown-keyup matches beyond pending state | ||
@buildPendingPartialMatchedModiferKeystrokes() | ||
|
||
@pendingPartialMatches = null | ||
|
||
# This is called by {::handleKeyboardEvent} when no matching bindings are | ||
|
@@ -718,7 +749,7 @@ class KeymapManager | |
# | ||
# Note that replaying events has a recursive behavior. Replaying will set | ||
# member state (e.g. @queuedKeyboardEvents) just like real events, and will | ||
# likely result in another call this function. The replay process will | ||
# likely result in another call to this function. The replay process will | ||
# potentially replay the events (or a subset of events) several times, while | ||
# disabling bindings here and there. See any spec that handles multiple | ||
# keystrokes failures to match a binding. | ||
|
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.
Nitpick: Should insert an empty line above this second
it
.