aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/main.coffee10
-rw-r--r--content_scripts/mode.coffee78
-rw-r--r--content_scripts/vimium_frontend.coffee106
-rw-r--r--lib/handler_stack.coffee10
-rw-r--r--tests/dom_tests/dom_tests.coffee1
-rw-r--r--tests/dom_tests/dom_tests.html1
-rw-r--r--tests/unit_tests/test_chrome_stubs.coffee2
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: