diff options
| -rw-r--r-- | background_scripts/main.coffee | 18 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 79 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 13 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 33 | ||||
| -rw-r--r-- | content_scripts/mode_passkeys.coffee | 13 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 12 | ||||
| -rw-r--r-- | lib/utils.coffee | 18 |
7 files changed, 86 insertions, 100 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index e8f39326..83a6b3f8 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -339,17 +339,29 @@ 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] + # This is Vimium blue (from the icon). + # color: [102, 176, 226, 255] + # This is a slightly darker blue. It makes the badge more striking in the corner of the eye, and the symbol + # easier to read. + color: [82, 156, 206, 255] setBadge = do -> current = "" + timer = null + updateBadge = (badge) -> -> chrome.browserAction.setBadgeText text: badge (request) -> badge = request.badge if badge? and badge != current - chrome.browserAction.setBadgeText {text: badge || ""} current = badge + clearTimeout timer if timer + if badge == "" + # We set an empty badge immediately. This is the common case when changing tabs. + updateBadge(badge)() + else + # We wait a few milliseconds before setting any other badge. This avoids badge flicker when there are + # rapid changes (e.g. InsertMode is activated by find, followed almost immediately by PostFindMode). + timer = setTimeout updateBadge(badge), 50 # 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. diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index cc1250b4..6bd09af2 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -9,7 +9,7 @@ # badge: # A badge (to appear on the browser popup). # Optional. Define a badge if the badge is constant; for example, in find mode the badge is always "/". -# Otherwise, do not define a badge, but instead override the chooseBadge method; for example, in passkeys +# Otherwise, do not define a badge, but instead override the updateBadge method; for example, in passkeys # mode, the badge may be "P" or "", depending on the configuration state. Or, if the mode *never* shows a # badge, then do neither. # @@ -51,18 +51,13 @@ class Mode @count = ++count @id = "#{@name}-#{@count}" - @log "activate:", @id if @debug + @log "activate:", @id @push keydown: @options.keydown || null keypress: @options.keypress || null keyup: @options.keyup || null - updateBadge: (badge) => @alwaysContinueBubbling => @chooseBadge badge - - # Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton - # if @options.singleton is truthy. The value of @options.singleton should be the key which is required to - # be unique. New instances deactivate existing instances. - @registerSingleton @options.singleton if @options.singleton + updateBadge: (badge) => @alwaysContinueBubbling => @updateBadge badge # If @options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed. if @options.exitOnEscape @@ -72,8 +67,8 @@ class Mode _name: "mode-#{@id}/exitOnEscape" "keydown": (event) => return @continueBubbling unless KeyboardUtils.isEscape event - @exit event, event.srcElement DomUtils.suppressKeyupAfterEscape handlerStack + @exit event, event.srcElement @suppressEvent # If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element @@ -89,22 +84,40 @@ class Mode _name: "mode-#{@id}/exitOnClick" "click": (event) => @alwaysContinueBubbling => @exit event + # Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton + # if @options.singleton is truthy. The value of @options.singleton should be the which is intended to + # be unique. New instances deactivate existing instances. + if @options.singleton + do => + singletons = Mode.singletons ||= {} + key = @options.singleton + @onExit => delete singletons[key] if singletons[key] == @ + if singletons[key] + @log "singleton:", "deactivating #{singletons[key].id}" + # We're currently installing a new mode, so we'll be updating the badge shortly. Therefore, we can + # suppress badge updates while deactivating the existing singleton. This can prevent badge flicker. + singletons[key].exit() + singletons[key] = @ + # 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. + # and calls @registerStateChange() (if defined) whenever the state changes. The mode also tracks the + # keyQueue in @keyQueue. if @options.trackState @enabled = false @passKeys = "" + @keyQueue = "" @push _name: "mode-#{@id}/registerStateChange" - "registerStateChange": ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => + registerStateChange: ({ enabled: enabled, passKeys: passKeys }) => @alwaysContinueBubbling => if enabled != @enabled or passKeys != @passKeys @enabled = enabled @passKeys = passKeys @registerStateChange?() + registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = keyQueue Mode.modes.push @ Mode.updateBadge() - @logStack() if @debug + @logStack() # End of Mode constructor. push: (handlers) -> @@ -121,7 +134,7 @@ class Mode exit: -> if @modeIsActive - @log "deactivate:", @id if @debug + @log "deactivate:", @id handler() for handler in @exitHandlers handlerStack.remove handlerId for handlerId in @handlers Mode.modes = Mode.modes.filter (mode) => mode != @ @@ -129,8 +142,8 @@ class Mode @modeIsActive = false # The badge is chosen by bubbling an "updateBadge" event down the handler stack allowing each mode the - # opportunity to choose a badge. chooseBadge, here, is the default. It is overridden in sub-classes. - chooseBadge: (badge) -> + # opportunity to choose a badge. This is overridden in sub-classes. + updateBadge: (badge) -> badge.badge ||= @badge # Shorthand for an otherwise long name. This wraps a handler with an arbitrary return value, and always @@ -138,40 +151,24 @@ class Mode # case), because they do not need to be concerned with the value they yield. alwaysContinueBubbling: handlerStack.alwaysContinueBubbling - # Used for sometimes suppressing badge updates. - @badgeSuppressor: new Utils.Suppressor() - # Static method. Used externally and internally to initiate bubbling of an updateBadge event and to send # the resulting badge to the background page. We only update the badge if this document (hence this frame) # has the focus. @updateBadge: -> - @badgeSuppressor.unlessSuppressed -> - if document.hasFocus() - handlerStack.bubbleEvent "updateBadge", badge = { badge: "" } - chrome.runtime.sendMessage - handler: "setBadge" - badge: badge.badge - - registerSingleton: do -> - singletons = {} # Static. - (key) -> - if singletons[key] - @log "singleton:", "deactivating #{singletons[key].id}" if @debug - # We're currently installing a new mode. So we'll be updating the badge shortly. Therefore, we can - # suppress badge updates while deactivating the existing singleton. This prevents the badge from - # flickering in some cases. - Mode.badgeSuppressor.runSuppresed -> singletons[key].exit() - singletons[key] = @ - - @onExit => delete singletons[key] if singletons[key] == @ + if document.hasFocus() + handlerStack.bubbleEvent "updateBadge", badge = badge: "" + chrome.runtime.sendMessage + handler: "setBadge" + badge: badge.badge # Debugging routines. logStack: -> - @log "active modes (top to bottom):" - @log " ", mode.id for mode in Mode.modes[..].reverse() + if @debug + @log "active modes (top to bottom):" + @log " ", mode.id for mode in Mode.modes[..].reverse() log: (args...) -> - console.log args... + console.log args... if @debug # Return the must-recently activated mode (only used in tests). @top: -> @@ -193,7 +190,7 @@ new class BadgeMode extends Mode _name: "mode-#{@id}/focus" "focus": => @alwaysContinueBubbling -> Mode.updateBadge() - chooseBadge: (badge) -> + updateBadge: (badge) -> # If we're not enabled, then post an empty badge. badge.badge = "" unless @enabled diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 21638a34..6b4f6bb1 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -5,6 +5,7 @@ class SuppressPrintable extends Mode constructor: (options) -> super options handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling + type = document.getSelection().type # We use unshift here, so we see events after normal mode, so we only see unmapped keys. @unshift @@ -12,13 +13,9 @@ class SuppressPrintable extends Mode keydown: handler keypress: handler keyup: (event) => - # If the selection is no longer a range, then the user is interacting with the input element, so we - # get out of the way. See discussion of option 5c from #1415. - if document.getSelection().type != "Range" - console.log "aaa", @options.targetElement - @exit() - else - handler event + # If the selection types has changed (usually, no longer "Range"), then the user is interacting with + # the input element, so we get out of the way. See discussion of option 5c from #1415. + if document.getSelection().type != type then @exit() else handler event # When we use find mode, the selection/focus can land in a focusable/editable element. In this situation, # special considerations apply. We implement three special cases: @@ -56,7 +53,7 @@ class PostFindMode extends SuppressPrintable true # Continue bubbling. # If PostFindMode is active, then we suppress the "I" badge from insert mode. - chooseBadge: (badge) -> InsertMode.suppressEvent badge + updateBadge: (badge) -> InsertMode.suppressEvent badge root = exports ? window root.PostFindMode = PostFindMode diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 204c629d..ef7223ad 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -1,10 +1,14 @@ class InsertMode extends Mode - # There is one permanently-installed instance of InsertMode. + # There is one permanently-installed instance of InsertMode. It watches for focus changes and + # activates/deactivates itself accordingly. @permanentInstance: null constructor: (options = {}) -> InsertMode.permanentInstance ||= @ + @permanent = (@ == InsertMode.permanentInstance) + + # If truthy, then options.global indicates that we were activated by the user (with "i"). @global = options.global defaults = @@ -17,7 +21,7 @@ class InsertMode extends Mode @insertModeLock = if document.activeElement and DomUtils.isEditable document.activeElement - # We have already focused an input element, so use it. + # An input element is already active, so use it. document.activeElement else null @@ -26,15 +30,16 @@ class InsertMode extends Mode "blur": (event) => @alwaysContinueBubbling => target = event.target # We can't rely on focus and blur events arriving in the expected order. When the active element - # changes, we might get "blur" before "focus". The approach we take is to track the active element in - # @insertModeLock, and exit only when the that element blurs. + # changes, we might get "blur" before "focus". We track the active element in @insertModeLock, and + # exit only when that element blurs. @exit event, target if target == @insertModeLock and DomUtils.isFocusable target "focus": (event) => @alwaysContinueBubbling => if @insertModeLock != event.target and DomUtils.isFocusable event.target @insertModeLock = event.target Mode.updateBadge() - isActive: -> + isActive: (event) -> + return false if event == InsertMode.suppressedEvent return true if @insertModeLock or @global # Some sites (e.g. inbox.google.com) change the contentEditable property on the fly (see #1245); and # unfortunately, the focus event fires *before* the change. Therefore, we need to re-check whether the @@ -45,7 +50,7 @@ class InsertMode extends Mode @insertModeLock != null handleKeydownEvent: (event) -> - return @continueBubbling if event == InsertMode.suppressedEvent or not @isActive() + return @continueBubbling unless @isActive event return @stopBubblingAndTrue unless KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack @exit event, event.srcElement @@ -53,26 +58,24 @@ class InsertMode extends Mode # Handles keypress and keyup events. handleKeyEvent: (event) -> - if @isActive() and event != InsertMode.suppressedEvent then @stopBubblingAndTrue else @continueBubbling + if @isActive event then @stopBubblingAndTrue else @continueBubbling exit: (_, target) -> if (target and target == @insertModeLock) or @global or target == undefined @insertModeLock = null if target and DomUtils.isFocusable target - # Remove the focus, so the user can't just get himself back into insert mode by typing in the same input - # box. + # Remove the focus, so the user can't just get 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. target.blur() - # Really exit, but only if this isn't the permanently-installed instance. - if @ == InsertMode.permanentInstance then Mode.updateBadge() else super() + # Exit, but only if this isn't the permanently-installed instance. + if @permanent then Mode.updateBadge() else super() - chooseBadge: (badge) -> - return if badge == InsertMode.suppressedEvent - badge.badge ||= "I" if @isActive() + updateBadge: (badge) -> + badge.badge ||= "I" if @isActive badge - # Static stuff to allow PostFindMode to suppress insert mode. + # Static stuff. This allows PostFindMode to suppress insert mode. @suppressedEvent: null @suppressEvent: (event) -> @suppressedEvent = event diff --git a/content_scripts/mode_passkeys.coffee b/content_scripts/mode_passkeys.coffee index a6cd7d2d..a40fe7a6 100644 --- a/content_scripts/mode_passkeys.coffee +++ b/content_scripts/mode_passkeys.coffee @@ -3,25 +3,20 @@ class PassKeysMode extends Mode constructor: -> super name: "passkeys" - trackState: true + trackState: true # Maintain @enabled, @passKeys and @keyQueue. keydown: (event) => @handleKeyChar KeyboardUtils.getKeyChar event keypress: (event) => @handleKeyChar String.fromCharCode event.charCode keyup: (event) => @handleKeyChar String.fromCharCode event.charCode - @keyQueue = "" - @push - registerKeyQueue: ({ keyQueue: keyQueue }) => @alwaysContinueBubbling => @keyQueue = keyQueue - - # Decide whether this event should be passed to the underlying page. 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. + # 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: (keyChar) -> if keyChar and not @keyQueue and 0 <= @passKeys.indexOf keyChar @stopBubblingAndTrue else @continueBubbling - chooseBadge: (badge) -> + updateBadge: (badge) -> badge.badge ||= "P" if @passKeys and not @keyQueue root = exports ? window diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index b2c591fd..0a034e28 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -118,7 +118,7 @@ initializePreDomReady = -> keyup: (event) => onKeyup.call @, event # Install the permanent modes and handlers. The permanent insert mode operates only when focusable/editable - # elements have the focus. + # elements are active. new NormalMode Scroller.init settings new PassKeysMode @@ -360,10 +360,6 @@ extend window, hint - hintContainingDiv = DomUtils.addElementList hints, - id: "vimiumInputMarkerContainer" - className: "vimiumReset" - new class FocusSelector extends Mode constructor: -> super @@ -386,6 +382,10 @@ extend window, @continueBubbling @onExit -> DomUtils.removeElement hintContainingDiv + hintContainingDiv = DomUtils.addElementList hints, + id: "vimiumInputMarkerContainer" + className: "vimiumReset" + visibleInputs[selectedInputIndex].element.focus() if visibleInputs.length == 1 @exit() @@ -396,7 +396,7 @@ extend window, # 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. isPassKey = ( keyChar ) -> - return false # Diabled. + return false # Disabled. return !keyQueue and passKeys and 0 <= passKeys.indexOf(keyChar) # Track which keydown events we have handled, so that we can subsequently suppress the corresponding keyup diff --git a/lib/utils.coffee b/lib/utils.coffee index a7bfc440..661f7e84 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -152,24 +152,6 @@ Utils = # locale-sensitive uppercase detection hasUpperCase: (s) -> s.toLowerCase() != s - # Utility class. This can help different software components to interact without having to share much logic - # and/or state. See InsertModeTrigger for an example. - # suppressedResult is the value to be returned when a function call is suppressed. - Suppressor: class Suppressor - constructor: (@suppressedResult = null)-> - @count = 0 - - suppress: -> @count += 1 - unsuppress: -> @count -= 1 - - runSuppresed: (func) -> - @suppress() - func() - @unsuppress() - - unlessSuppressed: (func) -> - if 0 < @count then @suppressedResult else func() - # This creates a new function out of an existing function, where the new function takes fewer arguments. This # allows us to pass around functions instead of functions + a partial list of arguments. Function::curry = -> |
