aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/main.coffee18
-rw-r--r--content_scripts/mode.coffee79
-rw-r--r--content_scripts/mode_find.coffee13
-rw-r--r--content_scripts/mode_insert.coffee33
-rw-r--r--content_scripts/mode_passkeys.coffee13
-rw-r--r--content_scripts/vimium_frontend.coffee12
-rw-r--r--lib/utils.coffee18
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 = ->