]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Handle non-late keys on the UI layer
[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 utils = require('./utils')
28
29 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
30
31 class UIEventManager
32 constructor: (@vimfx, @window) ->
33 @listen = utils.listen.bind(null, @window)
34 @listenOnce = utils.listenOnce.bind(null, @window)
35
36 # This flag controls whether to suppress the various key events or not.
37 @suppress = false
38
39 # If a matched shortcut has the `<late>` special key, this flag is set to
40 # `true`.
41 @late = false
42
43 # When a menu or panel is shown VimFx should temporarily stop processing
44 # keyboard input, allowing accesskeys to be used.
45 @popupPassthrough = false
46
47 @enteredKeys = new EnteredKeysManager(@window)
48
49 addListeners: ->
50 checkPassthrough = (value, event) =>
51 target = event.originalTarget
52 if target.localName in ['menupopup', 'panel'] and
53 # Don’t set `@popupPassthrough` to `false` if there actually are popups
54 # open. This is the case when a sub-menu closes.
55 (value or not anyPopupsOpen())
56 @popupPassthrough = value
57
58 anyPopupsOpen = =>
59 # The autocomplete popup in text inputs (for example) is technically a
60 # panel, but it does not respond to key presses. Therefore
61 # `[ignorekeys="true"]` is excluded.
62 #
63 # coffeelint: disable=max_line_length
64 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
65 # coffeelint: enable=max_line_length
66 popups = utils.querySelectorAllDeep(
67 @window,
68 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
69 )
70 for popup in popups
71 return true if popup.state == 'open'
72 return false
73
74 @listen('popupshown', checkPassthrough.bind(null, true))
75 @listen('popuphidden', checkPassthrough.bind(null, false))
76
77 @listen('keydown', (event) =>
78 # No matter what, always reset the `@suppress` flag, so we don't
79 # suppress more than intended.
80 @suppress = false
81
82 # Reset the `@late` flag, telling any late listeners for the previous
83 # event not to run.
84 @late = false
85
86 if @popupPassthrough
87 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
88 # can be stuck as `true` even though no popup is shown, effectively
89 # disabling the extension. Therefore we check if there actually _are_
90 # any open popups before stopping processing keyboard input. This is
91 # only done when popups (might) be open (not on every keystroke) for
92 # performance reasons.
93 return if anyPopupsOpen()
94 @popupPassthrough = false # No popup was actually open.
95
96 return unless vim = @vimfx.getCurrentVim(@window)
97
98 @consumeKeyEvent(vim, event)
99 if @suppress
100 utils.suppressEvent(event) # This also suppresses the 'keypress' event.
101 else
102 # If this keydown event wasn’t suppressed, it’s an obvious interaction
103 # with the page. If it _was_ suppressed, though, it’s an interaction
104 # depending on the command triggered; if _it_ calls
105 # `vim.markPageInteraction()` or not.
106 vim.markPageInteraction() if vim.isUIEvent(event)
107 )
108
109 @listen('keyup', (event) =>
110 utils.suppressEvent(event) if @suppress
111 @setHeldModifiers(event, {filterCurrentOnly: true})
112 )
113
114 handleFocusRelatedEvent = (options, event) =>
115 target = event.originalTarget
116 return unless vim = @vimfx.getCurrentVim(@window)
117
118 findBar = @window.gBrowser.getFindBar()
119 if target == findBar._findField.mInputField
120 vim.enterMode(options.mode)
121
122 if vim.isUIEvent(event)
123 focusType = utils.getFocusType(utils.getActiveElement(@window))
124 vim._setFocusType(focusType)
125
126 @listen('focus', handleFocusRelatedEvent.bind(null, {mode: 'find'}))
127 @listen('blur', handleFocusRelatedEvent.bind(null, {mode: 'normal'}))
128
129 @listen('click', (event) =>
130 target = event.originalTarget
131 return unless vim = @vimfx.getCurrentVim(@window)
132
133 # In multi-process, clicks simulated by VimFx cannot be caught here. In
134 # non-multi-process, they unfortunately can. This hack should be
135 # sufficient for that case until non-multi-process is removed from
136 # Firefox.
137 isVimFxGeneratedEvent = (
138 event.layerX == 0 and event.layerY == 0 and
139 event.movementX == 0 and event.movementY == 0
140 )
141
142 # If the user clicks the reload button or a link when in hints mode, we’re
143 # going to end up in hints mode without any markers. Or if the user clicks
144 # a text input, then that input will be focused, but you can’t type in it
145 # (instead markers will be matched). So if the user clicks anything in
146 # hints mode it’s better to leave it.
147 if vim.mode == 'hints' and not isVimFxGeneratedEvent and
148 # Exclude the VimFx button, though, since clicking it returns to normal
149 # mode. Otherwise we’d first return to normal mode and then the button
150 # would open the help dialog.
151 target != button.getButton(@window)
152 vim.enterMode('normal')
153
154 vim._send('clearHover') unless isVimFxGeneratedEvent
155 )
156
157 @listen('overflow', (event) =>
158 target = event.originalTarget
159 return unless vim = @vimfx.getCurrentVim(@window)
160 if vim._isUIElement(target)
161 vim._state.scrollableElements.addChecked(target)
162 )
163
164 @listen('underflow', (event) =>
165 target = event.originalTarget
166 return unless vim = @vimfx.getCurrentVim(@window)
167 if vim._isUIElement(target)
168 vim._state.scrollableElements.deleteChecked(target)
169 )
170
171 @listen('TabSelect', (event) =>
172 @vimfx.emit('TabSelect', event)
173
174 return unless vim = @vimfx.getCurrentVim(@window)
175 vim.hideNotification()
176 )
177
178 @listen('TabClose', (event) =>
179 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
180 return unless vim = @vimfx.vims.get(browser)
181 # Note: `lastClosedVim` must be stored so that any window can access it.
182 @vimfx.lastClosedVim = vim
183 )
184
185 messageManager.listen('cachedPageshow', ((data, callback, browser) =>
186 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
187 unless oldVim
188 callback(false)
189 return
190
191 if @vimfx.vims.has(browser)
192 vim = @vimfx.vims.get(browser)
193 if vim._messageManager == vim.browser.messageManager
194 callback(false)
195 return
196
197 # If we get here, it means that we’ve detected a tab dragged from one
198 # window to another. If so, the `vim` object from the last closed tab (the
199 # moved tab) should be re-used. See the commit message for commit bb70257d
200 # for more details.
201 oldVim._setBrowser(browser)
202 @vimfx.vims.set(browser, oldVim)
203 @vimfx.emit('modeChange', oldVim)
204 callback(true)
205 ), {messageManager: @window.messageManager})
206
207 consumeKeyEvent: (vim, event) ->
208 match = vim._consumeKeyEvent(event)
209
210 if match
211 if @vimfx.options.notify_entered_keys
212 if match.type in ['none', 'full'] or match.likelyConflict
213 @enteredKeys.clear(vim)
214 else
215 @enteredKeys.push(vim, match.keyStr, @vimfx.options.timeout)
216 else
217 vim.hideNotification()
218
219 if match.specialKeys['<late>']
220 @suppress = false
221 @consumeLateKeydown(vim, event, match)
222 else
223 @suppress = vim._onInput(match, event)
224 else
225 @suppress = null
226 @setHeldModifiers(event)
227
228 consumeLateKeydown: (vim, event, match) ->
229 @late = true
230
231 # The passed in `event` is the regular non-late browser UI keydown event.
232 # It is only used to set held keys. This is easier than sending an event
233 # subset from frame scripts.
234 listener = ({defaultPrevented}) =>
235 # `@late` is reset on every keydown. If it is no longer `true`, it means
236 # that the page called `event.stopPropagation()`, which prevented this
237 # listener from running for that event.
238 return unless @late
239 @suppress =
240 if defaultPrevented
241 false
242 else
243 vim._onInput(match, event)
244 @setHeldModifiers(event)
245 return @suppress
246
247 if vim.isUIEvent(event)
248 @listenOnce('keydown', ((lateEvent) =>
249 listener(lateEvent)
250 if @suppress
251 utils.suppressEvent(lateEvent)
252 @listenOnce('keyup', utils.suppressEvent, false)
253 ), false)
254 else
255 vim._listenOnce('lateKeydown', listener)
256
257 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
258 mainWindow = @window.document.documentElement
259 modifiers =
260 if filterCurrentOnly
261 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
262 else
263 if @suppress == null then 'alt ctrl meta shift' else ''
264 isHeld = (modifier) -> event["#{modifier}Key"]
265 mainWindow.setAttribute(HELD_MODIFIERS_ATTRIBUTE,
266 modifiers.split(' ').filter(isHeld).join(' '))
267
268 class EnteredKeysManager
269 constructor: (@window) ->
270 @keys = []
271 @timeout = null
272
273 clear: (notifier) ->
274 @keys = []
275 @clearTimeout()
276 notifier.hideNotification()
277
278 push: (notifier, keyStr, duration) ->
279 @keys.push(keyStr)
280 @clearTimeout()
281 notifier.notify(@keys.join(''))
282 clear = @clear.bind(this)
283 @timeout = @window.setTimeout((-> clear(notifier)), duration)
284
285 clearTimeout: ->
286 @window.clearTimeout(@timeout) if @timeout?
287 @timeout = null
288
289 module.exports = UIEventManager
Imprint / Impressum