diff options
| author | Stephen Blott | 2016-03-05 05:43:42 +0000 |
|---|---|---|
| committer | Stephen Blott | 2016-03-05 05:43:42 +0000 |
| commit | 62359adda7bc38917de38e3fc794d37817fa05ed (patch) | |
| tree | 816d696b785f8a58b06ddd01560fee05ca0299e7 | |
| parent | 27d3d0087c86a6effd25049cbf0d9273eb0af9db (diff) | |
| parent | fb67cfdd2ca8c09453cc896fd02d08ed5a74a8a4 (diff) | |
| download | vimium-62359adda7bc38917de38e3fc794d37817fa05ed.tar.bz2 | |
Merge pull request #2022 from smblott-github/generalised-key-bindings
Key handling in content scripts.
| -rw-r--r-- | background_scripts/commands.coffee | 32 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 173 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 16 | ||||
| -rw-r--r-- | content_scripts/mode_key_handler.coffee | 101 | ||||
| -rw-r--r-- | content_scripts/mode_passkeys.coffee | 21 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 214 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 1 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 2 | ||||
| -rw-r--r-- | manifest.json | 2 | ||||
| -rw-r--r-- | pages/help_dialog.coffee | 7 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 33 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.html | 2 | ||||
| -rw-r--r-- | tests/unit_tests/commands_test.coffee | 2 |
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", |
