window-event-handler.js
1 const { Disposable, CompositeDisposable } = require('event-kit'); 2 const listen = require('./delegated-listener'); 3 const { debounce } = require('underscore-plus'); 4 5 // Handles low-level events related to the `window`. 6 module.exports = class WindowEventHandler { 7 constructor({ atomEnvironment, applicationDelegate }) { 8 this.handleDocumentKeyEvent = this.handleDocumentKeyEvent.bind(this); 9 this.handleFocusNext = this.handleFocusNext.bind(this); 10 this.handleFocusPrevious = this.handleFocusPrevious.bind(this); 11 this.handleWindowBlur = this.handleWindowBlur.bind(this); 12 this.handleWindowResize = this.handleWindowResize.bind(this); 13 this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this); 14 this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this); 15 this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this); 16 this.handleWindowToggleFullScreen = this.handleWindowToggleFullScreen.bind( 17 this 18 ); 19 this.handleWindowClose = this.handleWindowClose.bind(this); 20 this.handleWindowReload = this.handleWindowReload.bind(this); 21 this.handleWindowToggleDevTools = this.handleWindowToggleDevTools.bind( 22 this 23 ); 24 this.handleWindowToggleMenuBar = this.handleWindowToggleMenuBar.bind(this); 25 this.handleLinkClick = this.handleLinkClick.bind(this); 26 this.handleDocumentContextmenu = this.handleDocumentContextmenu.bind(this); 27 this.atomEnvironment = atomEnvironment; 28 this.applicationDelegate = applicationDelegate; 29 this.reloadRequested = false; 30 this.subscriptions = new CompositeDisposable(); 31 32 this.handleNativeKeybindings(); 33 } 34 35 initialize(window, document) { 36 this.window = window; 37 this.document = document; 38 this.subscriptions.add( 39 this.atomEnvironment.commands.add(this.window, { 40 'window:toggle-full-screen': this.handleWindowToggleFullScreen, 41 'window:close': this.handleWindowClose, 42 'window:reload': this.handleWindowReload, 43 'window:toggle-dev-tools': this.handleWindowToggleDevTools 44 }) 45 ); 46 47 if (['win32', 'linux'].includes(process.platform)) { 48 this.subscriptions.add( 49 this.atomEnvironment.commands.add(this.window, { 50 'window:toggle-menu-bar': this.handleWindowToggleMenuBar 51 }) 52 ); 53 } 54 55 this.subscriptions.add( 56 this.atomEnvironment.commands.add(this.document, { 57 'core:focus-next': this.handleFocusNext, 58 'core:focus-previous': this.handleFocusPrevious 59 }) 60 ); 61 62 this.addEventListener( 63 this.window, 64 'beforeunload', 65 this.handleWindowBeforeunload 66 ); 67 this.addEventListener(this.window, 'focus', this.handleWindowFocus); 68 this.addEventListener(this.window, 'blur', this.handleWindowBlur); 69 this.addEventListener( 70 this.window, 71 'resize', 72 debounce(this.handleWindowResize, 500) 73 ); 74 75 this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent); 76 this.addEventListener( 77 this.document, 78 'keydown', 79 this.handleDocumentKeyEvent 80 ); 81 this.addEventListener(this.document, 'drop', this.handleDocumentDrop); 82 this.addEventListener( 83 this.document, 84 'dragover', 85 this.handleDocumentDragover 86 ); 87 this.addEventListener( 88 this.document, 89 'contextmenu', 90 this.handleDocumentContextmenu 91 ); 92 this.subscriptions.add( 93 listen(this.document, 'click', 'a', this.handleLinkClick) 94 ); 95 this.subscriptions.add( 96 listen(this.document, 'submit', 'form', this.handleFormSubmit) 97 ); 98 99 this.subscriptions.add( 100 this.applicationDelegate.onDidEnterFullScreen(this.handleEnterFullScreen) 101 ); 102 this.subscriptions.add( 103 this.applicationDelegate.onDidLeaveFullScreen(this.handleLeaveFullScreen) 104 ); 105 } 106 107 // Wire commands that should be handled by Chromium for elements with the 108 // `.native-key-bindings` class. 109 handleNativeKeybindings() { 110 const bindCommandToAction = (command, action) => { 111 this.subscriptions.add( 112 this.atomEnvironment.commands.add( 113 '.native-key-bindings', 114 command, 115 event => 116 this.applicationDelegate.getCurrentWindow().webContents[action](), 117 false 118 ) 119 ); 120 }; 121 122 bindCommandToAction('core:copy', 'copy'); 123 bindCommandToAction('core:paste', 'paste'); 124 bindCommandToAction('core:undo', 'undo'); 125 bindCommandToAction('core:redo', 'redo'); 126 bindCommandToAction('core:select-all', 'selectAll'); 127 bindCommandToAction('core:cut', 'cut'); 128 } 129 130 unsubscribe() { 131 this.subscriptions.dispose(); 132 } 133 134 on(target, eventName, handler) { 135 target.on(eventName, handler); 136 this.subscriptions.add( 137 new Disposable(function() { 138 target.removeListener(eventName, handler); 139 }) 140 ); 141 } 142 143 addEventListener(target, eventName, handler) { 144 target.addEventListener(eventName, handler); 145 this.subscriptions.add( 146 new Disposable(function() { 147 target.removeEventListener(eventName, handler); 148 }) 149 ); 150 } 151 152 handleDocumentKeyEvent(event) { 153 this.atomEnvironment.keymaps.handleKeyboardEvent(event); 154 event.stopImmediatePropagation(); 155 } 156 157 handleDrop(event) { 158 event.preventDefault(); 159 event.stopPropagation(); 160 } 161 162 handleDragover(event) { 163 event.preventDefault(); 164 event.stopPropagation(); 165 event.dataTransfer.dropEffect = 'none'; 166 } 167 168 eachTabIndexedElement(callback) { 169 for (let element of this.document.querySelectorAll('[tabindex]')) { 170 if (element.disabled) { 171 continue; 172 } 173 if (!(element.tabIndex >= 0)) { 174 continue; 175 } 176 callback(element, element.tabIndex); 177 } 178 } 179 180 handleFocusNext() { 181 const focusedTabIndex = 182 this.document.activeElement.tabIndex != null 183 ? this.document.activeElement.tabIndex 184 : -Infinity; 185 186 let nextElement = null; 187 let nextTabIndex = Infinity; 188 let lowestElement = null; 189 let lowestTabIndex = Infinity; 190 this.eachTabIndexedElement(function(element, tabIndex) { 191 if (tabIndex < lowestTabIndex) { 192 lowestTabIndex = tabIndex; 193 lowestElement = element; 194 } 195 196 if (focusedTabIndex < tabIndex && tabIndex < nextTabIndex) { 197 nextTabIndex = tabIndex; 198 nextElement = element; 199 } 200 }); 201 202 if (nextElement != null) { 203 nextElement.focus(); 204 } else if (lowestElement != null) { 205 lowestElement.focus(); 206 } 207 } 208 209 handleFocusPrevious() { 210 const focusedTabIndex = 211 this.document.activeElement.tabIndex != null 212 ? this.document.activeElement.tabIndex 213 : Infinity; 214 215 let previousElement = null; 216 let previousTabIndex = -Infinity; 217 let highestElement = null; 218 let highestTabIndex = -Infinity; 219 this.eachTabIndexedElement(function(element, tabIndex) { 220 if (tabIndex > highestTabIndex) { 221 highestTabIndex = tabIndex; 222 highestElement = element; 223 } 224 225 if (focusedTabIndex > tabIndex && tabIndex > previousTabIndex) { 226 previousTabIndex = tabIndex; 227 previousElement = element; 228 } 229 }); 230 231 if (previousElement != null) { 232 previousElement.focus(); 233 } else if (highestElement != null) { 234 highestElement.focus(); 235 } 236 } 237 238 handleWindowFocus() { 239 this.document.body.classList.remove('is-blurred'); 240 } 241 242 handleWindowBlur() { 243 this.document.body.classList.add('is-blurred'); 244 this.atomEnvironment.storeWindowDimensions(); 245 } 246 247 handleWindowResize() { 248 this.atomEnvironment.storeWindowDimensions(); 249 } 250 251 handleEnterFullScreen() { 252 this.document.body.classList.add('fullscreen'); 253 } 254 255 handleLeaveFullScreen() { 256 this.document.body.classList.remove('fullscreen'); 257 } 258 259 handleWindowBeforeunload(event) { 260 if ( 261 !this.reloadRequested && 262 !this.atomEnvironment.inSpecMode() && 263 this.atomEnvironment.getCurrentWindow().isWebViewFocused() 264 ) { 265 this.atomEnvironment.hide(); 266 } 267 this.reloadRequested = false; 268 this.atomEnvironment.storeWindowDimensions(); 269 this.atomEnvironment.unloadEditorWindow(); 270 this.atomEnvironment.destroy(); 271 } 272 273 handleWindowToggleFullScreen() { 274 this.atomEnvironment.toggleFullScreen(); 275 } 276 277 handleWindowClose() { 278 this.atomEnvironment.close(); 279 } 280 281 handleWindowReload() { 282 this.reloadRequested = true; 283 this.atomEnvironment.reload(); 284 } 285 286 handleWindowToggleDevTools() { 287 this.atomEnvironment.toggleDevTools(); 288 } 289 290 handleWindowToggleMenuBar() { 291 this.atomEnvironment.config.set( 292 'core.autoHideMenuBar', 293 !this.atomEnvironment.config.get('core.autoHideMenuBar') 294 ); 295 296 if (this.atomEnvironment.config.get('core.autoHideMenuBar')) { 297 const detail = 298 'To toggle, press the Alt key or execute the window:toggle-menu-bar command'; 299 this.atomEnvironment.notifications.addInfo('Menu bar hidden', { detail }); 300 } 301 } 302 303 handleLinkClick(event) { 304 event.preventDefault(); 305 const uri = event.currentTarget && event.currentTarget.getAttribute('href'); 306 if (uri && uri[0] !== '#') { 307 if (/^https?:\/\//.test(uri)) { 308 this.applicationDelegate.openExternal(uri); 309 } else if (uri.startsWith('atom://')) { 310 this.atomEnvironment.uriHandlerRegistry.handleURI(uri); 311 } 312 } 313 } 314 315 handleFormSubmit(event) { 316 // Prevent form submits from changing the current window's URL 317 event.preventDefault(); 318 } 319 320 handleDocumentContextmenu(event) { 321 event.preventDefault(); 322 this.atomEnvironment.contextMenu.showForEvent(event); 323 } 324 };