diff options
| -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: | 
