const {Disposable, CompositeDisposable} = require('event-kit') const listen = require('./delegated-listener') // 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', this.handleWindowResize) 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] !== '#') && /^https?:\/\//.test(uri)) { this.applicationDelegate.openExternal(uri) } } handleFormSubmit (event) { // Prevent form submits from changing the current window's URL event.preventDefault() } handleDocumentContextmenu (event) { event.preventDefault() this.atomEnvironment.contextMenu.showForEvent(event) } }