window-event-handler.js 9.3 KB

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