]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Fix handling of tabs dragged to new/other windows
[VimFx.git] / extension / lib / events.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
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 utils = require('./utils')
27
28 HELD_MODIFIERS_ATTRIBUTE = 'vimfx-held-modifiers'
29
30 class UIEventManager
31 constructor: (@vimfx, @window) ->
32 @listen = utils.listen.bind(null, @window)
33 @listenOnce = utils.listenOnce.bind(null, @window)
34
35 # This flag controls whether to suppress the various key events or not.
36 @suppress = false
37
38 # If a matched shortcut has the `<late>` special key, this flag is set to
39 # `true`.
40 @late = false
41
42 # When a menu or panel is shown VimFx should temporarily stop processing
43 # keyboard input, allowing accesskeys to be used.
44 @popupPassthrough = false
45
46 @locationState =
47 lastUrl: null
48 numToSkip: 0
49
50 addListeners: ->
51 checkPassthrough = (value, event) =>
52 target = event.originalTarget
53 if target.nodeName in ['menupopup', 'panel']
54 @popupPassthrough = value
55
56 @listen('popupshown', checkPassthrough.bind(null, true))
57 @listen('popuphidden', checkPassthrough.bind(null, false))
58
59 @listen('keydown', (event) =>
60 try
61 # No matter what, always reset the `@suppress` flag, so we don't
62 # suppress more than intended.
63 @suppress = false
64
65 # Reset the `@late` flag, telling any late listeners for the previous
66 # event not to run.
67 @late = false
68
69 if @popupPassthrough
70 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
71 # can be stuck as `true` even though no popup is shown, effectively
72 # disabling the extension. Therefore we check if there actually _are_
73 # any open popups before stopping processing keyboard input. This is
74 # only done when popups (might) be open (not on every keystroke) of
75 # performance reasons.
76 #
77 # The autocomplete popup in text inputs (for example) is technically a
78 # panel, but it does not respond to key presses. Therefore
79 # `[ignorekeys="true"]` is excluded.
80 #
81 # coffeelint: disable=max_line_length
82 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
83 # coffeelint: enable=max_line_length
84 popups = @window.document.querySelectorAll(
85 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
86 )
87 for popup in popups
88 return if popup.state == 'open'
89 @popupPassthrough = false # No popup was actually open.
90
91 return unless vim = @vimfx.getCurrentVim(@window)
92
93 if vim.isFrameEvent(event)
94 vim._listenOnce('consumeKeyEvent', ({ focusType }) =>
95 @consumeKeyEvent(vim, event, focusType, { isFrameEvent: true })
96 return @suppress
97 )
98 else
99 @consumeKeyEvent(vim, event, utils.getFocusType(event))
100 # This also suppresses the 'keypress' event.
101 utils.suppressEvent(event) if @suppress
102
103 catch error
104 console.error(utils.formatError(error))
105 )
106
107 @listen('keyup', (event) =>
108 utils.suppressEvent(event) if @suppress
109 @setHeldModifiers(event, {filterCurrentOnly: true})
110 )
111
112 checkFindbar = (mode, event) =>
113 target = event.originalTarget
114 findBar = @window.gBrowser.getFindBar()
115 if target == findBar._findField.mInputField
116 return unless vim = @vimfx.getCurrentVim(@window)
117 vim.enterMode(mode)
118
119 @listen('focus', checkFindbar.bind(null, 'find'))
120 @listen('blur', checkFindbar.bind(null, 'normal'))
121
122 @listen('click', (event) =>
123 target = event.originalTarget
124 return unless vim = @vimfx.getCurrentVim(@window)
125
126 # If the user clicks the reload button or a link when in hints mode, we’re
127 # going to end up in hints mode without any markers. Or if the user clicks
128 # a text input, then that input will be focused, but you can’t type in it
129 # (instead markers will be matched). So if the user clicks anything in
130 # hints mode it’s better to leave it.
131 if vim.mode == 'hints' and not vim.isFrameEvent(event) and
132 # Exclude the VimFx button, though, since clicking it returns to normal
133 # mode. Otherwise we’d first return to normal mode and then the button
134 # would open the help dialog.
135 target != button.getButton(@window)
136 vim.enterMode('normal')
137 )
138
139 @listen('TabSelect', @vimfx.emit.bind(@vimfx, 'TabSelect'))
140
141 @listen('TabOpen', (event) =>
142 browser = @window.gBrowser.getBrowserForTab(event.originalTarget)
143
144 if MULTI_PROCESS_ENABLED
145 unless @vimfx.vims.has(browser)
146 # If a tab is opened, but there’s no `vim` instance for it, it means
147 # that the tab has been dragged from another window. In such cases, a
148 # new `<browser>` is created but the page is not refreshed and the
149 # same frame script is re-used. The window the tab was dragged _from_
150 # is still the current window, and the tab’s `vim` instance is the
151 # current one there. Grab it and update its `.browser`.
152 vim = @vimfx.getCurrentVim(utils.getCurrentWindow())
153 vim._setBrowser(browser)
154 @vimfx.vims.set(browser, vim)
155
156 # For some reason, three 'onLocationChange' events will fire for this
157 # tab now, all of which are unwanted because the location didn’t
158 # really change. Otherwise another mode might be entered based on the
159 # “changed” URL. The mode should not change when dragging a tab to
160 # another window.
161 @locationState.numToSkip = 3
162 else
163 # In non-multi-process, a new frame script is created, which means that
164 # a new `vim` instance is created as well, and also that all state for
165 # the page is lost. The best we can do is to copy over the mode.
166 vim = @vimfx.vims.get(browser)
167
168 # If the new tab was opened in a background window it most likely means
169 # that a tab was dragged there.
170 unless @window == utils.getCurrentWindow()
171 oldVim = @vimfx.getCurrentVim(utils.getCurrentWindow())
172 vim._state.lastUrl = oldVim._state.lastUrl
173 vim.enterMode(oldVim.mode)
174 # In non-multi-process, the magic number seems to be four.
175 @locationState.numToSkip = 4
176 )
177
178 progressListener =
179 onLocationChange: (progress, request, location, flags) =>
180 if @locationState.numToSkip > 0
181 @locationState.numToSkip--
182 return
183
184 url = location.spec
185 refresh = (url == @locationState.lastUrl)
186 @locationState.lastUrl = url
187
188 unless flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
189 return unless vim = @vimfx.getCurrentVim(@window)
190 vim._onLocationChange(url, {refresh})
191
192 @window.gBrowser.addProgressListener(progressListener)
193 module.onShutdown(=>
194 @window.gBrowser.removeProgressListener(progressListener)
195 )
196
197 consumeKeyEvent: (vim, event, focusType, options = {}) ->
198 match = vim._consumeKeyEvent(event, focusType)
199 switch
200 when not match
201 @suppress = null
202 when match.specialKeys['<late>']
203 @suppress = false
204 @consumeLateKeydown(vim, event, match, options)
205 else
206 @suppress = vim._onInput(match, options)
207 @setHeldModifiers(event)
208
209 consumeLateKeydown: (vim, event, match, options) ->
210 { isFrameEvent = false } = options
211 @late = true
212
213 # The passed in `event` is the regular non-late browser UI keydown event.
214 # It is only used to set held keys. This is easier than sending an event
215 # subset from frame scripts.
216 listener = ({ defaultPrevented }) =>
217 # `@late` is reset on every keydown. If it is no longer `true`, it means
218 # that the page called `event.stopPropagation()`, which prevented this
219 # listener from running for that event.
220 return unless @late
221 @suppress =
222 if defaultPrevented
223 false
224 else
225 vim._onInput(match, options)
226 @setHeldModifiers(event)
227 return @suppress
228
229 if isFrameEvent
230 vim._listenOnce('lateKeydown', listener)
231 else
232 @listenOnce('keydown', ((lateEvent) =>
233 listener(lateEvent)
234 if @suppress
235 utils.suppressEvent(lateEvent)
236 @listenOnce('keyup', utils.suppressEvent, false)
237 ), false)
238
239 setHeldModifiers: (event, { filterCurrentOnly = false } = {}) ->
240 mainWindow = @window.document.documentElement
241 modifiers =
242 if filterCurrentOnly
243 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
244 else
245 if @suppress == null then 'alt ctrl meta shift' else ''
246 isHeld = (modifier) -> event["#{ modifier }Key"]
247 mainWindow.setAttribute(HELD_MODIFIERS_ATTRIBUTE,
248 modifiers.split(' ').filter(isHeld).join(' '))
249
250 module.exports = UIEventManager
Imprint / Impressum