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