2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015, 2016.
5 # This file is part of VimFx.
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.
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.
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/>.
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.
25 button = require('./button')
26 messageManager = require('./message-manager')
27 prefs = require('./prefs')
28 utils = require('./utils')
30 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
33 constructor: (@vimfx, @window) ->
34 @listen = utils.listen.bind(null, @window)
35 @listenOnce = utils.listenOnce.bind(null, @window)
37 # This flag controls whether to suppress the various key events or not.
40 # If a matched shortcut has the `<late>` special key, this flag is set to
44 # When a menu or panel is shown VimFx should temporarily stop processing
45 # keyboard input, allowing accesskeys to be used.
46 @popupPassthrough = false
48 @enteredKeys = new EnteredKeysManager(@window)
51 checkPassthrough = (value, event) =>
52 target = event.originalTarget
53 if target.localName in ['menupopup', 'panel'] and
54 # Don’t set `@popupPassthrough` to `false` if there actually are popups
55 # open. This is the case when a sub-menu closes.
56 (value or not @anyPopupsOpen())
57 @popupPassthrough = value
59 @listen('popupshown', checkPassthrough.bind(null, true))
60 @listen('popuphidden', checkPassthrough.bind(null, false))
62 @listen('keydown', (event) =>
63 # No matter what, always reset the `@suppress` flag, so we don't
64 # suppress more than intended.
67 # Reset the `@late` flag, telling any late listeners for the previous
68 # event not to run. Also reset the `late` pref, telling frame scripts not
69 # to do synchronous message passing on every keydown.
71 prefs.set('late', false)
74 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
75 # can be stuck as `true` even though no popup is shown, effectively
76 # disabling the extension. Therefore we check if there actually _are_
77 # any open popups before stopping processing keyboard input. This is
78 # only done when popups (might) be open (not on every keystroke) for
79 # performance reasons.
80 return if @anyPopupsOpen()
81 @popupPassthrough = false # No popup was actually open.
83 return unless vim = @vimfx.getCurrentVim(@window)
85 @consumeKeyEvent(vim, event)
87 utils.suppressEvent(event) # This also suppresses the 'keypress' event.
89 # If this keydown event wasn’t suppressed, it’s an obvious interaction
90 # with the page. If it _was_ suppressed, though, it’s an interaction
91 # depending on the command triggered; if _it_ calls
92 # `vim.markPageInteraction()` or not.
93 vim.markPageInteraction() if vim.isUIEvent(event)
96 @listen('keyup', (event) =>
97 utils.suppressEvent(event) if @suppress
98 @setHeldModifiers(event, {filterCurrentOnly: true})
101 handleFocusRelatedEvent = (event) =>
102 return unless vim = @vimfx.getCurrentVim(@window)
104 # We used to only continue if `vim.isUIEvent(event)` here, but that’s not
105 # entirely reliable. Elements focused and blurred inside the Evernote Web
106 # Clipper extension’s popup (which is an `<iframe>` injected into the
107 # browser UI) _are_ UI elements, but seem to be indistinguishable from
108 # non-UI elements. Luckily, it doesn’t matter if we happen to handle
109 # non-UI elements when determining the focus type. TODO: Remove this
110 # comment when non-multi-process is removed from Firefox.
111 focusType = utils.getFocusType(utils.getActiveElement(@window))
112 vim._setFocusType(focusType)
114 if focusType == 'editable' and vim.mode == 'caret'
115 vim.enterMode('normal')
117 @listen('focus', handleFocusRelatedEvent)
118 @listen('blur', (event) =>
119 @window.setTimeout((->
120 handleFocusRelatedEvent(event)
121 ), @vimfx.options.blur_timeout)
124 @listen('click', (event) =>
125 target = event.originalTarget
126 return unless vim = @vimfx.getCurrentVim(@window)
128 # In multi-process, clicks simulated by VimFx cannot be caught here. In
129 # non-multi-process, they unfortunately can. This hack should be
130 # sufficient for that case until non-multi-process is removed from
132 isVimFxGeneratedEvent = (
133 event.layerX == 0 and event.layerY == 0 and
134 event.movementX == 0 and event.movementY == 0
137 # If the user clicks the reload button or a link when in hints mode, we’re
138 # going to end up in hints mode without any markers. Or if the user clicks
139 # a text input, then that input will be focused, but you can’t type in it
140 # (instead markers will be matched). So if the user clicks anything in
141 # hints mode it’s better to leave it.
142 if vim.mode == 'hints' and not isVimFxGeneratedEvent and
143 # Exclude the VimFx button, though, since clicking it returns to normal
144 # mode. Otherwise we’d first return to normal mode and then the button
145 # would open the help dialog.
146 target != button.getButton(@window)
147 vim.enterMode('normal')
149 vim._send('clearHover') unless isVimFxGeneratedEvent
152 @listen('overflow', (event) =>
153 target = event.originalTarget
154 return unless vim = @vimfx.getCurrentVim(@window)
155 if vim._isUIElement(target)
156 vim._state.scrollableElements.addChecked(target)
159 @listen('underflow', (event) =>
160 target = event.originalTarget
161 return unless vim = @vimfx.getCurrentVim(@window)
162 if vim._isUIElement(target)
163 vim._state.scrollableElements.deleteChecked(target)
166 @listen('TabSelect', (event) =>
167 target = event.originalTarget
168 target.setAttribute('VimFx-visited', 'true')
169 @vimfx.emit('TabSelect', {event})
171 return unless vim = @vimfx.getCurrentVim(@window)
172 vim.hideNotification()
175 @listen('TabClose', (event) =>
176 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
177 return unless vim = @vimfx.vims.get(browser)
178 # Note: `lastClosedVim` must be stored so that any window can access it.
179 @vimfx.lastClosedVim = vim
182 messageManager.listen('cachedPageshow', ((data, callback, browser) =>
183 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
188 if @vimfx.vims.has(browser)
189 vim = @vimfx.vims.get(browser)
190 if vim._messageManager == vim.browser.messageManager
194 # If we get here, it means that we’ve detected a tab dragged from one
195 # window to another. If so, the `vim` object from the last closed tab (the
196 # moved tab) should be re-used. See the commit message for commit bb70257d
198 oldVim._setBrowser(browser)
199 @vimfx.vims.set(browser, oldVim)
200 @vimfx.emit('modeChange', {vim: oldVim})
202 ), {messageManager: @window.messageManager})
204 consumeKeyEvent: (vim, event) ->
205 match = vim._consumeKeyEvent(event)
208 if @vimfx.options.notify_entered_keys
209 if match.type in ['none', 'full'] or match.likelyConflict
210 @enteredKeys.clear(vim)
212 @enteredKeys.push(vim, match.keyStr, @vimfx.options.timeout)
214 vim.hideNotification()
216 if match.specialKeys['<late>']
218 @consumeLateKeydown(vim, event, match)
220 @suppress = vim._onInput(match, event)
223 @setHeldModifiers(event)
225 consumeLateKeydown: (vim, event, match) ->
228 # The passed in `event` is the regular non-late browser UI keydown event.
229 # It is only used to set held keys. This is easier than sending an event
230 # subset from frame scripts.
231 listener = ({defaultPrevented}) =>
232 # `@late` is reset on every keydown. If it is no longer `true`, it means
233 # that the page called `event.stopPropagation()`, which prevented this
234 # listener from running for that event.
240 vim._onInput(match, event)
241 @setHeldModifiers(event)
244 if vim.isUIEvent(event)
245 @listenOnce('keydown', ((lateEvent) =>
248 utils.suppressEvent(lateEvent)
249 @listenOnce('keyup', utils.suppressEvent, false)
252 # Hack to avoid synchronous messages on every keydown (see
253 # events-frame.coffee).
254 prefs.set('late', true)
255 vim._listenOnce('lateKeydown', listener)
257 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
258 mainWindow = @window.document.documentElement
261 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
263 if @suppress == null then 'alt ctrl meta shift' else ''
264 isHeld = (modifier) -> event["#{modifier}Key"]
265 mainWindow.setAttribute(
266 HELD_MODIFIERS_ATTRIBUTE, modifiers.split(' ').filter(isHeld).join(' ')
270 # The autocomplete popup in text inputs (for example) is technically a
271 # panel, but it does not respond to key presses. Therefore
272 # `[ignorekeys="true"]` is excluded.
274 # coffeelint: disable=max_line_length
275 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
276 # coffeelint: enable=max_line_length
277 popups = utils.querySelectorAllDeep(
278 @window, ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
281 return true if popup.state == 'open'
284 class EnteredKeysManager
285 constructor: (@window) ->
292 notifier.hideNotification()
294 push: (notifier, keyStr, duration) ->
297 notifier.notify(@keys.join(''))
298 clear = @clear.bind(this)
299 @timeout = @window.setTimeout((-> clear(notifier)), duration)
302 @window.clearTimeout(@timeout) if @timeout?
305 module.exports = UIEventManager