]> git.gir.st - VimFx.git/blob - extension/lib/events.coffee
Bump Firefox min version to 40
[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 # When a menu or panel is shown VimFx should temporarily stop processing
39 # keyboard input, allowing accesskeys to be used.
40 @popupPassthrough = false
41
42 addListeners: ->
43 # NOTE: When the browser starts, many events may fire before a `vim` object
44 # has been created for the current tab. Therefore always use the following
45 # snippet when getting the current `vim`:
46 #
47 # return unless vim = @vimfx.getCurrentVim(@window)
48
49 checkPassthrough = (value, event) =>
50 if event.target.nodeName in ['menupopup', 'panel']
51 @popupPassthrough = value
52
53 @listen('popupshown', checkPassthrough.bind(null, true))
54 @listen('popuphidden', checkPassthrough.bind(null, false))
55
56 @listen('keydown', (event) =>
57 try
58 # No matter what, always reset the `suppress` flag, so we don't suppress
59 # more than intended.
60 @suppress = false
61
62 if @popupPassthrough
63 # The `@popupPassthrough` flag is set a bit unreliably. Sometimes it
64 # can be stuck as `true` even though no popup is shown, effectively
65 # disabling the extension. Therefore we check if there actually _are_
66 # any open popups before stopping processing keyboard input. This is
67 # only done when popups (might) be open (not on every keystroke) of
68 # performance reasons.
69 #
70 # The autocomplete popup in text inputs (for example) is technically a
71 # panel, but it does not respond to key presses. Therefore
72 # `[ignorekeys="true"]` is excluded.
73 #
74 # coffeelint: disable=max_line_length
75 # <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/PopupGuide/PopupKeys#Ignoring_Keys>
76 # coffeelint: enable=max_line_length
77 popups = @window.document.querySelectorAll(
78 ':-moz-any(menupopup, panel):not([ignorekeys="true"])'
79 )
80 for popup in popups
81 return if popup.state == 'open'
82 @popupPassthrough = false # No popup was actually open.
83
84 return unless vim = @vimfx.getCurrentVim(@window)
85
86 if vim.isFrameEvent(event)
87 vim._listenOnce('consumeKeyEvent', ({ focusType }) =>
88 @consumeKeyEvent(vim, event, focusType, { isFrameEvent: true })
89 return @suppress
90 )
91 else
92 @consumeKeyEvent(vim, event, utils.getFocusType(event))
93 # This also suppresses the 'keypress' event.
94 utils.suppressEvent(event) if @suppress
95
96 catch error
97 console.error(utils.formatError(error))
98 )
99
100 @listen('keyup', (event) =>
101 utils.suppressEvent(event) if @suppress
102 @setHeldModifiers(event, {filterCurrentOnly: true})
103 )
104
105 checkFindbar = (mode, event) =>
106 target = event.originalTarget
107 findBar = @window.gBrowser.getFindBar()
108 if target == findBar._findField.mInputField
109 return unless vim = @vimfx.getCurrentVim(@window)
110 vim.enterMode(mode)
111
112 @listen('focus', (event) =>
113 target = event.originalTarget
114
115 if target == @window
116 return unless vim = @vimfx.getCurrentVim(@window)
117 vim._send('TabSelect')
118 return
119
120 checkFindbar('find', event)
121 )
122 @listen('blur', checkFindbar.bind(null, 'normal'))
123
124 @listen('click', (event) =>
125 target = event.originalTarget
126 return unless vim = @vimfx.getCurrentVim(@window)
127
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 not vim.isFrameEvent(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')
139 )
140
141 @listen('TabSelect', (event) =>
142 @vimfx.emit('TabSelect', event)
143
144 return unless vim = @vimfx.getCurrentVim(@window)
145 vim._send('TabSelect')
146 )
147
148 lastUrl = null
149 progressListener =
150 onLocationChange: (progress, request, location, flags) =>
151 url = location.spec
152 refresh = (url == lastUrl)
153 lastUrl = url
154 unless flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
155 return unless vim = @vimfx.getCurrentVim(@window)
156 vim._onLocationChange(url, {refresh})
157
158 @window.gBrowser.addProgressListener(progressListener)
159 module.onShutdown(=>
160 @window.gBrowser.removeProgressListener(progressListener)
161 )
162
163 consumeKeyEvent: (vim, event, focusType, options = {}) ->
164 match = vim._consumeKeyEvent(event, focusType)
165 switch
166 when not match
167 @suppress = null
168 when match.specialKeys['<late>']
169 @suppress = false
170 @consumeLateKeydown(vim, event, match, options)
171 else
172 @suppress = vim._onInput(match, options)
173 @setHeldModifiers(event)
174
175 consumeLateKeydown: (vim, event, match, options) ->
176 { isFrameEvent = false } = options
177
178 # The passed in `event` is the regular non-late browser UI keydown event.
179 # It is only used to set held keys. This is easier than sending an event
180 # subset from frame scripts.
181 listener = ({ defaultPrevented }) =>
182 @suppress =
183 if defaultPrevented
184 false
185 else
186 vim._onInput(match, options)
187 @setHeldModifiers(event)
188 return @suppress
189
190 if isFrameEvent
191 vim._listenOnce('lateKeydown', listener)
192 else
193 @listenOnce('keydown', ((lateEvent) =>
194 listener(lateEvent)
195 if @suppress
196 utils.suppressEvent(lateEvent)
197 @listenOnce('keyup', utils.suppressEvent, false)
198 ), false)
199
200 setHeldModifiers: (event, { filterCurrentOnly = false } = {}) ->
201 mainWindow = @window.document.documentElement
202 modifiers =
203 if filterCurrentOnly
204 mainWindow.getAttribute(HELD_MODIFIERS_ATTRIBUTE)
205 else
206 if @suppress == null then 'alt ctrl meta shift' else ''
207 isHeld = (modifier) -> event["#{ modifier }Key"]
208 mainWindow.setAttribute(HELD_MODIFIERS_ATTRIBUTE,
209 modifiers.split(' ').filter(isHeld).join(' '))
210
211 module.exports = UIEventManager
Imprint / Impressum