1 # This file sets up all event listeners needed to power VimFx: To know when to
2 # launch commands and to provide state to them. Events in web page content are
3 # listened for in events-frame.coffee.
5 button = require('./button')
6 messageManager = require('./message-manager')
7 prefs = require('./prefs')
8 utils = require('./utils')
10 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
13 constructor: (@vimfx, @window) ->
14 @listen = utils.listen.bind(null, @window)
15 @listenOnce = utils.listenOnce.bind(null, @window)
17 # This flag controls whether to suppress the various key events or not.
20 # If a matched shortcut has the `<late>` special key, this flag is set to
24 # When a menu or panel is shown VimFx should temporarily stop processing
25 # keyboard input, allowing accesskeys to be used.
26 @popupPassthrough = false
29 checkPassthrough = (value, event) =>
30 target = event.originalTarget
31 if target.localName in ['menupopup', 'panel'] and
32 # Don’t set `@popupPassthrough` to `false` if there actually are popups
33 # open. This is the case when a sub-menu closes.
34 (value or not @anyPopupsOpen())
35 @popupPassthrough = value
37 @listen('popupshown', checkPassthrough.bind(null, true))
38 @listen('popuphidden', checkPassthrough.bind(null, false))
40 @listen('keydown', (event) =>
41 # No matter what, always reset the `@suppress` flag, so we don't
42 # suppress more than intended.
45 # Reset the `@late` flag, telling any late listeners for the previous
46 # event not to run. Also reset the `late` pref, telling frame scripts not
47 # to do synchronous message passing on every keydown.
49 prefs.set('late', false)
52 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
53 # can be stuck as `true` even though no popup is shown, effectively
54 # disabling the extension. Therefore we check if there actually _are_
55 # any open popups before stopping processing keyboard input. This is
56 # only done when popups (might) be open (not on every keystroke) for
57 # performance reasons.
58 return if @anyPopupsOpen()
59 @popupPassthrough = false # No popup was actually open.
61 return unless vim = @vimfx.getCurrentVim(@window)
63 @consumeKeyEvent(vim, event)
65 utils.suppressEvent(event) # This also suppresses the 'keypress' event.
67 # If this keydown event wasn’t suppressed, it’s an obvious interaction
68 # with the page. If it _was_ suppressed, though, it’s an interaction
69 # depending on the command triggered; if _it_ calls
70 # `vim.markPageInteraction()` or not.
71 vim.markPageInteraction() if vim.isUIEvent(event)
74 @listen('keyup', (event) =>
75 utils.suppressEvent(event) if @suppress
76 @setHeldModifiers(event, {filterCurrentOnly: true})
79 @listen('focus', => @setFocusType())
81 @window.setTimeout((=>
83 ), @vimfx.options.blur_timeout)
86 @listen('click', (event) =>
87 target = event.originalTarget
88 return unless vim = @vimfx.getCurrentVim(@window)
90 vim.hideNotification()
92 # In multi-process, clicks simulated by VimFx cannot be caught here. In
93 # non-multi-process, they unfortunately can. This hack should be
94 # sufficient for that case until non-multi-process is removed from
96 isVimFxGeneratedEvent = (
97 event.layerX == 0 and event.layerY == 0 and
98 event.movementX == 0 and event.movementY == 0
101 # If the user clicks the reload button or a link when in hints mode, we’re
102 # going to end up in hints mode without any markers. Or if the user clicks
103 # a text input, then that input will be focused, but you can’t type in it
104 # (instead markers will be matched). So if the user clicks anything in
105 # hints mode it’s better to leave it.
106 if vim.mode == 'hints' and not isVimFxGeneratedEvent and
107 # Exclude the VimFx button, though, since clicking it returns to normal
108 # mode. Otherwise we’d first return to normal mode and then the button
109 # would open the help dialog.
110 target != button.getButton(@window)
111 vim._enterMode('normal')
113 vim._send('clearHover') unless isVimFxGeneratedEvent
116 @listen('overflow', (event) =>
117 target = event.originalTarget
118 return unless vim = @vimfx.getCurrentVim(@window)
119 if vim._isUIElement(target)
120 vim._state.scrollableElements.addChecked(target)
123 @listen('underflow', (event) =>
124 target = event.originalTarget
125 return unless vim = @vimfx.getCurrentVim(@window)
126 if vim._isUIElement(target)
127 vim._state.scrollableElements.deleteChecked(target)
130 @listen('TabSelect', (event) =>
131 target = event.originalTarget
132 target.setAttribute('VimFx-visited', 'true')
133 @vimfx.emit('TabSelect', {event})
135 return unless vim = @vimfx.getCurrentVim(@window)
136 vim.hideNotification()
137 @vimfx.emit('focusTypeChange', {vim})
140 @listen('TabClose', (event) =>
141 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
142 return unless vim = @vimfx.vims.get(browser)
143 # Note: `lastClosedVim` must be stored so that any window can access it.
144 @vimfx.lastClosedVim = vim
147 messageManager.listen('cachedPageshow', ((data, callback, browser) =>
148 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
153 if @vimfx.vims.has(browser)
154 vim = @vimfx.vims.get(browser)
155 if vim._messageManager == vim.browser.messageManager
159 # If we get here, it means that we’ve detected a tab dragged from one
160 # window to another. If so, the `vim` object from the last closed tab (the
161 # moved tab) should be re-used. See the commit message for commit bb70257d
163 oldVim._setBrowser(browser)
164 @vimfx.vims.set(browser, oldVim)
165 @vimfx.emit('modeChange', {vim: oldVim})
167 ), {messageManager: @window.messageManager})
170 return unless vim = @vimfx.getCurrentVim(@window)
172 activeElement = utils.getActiveElement(@window)
174 if activeElement == @window.gBrowser.selectedBrowser
175 vim._send('checkFocusType')
178 focusType = utils.getFocusType(activeElement)
179 vim._setFocusType(focusType)
181 if focusType == 'editable' and vim.mode == 'caret'
182 vim._enterMode('normal')
184 consumeKeyEvent: (vim, event) ->
185 match = vim._consumeKeyEvent(event)
187 if typeof match == 'boolean'
192 if match.specialKeys['<late>']
194 @consumeLateKeydown(vim, event, match)
196 @suppress = vim._onInput(match, event)
199 @setHeldModifiers(event)
201 consumeLateKeydown: (vim, event, match) ->
204 # The passed in `event` is the regular non-late browser UI keydown event.
205 # It is only used to set held keys. This is easier than sending an event
206 # subset from frame scripts.
207 listener = ({defaultPrevented}) =>
208 # `@late` is reset on every keydown. If it is no longer `true`, it means
209 # that the page called `event.stopPropagation()`, which prevented this
210 # listener from running for that event.
216 vim._onInput(match, event)
217 @setHeldModifiers(event)
220 if vim.isUIEvent(event)
221 @listenOnce('keydown', ((lateEvent) =>
224 utils.suppressEvent(lateEvent)
225 @listenOnce('keyup', utils.suppressEvent, false)
228 # Hack to avoid synchronous messages on every keydown (see
229 # events-frame.coffee).
230 prefs.set('late', true)
231 vim._listenOnce('lateKeydown', listener)
233 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
234 mainWindow = @window.document.documentElement
237 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
239 if @suppress == null then 'alt ctrl meta shift' else ''
240 isHeld = (modifier) -> event["#{modifier}Key"]
241 mainWindow.setAttribute(
242 HELD_MODIFIERS_ATTRIBUTE, modifiers.split(' ').filter(isHeld).join(' ')
246 # The autocomplete popup in text inputs (for example) is technically a
247 # panel, but it does not respond to keypresses. Therefore
248 # `[ignorekeys="true"]` is excluded.
250 # coffeelint: disable=max_line_length
251 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
252 # coffeelint: enable=max_line_length
253 popups = utils.querySelectorAllDeep(
254 @window, ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
257 return true if popup.state == 'open'
260 module.exports = UIEventManager