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 utils = require('./utils')
28 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
31 constructor: (@vimfx, @window) ->
32 @listen = utils.listen.bind(null, @window)
33 @listenOnce = utils.listenOnce.bind(null, @window)
35 # This flag controls whether to suppress the various key events or not.
38 # If a matched shortcut has the `<late>` special key, this flag is set to
42 # When a menu or panel is shown VimFx should temporarily stop processing
43 # keyboard input, allowing accesskeys to be used.
44 @popupPassthrough = false
47 checkPassthrough = (value, event) =>
48 target = event.originalTarget
49 if target.nodeName in ['menupopup', 'panel']
50 @popupPassthrough = value
52 @listen('popupshown', checkPassthrough.bind(null, true))
53 @listen('popuphidden', checkPassthrough.bind(null, false))
55 @listen('keydown', (event) =>
57 # No matter what, always reset the `@suppress` flag, so we don't
58 # suppress more than intended.
61 # Reset the `@late` flag, telling any late listeners for the previous
66 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
67 # can be stuck as `true` even though no popup is shown, effectively
68 # disabling the extension. Therefore we check if there actually _are_
69 # any open popups before stopping processing keyboard input. This is
70 # only done when popups (might) be open (not on every keystroke) of
71 # performance reasons.
73 # The autocomplete popup in text inputs (for example) is technically a
74 # panel, but it does not respond to key presses. Therefore
75 # `[ignorekeys="true"]` is excluded.
77 # coffeelint: disable=max_line_length
78 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
79 # coffeelint: enable=max_line_length
80 popups = @window.document.querySelectorAll(
81 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
84 return if popup.state == 'open'
85 @popupPassthrough = false # No popup was actually open.
87 return unless vim = @vimfx.getCurrentVim(@window)
89 if vim.isUIEvent(event)
90 focusType = utils.getFocusType(event.originalTarget)
91 @consumeKeyEvent(vim, event, focusType, event)
92 # This also suppresses the 'keypress' event.
93 utils.suppressEvent(event) if @suppress
95 vim._listenOnce('consumeKeyEvent', ({focusType}) =>
96 @consumeKeyEvent(vim, event, focusType)
101 console.error(utils.formatError(error))
104 @listen('keyup', (event) =>
105 utils.suppressEvent(event) if @suppress
106 @setHeldModifiers(event, {filterCurrentOnly: true})
109 handleFocusRelatedEvent = (options, event) =>
110 target = event.originalTarget
111 return unless vim = @vimfx.getCurrentVim(@window)
113 findBar = @window.gBrowser.getFindBar()
114 if target == findBar._findField.mInputField
115 vim.enterMode(options.mode)
117 if vim.isUIEvent(event)
118 focusType = utils.getFocusType(utils.getActiveElement(@window))
119 @vimfx.emit('focusTypeChange', {vim, focusType})
121 @listen('focus', handleFocusRelatedEvent.bind(null, {mode: 'find'}))
122 @listen('blur', handleFocusRelatedEvent.bind(null, {mode: 'normal'}))
124 @listen('click', (event) =>
125 target = event.originalTarget
126 return unless vim = @vimfx.getCurrentVim(@window)
128 # If the user clicks the reload button or a link when in hints mode, we’re
129 # going to end up in hints mode without any markers. Or if the user clicks
130 # a text input, then that input will be focused, but you can’t type in it
131 # (instead markers will be matched). So if the user clicks anything in
132 # hints mode it’s better to leave it.
133 if vim.mode == 'hints' and vim.isUIEvent(event) and
134 # Exclude the VimFx button, though, since clicking it returns to normal
135 # mode. Otherwise we’d first return to normal mode and then the button
136 # would open the help dialog.
137 target != button.getButton(@window)
138 vim.enterMode('normal')
141 @listen('TabSelect', (event) =>
142 @vimfx.emit(@vimfx, 'TabSelect', event)
144 return unless vim = @vimfx.getCurrentVim(@window)
145 vim.hideNotification()
148 @listen('TabOpen', (event) =>
149 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
150 focusedWindow = utils.getCurrentWindow()
152 # If a tab is opened in another window than the focused window, it might
153 # mean that a tab has been dragged to it from the focused window. Unless
154 # that’s the case, do nothing.
155 return if @window == focusedWindow
157 if MULTI_PROCESS_ENABLED
158 # In multi-process, tabs dragged to new windows re-use the frame script.
159 # This means that no new `vim` instance is created (which is good since
160 # state is kept). A new `<browser>` is created, though. So if we’re
161 # already tracking this `<browser>` there’s nothing to do.
162 return if @vimfx.vims.has(browser)
164 # Grab the current `vim` (which corresponds to the dragged tab) from the
165 # focused window and update its `.browser`.
166 vim = @vimfx.getCurrentVim(focusedWindow)
167 vim._setBrowser(browser)
168 @vimfx.vims.set(browser, vim)
171 # In non-multi-process, a new frame script _is_ created, which means
172 # that a new `vim` instance is created as well, and also that all state
173 # for the page is lost. The best we can do is to copy over the mode.
174 vim = @vimfx.vims.get(browser)
175 oldVim = @vimfx.getCurrentVim(focusedWindow)
176 vim.enterMode(oldVim.mode)
178 # When a new `vim` object is created, `._onLocationChange` is run in the
179 # next tick. In this case, we _don’t_ want that to happen. This is a
180 # hack, but it doesn’t matter since it will be removed when
181 # multi-process is enabled by default.
182 {_onLocationChange} = vim
183 vim._onLocationChange = -> vim._onLocationChange = _onLocationChange
186 consumeKeyEvent: (vim, event, focusType, uiEvent = false) ->
187 match = vim._consumeKeyEvent(event, focusType)
190 vim.hideNotification()
191 if match.specialKeys['<late>']
193 @consumeLateKeydown(vim, event, match, uiEvent)
195 @suppress = vim._onInput(match, uiEvent)
198 @setHeldModifiers(event)
200 consumeLateKeydown: (vim, event, match, uiEvent) ->
203 # The passed in `event` is the regular non-late browser UI keydown event.
204 # It is only used to set held keys. This is easier than sending an event
205 # subset from frame scripts.
206 listener = ({defaultPrevented}) =>
207 # `@late` is reset on every keydown. If it is no longer `true`, it means
208 # that the page called `event.stopPropagation()`, which prevented this
209 # listener from running for that event.
215 vim._onInput(match, uiEvent)
216 @setHeldModifiers(event)
220 @listenOnce('keydown', ((lateEvent) =>
223 utils.suppressEvent(lateEvent)
224 @listenOnce('keyup', utils.suppressEvent, false)
227 vim._listenOnce('lateKeydown', listener)
229 setHeldModifiers: (event, {filterCurrentOnly = false} = {}) ->
230 mainWindow = @window.document.documentElement
233 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
235 if @suppress == null then 'alt ctrl meta shift' else ''
236 isHeld = (modifier) -> event["#{modifier}Key"]
237 mainWindow.setAttribute(HELD_MODIFIERS_ATTRIBUTE,
238 modifiers.split(' ').filter(isHeld).join(' '))
240 module.exports = UIEventManager