2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014, 2015.
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 utils = require('./utils')
29 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
32 constructor: (@vimfx, @window) ->
33 @listen = utils.listen.bind(null, @window)
34 @listenOnce = utils.listenOnce.bind(null, @window)
36 # This flag controls whether to suppress the various key events or not.
39 # If a matched shortcut has the `<late>` special key, this flag is set to
43 # When a menu or panel is shown VimFx should temporarily stop processing
44 # keyboard input, allowing accesskeys to be used.
45 @popupPassthrough = false
48 checkPassthrough = (value, event) =>
49 target = event.originalTarget
50 if target.nodeName in ['menupopup', 'panel']
51 @popupPassthrough = value
53 @listen('popupshown', checkPassthrough.bind(null, true))
54 @listen('popuphidden', checkPassthrough.bind(null, false))
56 @listen('keydown', (event) =>
58 # No matter what, always reset the `@suppress` flag, so we don't
59 # suppress more than intended.
62 # Reset the `@late` flag, telling any late listeners for the previous
67 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
68 # can be stuck as `true` even though no popup is shown, effectively
69 # disabling the extension. Therefore we check if there actually _are_
70 # any open popups before stopping processing keyboard input. This is
71 # only done when popups (might) be open (not on every keystroke) of
72 # performance reasons.
74 # The autocomplete popup in text inputs (for example) is technically a
75 # panel, but it does not respond to key presses. Therefore
76 # `[ignorekeys="true"]` is excluded.
78 # coffeelint: disable=max_line_length
79 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
80 # coffeelint: enable=max_line_length
81 popups = @window.document.querySelectorAll(
82 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
85 return if popup.state == 'open'
86 @popupPassthrough = false # No popup was actually open.
88 return unless vim = @vimfx.getCurrentVim(@window)
90 if vim.isUIEvent(event)
91 focusType = utils.getFocusType(event.originalTarget)
92 @consumeKeyEvent(vim, event, focusType, event)
93 # This also suppresses the 'keypress' event.
94 utils.suppressEvent(event) if @suppress
96 vim._listenOnce('consumeKeyEvent', ({focusType}) =>
97 @consumeKeyEvent(vim, event, focusType)
102 console.error(utils.formatError(error))
105 @listen('keyup', (event) =>
106 utils.suppressEvent(event) if @suppress
107 @setHeldModifiers(event, {filterCurrentOnly: true})
110 handleFocusRelatedEvent = (options, event) =>
111 target = event.originalTarget
112 return unless vim = @vimfx.getCurrentVim(@window)
114 findBar = @window.gBrowser.getFindBar()
115 if target == findBar._findField.mInputField
116 vim.enterMode(options.mode)
118 if vim.isUIEvent(event)
119 focusType = utils.getFocusType(utils.getActiveElement(@window))
120 @vimfx.emit('focusTypeChange', {vim, focusType})
122 @listen('focus', handleFocusRelatedEvent.bind(null, {mode: 'find'}))
123 @listen('blur', handleFocusRelatedEvent.bind(null, {mode: 'normal'}))
125 @listen('click', (event) =>
126 target = event.originalTarget
127 return unless vim = @vimfx.getCurrentVim(@window)
129 # If the user clicks the reload button or a link when in hints mode, we’re
130 # going to end up in hints mode without any markers. Or if the user clicks
131 # a text input, then that input will be focused, but you can’t type in it
132 # (instead markers will be matched). So if the user clicks anything in
133 # hints mode it’s better to leave it.
134 if vim.mode == 'hints' and vim.isUIEvent(event) and
135 # Exclude the VimFx button, though, since clicking it returns to normal
136 # mode. Otherwise we’d first return to normal mode and then the button
137 # would open the help dialog.
138 target != button.getButton(@window)
139 vim.enterMode('normal')
142 @listen('TabSelect', (event) =>
143 @vimfx.emit('TabSelect', event)
145 return unless vim = @vimfx.getCurrentVim(@window)
146 vim.hideNotification()
149 @listen('TabClose', (event) =>
150 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
151 return unless vim = @vimfx.vims.get(browser)
152 # Note: `lastClosedVim` must be stored so that any window can access it.
153 @vimfx.lastClosedVim = vim
156 messageManager.listen('cachedPageshow', ((data, args) =>
157 {target: browser, callback} = args
158 exit = (movedToNewTab) ->
159 messageManager.send(callback, movedToNewTab) if callback
161 [oldVim, @vimfx.lastClosedVim] = [@vimfx.lastClosedVim, null]
166 if @vimfx.vims.has(browser)
167 vim = @vimfx.vims.get(browser)
168 if vim._messageManager == vim.browser.messageManager
172 # If we get here, it means that we’ve detected a tab dragged from one
173 # window to another. If so, the `vim` object from the last closed tab (the
174 # moved tab) should be re-used. See the commit message for commit bb70257d
176 oldVim._setBrowser(browser)
177 @vimfx.vims.set(browser, oldVim)
178 @vimfx.emit('modeChange', oldVim)
180 ), @window.messageManager)
182 consumeKeyEvent: (vim, event, focusType, uiEvent = false) ->
183 match = vim._consumeKeyEvent(event, focusType)
186 vim.hideNotification()
187 if match.specialKeys['<late>']
189 @consumeLateKeydown(vim, event, match, uiEvent)
191 @suppress = vim._onInput(match, uiEvent)
194 @setHeldModifiers(event)
196 consumeLateKeydown: (vim, event, match, uiEvent) ->
199 # The passed in `event` is the regular non-late browser UI keydown event.
200 # It is only used to set held keys. This is easier than sending an event
201 # subset from frame scripts.
202 listener = ({defaultPrevented}) =>
203 # `@late` is reset on every keydown. If it is no longer `true`, it means
204 # that the page called `event.stopPropagation()`, which prevented this
205 # listener from running for that event.
211 vim._onInput(match, uiEvent)
212 @setHeldModifiers(event)
216 @listenOnce('keydown', ((lateEvent) =>
219 utils.suppressEvent(lateEvent)
220 @listenOnce('keyup', utils.suppressEvent, false)
223 vim._listenOnce('lateKeydown', listener)
225 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
226 mainWindow = @window.document.documentElement
229 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
231 if @suppress == null then 'alt ctrl meta shift' else ''
232 isHeld = (modifier) -> event["#{modifier}Key"]
233 mainWindow.setAttribute(HELD_MODIFIERS_ATTRIBUTE,
234 modifiers.split(' ').filter(isHeld).join(' '))
236 module.exports = UIEventManager