appSwitcher.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
  2. const Lang = imports.lang;
  3. const Clutter = imports.gi.Clutter;
  4. const St = imports.gi.St;
  5. const Meta = imports.gi.Meta;
  6. const Mainloop = imports.mainloop;
  7. const Main = imports.ui.main;
  8. const Cinnamon = imports.gi.Cinnamon;
  9. const DISABLE_HOVER_TIMEOUT = 500; // milliseconds
  10. function sortWindowsByUserTime(win1, win2) {
  11. let t1 = win1.get_user_time();
  12. let t2 = win2.get_user_time();
  13. this.minimizedAwareAltTab = global.settings.get_boolean("alttab-minimized-aware");
  14. if (this.minimizedAwareAltTab) {
  15. let m1 = win1.minimized;
  16. let m2 = win2.minimized;
  17. if (m1 == m2) {
  18. return (t2 > t1) ? 1 : -1;
  19. }
  20. else {
  21. return m1 ? 1 : -1;
  22. }
  23. }
  24. else {
  25. return (t2 > t1) ? 1 : -1;
  26. }
  27. }
  28. function matchSkipTaskbar(win) {
  29. return !win.is_skip_taskbar();
  30. }
  31. function matchWmClass(win) {
  32. return win.get_wm_class() == this && !win.is_skip_taskbar();
  33. }
  34. function matchWorkspace(win) {
  35. return win.get_workspace() == this && !win.is_skip_taskbar();
  36. }
  37. function primaryModifier(mask) {
  38. if (mask == 0)
  39. return 0;
  40. let primary = 1;
  41. while (mask > 1) {
  42. mask >>= 1;
  43. primary <<= 1;
  44. }
  45. return primary;
  46. }
  47. function getWindowsForBinding(binding) {
  48. // Construct a list with all windows
  49. let windows = [];
  50. let windowActors = global.get_window_actors();
  51. for (let i in windowActors)
  52. windows.push(windowActors[i].get_meta_window());
  53. windows = windows.filter(Main.isInteresting);
  54. windows = windows.filter(w => w.get_monitor() === global.screen.get_current_monitor()) // 过滤当前显示器的窗口
  55. switch (binding.get_name()) {
  56. case 'switch-panels':
  57. case 'switch-panels-backward':
  58. // Switch between windows of all workspaces
  59. windows = windows.filter(matchSkipTaskbar);
  60. break;
  61. case 'switch-group':
  62. case 'switch-group-backward':
  63. // Switch between windows of the same application
  64. let focused = global.display.focus_window ? global.display.focus_window : windows[0];
  65. windows = windows.filter(matchWmClass, focused.get_wm_class());
  66. this._showAllWorkspaces = global.settings.get_boolean("alttab-switcher-show-all-workspaces");
  67. if (!this._showAllWorkspaces) {
  68. windows = windows.filter(matchWorkspace, global.workspace_manager.get_active_workspace());
  69. }
  70. break;
  71. default:
  72. // Switch between windows of current workspace
  73. this._showAllWorkspaces = global.settings.get_boolean("alttab-switcher-show-all-workspaces");
  74. if (!this._showAllWorkspaces) {
  75. windows = windows.filter(matchWorkspace, global.workspace_manager.get_active_workspace());
  76. }
  77. break;
  78. }
  79. // Sort by user time
  80. windows.sort(sortWindowsByUserTime);
  81. return windows;
  82. }
  83. function AppSwitcher() {
  84. this._init.apply(this, arguments);
  85. }
  86. AppSwitcher.prototype = {
  87. _init: function (binding) {
  88. this._initialDelayTimeoutId = null;
  89. this._binding = binding;
  90. this._windows = getWindowsForBinding(binding);
  91. this._haveModal = false;
  92. this._destroyed = false;
  93. this._motionTimeoutId = 0;
  94. this._currentIndex = this._windows.indexOf(global.display.focus_window);
  95. if (this._currentIndex < 0) {
  96. this._currentIndex = 0;
  97. }
  98. this._modifierMask = primaryModifier(binding.get_mask());
  99. this._tracker = Cinnamon.WindowTracker.get_default();
  100. this._windowManager = global.window_manager;
  101. this._dcid = this._windowManager.connect('destroy', Lang.bind(this, this._windowDestroyed));
  102. this._mcid = this._windowManager.connect('map', Lang.bind(this, this._activateSelected));
  103. this._enforcePrimaryMonitor = global.settings.get_boolean("alttab-switcher-enforce-primary-monitor");
  104. this._updateActiveMonitor();
  105. },
  106. _setupModal: function () {
  107. this._haveModal = Main.pushModal(this.actor);
  108. if (!this._haveModal) {
  109. // Probably someone else has a pointer grab, try again with keyboard only
  110. this._haveModal = Main.pushModal(this.actor, global.get_current_time(), Meta.ModalOptions.POINTER_ALREADY_GRABBED);
  111. }
  112. if (!this._haveModal)
  113. this._failedGrabAction();
  114. else {
  115. // Initially disable hover so we ignore the enter-event if
  116. // the switcher appears underneath the current pointer location
  117. this._disableHover();
  118. this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent));
  119. this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent));
  120. this.actor.connect('scroll-event', Lang.bind(this, this._scrollEvent));
  121. this.actor.connect('button-press-event', Lang.bind(this, this.destroy));
  122. // There's a race condition; if the user released Alt before
  123. // we got the grab, then we won't be notified. (See
  124. // https://bugzilla.gnome.org/show_bug.cgi?id=596695 for
  125. // details) So we check now. (Have to do this after updating
  126. // selection.)
  127. let [x, y, mods] = global.get_pointer();
  128. if (!(mods & this._modifierMask)) {
  129. this._failedGrabAction();
  130. return false;
  131. }
  132. // We delay showing the popup so that fast Alt+Tab users aren't
  133. // disturbed by the popup briefly flashing.
  134. let delay = global.settings.get_int("alttab-switcher-delay");
  135. this._initialDelayTimeoutId = Mainloop.timeout_add(delay, Lang.bind(this, this._show));
  136. }
  137. return this._haveModal;
  138. },
  139. _popModal: function () {
  140. if (this._haveModal) {
  141. Main.popModal(this.actor);
  142. this._haveModal = false;
  143. }
  144. },
  145. _show: function () {
  146. throw new Error("Abstract method _show not implemented");
  147. },
  148. _hide: function () {
  149. throw new Error("Abstract method _hide not implemented");
  150. },
  151. _onDestroy: function () {
  152. throw new Error("Abstract method _onDestroy not implemented");
  153. },
  154. _createList: function () {
  155. throw new Error("Abstract method _createList not implemented");
  156. },
  157. _updateList: function () {
  158. throw new Error("Abstract method _updateList not implemented");
  159. },
  160. _selectNext: function () {
  161. throw new Error("Abstract method _selectNext not implemented");
  162. },
  163. _selectPrevious: function () {
  164. throw new Error("Abstract method _selectPrevious not implemented");
  165. },
  166. _onWorkspaceSelected: function () {
  167. throw new Error("Abstract method _onWorkspaceSelected not implemented");
  168. },
  169. _checkSwitchTime: function () {
  170. return true;
  171. },
  172. _setCurrentWindow: function (window) {
  173. },
  174. _next: function () {
  175. if (!this._windows)
  176. return;
  177. if (this._windows.length <= 1) {
  178. this._currentIndex = 0;
  179. this._updateList(0);
  180. } else {
  181. this.actor.set_reactive(false);
  182. this._selectNext();
  183. this.actor.set_reactive(true);
  184. }
  185. this._setCurrentWindow(this._windows[this._currentIndex]);
  186. },
  187. _previous: function () {
  188. if (!this._windows)
  189. return;
  190. if (this._windows.length <= 1) {
  191. this._currentIndex = 0;
  192. this._updateList(0);
  193. } else {
  194. this.actor.set_reactive(false);
  195. this._selectPrevious();
  196. this.actor.set_reactive(true);
  197. }
  198. this._setCurrentWindow(this._windows[this._currentIndex]);
  199. },
  200. _select: function (index) {
  201. if (!this._windows)
  202. return;
  203. this._currentIndex = index;
  204. this._setCurrentWindow(this._windows[this._currentIndex]);
  205. },
  206. _updateActiveMonitor: function () {
  207. this._activeMonitor = null;
  208. if (!this._enforcePrimaryMonitor)
  209. this._activeMonitor = Main.layoutManager.currentMonitor;
  210. if (!this._activeMonitor)
  211. this._activeMonitor = Main.layoutManager.primaryMonitor;
  212. return this._activeMonitor;
  213. },
  214. _keyPressEvent: function (actor, event) {
  215. let modifiers = Cinnamon.get_event_state(event);
  216. let symbol = event.get_key_symbol();
  217. let keycode = event.get_key_code();
  218. // This relies on the fact that Clutter.ModifierType is the same as Gdk.ModifierType
  219. let action = global.display.get_keybinding_action(keycode, modifiers);
  220. this._disableHover();
  221. // Switch workspace
  222. if (modifiers & Clutter.ModifierType.CONTROL_MASK &&
  223. (symbol === Clutter.KEY_Right || symbol === Clutter.KEY_Left)) {
  224. if (this._switchWorkspace(symbol))
  225. return true;
  226. }
  227. // Extra keys
  228. switch (symbol) {
  229. case Clutter.KEY_Escape:
  230. // Esc -> Close switcher
  231. this.destroy();
  232. return true;
  233. case Clutter.KEY_Return:
  234. case Clutter.KEY_KP_Enter:
  235. // Enter -> Select active window
  236. this._activateSelected();
  237. return true;
  238. case Clutter.KEY_d:
  239. case Clutter.KEY_D:
  240. // D -> Show desktop
  241. this._showDesktop();
  242. return true;
  243. case Clutter.KEY_Right:
  244. case Clutter.KEY_Down:
  245. // Right/Down -> navigate to next preview
  246. if (this._checkSwitchTime())
  247. this._next();
  248. return true;
  249. case Clutter.KEY_Left:
  250. case Clutter.KEY_Up:
  251. // Left/Up -> navigate to previous preview
  252. if (this._checkSwitchTime())
  253. this._previous();
  254. return true;
  255. }
  256. // Default alt-tab
  257. switch (action) {
  258. case Meta.KeyBindingAction.SWITCH_GROUP:
  259. case Meta.KeyBindingAction.SWITCH_WINDOWS:
  260. case Meta.KeyBindingAction.SWITCH_PANELS:
  261. if (this._checkSwitchTime()) {
  262. // shift -> backwards
  263. if (modifiers & Clutter.ModifierType.SHIFT_MASK)
  264. this._previous();
  265. else
  266. this._next();
  267. }
  268. return true;
  269. case Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD:
  270. case Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD:
  271. case Meta.KeyBindingAction.SWITCH_PANELS_BACKWARD:
  272. if (this._checkSwitchTime())
  273. this._previous();
  274. return true;
  275. }
  276. return true;
  277. },
  278. _keyReleaseEvent: function (actor, event) {
  279. let [x, y, mods] = global.get_pointer();
  280. let state = mods & this._modifierMask;
  281. if (state == 0) {
  282. if (this._initialDelayTimeoutId !== 0)
  283. this._currentIndex = (this._currentIndex + 1) % this._windows.length;
  284. this._activateSelected();
  285. }
  286. return true;
  287. },
  288. _failedGrabAction: function () {
  289. if (!["coverflow", "timeline"].includes(global.settings.get_string('alttab-switcher-style'))) {
  290. this._keyReleaseEvent(null, null);
  291. }
  292. },
  293. // allow navigating by mouse-wheel scrolling
  294. _scrollEvent: function (actor, event) {
  295. if (event.get_scroll_direction() == Clutter.ScrollDirection.SMOOTH)
  296. return Clutter.EVENT_STOP;
  297. if (this._checkSwitchTime()) {
  298. actor.set_reactive(false);
  299. if (event.get_scroll_direction() == Clutter.ScrollDirection.UP)
  300. this._previous();
  301. else if (event.get_scroll_direction() == Clutter.ScrollDirection.DOWN)
  302. this._next();
  303. actor.set_reactive(true);
  304. }
  305. return true;
  306. },
  307. _disableHover: function () {
  308. this._mouseActive = false;
  309. if (this._motionTimeoutId != 0)
  310. Mainloop.source_remove(this._motionTimeoutId);
  311. this._motionTimeoutId = Mainloop.timeout_add(DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut));
  312. },
  313. _mouseTimedOut: function () {
  314. this._motionTimeoutId = 0;
  315. this._mouseActive = true;
  316. },
  317. _switchWorkspace: function (direction) {
  318. if (global.workspace_manager.n_workspaces < 2)
  319. return false;
  320. let current = global.workspace_manager.get_active_workspace_index();
  321. if (direction === Clutter.KEY_Left)
  322. Main.wm.actionMoveWorkspaceLeft();
  323. else if (direction === Clutter.KEY_Right)
  324. Main.wm.actionMoveWorkspaceRight();
  325. else
  326. return false;
  327. if (current === global.workspace_manager.get_active_workspace_index())
  328. return false;
  329. let workspace = global.workspace_manager.get_active_workspace();
  330. this._onWorkspaceSelected(workspace);
  331. return true;
  332. },
  333. _windowDestroyed: function (wm, actor) {
  334. this._removeDestroyedWindow(actor.meta_window);
  335. },
  336. _removeDestroyedWindow: function (window) {
  337. for (let i in this._windows) {
  338. if (window == this._windows[i]) {
  339. if (this._windows.length == 1)
  340. this.destroy();
  341. else {
  342. this._windows.splice(i, 1);
  343. if (this._previews && this._previews[i]) {
  344. this._previews[i].destroy();
  345. this._previews.splice(i, 1);
  346. }
  347. if (i < this._currentIndex)
  348. this._currentIndex--;
  349. else
  350. this._currentIndex %= this._windows.length;
  351. this._updateList(0);
  352. this._setCurrentWindow(this._windows[this._currentIndex]);
  353. }
  354. return;
  355. }
  356. }
  357. },
  358. _activateSelected: function () {
  359. const _window = this._windows[this._currentIndex]
  360. const workspace_num = _window.get_workspace().index();
  361. Main.activateWindow(_window, global.get_current_time(), workspace_num);
  362. this._warpMouse = global.settings.get_boolean("alttab-switcher-warp-mouse-pointer");
  363. if (this._warpMouse) {
  364. const rect = _window.get_frame_rect();
  365. const x = rect.x + rect.width / 2;
  366. const y = rect.y + rect.height / 2;
  367. this._pointer = Clutter.get_default_backend().get_default_seat().create_virtual_device(Clutter.InputDeviceType.POINTER_DEVICE);
  368. this._pointer.notify_absolute_motion(global.get_current_time(), x, y);
  369. }
  370. if (!this._destroyed)
  371. this.destroy();
  372. },
  373. _showDesktop: function () {
  374. for (let i in this._windows) {
  375. if (!this._windows[i].minimized)
  376. this._windows[i].minimize();
  377. }
  378. this.destroy();
  379. },
  380. destroy: function () {
  381. this._destroyed = true;
  382. this._popModal();
  383. if (this._initialDelayTimeoutId !== 0)
  384. this._destroyActors();
  385. else
  386. this._hide();
  387. if (this._initialDelayTimeoutId !== null && this._initialDelayTimeoutId > 0) {
  388. Mainloop.source_remove(this._initialDelayTimeoutId);
  389. this._initialDelayTimeoutId = 0;
  390. }
  391. this._onDestroy();
  392. this._windows = null;
  393. if (this._motionTimeoutId != 0) {
  394. Mainloop.source_remove(this._motionTimeoutId);
  395. this._motionTimeoutId = 0;
  396. }
  397. if (this._dcid > 0) {
  398. this._windowManager.disconnect(this._dcid);
  399. this._dcid = 0;
  400. }
  401. if (this._mcid > 0) {
  402. this._windowManager.disconnect(this._mcid);
  403. this._mcid = 0;
  404. }
  405. }
  406. };