/ src / window-event-handler.js
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  };