]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Consistently refer to keypresses as "keypresses"
[VimFx.git] / extension / lib / events.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015, 2016.
4 #
5 # This file is part of VimFx.
6 #
7 # VimFx is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # VimFx is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
19 ###
20
21 # This file sets up all event listeners needed to power VimFx: To know when to
22 # launch commands and to provide state to them. Events in web page content are
23 # listened for in events-frame.coffee.
24
25 button = require('./button')
26 messageManager = require('./message-manager')
27 prefs = require('./prefs')
28 utils = require('./utils')
29
30 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
31
32 class UIEventManager
33 constructor: (@vimfx, @window) ->
34 @listen = utils.listen.bind(null, @window)
35 @listenOnce = utils.listenOnce.bind(null, @window)
36
37 # This flag controls whether to suppress the various key events or not.
38 @suppress = false
39
40 # If a matched shortcut has the `<late>` special key, this flag is set to
41 # `true`.
42 @late = false
43
44 # When a menu or panel is shown VimFx should temporarily stop processing
45 # keyboard input, allowing accesskeys to be used.
46 @popupPassthrough = false
47
48 addListeners: ->
49 checkPassthrough = (value, event) =>
50 target = event.originalTarget
51 if target.localName in ['menupopup', 'panel'] and
52 # Don’t set `@popupPassthrough` to `false` if there actually are popups
53 # open. This is the case when a sub-menu closes.
54 (value or not @anyPopupsOpen())
55 @popupPassthrough = value
56
57 @listen('popupshown', checkPassthrough.bind(null, true))
58 @listen('popuphidden', checkPassthrough.bind(null, false))
59
60 @listen('keydown', (event) =>
61 # No matter what, always reset the `@suppress` flag, so we don't
62 # suppress more than intended.
63 @suppress = false
64
65 # Reset the `@late` flag, telling any late listeners for the previous
66 # event not to run. Also reset the `late` pref, telling frame scripts not
67 # to do synchronous message passing on every keydown.
68 @late = false
69 prefs.set('late', false)
70
71 if @popupPassthrough
72 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
73 # can be stuck as `true` even though no popup is shown, effectively
74 # disabling the extension. Therefore we check if there actually _are_
75 # any open popups before stopping processing keyboard input. This is
76 # only done when popups (might) be open (not on every keystroke) for
77 # performance reasons.
78 return if @anyPopupsOpen()
79 @popupPassthrough = false # No popup was actually open.
80
81 return unless vim = @vimfx.getCurrentVim(@window)
82
83 @consumeKeyEvent(vim, event)
84 if @suppress
85 utils.suppressEvent(event) # This also suppresses the 'keypress' event.
86 else
87 # If this keydown event wasn’t suppressed, it’s an obvious interaction
88 # with the page. If it _was_ suppressed, though, it’s an interaction
89 # depending on the command triggered; if _it_ calls
90 # `vim.markPageInteraction()` or not.
91 vim.markPageInteraction() if vim.isUIEvent(event)
92 )
93
94 @listen('keyup', (event) =>
95 utils.suppressEvent(event) if @suppress
96 @setHeldModifiers(event, {filterCurrentOnly: true})
97 )
98
99 @listen('focus', => @setFocusType())
100 @listen('blur', =>
101 @window.setTimeout((=>
102 @setFocusType()
103 ), @vimfx.options.blur_timeout)
104 )
105
106 @listen('click', (event) =>
107 target = event.originalTarget
108 return unless vim = @vimfx.getCurrentVim(@window)
109
110 vim.hideNotification()
111
112 # In multi-process, clicks simulated by VimFx cannot be caught here. In
113 # non-multi-process, they unfortunately can. This hack should be
114 # sufficient for that case until non-multi-process is removed from
115 # Firefox.
116 isVimFxGeneratedEvent = (
117 event.layerX == 0 and event.layerY == 0 and
118 event.movementX == 0 and event.movementY == 0
119 )
120
121 # If the user clicks the reload button or a link when in hints mode, we’re
122 # going to end up in hints mode without any markers. Or if the user clicks
123 # a text input, then that input will be focused, but you can’t type in it
124 # (instead markers will be matched). So if the user clicks anything in
125 # hints mode it’s better to leave it.
126 if vim.mode == 'hints' and not isVimFxGeneratedEvent and
127 # Exclude the VimFx button, though, since clicking it returns to normal
128 # mode. Otherwise we’d first return to normal mode and then the button
129 # would open the help dialog.
130 target != button.getButton(@window)
131 vim._enterMode('normal')
132
133 vim._send('clearHover') unless isVimFxGeneratedEvent
134 )
135
136 @listen('overflow', (event) =>
137 target = event.originalTarget
138 return unless vim = @vimfx.getCurrentVim(@window)
139 if vim._isUIElement(target)
140 vim._state.scrollableElements.addChecked(target)
141 )
142
143 @listen('underflow', (event) =>
144 target = event.originalTarget
145 return unless vim = @vimfx.getCurrentVim(@window)
146 if vim._isUIElement(target)
147 vim._state.scrollableElements.deleteChecked(target)
148 )
149
150 @listen('TabSelect', (event) =>
151 target = event.originalTarget
152 target.setAttribute('VimFx-visited', 'true')
153 @vimfx.emit('TabSelect', {event})
154
155 return unless vim = @vimfx.getCurrentVim(@window)
156 vim.hideNotification()
157 @vimfx.emit('focusTypeChange', {vim})
158 )
159
160 @listen('TabClose', (event) =>
161 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
162 return unless vim = @vimfx.vims.get(browser)
163 # Note: `lastClosedVim` must be stored so that any window can access it.
164 @vimfx.lastClosedVim = vim
165 )
166
167 messageManager.listen('cachedPageshow', ((data, callback, browser) =>
168 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
169 unless oldVim
170 callback(false)
171 return
172
173 if @vimfx.vims.has(browser)
174 vim = @vimfx.vims.get(browser)
175 if vim._messageManager == vim.browser.messageManager
176 callback(false)
177 return
178
179 # If we get here, it means that we’ve detected a tab dragged from one
180 # window to another. If so, the `vim` object from the last closed tab (the
181 # moved tab) should be re-used. See the commit message for commit bb70257d
182 # for more details.
183 oldVim._setBrowser(browser)
184 @vimfx.vims.set(browser, oldVim)
185 @vimfx.emit('modeChange', {vim: oldVim})
186 callback(true)
187 ), {messageManager: @window.messageManager})
188
189 setFocusType: ->
190 return unless vim = @vimfx.getCurrentVim(@window)
191
192 activeElement = utils.getActiveElement(@window)
193
194 if activeElement == @window.gBrowser.selectedBrowser
195 vim._send('checkFocusType')
196 return
197
198 focusType = utils.getFocusType(activeElement)
199 vim._setFocusType(focusType)
200
201 if focusType == 'editable' and vim.mode == 'caret'
202 vim._enterMode('normal')
203
204 consumeKeyEvent: (vim, event) ->
205 match = vim._consumeKeyEvent(event)
206
207 if typeof match == 'boolean'
208 @suppress = match
209 return
210
211 if match
212 if match.specialKeys['<late>']
213 @suppress = false
214 @consumeLateKeydown(vim, event, match)
215 else
216 @suppress = vim._onInput(match, event)
217 else
218 @suppress = null
219 @setHeldModifiers(event)
220
221 consumeLateKeydown: (vim, event, match) ->
222 @late = true
223
224 # The passed in `event` is the regular non-late browser UI keydown event.
225 # It is only used to set held keys. This is easier than sending an event
226 # subset from frame scripts.
227 listener = ({defaultPrevented}) =>
228 # `@late` is reset on every keydown. If it is no longer `true`, it means
229 # that the page called `event.stopPropagation()`, which prevented this
230 # listener from running for that event.
231 return unless @late
232 @suppress =
233 if defaultPrevented
234 false
235 else
236 vim._onInput(match, event)
237 @setHeldModifiers(event)
238 return @suppress
239
240 if vim.isUIEvent(event)
241 @listenOnce('keydown', ((lateEvent) =>
242 listener(lateEvent)
243 if @suppress
244 utils.suppressEvent(lateEvent)
245 @listenOnce('keyup', utils.suppressEvent, false)
246 ), false)
247 else
248 # Hack to avoid synchronous messages on every keydown (see
249 # events-frame.coffee).
250 prefs.set('late', true)
251 vim._listenOnce('lateKeydown', listener)
252
253 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
254 mainWindow = @window.document.documentElement
255 modifiers =
256 if filterCurrentOnly
257 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
258 else
259 if @suppress == null then 'alt ctrl meta shift' else ''
260 isHeld = (modifier) -> event["#{modifier}Key"]
261 mainWindow.setAttribute(
262 HELD_MODIFIERS_ATTRIBUTE, modifiers.split(' ').filter(isHeld).join(' ')
263 )
264
265 anyPopupsOpen: ->
266 # The autocomplete popup in text inputs (for example) is technically a
267 # panel, but it does not respond to keypresses. Therefore
268 # `[ignorekeys="true"]` is excluded.
269 #
270 # coffeelint: disable=max_line_length
271 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
272 # coffeelint: enable=max_line_length
273 popups = utils.querySelectorAllDeep(
274 @window, ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
275 )
276 for popup in popups
277 return true if popup.state == 'open'
278 return false
279
280 module.exports = UIEventManager
Imprint / Impressum