diff options
| author | Stephen Blott | 2015-01-10 08:25:02 +0000 |
|---|---|---|
| committer | Stephen Blott | 2015-01-10 11:03:01 +0000 |
| commit | fdcdd0113049042c94b2b56a6b716e2da58b860e (patch) | |
| tree | 5cbeacce234df58fc1f5c125f2055a591578a510 | |
| parent | ac90db47aa2671cd663cc6a9cdf783dc30a582e9 (diff) | |
| download | vimium-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.coffee | 55 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 1 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 20 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 15 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 14 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 1 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 34 |
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 |
