aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2016-03-05 05:43:42 +0000
committerStephen Blott2016-03-05 05:43:42 +0000
commit62359adda7bc38917de38e3fc794d37817fa05ed (patch)
tree816d696b785f8a58b06ddd01560fee05ca0299e7
parent27d3d0087c86a6effd25049cbf0d9273eb0af9db (diff)
parentfb67cfdd2ca8c09453cc896fd02d08ed5a74a8a4 (diff)
downloadvimium-62359adda7bc38917de38e3fc794d37817fa05ed.tar.bz2
Merge pull request #2022 from smblott-github/generalised-key-bindings
Key handling in content scripts.
-rw-r--r--background_scripts/commands.coffee32
-rw-r--r--background_scripts/main.coffee173
-rw-r--r--content_scripts/mode.coffee16
-rw-r--r--content_scripts/mode_key_handler.coffee101
-rw-r--r--content_scripts/mode_passkeys.coffee21
-rw-r--r--content_scripts/vimium_frontend.coffee214
-rw-r--r--content_scripts/vomnibar.coffee1
-rw-r--r--lib/handler_stack.coffee2
-rw-r--r--manifest.json2
-rw-r--r--pages/help_dialog.coffee7
-rw-r--r--tests/dom_tests/dom_tests.coffee33
-rw-r--r--tests/dom_tests/dom_tests.html2
-rw-r--r--tests/unit_tests/commands_test.coffee2
13 files changed, 198 insertions, 408 deletions
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index ab9992b3..7429b6f0 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -2,6 +2,13 @@ Commands =
init: ->
for own command, description of commandDescriptions
@addCommand(command, description[0], description[1])
+ @loadKeyMappings Settings.get "keyMappings"
+ Settings.postUpdateHooks["keyMappings"] = @loadKeyMappings.bind this
+
+ loadKeyMappings: (customKeyMappings) ->
+ @clearKeyMappingsAndSetDefaults()
+ @parseCustomKeyMappings customKeyMappings
+ @generateKeyStateMapping()
availableCommands: {}
keyToCommandRegistry: {}
@@ -94,6 +101,25 @@ Commands =
@keyToCommandRegistry = {}
@mapKeyToCommand { key, command } for own key, command of defaultKeyMappings
+ # This generates a nested key-to-command mapping structure. There is an example in mode_key_handler.coffee.
+ generateKeyStateMapping: ->
+ # Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
+ # This regular expression captures two groups: the first is a named key, the second is the remainder of
+ # the string.
+ namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/
+ keyStateMapping = {}
+ for own keys, registryEntry of @keyToCommandRegistry
+ currentMapping = keyStateMapping
+ while 0 < keys.length
+ [key, keys] = if 0 == keys.search namedKeyRegex then [RegExp.$1, RegExp.$2] else [keys[0], keys[1..]]
+ if currentMapping[key]?.command
+ break # Do not overwrite existing command bindings, they take priority.
+ else if 0 < keys.length
+ currentMapping = currentMapping[key] ?= {}
+ else
+ currentMapping[key] = registryEntry
+ chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping
+
# An ordered listing of all available commands, grouped by type. This is the order they will
# be shown in the help page.
commandGroups:
@@ -371,11 +397,5 @@ commandDescriptions =
Commands.init()
-# Register postUpdateHook for keyMappings setting.
-Settings.postUpdateHooks["keyMappings"] = (value) ->
- Commands.clearKeyMappingsAndSetDefaults()
- Commands.parseCustomKeyMappings value
- refreshCompletionKeysAfterMappingSave()
-
root = exports ? window
root.Commands = Commands
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 7c970866..49199cae 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -19,17 +19,9 @@ chrome.runtime.onInstalled.addListener ({ reason }) ->
func tab.id, { file: file, allFrames: contentScripts.all_frames }, checkLastRuntimeError
currentVersion = Utils.getCurrentVersion()
-keyQueue = "" # Queue of keys typed
-validFirstKeys = {}
-singleKeyCommands = []
frameIdsForTab = {}
root.urlForTab = {}
-# Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
-# This regular expression captures two groups: the first is a named key, the second is the remainder of
-# the string.
-namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/
-
# This is exported for use by "marks.coffee".
root.tabLoadedHandlers = {} # tabId -> function()
@@ -189,14 +181,6 @@ fetchFileContents = (extensionFileName) ->
req.send()
req.responseText
-#
-# Returns the keys that can complete a valid command given the current key queue.
-#
-getCompletionKeysRequest = (request, keysToCheck = "") ->
- name: "refreshCompletionKeys"
- completionKeys: generateCompletionKeys(keysToCheck)
- validFirstKeys: validFirstKeys
-
TabOperations =
# Opens the url in the current tab.
openUrlInCurrentTab: (request, callback = (->)) ->
@@ -385,148 +369,13 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) ->
# End action functions
-splitKeyIntoFirstAndSecond = (key) ->
- if (key.search(namedKeyRegex) == 0)
- { first: RegExp.$1, second: RegExp.$2 }
+runBackgroundCommand = ({frameId, registryEntry, count}, sender) ->
+ if registryEntry.passCountToFunction
+ BackgroundCommands[registryEntry.command] count, frameId
+ else if registryEntry.noRepeat
+ BackgroundCommands[registryEntry.command] frameId
else
- { first: key[0], second: key.slice(1) }
-
-getActualKeyStrokeLength = (key) ->
- if (key.search(namedKeyRegex) == 0)
- 1 + getActualKeyStrokeLength(RegExp.$2)
- else
- key.length
-
-populateValidFirstKeys = ->
- for own key of Commands.keyToCommandRegistry
- if (getActualKeyStrokeLength(key) == 2)
- validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true
-
-populateSingleKeyCommands = ->
- for own key of Commands.keyToCommandRegistry
- if (getActualKeyStrokeLength(key) == 1)
- singleKeyCommands.push(key)
-
-# Invoked by options.coffee.
-root.refreshCompletionKeysAfterMappingSave = ->
- validFirstKeys = {}
- singleKeyCommands = []
-
- populateValidFirstKeys()
- populateSingleKeyCommands()
-
- sendRequestToAllTabs(getCompletionKeysRequest())
-
-# Generates a list of keys that can complete a valid command given the current key queue or the one passed in
-generateCompletionKeys = (keysToCheck) ->
- splitHash = splitKeyQueue(keysToCheck || keyQueue)
- command = splitHash.command
- count = splitHash.count
-
- completionKeys = singleKeyCommands.slice(0)
-
- if (getActualKeyStrokeLength(command) == 1)
- for own key of Commands.keyToCommandRegistry
- splitKey = splitKeyIntoFirstAndSecond(key)
- if (splitKey.first == command)
- completionKeys.push(splitKey.second)
-
- completionKeys
-
-splitKeyQueue = (queue) ->
- match = /([1-9][0-9]*)?(.*)/.exec(queue)
- count = parseInt(match[1], 10)
- command = match[2]
-
- { count: count, command: command }
-
-handleKeyDown = (sender) -> (request, port) ->
- key = request.keyChar
- if (key == "<ESC>")
- logMessage "clearing keyQueue", sender
- keyQueue = ""
- else
- logMessage "checking keyQueue: [#{keyQueue + key}]", sender
- keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId)
- logMessage "new KeyQueue: #{keyQueue}", sender
- # Tell the content script whether there are keys in the queue.
- # FIXME: There is a race condition here. The behaviour in the content script depends upon whether this message gets
- # back there before or after the next keystroke.
- # That being said, I suspect there are other similar race conditions here, for example in checkKeyQueue().
- # Steve (23 Aug, 14).
- chrome.tabs.sendMessage(port.sender.tab.id,
- name: "currentKeyQueue",
- keyQueue: keyQueue)
-
-checkKeyQueue = (keysToCheck, tabId, frameId) ->
- refreshedCompletionKeys = false
- splitHash = splitKeyQueue(keysToCheck)
- command = splitHash.command
- count = splitHash.count
-
- return keysToCheck if command.length == 0
- count = 1 if isNaN(count)
-
- if (Commands.keyToCommandRegistry[command])
- registryEntry = Commands.keyToCommandRegistry[command]
- runCommand = true
- count *= registryEntry.options.count ? 1
-
- if registryEntry.noRepeat
- count = 1
- else if registryEntry.repeatLimit and count > registryEntry.repeatLimit
- runCommand = confirm """
- You have asked Vimium to perform #{count} repeats of the command:
- #{Commands.availableCommands[registryEntry.command].description}
-
- Are you sure you want to continue?
- """
-
- if runCommand
- if not registryEntry.isBackgroundCommand
- chrome.tabs.sendMessage tabId,
- name: "executePageCommand"
- command: registryEntry.command
- frameId: frameId
- count: count
- completionKeys: generateCompletionKeys ""
- registryEntry: registryEntry
- refreshedCompletionKeys = true
- else
- if registryEntry.passCountToFunction
- BackgroundCommands[registryEntry.command](count, frameId)
- else if registryEntry.noRepeat
- BackgroundCommands[registryEntry.command](frameId)
- else
- repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId)
-
- newKeyQueue = ""
- else if (getActualKeyStrokeLength(command) > 1)
- splitKey = splitKeyIntoFirstAndSecond(command)
-
- # The second key might be a valid command by its self.
- if (Commands.keyToCommandRegistry[splitKey.second])
- newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId)
- else
- newKeyQueue = (if validFirstKeys[splitKey.second] then splitKey.second else "")
- else
- newKeyQueue = (if validFirstKeys[command] then count.toString() + command else "")
-
- # If we haven't sent the completion keys piggybacked on executePageCommand,
- # send them by themselves.
- unless refreshedCompletionKeys
- chrome.tabs.sendMessage(tabId, getCompletionKeysRequest(null, newKeyQueue), null)
-
- newKeyQueue
-
-#
-# Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API.
-#
-sendRequestToAllTabs = (args) ->
- chrome.windows.getAll({ populate: true }, (windows) ->
- for window in windows
- for tab in window.tabs
- chrome.tabs.sendMessage(tab.id, args, null))
+ repeatFunction BackgroundCommands[registryEntry.command], count, 0, frameId
openOptionsPageInNewTab = ->
chrome.tabs.getSelected(null, (tab) ->
@@ -574,11 +423,10 @@ bgLog = (request, sender) ->
# Port handler mapping
portHandlers =
- keyDown: handleKeyDown,
completions: handleCompletions
sendRequestHandlers =
- getCompletionKeys: getCompletionKeysRequest
+ runBackgroundCommand: runBackgroundCommand
getCurrentTabUrl: getCurrentTabUrl
openUrlInNewTab: TabOperations.openUrlInNewTab
openUrlInIncognito: TabOperations.openUrlInIncognito
@@ -624,13 +472,6 @@ window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html'
#
# Begin initialization.
#
-Commands.clearKeyMappingsAndSetDefaults()
-
-if Settings.has("keyMappings")
- Commands.parseCustomKeyMappings(Settings.get("keyMappings"))
-
-populateValidFirstKeys()
-populateSingleKeyCommands()
# Show notification on upgrade.
showUpgradeMessage = ->
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 454a6ed9..205b8288 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -119,22 +119,6 @@ class Mode
@deactivateSingleton @options.singleton
singletons[key] = this
- # If @options.trackState is truthy, then the mode mainatins the current state in @enabled and @passKeys,
- # and calls @registerStateChange() (if defined) whenever the state changes. The mode also tracks the
- # current keyQueue in @keyQueue.
- if @options.trackState
- @enabled = false
- @passKeys = ""
- @keyQueue = ""
- @push
- _name: "mode-#{@id}/registerStateChange"
- registerStateChange: ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling =>
- if enabled != @enabled or passKeys != @passKeys
- @enabled = enabled
- @passKeys = passKeys
- @registerStateChange?()
- registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = keyQueue
-
# If @options.passInitialKeyupEvents is set, then we pass initial non-printable keyup events to the page
# or to other extensions (because the corresponding keydown events were passed). This is used when
# activating link hints, see #1522.
diff --git a/content_scripts/mode_key_handler.coffee b/content_scripts/mode_key_handler.coffee
new file mode 100644
index 00000000..fdcac430
--- /dev/null
+++ b/content_scripts/mode_key_handler.coffee
@@ -0,0 +1,101 @@
+# Example key mapping (@keyMapping):
+# i:
+# command: "enterInsertMode", ... # This is a registryEntry object (as too are the other commands).
+# g:
+# g:
+# command: "scrollToTop", ...
+# t:
+# command: "nextTab", ...
+#
+# This key-mapping structure is generated by Commands.generateKeyStateMapping() and may be arbitrarily deep.
+# Observe that @keyMapping["g"] is itself also a valid key mapping. At any point, the key state (@keyState)
+# consists of a (non-empty) list of such mappings.
+
+class KeyHandlerMode extends Mode
+ keydownEvents: {}
+ setKeyMapping: (@keyMapping) -> @reset()
+ setPassKeys: (@passKeys) -> @reset()
+
+ # Reset the key state, optionally retaining the count provided.
+ reset: (@countPrefix = 0) ->
+ bgLog "Clearing key state, set count=#{@countPrefix}."
+ @keyState = [@keyMapping]
+
+ constructor: (options) ->
+ @commandHandler = options.commandHandler ? (->)
+ @setKeyMapping options.keyMapping ? {}
+
+ super extend options,
+ keydown: @onKeydown.bind this
+ keypress: @onKeypress.bind this
+ keyup: @onKeyup.bind this
+ # We cannot track keyup events if we lose the focus.
+ blur: (event) => @alwaysContinueBubbling => @keydownEvents = {} if event.target == window
+
+ onKeydown: (event) ->
+ keyChar = KeyboardUtils.getKeyCharString event
+ isEscape = KeyboardUtils.isEscape event
+ if isEscape and @countPrefix == 0 and @keyState.length == 1
+ @continueBubbling
+ else if isEscape
+ @keydownEvents[event.keyCode] = true
+ @reset()
+ false # Suppress event.
+ else if @isMappedKey keyChar
+ @keydownEvents[event.keyCode] = true
+ @handleKeyChar keyChar
+ else if not keyChar and (keyChar = KeyboardUtils.getKeyChar event) and
+ (@isMappedKey(keyChar) or @isCountKey keyChar)
+ # We will possibly be handling a subsequent keypress event, so suppress propagation of this event to
+ # prevent triggering page event listeners (e.g. Google instant Search).
+ @keydownEvents[event.keyCode] = true
+ DomUtils.suppressPropagation event
+ @stopBubblingAndTrue
+ else
+ @continueBubbling
+
+ onKeypress: (event) ->
+ keyChar = KeyboardUtils.getKeyCharString event
+ if @isMappedKey keyChar
+ @handleKeyChar keyChar
+ else if @isCountKey keyChar
+ digit = parseInt keyChar
+ @reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit
+ false # Suppress event.
+ else
+ @reset()
+ @continueBubbling
+
+ onKeyup: (event) ->
+ return @continueBubbling unless event.keyCode of @keydownEvents
+ delete @keydownEvents[event.keyCode]
+ DomUtils.suppressPropagation event
+ @stopBubblingAndTrue
+
+ # This tests whether there is a mapping of keyChar in the current key state (and accounts for pass keys).
+ isMappedKey: (keyChar) ->
+ (mapping for mapping in @keyState when keyChar of mapping)[0]? and not @isPassKey keyChar
+
+ # This tests whether keyChar is a digit (and accounts for pass keys).
+ isCountKey: (keyChar) ->
+ keyChar and (if 0 < @countPrefix then '0' else '1') <= keyChar <= '9' and not @isPassKey keyChar
+
+ # Keystrokes are *never* considered pass keys if the user has begun entering a command. So, for example, if
+ # 't' is a passKey, then the "t"-s of 'gt' and '99t' are neverthless handled as regular keys.
+ isPassKey: (keyChar) ->
+ @countPrefix == 0 and @keyState.length == 1 and keyChar in (@passKeys ? "")
+
+ handleKeyChar: (keyChar) ->
+ bgLog "Handling key #{keyChar}, mode=#{@name}."
+ # Advance the key state. The new key state is the current mappings of keyChar, plus @keyMapping.
+ @keyState = [(mapping[keyChar] for mapping in @keyState when keyChar of mapping)..., @keyMapping]
+ command = (mapping for mapping in @keyState when "command" of mapping)[0]
+ if command
+ count = if 0 < @countPrefix then @countPrefix else 1
+ bgLog "Calling mode=#{@name}, command=#{command.command}, count=#{count}."
+ @reset()
+ @commandHandler {command, count}
+ false # Suppress event.
+
+root = exports ? window
+root.KeyHandlerMode = KeyHandlerMode
diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee
deleted file mode 100644
index 631eb621..00000000
--- a/content_scripts/mode_passkeys.coffee
+++ /dev/null
@@ -1,21 +0,0 @@
-
-class PassKeysMode extends Mode
- constructor: ->
- super
- name: "passkeys"
- trackState: true # Maintain @enabled, @passKeys and @keyQueue.
- keydown: (event) => @handleKeyChar event, KeyboardUtils.getKeyChar event
- keypress: (event) => @handleKeyChar event, String.fromCharCode event.charCode
- keyup: (event) => @handleKeyChar event, KeyboardUtils.getKeyChar event
-
- # Keystrokes are *never* considered passKeys if the keyQueue is not empty. So, for example, if 't' is a
- # passKey, then 'gt' and '99t' will neverthless be handled by Vimium.
- handleKeyChar: (event, keyChar) ->
- return @continueBubbling if event.altKey or event.ctrlKey or event.metaKey
- if keyChar and not @keyQueue and keyChar.length == 1 and 0 <= @passKeys.indexOf keyChar
- @stopBubblingAndTrue
- else
- @continueBubbling
-
-root = exports ? window
-root.PassKeysMode = PassKeysMode
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 667031dc..0ae3d229 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -5,14 +5,8 @@
# "domReady".
#
-keyPort = null
isEnabledForUrl = true
isIncognitoMode = chrome.extension.inIncognitoContext
-passKeys = null
-keyQueue = null
-# The user's operating system.
-currentCompletionKeys = ""
-validFirstKeys = ""
# We track whther the current window has the focus or not.
windowIsFocused = do ->
@@ -99,21 +93,45 @@ handlerStack.push
target = target.parentElement
true
-# Only exported for tests.
-window.initializeModes = ->
- class NormalMode extends Mode
- constructor: ->
- super
- name: "normal"
- indicator: false # There is no mode indicator in normal mode.
- keydown: (event) => onKeydown.call @, event
- keypress: (event) => onKeypress.call @, event
- keyup: (event) => onKeyup.call @, event
+class NormalMode extends KeyHandlerMode
+ constructor: (options = {}) ->
+ super extend options,
+ name: "normal"
+ indicator: false # There is no mode indicator in normal mode.
+ commandHandler: @commandHandler.bind this
+ chrome.storage.local.get "normalModeKeyStateMapping", (items) =>
+ @setKeyMapping items.normalModeKeyStateMapping
+
+ chrome.storage.onChanged.addListener (changes, area) =>
+ if area == "local" and changes.normalModeKeyStateMapping?.newValue
+ @setKeyMapping changes.normalModeKeyStateMapping.newValue
+
+ commandHandler: ({command: registryEntry, count}) ->
+ count *= registryEntry.options.count ? 1
+ count = 1 if registryEntry.noRepeat
+
+ if registryEntry.repeatLimit? and registryEntry.repeatLimit < count
+ return unless confirm """
+ You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n
+ Are you sure you want to continue?"""
+
+ # The Vomnibar needs special handling because it is always activated in the tab's main frame.
+ if registryEntry.command.startsWith "Vomnibar."
+ chrome.runtime.sendMessage
+ handler: "sendMessageToFrames", message: {name: "openVomnibar", sourceFrameId: frameId, registryEntry}
+ else if registryEntry.isBackgroundCommand
+ chrome.runtime.sendMessage {handler: "runBackgroundCommand", frameId, registryEntry, count}
+ else if registryEntry.passCountToFunction
+ Utils.invokeCommandString registryEntry.command, [count]
+ else
+ Utils.invokeCommandString registryEntry.command for i in [0...count]
+
+# Only exported for tests; also, "args..." is only for the tests.
+window.initializeModes = (args...) ->
# Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and
- # activates/deactivates itself accordingly.
- new NormalMode
- new PassKeysMode
+ # activates/deactivates itself accordingly. normalMode is exported only for the tests.
+ window.normalMode = new NormalMode args...
new InsertMode permanent: true
Scroller.init()
@@ -122,33 +140,18 @@ window.initializeModes = ->
#
initializePreDomReady = ->
checkIfEnabledForUrl()
- refreshCompletionKeys()
-
- # Send the key to the key handler in the background page.
- keyPort = chrome.runtime.connect({ name: "keyDown" })
- # If the port is closed, the background page has gone away (since we never close it ourselves). Disable all
- # our event listeners, and stub out chrome.runtime.sendMessage/connect (to prevent errors).
- # TODO(mrmr1993): Do some actual cleanup to free resources, hide UI, etc.
- keyPort.onDisconnect.addListener ->
- isEnabledForUrl = false
- chrome.runtime.sendMessage = ->
- chrome.runtime.connect = ->
- window.removeEventListener "focus", onFocus
requestHandlers =
showHUDforDuration: handleShowHUDforDuration
toggleHelpDialog: (request) -> if frameId == request.frameId then HelpDialog.toggle request.dialogHtml
focusFrame: (request) -> if (frameId == request.frameId) then focusThisFrame request
- refreshCompletionKeys: refreshCompletionKeys
getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY
setScrollPosition: setScrollPosition
- executePageCommand: executePageCommand
- currentKeyQueue: (request) ->
- keyQueue = request.keyQueue
- handlerStack.bubbleEvent "registerKeyQueue", { keyQueue: keyQueue }
# A frame has received the focus. We don't care here (the Vomnibar/UI-component handles this).
frameFocused: ->
checkEnabledAfterURLChange: checkEnabledAfterURLChange
+ openVomnibar: ({sourceFrameId, registryEntry}) ->
+ Utils.invokeCommandString registryEntry.command, [sourceFrameId, registryEntry] if DomUtils.isTopFrame()
chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
# In the options page, we will receive requests from both content and background scripts. ignore those
@@ -159,12 +162,7 @@ initializePreDomReady = ->
return if request.handler and not request.name
shouldHandleRequest = isEnabledForUrl
# We always handle the message if it's one of these listed message types.
- shouldHandleRequest ||= request.name in [ "executePageCommand", "checkEnabledAfterURLChange" ]
- # Requests with a frameId of zero should always and only be handled in the main/top frame (regardless of
- # whether Vimium is enabled there).
- if request.frameId == 0 and DomUtils.isTopFrame()
- request.frameId = frameId
- shouldHandleRequest = true
+ shouldHandleRequest ||= request.name in ["checkEnabledAfterURLChange", "openVomnibar"]
sendResponse requestHandlers[request.name](request, sender) if shouldHandleRequest
# Ensure the sendResponse callback is freed.
false
@@ -216,7 +214,11 @@ window.addEventListener "hashchange", onFocus
#
initializeOnDomReady = ->
# Tell the background page we're in the dom ready state.
- chrome.runtime.connect({ name: "domReady" })
+ chrome.runtime.connect(name: "domReady").onDisconnect.addListener ->
+ # We disable content scripts when we lose contact with the background page.
+ isEnabledForUrl = false
+ chrome.runtime.sendMessage = ->
+ window.removeEventListener "focus", onFocus
# We only initialize the vomnibar in the tab's main frame, because it's only ever opened there.
Vomnibar.init() if DomUtils.isTopFrame()
HUD.init()
@@ -235,28 +237,6 @@ unregisterFrame = ->
frameId: frameId
tab_is_closing: DomUtils.isTopFrame()
-executePageCommand = (request) ->
- commandType = request.command.split(".")[0]
- # Vomnibar commands are handled in the tab's main/top frame. They are handled even if Vimium is otherwise
- # disabled in the frame.
- if commandType == "Vomnibar"
- if DomUtils.isTopFrame()
- # We pass the frameId from request. That's the frame which originated the request, so that's the frame
- # which should receive the focus when the vomnibar closes.
- Utils.invokeCommandString request.command, [ request.frameId, request.registryEntry ]
- refreshCompletionKeys request
- return
-
- # All other commands are handled in their frame (but only if Vimium is enabled).
- return unless frameId == request.frameId and isEnabledForUrl
-
- if request.registryEntry.passCountToFunction
- Utils.invokeCommandString(request.command, [request.count])
- else
- Utils.invokeCommandString(request.command) for i in [0...request.count]
-
- refreshCompletionKeys(request)
-
handleShowHUDforDuration = ({ text, duration }) ->
if DomUtils.isTopFrame()
DomUtils.documentReady -> HUD.showForDuration text, duration
@@ -456,94 +436,6 @@ extend window,
targetElement: document.activeElement
indicator: false
-# Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup
-# event.
-KeydownEvents =
- handledEvents: {}
-
- getEventCode: (event) -> event.keyCode
-
- push: (event) ->
- @handledEvents[@getEventCode event] = true
-
- # Yields truthy or falsy depending upon whether a corresponding keydown event is present (and removes that
- # event).
- pop: (event) ->
- detailString = @getEventCode event
- value = @handledEvents[detailString]
- delete @handledEvents[detailString]
- value
-
- clear: -> @handledEvents = {}
-
-handlerStack.push
- _name: "KeydownEvents-cleanup"
- blur: (event) -> KeydownEvents.clear() if event.target == window; true
-
-#
-# Sends everything except i & ESC to the handler in background_page. i & ESC are special because they control
-# insert mode which is local state to the page. The key will be are either a single ascii letter or a
-# key-modifier pair, e.g. <c-a> for control a.
-#
-# Note that some keys will only register keydown events and not keystroke events, e.g. ESC.
-#
-# @/this, here, is the the normal-mode Mode object.
-onKeypress = (event) ->
- keyChar = KeyboardUtils.getKeyCharString event
- if keyChar
- if currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey keyChar
- DomUtils.suppressEvent(event)
- keyPort.postMessage keyChar:keyChar, frameId:frameId
- return @stopBubblingAndTrue
-
- keyPort.postMessage keyChar:keyChar, frameId:frameId
-
- return @continueBubbling
-
-# @/this, here, is the the normal-mode Mode object.
-onKeydown = (event) ->
- keyChar = KeyboardUtils.getKeyCharString event
-
- if (HelpDialog.showing && KeyboardUtils.isEscape(event))
- HelpDialog.hide()
- DomUtils.suppressEvent event
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- else
- if (keyChar)
- if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar))
- DomUtils.suppressEvent event
- KeydownEvents.push event
- keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
- return @stopBubblingAndTrue
-
- keyPort.postMessage({ keyChar:keyChar, frameId:frameId })
-
- else if (KeyboardUtils.isEscape(event))
- keyPort.postMessage({ keyChar:"<ESC>", frameId:frameId })
-
- # Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command.
- # The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us
- # back into the search box. As a side effect, this should also prevent overriding by other sites.
- #
- # Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress).
- #
- # TOOD(ilya): Revisit this. Not sure it's the absolute best approach.
- if not keyChar &&
- (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 ||
- isValidFirstKey(KeyboardUtils.getKeyChar(event)))
- DomUtils.suppressPropagation(event)
- KeydownEvents.push event
- return @stopBubblingAndTrue
-
- return @continueBubbling
-
-# @/this, here, is the the normal-mode Mode object.
-onKeyup = (event) ->
- return @continueBubbling unless KeydownEvents.pop event
- DomUtils.suppressPropagation(event)
- @stopBubblingAndTrue
# Checks if Vimium should be enabled or not in this frame. As a side effect, it also informs the background
# page whether this frame has the focus, allowing the background page to track the active frame's URL.
@@ -555,9 +447,7 @@ checkIfEnabledForUrl = (frameIsFocused = windowIsFocused()) ->
if HUD.isReady() and not isEnabledForUrl
# Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load.
HUD.hide()
- handlerStack.bubbleEvent "registerStateChange",
- enabled: isEnabledForUrl
- passKeys: passKeys
+ normalMode?.setPassKeys passKeys
# Update the page icon, if necessary.
if windowIsFocused()
chrome.runtime.sendMessage
@@ -573,18 +463,6 @@ checkIfEnabledForUrl = (frameIsFocused = windowIsFocused()) ->
checkEnabledAfterURLChange = ->
checkIfEnabledForUrl() if windowIsFocused()
-# Exported to window, but only for DOM tests.
-window.refreshCompletionKeys = (response) ->
- if (response)
- currentCompletionKeys = response.completionKeys
-
- if (response.validFirstKeys)
- validFirstKeys = response.validFirstKeys
- else
- chrome.runtime.sendMessage({ handler: "getCompletionKeys" }, refreshCompletionKeys)
-
-isValidFirstKey = (keyChar) ->
- validFirstKeys[keyChar] || /^[1-9]/.test(keyChar)
window.handleEscapeForFindMode = ->
document.body.classList.remove("vimiumFindMode")
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 55a46777..67a79ff4 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -7,7 +7,6 @@ Vomnibar =
# Parse any additional options from the command's registry entry. Currently, this only includes a flag of
# the form "keyword=X", for direct activation of a custom search engine.
parseRegistryEntry: (registryEntry = { options: [] }, callback = null) ->
- options = {}
searchEngines = Settings.get("searchEngines") ? ""
SearchEngines.refreshAndUse searchEngines, (engines) ->
callback? registryEntry.options
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index bb0f19a6..2a44d26b 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -84,8 +84,6 @@ class HandlerStack
# Debugging.
logResult: (eventNumber, type, event, handler, result) ->
- # Key queue events aren't usually useful for debugging, so we filter them out.
- return if type in [ "registerKeyQueue" ]
label =
switch result
when @stopBubblingAndTrue then "stop/true"
diff --git a/manifest.json b/manifest.json
index f66319a7..07647da5 100644
--- a/manifest.json
+++ b/manifest.json
@@ -50,9 +50,9 @@
"content_scripts/scroller.js",
"content_scripts/marks.js",
"content_scripts/mode_insert.js",
- "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"
],
diff --git a/pages/help_dialog.coffee b/pages/help_dialog.coffee
index 0da4977b..e16fd5d3 100644
--- a/pages/help_dialog.coffee
+++ b/pages/help_dialog.coffee
@@ -48,7 +48,12 @@ HelpDialog =
chrome.runtime.sendMessage handler: "copyToClipboard", data: commandName
HUD.showForDuration("Yanked #{commandName}.", 2000)
- hide: -> UIComponentServer.postMessage "hide"
+ @exitOnEscape = new Mode name: "help-page-escape", exitOnEscape: true
+ @exitOnEscape.onExit (event) => @hide() if event?.type == "keydown"
+
+ hide: ->
+ @exitOnEscape?.exit()
+ UIComponentServer.postMessage "hide"
toggle: (html) ->
if @showing then @hide() else @show html
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index 8a96913f..c2c34fa8 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -24,15 +24,8 @@ for type in [ "keydown", "keypress", "keyup" ]
initializeModeState = ->
Mode.reset()
handlerStack.reset()
- initializeModes()
- # We use "m" as the only mapped key, "p" as a passkey, and "u" as an unmapped key.
- refreshCompletionKeys
- completionKeys: "mp"
- handlerStack.bubbleEvent "registerStateChange",
- enabled: true
- passKeys: "p"
- handlerStack.bubbleEvent "registerKeyQueue",
- keyQueue: ""
+ initializeModes keyMapping: {m: {}, p: {}, z: {p: {}}}
+ normalMode.setPassKeys "p"
# Tell Settings that it's been loaded.
Settings.isLoaded = true
@@ -375,8 +368,13 @@ context "Normal mode",
sendKeyboardEvent "p"
assert.equal pageKeyboardEventCount, 3
- should "suppress passKeys with a non-empty keyQueue", ->
- handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "p"
+ should "suppress passKeys with a non-empty key state (a count)", ->
+ sendKeyboardEvent "5"
+ sendKeyboardEvent "p"
+ assert.equal 0, pageKeyboardEventCount
+
+ should "suppress passKeys with a non-empty key state (a key)", ->
+ sendKeyboardEvent "z"
sendKeyboardEvent "p"
assert.equal pageKeyboardEventCount, 0
@@ -495,19 +493,6 @@ context "Mode utilities",
element.blur()
assert.isTrue test.modeIsActive
- should "register state change", ->
- test = new Mode trackState: true
- handlerStack.bubbleEvent "registerStateChange", { enabled: "one", passKeys: "two" }
-
- assert.isTrue test.enabled == "one"
- assert.isTrue test.passKeys == "two"
-
- should "register the keyQueue", ->
- test = new Mode trackState: true
- handlerStack.bubbleEvent "registerKeyQueue", keyQueue: "hello"
-
- assert.isTrue test.keyQueue == "hello"
-
context "PostFindMode",
setup ->
initializeModeState()
diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html
index 8d355c6d..9aa804d1 100644
--- a/tests/dom_tests/dom_tests.html
+++ b/tests/dom_tests/dom_tests.html
@@ -42,10 +42,10 @@
<script type="text/javascript" src="../../content_scripts/link_hints.js"></script>
<script type="text/javascript" src="../../content_scripts/vomnibar.js"></script>
<script type="text/javascript" src="../../content_scripts/scroller.js"></script>
- <script type="text/javascript" src="../../content_scripts/mode_passkeys.js"></script>
<script type="text/javascript" src="../../content_scripts/mode_insert.js"></script>
<script type="text/javascript" src="../../content_scripts/mode_find.js"></script>
<script type="text/javascript" src="../../content_scripts/mode_visual_edit.js"></script>
+ <script type="text/javascript" src="../../content_scripts/mode_key_handler.js"></script>
<script type="text/javascript" src="../../content_scripts/hud.js"></script>
<script type="text/javascript" src="../../content_scripts/vimium_frontend.js"></script>
diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee
index 45cb4cf4..f501a960 100644
--- a/tests/unit_tests/commands_test.coffee
+++ b/tests/unit_tests/commands_test.coffee
@@ -1,6 +1,6 @@
require "./test_helper.js"
extend global, require "./test_chrome_stubs.js"
-global.Settings = {postUpdateHooks: {}}
+global.Settings = {postUpdateHooks: {}, get: (-> ""), set: ->}
{Commands} = require "../../background_scripts/commands.js"
context "Key mappings",