aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2016-02-26 16:47:17 +0000
committerStephen Blott2016-03-05 05:37:40 +0000
commit7c5fb2c312b9140c2dd091f792535ae8f592ecdb (patch)
tree7b9fca6597f6b58f430f633e53a5f4a6b0199fa7
parent27d3d0087c86a6effd25049cbf0d9273eb0af9db (diff)
downloadvimium-7c5fb2c312b9140c2dd091f792535ae8f592ecdb.tar.bz2
Key bindings; initial "generic" class.
This implements a generic front-end class for key handling (a la normal mode). Also: - supports count prefixes (or not) - supports multi-key mappings (longer than two) Also included is a very poor-man's demo. See the bottom of mode_key_handler.coffee for some hard-wired key bindings. IMPORTANT: This does not actually work as Vimium. It's just a demo.
-rw-r--r--content_scripts/mode_key_handler.coffee158
-rw-r--r--content_scripts/vimium_frontend.coffee3
-rw-r--r--manifest.json1
3 files changed, 161 insertions, 1 deletions
diff --git a/content_scripts/mode_key_handler.coffee b/content_scripts/mode_key_handler.coffee
new file mode 100644
index 00000000..c79f1991
--- /dev/null
+++ b/content_scripts/mode_key_handler.coffee
@@ -0,0 +1,158 @@
+
+# The important data structure here is the "keyState". The key state is a non-empty list of objects, the keys
+# of which are key names, and the values are other key-mapping objects or commands (strings). Key-mapping
+# objects can be arbitrarily nested; so we support any length of multi-key mapping.
+#
+# Whenever we consume a key, we append a new copy of the global key mapping to the key state (hence, the
+# global mappings are always available, and the key state is always non-empty).
+
+class KeyHandlerMode extends Mode
+ useCount: true
+ countPrefix: 0
+ keydownEvents: {}
+ keyState: []
+
+ constructor: (options) ->
+ # A function accepting a command name and a count; required.
+ @commandHandler = options.commandHandler ? (->)
+ # A Key mapping structure; required.
+ @keyMapping = options.keyMapping ? {}
+ @useCount = false if options.noCount
+ @reset()
+
+ # We don't pass these options on to super().
+ options = Utils.copyObjectOmittingProperties options, "commandHandler", "keyMapping", "noCount"
+
+ super extend options,
+ keydown: @onKeydown.bind this
+ keypress: @onKeypress.bind this
+ keyup: @onKeyup.bind this
+ # We cannot track matching keydown/keyup events if we lose the focus.
+ blur: (event) => @alwaysContinueBubbling =>
+ @keydownEvents = {} if event.target == window
+
+ onKeydown: (event) ->
+ keyChar = KeyboardUtils.getKeyCharString event
+
+ if KeyboardUtils.isEscape event
+ if @isInResetState()
+ @continueBubbling
+ else
+ @reset()
+ DomUtils.suppressKeyupAfterEscape handlerStack
+ false # Suppress event.
+
+ else if keyChar and @keyCharIsKeyStatePrefix keyChar
+ @advanceKeyState keyChar
+ commands = @keyState.filter (entry) -> "string" == typeof entry
+ @invokeCommand commands[0] if 0 < commands.length
+ false # Suppress event.
+
+ else
+ # We did not handle the event, but we might handle the subsequent keypress event. If we *will* be
+ # handling that event, then we need to suppress propagation of this keydown event to prevent triggering
+ # page features like Google instant search.
+ keyChar = KeyboardUtils.getKeyChar event
+ if keyChar and (@keyCharIsKeyStatePrefix(keyChar) or @isCountKey keyChar)
+ DomUtils.suppressPropagation event
+ @keydownEvents[@getEventCode event] = true
+ @stopBubblingAndTrue
+ else
+ @countPrefix = 0 if keyChar
+ @continueBubbling
+
+ onKeypress: (event) ->
+ keyChar = KeyboardUtils.getKeyCharString event
+ if keyChar and @keyCharIsKeyStatePrefix keyChar
+ @advanceKeyState keyChar
+ commands = @keyState.filter (entry) -> "string" == typeof entry
+ @invokeCommand commands[0] if 0 < commands.length
+ false # Suppress event.
+ else if keyChar and @isCountKey keyChar
+ @countPrefix = @countPrefix * 10 + parseInt keyChar
+ false # Suppress event.
+ else
+ @continueBubbling
+
+ onKeyup: (event) ->
+ eventCode = @getEventCode event
+ if eventCode of @keydownEvents
+ delete @keydownEvents[eventCode]
+ DomUtils.suppressPropagation event
+ @stopBubblingAndTrue
+ else
+ @continueBubbling
+
+ # This tests whether keyChar is a prefix of any current mapping in the key state.
+ keyCharIsKeyStatePrefix: (keyChar) ->
+ for mapping in @keyState
+ return true if keyChar of mapping
+ false
+
+ # This is called whenever a keyChar is matched. We keep any existing entries matching keyChar, and append a
+ # new copy of the global key mappings.
+ advanceKeyState: (keyChar) ->
+ newKeyState =
+ for mapping in @keyState
+ continue unless keyChar of mapping
+ mapping[keyChar]
+ @keyState = [newKeyState..., @keyMapping]
+
+ # This is called to invoke a command and reset the key state.
+ invokeCommand: (command) ->
+ countPrefix = if 0 < @countPrefix then @countPrefix else 1
+ @reset()
+ @commandHandler command, countPrefix
+
+ # Reset the state (as if no keys had been handled).
+ reset: ->
+ @countPrefix = 0
+ @keyState = [@keyMapping]
+
+ # This tests whether we are in the reset state. It is used to check whether we should be using escape to
+ # reset the key state, or passing it to the page.
+ isInResetState: ->
+ @countPrefix == 0 and @keyState.length == 1
+
+ # This tests whether keyChar should be treated as a count key.
+ isCountKey: (keyChar) ->
+ return false unless @useCount and keyChar.length == 1
+ if 0 < @countPrefix
+ '0' <= keyChar <= '9'
+ else
+ '1' <= keyChar <= '9'
+
+ getEventCode: (event) -> event.keyCode
+
+# Demo/test code.
+# A (very) poor-man's normal mode.
+
+demoKeyMapping =
+ j: "scrollDown"
+ k: "scrollUp"
+ i: "enterInsertMode"
+ g:
+ g: "scrollToTop"
+ a: "scrollToTop"
+ z: "scrollToBottom"
+ i: "focusInput"
+ # A three-key binding.
+ a:
+ b:
+ c: "enterInsertMode"
+ # And this should override "j" on its own.
+ j: "enterInsertMode"
+
+demoCommandHandler = (command, count) ->
+ switch command
+ when "scrollDown" then scrollDown()
+ when "scrollUp" then scrollUp()
+ when "scrollToTop" then scrollToTop count
+ when "scrollToBottom" then scrollToBottom()
+ when "enterInsertMode" then enterInsertMode()
+ when "focusInput" then focusInput count
+
+root = exports ? window
+root.KeyHandlerMode = KeyHandlerMode
+root.demoKeyMapping = demoKeyMapping
+root.demoCommandHandler = demoCommandHandler
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 667031dc..bce5f632 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -112,7 +112,8 @@ window.initializeModes = ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
# activates/deactivates itself accordingly.
- new NormalMode
+ # new NormalMode
+ new KeyHandlerMode commandHandler: demoCommandHandler, keyMapping: demoKeyMapping, indicator: "Demo mode."
new PassKeysMode
new InsertMode permanent: true
Scroller.init()
diff --git a/manifest.json b/manifest.json
index f66319a7..ae50d995 100644
--- a/manifest.json
+++ b/manifest.json
@@ -53,6 +53,7 @@
"content_scripts/mode_passkeys.js",
"content_scripts/mode_find.js",
"content_scripts/mode_visual_edit.js",
+ "content_scripts/mode_key_handler.js",
"content_scripts/hud.js",
"content_scripts/vimium_frontend.js"
],