diff options
| author | Stephen Blott | 2014-12-31 20:52:27 +0000 |
|---|---|---|
| committer | Stephen Blott | 2015-01-01 09:37:20 +0000 |
| commit | acefe43cef5a216cb2504e85799699c359b6b4d8 (patch) | |
| tree | 280e4d312cab11eb3b825b1fde73fc0654955e82 | |
| parent | f2b428b4fe1eecd66ee95513da779470f7c621aa (diff) | |
| download | vimium-acefe43cef5a216cb2504e85799699c359b6b4d8.tar.bz2 | |
Modes; incorporate three test modes.
As a proof of concept, this incorporates normal mode, passkeys mode and
insert mode.
| -rw-r--r-- | background_scripts/main.coffee | 10 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 78 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 106 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 10 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 1 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.html | 1 | ||||
| -rw-r--r-- | tests/unit_tests/test_chrome_stubs.coffee | 2 |
7 files changed, 164 insertions, 44 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 4c1b9ae7..7d7359b8 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -339,6 +339,13 @@ updateOpenTabs = (tab) -> setBrowserActionIcon = (tabId,path) -> chrome.browserAction.setIcon({ tabId: tabId, path: path }) +# This color should match the blue of the Vimium browser popup (although it looks a little darker, to me?). +chrome.browserAction.setBadgeBackgroundColor {color: [102, 176, 226, 255]} + +setBadge = (response) -> + badge = response?.badge || "" + chrome.browserAction.setBadgeText {text: badge} + # Updates the browserAction icon to indicate whether Vimium is enabled or disabled on the current page. # Also propagates new enabled/disabled/passkeys state to active window, if necessary. # This lets you disable Vimium on a page without needing to reload. @@ -349,6 +356,7 @@ root.updateActiveState = updateActiveState = (tabId) -> partialIcon = "icons/browser_action_partial.png" chrome.tabs.get tabId, (tab) -> chrome.tabs.sendMessage tabId, { name: "getActiveState" }, (response) -> + setBadge response if response isCurrentlyEnabled = response.enabled currentPasskeys = response.passKeys @@ -602,6 +610,7 @@ unregisterFrame = (request, sender) -> frameIdsForTab[tabId] = frameIdsForTab[tabId].filter (id) -> id != request.frameId handleFrameFocused = (request, sender) -> + setBadge request tabId = sender.tab.id if frameIdsForTab[tabId]? frameIdsForTab[tabId] = @@ -633,6 +642,7 @@ sendRequestHandlers = refreshCompleter: refreshCompleter createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) + setBadge: setBadge # Convenience function for development use. window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html')) diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index f7bf9e69..e4b6017c 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -1,12 +1,74 @@ -root = exports ? window -class root.Mode - constructor: (onKeydown, onKeypress, onKeyup, @popModeCallback) -> +class Mode + # Static members. + @modes: [] + @current: -> Mode.modes[0] + @suppressPropagation = false + @propagate = true + + # Default values. + name: "" # The name of this mode. + badge: "" # A badge to display on the popup when this mode is active. + keydown: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. + keypress: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. + keyup: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions. + onDeactivate: -> # Called when leaving this mode. + onReactivate: -> # Called when this mode is reactivated. + + constructor: (options) -> + extend @, options + @handlerId = handlerStack.push - keydown: onKeydown - keypress: onKeypress - keyup: onKeyup + keydown: @checkForBuiltInHandler "keydown", @keydown + keypress: @checkForBuiltInHandler "keypress", @keypress + keyup: @checkForBuiltInHandler "keyup", @keyup + reactivateMode: => + @onReactivate() + Mode.setBadge() + return Mode.suppressPropagation + + Mode.modes.unshift @ + Mode.setBadge() + + # Allow the strings "suppress" and "pass" to be used as proxies for the built-in handlers. + checkForBuiltInHandler: (type, handler) -> + switch handler + when "suppress" then @generateSuppressPropagation type + when "pass" then @generatePassThrough type + else handler - popMode: -> + # Generate a default handler which always passes through; except Esc, which pops the current mode. + generatePassThrough: (type) -> + me = @ + (event) -> + if type == "keydown" and KeyboardUtils.isEscape event + me.popMode event + return Mode.suppressPropagation + handlerStack.passThrough + + # Generate a default handler which always suppresses propagation; except Esc, which pops the current mode. + generateSuppressPropagation: (type) -> + handler = @generatePassThrough type + (event) -> handler(event) and Mode.suppressPropagation # Always falsy. + + # Leave the current mode; event may or may not be provide. It is the responsibility of the creator of this + # object to know whether or not an event will be provided. Bubble a "reactivateMode" event to notify the + # now-active mode that it is once again top dog. + popMode: (event) -> + Mode.modes = Mode.modes.filter (mode) => mode != @ handlerStack.remove @handlerId - @popModeCallback() + @onDeactivate event + handlerStack.bubbleEvent "reactivateMode", event + + # Set the badge on the browser popup to indicate the current mode; static method. + @setBadge: -> + badge = Mode.getBadge() + chrome.runtime.sendMessage({ handler: "setBadge", badge: badge }) + + # Static convenience methods. + @is: (mode) -> Mode.current()?.name == mode + @getBadge: -> Mode.current()?.badge || "" + @isInsert: -> Mode.is "insert" + +root = exports ? window +root.Mode = Mode diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 5f8b050f..969e9209 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -20,8 +20,8 @@ isEnabledForUrl = true passKeys = null keyQueue = null # The user's operating system. -currentCompletionKeys = null -validFirstKeys = null +currentCompletionKeys = "" +validFirstKeys = "" # The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in # each content script. Alternatively we could calculate it once in the background page and use a request to @@ -109,10 +109,30 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - nf = -> true - new Mode(onKeydown, onKeypress, onKeyup, nf) + # Install normal mode. This will be at the bottom of both the mode stack and the handler stack, and is never + # deactivated. + new Mode + name: "normal" + keydown: onKeydown + keypress: onKeypress + keyup: onKeyup + + # Initialize the scroller. The scroller installs key handlers, and these will be next on the handler stack, + # immediately above normal mode. Scroller.init settings + handlePassKeyEvent = (event) -> + for keyChar in [ KeyboardUtils.getKeyChar(event), String.fromCharCode(event.charCode) ] + return handlerStack.passThrough if keyChar and isPassKey keyChar + true + + # Install passKeys mode. This mode is never deactivated. + new Mode + name: "passkeys" + keydown: handlePassKeyEvent + keypress: handlePassKeyEvent + keyup: -> true # Allow event to propagate. + checkIfEnabledForUrl() refreshCompletionKeys() @@ -137,7 +157,7 @@ initializePreDomReady = -> getScrollPosition: -> scrollX: window.scrollX, scrollY: window.scrollY setScrollPosition: (request) -> setScrollPosition request.scrollX, request.scrollY executePageCommand: executePageCommand - getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys } + getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys, badge: Mode.getBadge() } setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue @@ -171,10 +191,7 @@ initializeWhenEnabled = (newPassKeys) -> # Key event handlers fire on window before they do on document. Prefer window for key events so the page # can't set handlers to grab the keys before us. for type in ["keydown", "keypress", "keyup"] - do (type) -> - installListener window, type, (event) -> - console.log type - handlerStack.bubbleEvent type, event + do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event installListener document, "focus", onFocusCapturePhase installListener document, "blur", onBlurCapturePhase installListener document, "DOMActivate", onDOMActivate @@ -192,7 +209,7 @@ setState = (request) -> window.addEventListener "focus", -> # settings may have changed since the frame last had focus settings.load() - chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId }) + chrome.runtime.sendMessage({ handler: "frameFocused", frameId: frameId, badge: Mode.getBadge() }) # # Initialization tasks that must wait for the document to be ready. @@ -410,22 +427,24 @@ onKeypress = (event) -> # Enter insert mode when the user enables the native find interface. if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) enterInsertModeWithoutShowingIndicator() - return + return Mode.propagate if (keyChar) if (findMode) handleKeyCharForFindMode(keyChar) - DomUtils.suppressEvent(event) + return Mode.suppressPropagation else if (!isInsertMode() && !findMode) if (isPassKey keyChar) - return undefined + return Mode.propagate if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) - DomUtils.suppressEvent(event) + keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return Mode.suppressPropagation keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return Mode.propagate + onKeydown = (event) -> - console.log "onKeydown" keyChar = "" # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to @@ -463,38 +482,39 @@ onKeydown = (event) -> event.srcElement.blur() exitInsertMode() DomUtils.suppressEvent event - handledKeydownEvents.push event + KeydownEvents.push event else if (findMode) if (KeyboardUtils.isEscape(event)) handleEscapeForFindMode() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) handleDeleteForFindMode() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (event.keyCode == keyCodes.enter) handleEnterForFindMode() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (!modifiers) - DomUtils.suppressPropagation(event) KeydownEvents.push event + return Mode.suppressPropagation else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) hideHelpDialog() - DomUtils.suppressEvent event KeydownEvents.push event + return Mode.suppressPropagation else if (!isInsertMode() && !findMode) if (keyChar) if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) - DomUtils.suppressEvent event KeydownEvents.push event + keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + return Mode.suppressPropagation keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -502,7 +522,7 @@ onKeydown = (event) -> keyPort.postMessage({ keyChar:"<ESC>", frameId:frameId }) else if isPassKey KeyboardUtils.getKeyChar(event) - return undefined + return Mode.propagate # 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 @@ -514,11 +534,14 @@ onKeydown = (event) -> if (keyChar == "" && !isInsertMode() && (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || isValidFirstKey(KeyboardUtils.getKeyChar(event)))) - DomUtils.suppressPropagation(event) + # Suppress chrome propagation of this event, but drop through, and continue handler-stack processing. + DomUtils.suppressPropagation event KeydownEvents.push event + return Mode.propagate + onKeyup = (event) -> - DomUtils.suppressPropagation(event) if KeydownEvents.pop event + if KeydownEvents.pop event then Mode.suppressPropagation else Mode.propagate checkIfEnabledForUrl = -> url = window.location.toString() @@ -584,7 +607,7 @@ isEditable = (target) -> # window.enterInsertMode = (target) -> enterInsertModeWithoutShowingIndicator(target) - HUD.show("Insert mode") + # HUD.show("Insert mode") # With this proof-of-concept, visual feedback is given via badges on the browser popup. # # We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A @@ -594,15 +617,36 @@ window.enterInsertMode = (target) -> # leave insert mode when the user presses <ESC>. # Note. This returns the truthiness of target, which is required by isInsertMode. # -enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target +enterInsertModeWithoutShowingIndicator = (target) -> + insertModeLock = target + unless Mode.isInsert() + # Install insert-mode handler. Hereafter, all key events will be passed directly to the underlying page. + # The current isInsertMode logic in the normal-mode handlers is now redundant.. + new Mode + name: "insert" + badge: "I" + keydown: "pass" + keypress: "pass" + keyup: "pass" + onDeactivate: (event) -> + if isEditable(event.srcElement) or isEmbed(event.srcElement) + # Remove focus so the user can't just get himself back into insert mode by typing in the same input + # box. + # NOTE(smblott, 2014/12/22) Including embeds for .blur() etc. here is experimental. It appears to be + # the right thing to do for most common use cases. However, it could also cripple flash-based sites and + # games. See discussion in #1211 and #1194. + event.srcElement.blur() + insertModeLock = null + HUD.hide() exitInsertMode = (target) -> - if (target == undefined || insertModeLock == target) - insertModeLock = null - HUD.hide() + # This assumes that, if insert mode is active at all, then it *must* be the current mode. That is, we + # cannot enter any other mode from insert mode. + if Mode.isInsert() and (target == null or target == insertModeLock) + Mode.popMode() isInsertMode = -> - return true if insertModeLock != null + return true if Mode.isInsert() # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and # unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check whether # the active element is contentEditable. diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee index 728ea4bc..1c334210 100644 --- a/lib/handler_stack.coffee +++ b/lib/handler_stack.coffee @@ -1,11 +1,11 @@ root = exports ? window -class root.HandlerStack +class HandlerStack constructor: -> @stack = [] @counter = 0 - @passThrough = {} + @passThrough = new Object() # Used only as a constant, distinct from any other value. genId: -> @counter = ++@counter & 0xffff @@ -19,7 +19,6 @@ class root.HandlerStack # propagation by returning a falsy value. bubbleEvent: (type, event) -> for i in [(@stack.length - 1)..0] by -1 - console.log i, type handler = @stack[i] # We need to check for existence of handler because the last function call may have caused the release # of more than one handler. @@ -29,8 +28,8 @@ class root.HandlerStack if not passThrough DomUtils.suppressEvent(event) return false - # If @passThrough is returned, then discontinue further bubbling and pass the event through to the - # underlying page. The event is not suppresssed. + # If the constant @passThrough is returned, then discontinue further bubbling and pass the event + # through to the underlying page. The event is not suppresssed. if passThrough == @passThrough return false true @@ -42,4 +41,5 @@ class root.HandlerStack @stack.splice(i, 1) break +root. HandlerStack = HandlerStack root.handlerStack = new HandlerStack diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index 4a61877c..124fae06 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -178,6 +178,7 @@ context "Input focus", focusInput 1 assert.equal "first", document.activeElement.id # deactivate the tabbing mode and its overlays + currentCompletionKeys = "" handlerStack.bubbleEvent 'keydown', mockKeyboardEvent("A") focusInput 100 diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index a764b42d..33ccc95c 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -39,6 +39,7 @@ <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.js"></script> <script type="text/javascript" src="../../content_scripts/vimium_frontend.js"></script> <script type="text/javascript" src="../shoulda.js/shoulda.js"></script> diff --git a/tests/unit_tests/test_chrome_stubs.coffee b/tests/unit_tests/test_chrome_stubs.coffee index 3258bcd6..7f666068 100644 --- a/tests/unit_tests/test_chrome_stubs.coffee +++ b/tests/unit_tests/test_chrome_stubs.coffee @@ -41,6 +41,8 @@ exports.chrome = addListener: () -> true getAll: () -> true + browserAction: + setBadgeBackgroundColor: -> storage: # chrome.storage.local local: |
