aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2015-01-10 08:25:02 +0000
committerStephen Blott2015-01-10 11:03:01 +0000
commitfdcdd0113049042c94b2b56a6b716e2da58b860e (patch)
tree5cbeacce234df58fc1f5c125f2055a591578a510
parentac90db47aa2671cd663cc6a9cdf783dc30a582e9 (diff)
downloadvimium-fdcdd0113049042c94b2b56a6b716e2da58b860e.tar.bz2
Modes; instrument for debugging...
- Set Mode.debug to true to see mode activation/deactivation on the console. - Use Mode.log() to see a list of currently-active modes. - Use handlerStack.debugOn() to enable debugging of the handler stack.
-rw-r--r--content_scripts/mode.coffee55
-rw-r--r--content_scripts/mode_find.coffee1
-rw-r--r--content_scripts/mode_insert.coffee20
-rw-r--r--content_scripts/scroller.coffee15
-rw-r--r--content_scripts/vimium_frontend.coffee14
-rw-r--r--lib/dom_utils.coffee1
-rw-r--r--lib/handler_stack.coffee34
7 files changed, 107 insertions, 33 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index 37f3a8c2..a33197b0 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -38,12 +38,14 @@
# myMode.exit() # externally triggered.
#
-# For debug only; to be stripped out.
+# For debug only.
count = 0
class Mode
- # If this is true, then we generate a trace of modes being activated and deactivated on the console.
- @debug = true
+ # If Mode.debug is true, then we generate a trace of modes being activated and deactivated on the console, along
+ # with a list of the currently active modes.
+ debug: true
+ @modes: []
# Constants; short, readable names for handlerStack event-handler return values.
continueBubbling: true
@@ -60,7 +62,8 @@ class Mode
@name = @options.name || "anonymous"
@count = ++count
- console.log @count, "create:", @name if Mode.debug
+ @id = "#{@name}-#{@count}"
+ @logger "activate:", @id if @debug
@push
keydown: @options.keydown || null
@@ -80,6 +83,7 @@ class Mode
# Note. This handler ends up above the mode's own key handlers on the handler stack, so it takes
# priority.
@push
+ _name: "mode-#{@id}/exitOnEscape"
"keydown": (event) =>
return @continueBubbling unless KeyboardUtils.isEscape event
@exit event
@@ -90,6 +94,7 @@ class Mode
# loses the focus.
if @options.exitOnBlur
@push
+ _name: "mode-#{@id}/exitOnBlur"
"blur": (event) => @alwaysContinueBubbling => @exit() if event.srcElement == @options.exitOnBlur
# If @options.trackState is truthy, then the mode mainatins the current state in @enabled and @passKeys,
@@ -98,6 +103,7 @@ class Mode
@enabled = false
@passKeys = ""
@push
+ _name: "mode-#{@id}/registerStateChange"
"registerStateChange": ({ enabled: enabled, passKeys: passKeys }) =>
@alwaysContinueBubbling =>
if enabled != @enabled or passKeys != @passKeys
@@ -110,30 +116,38 @@ class Mode
# from propagating to other extensions or the host page.
if @options.trapAllKeyboardEvents
@unshift
- keydown: (event) => @alwaysContinueBubbling =>
- DomUtils.suppressPropagation event if event.srcElement == @options.trapAllKeyboardEvents
- keypress: (event) => @alwaysContinueBubbling =>
- DomUtils.suppressEvent event if event.srcElement == @options.trapAllKeyboardEvents
- keyup: (event) => @alwaysContinueBubbling =>
- DomUtils.suppressPropagation event if event.srcElement == @options.trapAllKeyboardEvents
+ _name: "mode-#{@id}/trapAllKeyboardEvents"
+ keydown: (event) =>
+ if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling
+ keypress: (event) =>
+ if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling
+ keyup: (event) =>
+ if event.srcElement == @options.trapAllKeyboardEvents then @suppressEvent else @continueBubbling
Mode.updateBadge() if @badge
- # End of Mode.constructor().
+ Mode.modes.push @
+ @log() if @debug
+ handlerStack.debugOn()
+ # End of Mode constructor.
push: (handlers) ->
+ handlers._name ||= "mode-#{@id}"
@handlers.push handlerStack.push handlers
unshift: (handlers) ->
- @handlers.unshift handlerStack.push handlers
+ handlers._name ||= "mode-#{@id}"
+ handlers._name += "/unshifted"
+ @handlers.push handlerStack.unshift handlers
onExit: (handler) ->
@exitHandlers.push handler
exit: ->
if @modeIsActive
- console.log @count, "exit:", @name if Mode.debug
+ @logger "deactivate:", @id if @debug
handler() for handler in @exitHandlers
handlerStack.remove handlerId for handlerId in @handlers
+ Mode.modes = Mode.modes.filter (mode) => mode != @
Mode.updateBadge()
@modeIsActive = false
@@ -177,12 +191,24 @@ class Mode
# flickering in some cases.
Mode.badgeSuppressor.runSuppresed =>
if singletons[key]
- console.log singletons[key].count, "singleton:", @name, "(deactivating)"
+ @logger "singleton:", "deactivating #{singletons[key].id}" if @debug
singletons[key].exit()
singletons[key] = @
@onExit => delete singletons[key] if singletons[key] == @
+ # Debugging routines.
+ log: ->
+ if Mode.modes.length == 0
+ @logger "It looks like debugging is not enabled in modes.coffee."
+ else
+ @logger "active modes (top to bottom), current: #{@id}"
+ for mode in Mode.modes[..].reverse()
+ @logger " ", mode.id
+
+ logger: (args...) ->
+ handlerStack.log args...
+
# BadgeMode is a pseudo mode for triggering badge updates on focus changes and state updates. It sits at the
# bottom of the handler stack, and so it receives state changes *after* all other modes, and can override the
# badge choice of the other active modes.
@@ -194,6 +220,7 @@ new class BadgeMode extends Mode
trackState: true
@push
+ _name: "mode-#{@id}/focus"
"focus": => @alwaysContinueBubbling -> Mode.updateBadge()
chooseBadge: (badge) ->
diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee
index d63b3319..0ce03af6 100644
--- a/content_scripts/mode_find.coffee
+++ b/content_scripts/mode_find.coffee
@@ -33,6 +33,7 @@ class PostFindMode extends InsertModeBlocker
self = @
@push
+ _name: "mode-#{@id}/handle-escape"
keydown: (event) ->
if element == document.activeElement and KeyboardUtils.isEscape event
self.exit()
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
index 144b0be6..7668d794 100644
--- a/content_scripts/mode_insert.coffee
+++ b/content_scripts/mode_insert.coffee
@@ -51,6 +51,7 @@ class InsertModeTrigger extends Mode
@stopBubblingAndTrue
@push
+ _name: "mode-#{@id}/activate-on-focus"
focus: (event) =>
triggerSuppressor.unlessSuppressed =>
@alwaysContinueBubbling =>
@@ -78,6 +79,7 @@ class InsertModeBlocker extends Mode
@onExit -> triggerSuppressor.unsuppress()
@push
+ _name: "mode-#{@id}/bail-on-click"
"click": (event) =>
@alwaysContinueBubbling =>
# The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the
@@ -92,18 +94,18 @@ class InsertModeBlocker extends Mode
new @options.onClickMode
targetElement: document.activeElement
-# There's some unfortunate feature interaction with chrome's content editable handling. If the selection is
-# content editable and a descendant of the active element, then chrome focuses it on any unsuppressed keyboard
-# event. This has the unfortunate effect of dropping us unintentally into insert mode. See #1415.
-# A single instance of this mode sits near the bottom of the handler stack and suppresses keyboard events if:
-# - they haven't been handled by any other mode (so not by normal mode, passkeys mode, insert mode, and so
-# on),
+# There's some unfortunate feature interaction with chrome's contentEditable handling. If the selection is
+# contentEditable and a descendant of the active element, then chrome focuses it on any unsuppressed keyboard
+# event. This has the unfortunate effect of dropping us unintentally into insert mode. See #1415. A single
+# instance of this mode sits near the bottom of the handler stack and suppresses keyboard events if:
+# - they haven't been handled by any other mode (so not by normal mode, passkeys, insert, ...),
# - the selection is content editable, and
# - the selection is a descendant of the active element.
# This should rarely fire, typically only on fudged keypresses in normal mode. And, even then, only in the
# circumstances outlined above. So, we shouldn't usually be blocking keyboard events for other extensions or
# the page itself.
-# handling keyboard events.
+# There's some controversy as to whether this is the right thing to do. See discussion in #1415. This
+# implements Option 2 from there, although Option 3 would be a reasonable alternative.
new class ContentEditableTrap extends Mode
constructor: ->
super
@@ -112,10 +114,10 @@ new class ContentEditableTrap extends Mode
keypress: (event) => @handle => @suppressEvent
keyup: (event) => @handle => @suppressEvent
- handle: (func) -> if @isContentEditableFocused() then func() else @continueBubbling
+ handle: (func) -> if @wouldTriggerInsert() then func() else @continueBubbling
# True if the selection is content editable and a descendant of the active element.
- isContentEditableFocused: ->
+ wouldTriggerInsert: ->
element = document.getSelection()?.anchorNode?.parentElement
return element?.isContentEditable and
document.activeElement and
diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee
index 889dc042..f70d3aed 100644
--- a/content_scripts/scroller.coffee
+++ b/content_scripts/scroller.coffee
@@ -124,12 +124,15 @@ CoreScroller =
@keyIsDown = false
handlerStack.push
+ _name: 'scroller/track-key-down/up'
keydown: (event) =>
- @keyIsDown = true
- @lastEvent = event
+ handlerStack.alwaysContinueBubbling =>
+ @keyIsDown = true
+ @lastEvent = event
keyup: =>
- @keyIsDown = false
- @time += 1
+ handlerStack.alwaysContinueBubbling =>
+ @keyIsDown = false
+ @time += 1
# Return true if CoreScroller would not initiate a new scroll right now.
wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll"
@@ -205,7 +208,9 @@ CoreScroller =
# Scroller contains the two main scroll functions (scrollBy and scrollTo) which are exported to clients.
Scroller =
init: (frontendSettings) ->
- handlerStack.push DOMActivate: -> activatedElement = event.target
+ handlerStack.push
+ _name: 'scroller/active-element'
+ DOMActivate: (event) -> handlerStack.alwaysContinueBubbling -> activatedElement = event.target
CoreScroller.init frontendSettings
# scroll the active element in :direction by :amount * :factor.
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index a9bf30a3..1406b1e7 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -396,10 +396,16 @@ extend window,
visibleInputs[selectedInputIndex].element.focus()
@suppressEvent
else unless event.keyCode == KeyboardUtils.keyCodes.shiftKey
- @exit event
- # In @exit(), we just pushed a new mode (usually insert mode). Restart bubbling, so that the
- # new mode can now see the event too.
- @restartBubbling
+ mode = @exit event
+ if mode
+ # In @exit(), we just pushed a new mode (usually insert mode). Restart bubbling, so that the
+ # new mode can now see the event too.
+ # Exception: If the new mode exits on Escape, and this key event is Escape, then rebubbling the
+ # event will just cause the mode to exit immediately. So we suppress Escapes.
+ if mode.options.exitOnEscape and KeyboardUtils.isEscape event
+ @suppressEvent
+ else
+ @restartBubbling
visibleInputs[selectedInputIndex].element.focus()
return @exit() if visibleInputs.length == 1
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 9d7ca867..322188b3 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -206,6 +206,7 @@ DomUtils =
# Suppress the next keyup event for Escape.
suppressKeyupAfterEscape: (handlerStack) ->
handlerStack.push
+ _name: "dom_utils/suppressKeyupAfterEscape"
keyup: (event) ->
return true unless KeyboardUtils.isEscape event
@remove()
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 44c7538b..22d04941 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -3,6 +3,8 @@ root = exports ? window
class HandlerStack
constructor: ->
+ @debug = false
+ @eventNumber = 0
@stack = []
@counter = 0
@@ -22,8 +24,10 @@ class HandlerStack
# Adds a handler to the top of the stack. Returns a unique ID for that handler that can be used to remove it
# later.
push: (handler) ->
- @stack.push handler
handler.id = ++@counter
+ handler._name ||= "anon-#{@counter}"
+ @stack.push handler
+ handler.id
# Adds a handler to the bottom of the stack. Returns a unique ID for that handler that can be used to remove
# it later.
@@ -35,6 +39,7 @@ class HandlerStack
# event's propagation by returning a falsy value, or stop bubbling by returning @stopBubblingAndFalse or
# @stopBubblingAndTrue.
bubbleEvent: (type, event) ->
+ @eventNumber += 1
# We take a copy of the array in order to avoid interference from concurrent removes (for example, to
# avoid calling the same handler twice, because elements have been spliced out of the array by remove).
for handler in @stack[..].reverse()
@@ -42,6 +47,7 @@ class HandlerStack
if handler?.id and handler[type]
@currentId = handler.id
result = handler[type].call @, event
+ @logResult type, event, handler, result if @debug
if not result
DomUtils.suppressEvent(event) if @isChromeEvent event
return false
@@ -75,5 +81,31 @@ class HandlerStack
handler()
false
+ # Debugging.
+ debugOn: -> @debug = true
+ debugOff: -> @debug = false
+
+ logResult: (type, event, handler, result) ->
+ # FIXME(smblott). Badge updating is too noisy, so we filter it out. However, we do need to look at how
+ # many badge update events are happening. It seems to be more than necessary.
+ return if type == "updateBadge"
+ label =
+ switch result
+ when @stopBubblingAndTrue then "stop/true"
+ when @stopBubblingAndFalse then "stop/false"
+ when @restartBubbling then "rebubble"
+ when true then "continue"
+ label ||= if result then "continue/truthy" else "suppress"
+ @log @eventNumber, type, handler._name, label
+
+ logRecords: []
+ log: (args...) ->
+ line = args.join " "
+ @logRecords.push line
+ console.log line
+
+ clipLog: ->
+ Clipboard.copy logRecords.join "\n"
+
root.HandlerStack = HandlerStack
root.handlerStack = new HandlerStack