const { Disposable, CompositeDisposable } = require('event-kit'); const listen = require('./delegated-listener'); const { debounce } = require('underscore-plus'); // Handles low-level events related to the `window`. module.exports = class WindowEventHandler { constructor({ atomEnvironment, applicationDelegate }) { this.handleDocumentKeyEvent = this.handleDocumentKeyEvent.bind(this); this.handleFocusNext = this.handleFocusNext.bind(this); this.handleFocusPrevious = this.handleFocusPrevious.bind(this); this.handleWindowBlur = this.handleWindowBlur.bind(this); this.handleWindowResize = this.handleWindowResize.bind(this); this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this); this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this); this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this); this.handleWindowToggleFullScreen = this.handleWindowToggleFullScreen.bind( this ); this.handleWindowClose = this.handleWindowClose.bind(this); this.handleWindowReload = this.handleWindowReload.bind(this); this.handleWindowToggleDevTools = this.handleWindowToggleDevTools.bind( this ); this.handleWindowToggleMenuBar = this.handleWindowToggleMenuBar.bind(this); this.handleLinkClick = this.handleLinkClick.bind(this); this.handleDocumentContextmenu = this.handleDocumentContextmenu.bind(this); this.atomEnvironment = atomEnvironment; this.applicationDelegate = applicationDelegate; this.reloadRequested = false; this.subscriptions = new CompositeDisposable(); this.handleNativeKeybindings(); } initialize(window, document) { this.window = window; this.document = document; this.subscriptions.add( this.atomEnvironment.commands.add(this.window, { 'window:toggle-full-screen': this.handleWindowToggleFullScreen, 'window:close': this.handleWindowClose, 'window:reload': this.handleWindowReload, 'window:toggle-dev-tools': this.handleWindowToggleDevTools }) ); if (['win32', 'linux'].includes(process.platform)) { this.subscriptions.add( this.atomEnvironment.commands.add(this.window, { 'window:toggle-menu-bar': this.handleWindowToggleMenuBar }) ); } this.subscriptions.add( this.atomEnvironment.commands.add(this.document, { 'core:focus-next': this.handleFocusNext, 'core:focus-previous': this.handleFocusPrevious }) ); this.addEventListener( this.window, 'beforeunload', this.handleWindowBeforeunload ); this.addEventListener(this.window, 'focus', this.handleWindowFocus); this.addEventListener(this.window, 'blur', this.handleWindowBlur); this.addEventListener( this.window, 'resize', debounce(this.handleWindowResize, 500) ); this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent); this.addEventListener( this.document, 'keydown', this.handleDocumentKeyEvent ); this.addEventListener(this.document, 'drop', this.handleDocumentDrop); this.addEventListener( this.document, 'dragover', this.handleDocumentDragover ); this.addEventListener( this.document, 'contextmenu', this.handleDocumentContextmenu ); this.subscriptions.add( listen(this.document, 'click', 'a', this.handleLinkClick) ); this.subscriptions.add( listen(this.document, 'submit', 'form', this.handleFormSubmit) ); this.subscriptions.add( this.applicationDelegate.onDidEnterFullScreen(this.handleEnterFullScreen) ); this.subscriptions.add( this.applicationDelegate.onDidLeaveFullScreen(this.handleLeaveFullScreen) ); } // Wire commands that should be handled by Chromium for elements with the // `.native-key-bindings` class. handleNativeKeybindings() { const bindCommandToAction = (command, action) => { this.subscriptions.add( this.atomEnvironment.commands.add( '.native-key-bindings', command, event => this.applicationDelegate.getCurrentWindow().webContents[action](), false ) ); }; bindCommandToAction('core:copy', 'copy'); bindCommandToAction('core:paste', 'paste'); bindCommandToAction('core:undo', 'undo'); bindCommandToAction('core:redo', 'redo'); bindCommandToAction('core:select-all', 'selectAll'); bindCommandToAction('core:cut', 'cut'); } unsubscribe() { this.subscriptions.dispose(); } on(target, eventName, handler) { target.on(eventName, handler); this.subscriptions.add( new Disposable(function() { target.removeListener(eventName, handler); }) ); } addEventListener(target, eventName, handler) { target.addEventListener(eventName, handler); this.subscriptions.add( new Disposable(function() { target.removeEventListener(eventName, handler); }) ); } handleDocumentKeyEvent(event) { this.atomEnvironment.keymaps.handleKeyboardEvent(event); event.stopImmediatePropagation(); } handleDrop(event) { event.preventDefault(); event.stopPropagation(); } handleDragover(event) { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'none'; } eachTabIndexedElement(callback) { for (let element of this.document.querySelectorAll('[tabindex]')) { if (element.disabled) { continue; } if (!(element.tabIndex >= 0)) { continue; } callback(element, element.tabIndex); } } handleFocusNext() { const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : -Infinity; let nextElement = null; let nextTabIndex = Infinity; let lowestElement = null; let lowestTabIndex = Infinity; this.eachTabIndexedElement(function(element, tabIndex) { if (tabIndex < lowestTabIndex) { lowestTabIndex = tabIndex; lowestElement = element; } if (focusedTabIndex < tabIndex && tabIndex < nextTabIndex) { nextTabIndex = tabIndex; nextElement = element; } }); if (nextElement != null) { nextElement.focus(); } else if (lowestElement != null) { lowestElement.focus(); } } handleFocusPrevious() { const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : Infinity; let previousElement = null; let previousTabIndex = -Infinity; let highestElement = null; let highestTabIndex = -Infinity; this.eachTabIndexedElement(function(element, tabIndex) { if (tabIndex > highestTabIndex) { highestTabIndex = tabIndex; highestElement = element; } if (focusedTabIndex > tabIndex && tabIndex > previousTabIndex) { previousTabIndex = tabIndex; previousElement = element; } }); if (previousElement != null) { previousElement.focus(); } else if (highestElement != null) { highestElement.focus(); } } handleWindowFocus() { this.document.body.classList.remove('is-blurred'); } handleWindowBlur() { this.document.body.classList.add('is-blurred'); this.atomEnvironment.storeWindowDimensions(); } handleWindowResize() { this.atomEnvironment.storeWindowDimensions(); } handleEnterFullScreen() { this.document.body.classList.add('fullscreen'); } handleLeaveFullScreen() { this.document.body.classList.remove('fullscreen'); } handleWindowBeforeunload(event) { if ( !this.reloadRequested && !this.atomEnvironment.inSpecMode() && this.atomEnvironment.getCurrentWindow().isWebViewFocused() ) { this.atomEnvironment.hide(); } this.reloadRequested = false; this.atomEnvironment.storeWindowDimensions(); this.atomEnvironment.unloadEditorWindow(); this.atomEnvironment.destroy(); } handleWindowToggleFullScreen() { this.atomEnvironment.toggleFullScreen(); } handleWindowClose() { this.atomEnvironment.close(); } handleWindowReload() { this.reloadRequested = true; this.atomEnvironment.reload(); } handleWindowToggleDevTools() { this.atomEnvironment.toggleDevTools(); } handleWindowToggleMenuBar() { this.atomEnvironment.config.set( 'core.autoHideMenuBar', !this.atomEnvironment.config.get('core.autoHideMenuBar') ); if (this.atomEnvironment.config.get('core.autoHideMenuBar')) { const detail = 'To toggle, press the Alt key or execute the window:toggle-menu-bar command'; this.atomEnvironment.notifications.addInfo('Menu bar hidden', { detail }); } } handleLinkClick(event) { event.preventDefault(); const uri = event.currentTarget && event.currentTarget.getAttribute('href'); if (uri && uri[0] !== '#') { if (/^https?:\/\//.test(uri)) { this.applicationDelegate.openExternal(uri); } else if (uri.startsWith('atom://')) { this.atomEnvironment.uriHandlerRegistry.handleURI(uri); } } } handleFormSubmit(event) { // Prevent form submits from changing the current window's URL event.preventDefault(); } handleDocumentContextmenu(event) { event.preventDefault(); this.atomEnvironment.contextMenu.showForEvent(event); } };