]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
fix hintsmode clicking in ff56, squash some exceptions
[VimFx.git] / extension / lib / events.coffee
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.
4
5 button = require('./button')
6 messageManager = require('./message-manager')
7 prefs = require('./prefs')
8 utils = require('./utils')
9
10 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
11
12 class UIEventManager
13 constructor: (@vimfx, @window) ->
14 @listen = utils.listen.bind(null, @window)
15 @listenOnce = utils.listenOnce.bind(null, @window)
16
17 # This flag controls whether to suppress the various key events or not.
18 @suppress = false
19
20 # If a matched shortcut has the `<late>` special key, this flag is set to
21 # `true`.
22 @late = false
23
24 # When a menu or panel is shown VimFx should temporarily stop processing
25 # keyboard input, allowing accesskeys to be used.
26 @popupPassthrough = false
27
28 addListeners: ->
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
36
37 @listen('popupshown', checkPassthrough.bind(null, true))
38 @listen('popuphidden', checkPassthrough.bind(null, false))
39
40 @listen('keydown', (event) =>
41 # No matter what, always reset the `@suppress` flag, so we don't
42 # suppress more than intended.
43 @suppress = false
44
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.
48 @late = false
49 prefs.set('late', false)
50
51 if @popupPassthrough
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.
60
61 return unless vim = @vimfx.getCurrentVim(@window)
62
63 @consumeKeyEvent(vim, event)
64 if @suppress
65 utils.suppressEvent(event) # This also suppresses the 'keypress' event.
66 else
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)
72 )
73
74 @listen('keyup', (event) =>
75 utils.suppressEvent(event) if @suppress
76 @setHeldModifiers(event, {filterCurrentOnly: true})
77 )
78
79 @listen('focus', => @setFocusType())
80 @listen('blur', =>
81 @window.setTimeout((=>
82 @setFocusType()
83 ), @vimfx.options.blur_timeout)
84 )
85
86 @listen('click', (event) =>
87 target = event.originalTarget
88 return unless vim = @vimfx.getCurrentVim(@window)
89
90 vim.hideNotification()
91
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
95 # Firefox.
96 isVimFxGeneratedEvent = (
97 event.layerX == 0 and event.layerY == 0 and
98 event.movementX == 0 and event.movementY == 0
99 )
100
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')
112
113 vim._send('clearHover') unless isVimFxGeneratedEvent
114 )
115
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)
121 )
122
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)
128 )
129
130 @listen('TabSelect', (event) =>
131 target = event.originalTarget
132 target.setAttribute('VimFx-visited', 'true')
133 @vimfx.emit('TabSelect', {event})
134
135 return unless vim = @vimfx.getCurrentVim(@window)
136 vim.hideNotification()
137 @vimfx.emit('focusTypeChange', {vim})
138 )
139
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
145 )
146
147 messageManager.listen('cachedPageshow', ((data, callback, browser) =>
148 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
149 unless oldVim
150 callback(false)
151 return
152
153 if @vimfx.vims.has(browser)
154 vim = @vimfx.vims.get(browser)
155 if vim._messageManager == vim.browser.messageManager
156 callback(false)
157 return
158
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
162 # for more details.
163 oldVim._setBrowser(browser)
164 @vimfx.vims.set(browser, oldVim)
165 @vimfx.emit('modeChange', {vim: oldVim})
166 callback(true)
167 ), {messageManager: @window.messageManager})
168
169 setFocusType: ->
170 return unless vim = @vimfx.getCurrentVim(@window)
171
172 try
173 activeElement = utils.getActiveElement(@window)
174 catch
175 return # NOTE: throws "TypeError: utils is null" when clicking on a doorhanger
176
177 if activeElement == @window.gBrowser.selectedBrowser
178 vim._send('checkFocusType')
179 return
180
181 focusType = utils.getFocusType(activeElement)
182 vim._setFocusType(focusType)
183
184 if focusType == 'editable' and vim.mode == 'caret'
185 vim._enterMode('normal')
186
187 consumeKeyEvent: (vim, event) ->
188 match = vim._consumeKeyEvent(event)
189
190 if typeof match == 'boolean'
191 @suppress = match
192 return
193
194 if match
195 if match.specialKeys['<late>']
196 @suppress = false
197 @consumeLateKeydown(vim, event, match)
198 else
199 @suppress = vim._onInput(match, event)
200 else
201 @suppress = null
202 @setHeldModifiers(event)
203
204 consumeLateKeydown: (vim, event, match) ->
205 @late = true
206
207 # The passed in `event` is the regular non-late browser UI keydown event.
208 # It is only used to set held keys. This is easier than sending an event
209 # subset from frame scripts.
210 listener = ({defaultPrevented}) =>
211 # `@late` is reset on every keydown. If it is no longer `true`, it means
212 # that the page called `event.stopPropagation()`, which prevented this
213 # listener from running for that event.
214 return unless @late
215 @suppress =
216 if defaultPrevented
217 false
218 else
219 vim._onInput(match, event)
220 @setHeldModifiers(event)
221 return @suppress
222
223 if vim.isUIEvent(event)
224 @listenOnce('keydown', ((lateEvent) =>
225 listener(lateEvent)
226 if @suppress
227 utils.suppressEvent(lateEvent)
228 @listenOnce('keyup', utils.suppressEvent, false)
229 ), false)
230 else
231 # Hack to avoid synchronous messages on every keydown (see
232 # events-frame.coffee).
233 prefs.set('late', true)
234 vim._listenOnce('lateKeydown', listener)
235
236 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
237 mainWindow = @window.document.documentElement
238 modifiers =
239 if filterCurrentOnly
240 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
241 else
242 if @suppress == null then 'alt ctrl meta shift' else ''
243 isHeld = (modifier) -> event["#{modifier}Key"]
244 mainWindow.setAttribute(
245 HELD_MODIFIERS_ATTRIBUTE, modifiers.split(' ').filter(isHeld).join(' ')
246 )
247
248 anyPopupsOpen: ->
249 # The autocomplete popup in text inputs (for example) is technically a
250 # panel, but it does not respond to keypresses. Therefore
251 # `[ignorekeys="true"]` is excluded.
252 #
253 # coffeelint: disable=max_line_length
254 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
255 # coffeelint: enable=max_line_length
256 popups = utils.querySelectorAllDeep(
257 @window, ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
258 )
259 for popup in popups
260 return true if popup.state == 'open'
261 return false
262
263 module.exports = UIEventManager
Imprint / Impressum