summaryrefslogtreecommitdiffstats
path: root/js/ui/altTab.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/ui/altTab.js1134
1 files changed, 1134 insertions, 0 deletions
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
new file mode 100644
index 0000000..a3daebc
--- /dev/null
+++ b/js/ui/altTab.js
@@ -0,0 +1,1134 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported AppSwitcherPopup, GroupCyclerPopup, WindowSwitcherPopup,
+ WindowCyclerPopup */
+
+const { Atk, Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
+
+const Main = imports.ui.main;
+const SwitcherPopup = imports.ui.switcherPopup;
+
+var APP_ICON_HOVER_TIMEOUT = 200; // milliseconds
+
+var THUMBNAIL_DEFAULT_SIZE = 256;
+var THUMBNAIL_POPUP_TIME = 500; // milliseconds
+var THUMBNAIL_FADE_TIME = 100; // milliseconds
+
+var WINDOW_PREVIEW_SIZE = 128;
+var APP_ICON_SIZE = 96;
+var APP_ICON_SIZE_SMALL = 48;
+
+const baseIconSizes = [96, 64, 48, 32, 22];
+
+var AppIconMode = {
+ THUMBNAIL_ONLY: 1,
+ APP_ICON_ONLY: 2,
+ BOTH: 3,
+};
+
+function _createWindowClone(window, size) {
+ let [width, height] = window.get_size();
+ let scale = Math.min(1.0, size / width, size / height);
+ return new Clutter.Clone({
+ source: window,
+ width: width * scale,
+ height: height * scale,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ // usual hack for the usual bug in ClutterBinLayout...
+ x_expand: true,
+ y_expand: true,
+ });
+}
+
+function getWindows(workspace) {
+ // We ignore skip-taskbar windows in switchers, but if they are attached
+ // to their parent, their position in the MRU list may be more appropriate
+ // than the parent; so start with the complete list ...
+ let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL,
+ workspace);
+ // ... map windows to their parent where appropriate ...
+ return windows.map(w => {
+ return w.is_attached_dialog() ? w.get_transient_for() : w;
+ // ... and filter out skip-taskbar windows and duplicates
+ }).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) == i);
+}
+
+var AppSwitcherPopup = GObject.registerClass(
+class AppSwitcherPopup extends SwitcherPopup.SwitcherPopup {
+ _init() {
+ super._init();
+
+ this._thumbnails = null;
+ this._thumbnailTimeoutId = 0;
+ this._currentWindow = -1;
+
+ this.thumbnailsVisible = false;
+
+ let apps = Shell.AppSystem.get_default().get_running();
+
+ this._switcherList = new AppSwitcher(apps, this);
+ this._items = this._switcherList.icons;
+ }
+
+ vfunc_allocate(box) {
+ super.vfunc_allocate(box);
+
+ // Allocate the thumbnails
+ // We try to avoid overflowing the screen so we base the resulting size on
+ // those calculations
+ if (this._thumbnails) {
+ let childBox = this._switcherList.get_allocation_box();
+ let primary = Main.layoutManager.primaryMonitor;
+
+ let leftPadding = this.get_theme_node().get_padding(St.Side.LEFT);
+ let rightPadding = this.get_theme_node().get_padding(St.Side.RIGHT);
+ let bottomPadding = this.get_theme_node().get_padding(St.Side.BOTTOM);
+ let hPadding = leftPadding + rightPadding;
+
+ let icon = this._items[this._selectedIndex];
+ let [posX] = icon.get_transformed_position();
+ let thumbnailCenter = posX + icon.width / 2;
+ let [, childNaturalWidth] = this._thumbnails.get_preferred_width(-1);
+ childBox.x1 = Math.max(primary.x + leftPadding, Math.floor(thumbnailCenter - childNaturalWidth / 2));
+ if (childBox.x1 + childNaturalWidth > primary.x + primary.width - hPadding) {
+ let offset = childBox.x1 + childNaturalWidth - primary.width + hPadding;
+ childBox.x1 = Math.max(primary.x + leftPadding, childBox.x1 - offset - hPadding);
+ }
+
+ let spacing = this.get_theme_node().get_length('spacing');
+
+ childBox.x2 = childBox.x1 + childNaturalWidth;
+ if (childBox.x2 > primary.x + primary.width - rightPadding)
+ childBox.x2 = primary.x + primary.width - rightPadding;
+ childBox.y1 = this._switcherList.allocation.y2 + spacing;
+ this._thumbnails.addClones(primary.y + primary.height - bottomPadding - childBox.y1);
+ let [, childNaturalHeight] = this._thumbnails.get_preferred_height(-1);
+ childBox.y2 = childBox.y1 + childNaturalHeight;
+ this._thumbnails.allocate(childBox);
+ }
+ }
+
+ _initialSelection(backward, binding) {
+ if (binding == 'switch-group') {
+ if (backward)
+ this._select(0, this._items[0].cachedWindows.length - 1);
+ else if (this._items[0].cachedWindows.length > 1)
+ this._select(0, 1);
+ else
+ this._select(0, 0);
+ } else if (binding == 'switch-group-backward') {
+ this._select(0, this._items[0].cachedWindows.length - 1);
+ } else if (binding == 'switch-applications-backward') {
+ this._select(this._items.length - 1);
+ } else if (this._items.length == 1) {
+ this._select(0);
+ } else if (backward) {
+ this._select(this._items.length - 1);
+ } else {
+ this._select(1);
+ }
+ }
+
+ _nextWindow() {
+ // We actually want the second window if we're in the unset state
+ if (this._currentWindow == -1)
+ this._currentWindow = 0;
+ return SwitcherPopup.mod(this._currentWindow + 1,
+ this._items[this._selectedIndex].cachedWindows.length);
+ }
+
+ _previousWindow() {
+ // Also assume second window here
+ if (this._currentWindow == -1)
+ this._currentWindow = 1;
+ return SwitcherPopup.mod(this._currentWindow - 1,
+ this._items[this._selectedIndex].cachedWindows.length);
+ }
+
+ _closeAppWindow(appIndex, windowIndex) {
+ let appIcon = this._items[appIndex];
+ if (!appIcon)
+ return;
+
+ let window = appIcon.cachedWindows[windowIndex];
+ if (!window)
+ return;
+
+ window.delete(global.get_current_time());
+ }
+
+ _quitApplication(appIndex) {
+ let appIcon = this._items[appIndex];
+ if (!appIcon)
+ return;
+
+ appIcon.app.request_quit();
+ }
+
+ _keyPressHandler(keysym, action) {
+ const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+ if (action == Meta.KeyBindingAction.SWITCH_GROUP) {
+ if (!this._thumbnailsFocused)
+ this._select(this._selectedIndex, 0);
+ else
+ this._select(this._selectedIndex, this._nextWindow());
+ } else if (action == Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD) {
+ this._select(this._selectedIndex, this._previousWindow());
+ } else if (action == Meta.KeyBindingAction.SWITCH_APPLICATIONS) {
+ this._select(this._next());
+ } else if (action == Meta.KeyBindingAction.SWITCH_APPLICATIONS_BACKWARD) {
+ this._select(this._previous());
+ } else if (keysym == Clutter.KEY_q || keysym === Clutter.KEY_Q) {
+ this._quitApplication(this._selectedIndex);
+ } else if (this._thumbnailsFocused) {
+ if (keysym === Clutter.KEY_Left)
+ this._select(this._selectedIndex, rtl ? this._nextWindow() : this._previousWindow());
+ else if (keysym === Clutter.KEY_Right)
+ this._select(this._selectedIndex, rtl ? this._previousWindow() : this._nextWindow());
+ else if (keysym === Clutter.KEY_Up)
+ this._select(this._selectedIndex, null, true);
+ else if (keysym === Clutter.KEY_w || keysym === Clutter.KEY_W || keysym === Clutter.KEY_F4)
+ this._closeAppWindow(this._selectedIndex, this._currentWindow);
+ else
+ return Clutter.EVENT_PROPAGATE;
+ } else if (keysym == Clutter.KEY_Left) {
+ this._select(rtl ? this._next() : this._previous());
+ } else if (keysym == Clutter.KEY_Right) {
+ this._select(rtl ? this._previous() : this._next());
+ } else if (keysym == Clutter.KEY_Down) {
+ this._select(this._selectedIndex, 0);
+ } else {
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ return Clutter.EVENT_STOP;
+ }
+
+ _scrollHandler(direction) {
+ if (direction == Clutter.ScrollDirection.UP) {
+ if (this._thumbnailsFocused) {
+ if (this._currentWindow == 0 || this._currentWindow == -1)
+ this._select(this._previous());
+ else
+ this._select(this._selectedIndex, this._previousWindow());
+ } else {
+ let nwindows = this._items[this._selectedIndex].cachedWindows.length;
+ if (nwindows > 1)
+ this._select(this._selectedIndex, nwindows - 1);
+ else
+ this._select(this._previous());
+ }
+ } else if (direction == Clutter.ScrollDirection.DOWN) {
+ if (this._thumbnailsFocused) {
+ if (this._currentWindow == this._items[this._selectedIndex].cachedWindows.length - 1)
+ this._select(this._next());
+ else
+ this._select(this._selectedIndex, this._nextWindow());
+ } else {
+ let nwindows = this._items[this._selectedIndex].cachedWindows.length;
+ if (nwindows > 1)
+ this._select(this._selectedIndex, 0);
+ else
+ this._select(this._next());
+ }
+ }
+ }
+
+ _itemActivatedHandler(n) {
+ // If the user clicks on the selected app, activate the
+ // selected window; otherwise (eg, they click on an app while
+ // !mouseActive) activate the clicked-on app.
+ if (n == this._selectedIndex && this._currentWindow >= 0)
+ this._select(n, this._currentWindow);
+ else
+ this._select(n);
+ }
+
+ _windowActivated(thumbnailSwitcher, n) {
+ let appIcon = this._items[this._selectedIndex];
+ Main.activateWindow(appIcon.cachedWindows[n]);
+ this.fadeAndDestroy();
+ }
+
+ _windowEntered(thumbnailSwitcher, n) {
+ if (!this.mouseActive)
+ return;
+
+ this._select(this._selectedIndex, n);
+ }
+
+ _windowRemoved(thumbnailSwitcher, n) {
+ let appIcon = this._items[this._selectedIndex];
+ if (!appIcon)
+ return;
+
+ if (appIcon.cachedWindows.length > 0) {
+ let newIndex = Math.min(n, appIcon.cachedWindows.length - 1);
+ this._select(this._selectedIndex, newIndex);
+ }
+ }
+
+ _finish(timestamp) {
+ let appIcon = this._items[this._selectedIndex];
+ if (this._currentWindow < 0)
+ appIcon.app.activate_window(appIcon.cachedWindows[0], timestamp);
+ else if (appIcon.cachedWindows[this._currentWindow])
+ Main.activateWindow(appIcon.cachedWindows[this._currentWindow], timestamp);
+
+ super._finish(timestamp);
+ }
+
+ _onDestroy() {
+ if (this._thumbnailTimeoutId != 0)
+ GLib.source_remove(this._thumbnailTimeoutId);
+
+ super._onDestroy();
+ }
+
+ /**
+ * _select:
+ * @param {number} app: index of the app to select
+ * @param {number=} window: index of which of @app's windows to select
+ * @param {bool} forceAppFocus: optional flag, see below
+ *
+ * Selects the indicated @app, and optional @window, and sets
+ * this._thumbnailsFocused appropriately to indicate whether the
+ * arrow keys should act on the app list or the thumbnail list.
+ *
+ * If @app is specified and @window is unspecified or %null, then
+ * the app is highlighted (ie, given a light background), and the
+ * current thumbnail list, if any, is destroyed. If @app has
+ * multiple windows, and @forceAppFocus is not %true, then a
+ * timeout is started to open a thumbnail list.
+ *
+ * If @app and @window are specified (and @forceAppFocus is not),
+ * then @app will be outlined, a thumbnail list will be created
+ * and focused (if it hasn't been already), and the @window'th
+ * window in it will be highlighted.
+ *
+ * If @app and @window are specified and @forceAppFocus is %true,
+ * then @app will be highlighted, and @window outlined, and the
+ * app list will have the keyboard focus.
+ */
+ _select(app, window, forceAppFocus) {
+ if (app != this._selectedIndex || window == null) {
+ if (this._thumbnails)
+ this._destroyThumbnails();
+ }
+
+ if (this._thumbnailTimeoutId != 0) {
+ GLib.source_remove(this._thumbnailTimeoutId);
+ this._thumbnailTimeoutId = 0;
+ }
+
+ this._thumbnailsFocused = (window != null) && !forceAppFocus;
+
+ this._selectedIndex = app;
+ this._currentWindow = window ? window : -1;
+ this._switcherList.highlight(app, this._thumbnailsFocused);
+
+ if (window != null) {
+ if (!this._thumbnails)
+ this._createThumbnails();
+ this._currentWindow = window;
+ this._thumbnails.highlight(window, forceAppFocus);
+ } else if (this._items[this._selectedIndex].cachedWindows.length > 1 &&
+ !forceAppFocus) {
+ this._thumbnailTimeoutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ THUMBNAIL_POPUP_TIME,
+ this._timeoutPopupThumbnails.bind(this));
+ GLib.Source.set_name_by_id(this._thumbnailTimeoutId, '[gnome-shell] this._timeoutPopupThumbnails');
+ }
+ }
+
+ _timeoutPopupThumbnails() {
+ if (!this._thumbnails)
+ this._createThumbnails();
+ this._thumbnailTimeoutId = 0;
+ this._thumbnailsFocused = false;
+ return GLib.SOURCE_REMOVE;
+ }
+
+ _destroyThumbnails() {
+ let thumbnailsActor = this._thumbnails;
+ this._thumbnails.ease({
+ opacity: 0,
+ duration: THUMBNAIL_FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ thumbnailsActor.destroy();
+ this.thumbnailsVisible = false;
+ },
+ });
+ this._thumbnails = null;
+ this._switcherList.removeAccessibleState(this._selectedIndex, Atk.StateType.EXPANDED);
+ }
+
+ _createThumbnails() {
+ this._thumbnails = new ThumbnailSwitcher(this._items[this._selectedIndex].cachedWindows);
+ this._thumbnails.connect('item-activated', this._windowActivated.bind(this));
+ this._thumbnails.connect('item-entered', this._windowEntered.bind(this));
+ this._thumbnails.connect('item-removed', this._windowRemoved.bind(this));
+ this._thumbnails.connect('destroy', () => {
+ this._thumbnails = null;
+ this._thumbnailsFocused = false;
+ });
+
+ this.add_actor(this._thumbnails);
+
+ // Need to force an allocation so we can figure out whether we
+ // need to scroll when selecting
+ this._thumbnails.get_allocation_box();
+
+ this._thumbnails.opacity = 0;
+ this._thumbnails.ease({
+ opacity: 255,
+ duration: THUMBNAIL_FADE_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this.thumbnailsVisible = true;
+ },
+ });
+
+ this._switcherList.addAccessibleState(this._selectedIndex, Atk.StateType.EXPANDED);
+ }
+});
+
+var CyclerHighlight = GObject.registerClass(
+class CyclerHighlight extends St.Widget {
+ _init() {
+ super._init({ layout_manager: new Clutter.BinLayout() });
+ this._window = null;
+
+ this._clone = new Clutter.Clone();
+ this.add_actor(this._clone);
+
+ this._highlight = new St.Widget({ style_class: 'cycler-highlight' });
+ this.add_actor(this._highlight);
+
+ let coordinate = Clutter.BindCoordinate.ALL;
+ let constraint = new Clutter.BindConstraint({ coordinate });
+ this._clone.bind_property('source', constraint, 'source', 0);
+
+ this.add_constraint(constraint);
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ set window(w) {
+ if (this._window == w)
+ return;
+
+ this._window?.disconnectObject(this);
+
+ this._window = w;
+
+ if (this._clone.source)
+ this._clone.source.sync_visibility();
+
+ const windowActor = this._window?.get_compositor_private() ?? null;
+
+ if (windowActor)
+ windowActor.hide();
+
+ this._clone.source = windowActor;
+
+ if (this._window) {
+ this._onSizeChanged();
+ this._window.connectObject('size-changed',
+ this._onSizeChanged.bind(this), this);
+ } else {
+ this._highlight.set_size(0, 0);
+ this._highlight.hide();
+ }
+ }
+
+ _onSizeChanged() {
+ const bufferRect = this._window.get_buffer_rect();
+ const rect = this._window.get_frame_rect();
+ this._highlight.set_size(rect.width, rect.height);
+ this._highlight.set_position(
+ rect.x - bufferRect.x,
+ rect.y - bufferRect.y);
+ this._highlight.show();
+ }
+
+ _onDestroy() {
+ this.window = null;
+ }
+});
+
+// We don't show an actual popup, so just provide what SwitcherPopup
+// expects instead of inheriting from SwitcherList
+var CyclerList = GObject.registerClass({
+ Signals: {
+ 'item-activated': { param_types: [GObject.TYPE_INT] },
+ 'item-entered': { param_types: [GObject.TYPE_INT] },
+ 'item-removed': { param_types: [GObject.TYPE_INT] },
+ 'item-highlighted': { param_types: [GObject.TYPE_INT] },
+ },
+}, class CyclerList extends St.Widget {
+ highlight(index, _justOutline) {
+ this.emit('item-highlighted', index);
+ }
+});
+
+var CyclerPopup = GObject.registerClass({
+ GTypeFlags: GObject.TypeFlags.ABSTRACT,
+}, class CyclerPopup extends SwitcherPopup.SwitcherPopup {
+ _init() {
+ super._init();
+
+ this._items = this._getWindows();
+
+ this._highlight = new CyclerHighlight();
+ global.window_group.add_actor(this._highlight);
+
+ this._switcherList = new CyclerList();
+ this._switcherList.connect('item-highlighted', (list, index) => {
+ this._highlightItem(index);
+ });
+ }
+
+ _highlightItem(index, _justOutline) {
+ this._highlight.window = this._items[index];
+ global.window_group.set_child_above_sibling(this._highlight, null);
+ }
+
+ _finish() {
+ let window = this._items[this._selectedIndex];
+ let ws = window.get_workspace();
+ let workspaceManager = global.workspace_manager;
+ let activeWs = workspaceManager.get_active_workspace();
+
+ if (window.minimized) {
+ Main.wm.skipNextEffect(window.get_compositor_private());
+ window.unminimize();
+ }
+
+ if (activeWs == ws) {
+ Main.activateWindow(window);
+ } else {
+ // If the selected window is on a different workspace, we don't
+ // want it to disappear, then slide in with the workspace; instead,
+ // always activate it on the active workspace ...
+ activeWs.activate_with_focus(window, global.get_current_time());
+
+ // ... then slide it over to the original workspace if necessary
+ Main.wm.actionMoveWindow(window, ws);
+ }
+
+ super._finish();
+ }
+
+ _onDestroy() {
+ this._highlight.destroy();
+
+ super._onDestroy();
+ }
+});
+
+
+var GroupCyclerPopup = GObject.registerClass(
+class GroupCyclerPopup extends CyclerPopup {
+ _init() {
+ this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' });
+ super._init();
+ }
+
+ _getWindows() {
+ let app = Shell.WindowTracker.get_default().focus_app;
+ let appWindows = app?.get_windows() ?? [];
+
+ if (this._settings.get_boolean('current-workspace-only')) {
+ const workspaceManager = global.workspace_manager;
+ const workspace = workspaceManager.get_active_workspace();
+ appWindows = appWindows.filter(
+ window => window.located_on_workspace(workspace));
+ }
+
+ return appWindows;
+ }
+
+ _keyPressHandler(keysym, action) {
+ if (action == Meta.KeyBindingAction.CYCLE_GROUP)
+ this._select(this._next());
+ else if (action == Meta.KeyBindingAction.CYCLE_GROUP_BACKWARD)
+ this._select(this._previous());
+ else
+ return Clutter.EVENT_PROPAGATE;
+
+ return Clutter.EVENT_STOP;
+ }
+});
+
+var WindowSwitcherPopup = GObject.registerClass(
+class WindowSwitcherPopup extends SwitcherPopup.SwitcherPopup {
+ _init() {
+ super._init();
+ this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell.window-switcher' });
+
+ let windows = this._getWindowList();
+
+ let mode = this._settings.get_enum('app-icon-mode');
+ this._switcherList = new WindowSwitcher(windows, mode);
+ this._items = this._switcherList.icons;
+ }
+
+ _getWindowList() {
+ let workspace = null;
+
+ if (this._settings.get_boolean('current-workspace-only')) {
+ let workspaceManager = global.workspace_manager;
+
+ workspace = workspaceManager.get_active_workspace();
+ }
+
+ return getWindows(workspace);
+ }
+
+ _closeWindow(windowIndex) {
+ let windowIcon = this._items[windowIndex];
+ if (!windowIcon)
+ return;
+
+ windowIcon.window.delete(global.get_current_time());
+ }
+
+ _keyPressHandler(keysym, action) {
+ const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
+ if (action == Meta.KeyBindingAction.SWITCH_WINDOWS)
+ this._select(this._next());
+ else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD)
+ this._select(this._previous());
+ else if (keysym == Clutter.KEY_Left)
+ this._select(rtl ? this._next() : this._previous());
+ else if (keysym == Clutter.KEY_Right)
+ this._select(rtl ? this._previous() : this._next());
+ else if (keysym === Clutter.KEY_w || keysym === Clutter.KEY_W || keysym === Clutter.KEY_F4)
+ this._closeWindow(this._selectedIndex);
+ else
+ return Clutter.EVENT_PROPAGATE;
+
+ return Clutter.EVENT_STOP;
+ }
+
+ _finish() {
+ Main.activateWindow(this._items[this._selectedIndex].window);
+
+ super._finish();
+ }
+});
+
+var WindowCyclerPopup = GObject.registerClass(
+class WindowCyclerPopup extends CyclerPopup {
+ _init() {
+ this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell.window-switcher' });
+ super._init();
+ }
+
+ _getWindows() {
+ let workspace = null;
+
+ if (this._settings.get_boolean('current-workspace-only')) {
+ let workspaceManager = global.workspace_manager;
+
+ workspace = workspaceManager.get_active_workspace();
+ }
+
+ return getWindows(workspace);
+ }
+
+ _keyPressHandler(keysym, action) {
+ if (action == Meta.KeyBindingAction.CYCLE_WINDOWS)
+ this._select(this._next());
+ else if (action == Meta.KeyBindingAction.CYCLE_WINDOWS_BACKWARD)
+ this._select(this._previous());
+ else
+ return Clutter.EVENT_PROPAGATE;
+
+ return Clutter.EVENT_STOP;
+ }
+});
+
+var AppIcon = GObject.registerClass(
+class AppIcon extends St.BoxLayout {
+ _init(app) {
+ super._init({
+ style_class: 'alt-tab-app',
+ vertical: true,
+ });
+
+ this.app = app;
+ this.icon = null;
+ this._iconBin = new St.Bin();
+
+ this.add_child(this._iconBin);
+ this.label = new St.Label({
+ text: this.app.get_name(),
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this.add_child(this.label);
+ }
+
+ // eslint-disable-next-line camelcase
+ set_size(size) {
+ this.icon = this.app.create_icon_texture(size);
+ this._iconBin.child = this.icon;
+ }
+});
+
+var AppSwitcher = GObject.registerClass(
+class AppSwitcher extends SwitcherPopup.SwitcherList {
+ _init(apps, altTabPopup) {
+ super._init(true);
+
+ this.icons = [];
+ this._arrows = [];
+
+ let windowTracker = Shell.WindowTracker.get_default();
+ let settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' });
+
+ let workspace = null;
+ if (settings.get_boolean('current-workspace-only')) {
+ let workspaceManager = global.workspace_manager;
+
+ workspace = workspaceManager.get_active_workspace();
+ }
+
+ let allWindows = getWindows(workspace);
+
+ // Construct the AppIcons, add to the popup
+ for (let i = 0; i < apps.length; i++) {
+ let appIcon = new AppIcon(apps[i]);
+ // Cache the window list now; we don't handle dynamic changes here,
+ // and we don't want to be continually retrieving it
+ appIcon.cachedWindows = allWindows.filter(
+ w => windowTracker.get_window_app(w) === appIcon.app);
+ if (appIcon.cachedWindows.length > 0)
+ this._addIcon(appIcon);
+ }
+
+ this._altTabPopup = altTabPopup;
+ this._delayedHighlighted = -1;
+ this._mouseTimeOutId = 0;
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ if (this._mouseTimeOutId != 0)
+ GLib.source_remove(this._mouseTimeOutId);
+
+ this.icons.forEach(
+ icon => icon.app.disconnectObject(this));
+ }
+
+ _setIconSize() {
+ let j = 0;
+ while (this._items.length > 1 && this._items[j].style_class != 'item-box')
+ j++;
+
+ let themeNode = this._items[j].get_theme_node();
+ this._list.ensure_style();
+
+ let iconPadding = themeNode.get_horizontal_padding();
+ let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT);
+ let [, labelNaturalHeight] = this.icons[j].label.get_preferred_height(-1);
+ let iconSpacing = labelNaturalHeight + iconPadding + iconBorder;
+ let totalSpacing = this._list.spacing * (this._items.length - 1);
+
+ // We just assume the whole screen here due to weirdness happening with the passed width
+ let primary = Main.layoutManager.primaryMonitor;
+ let parentPadding = this.get_parent().get_theme_node().get_horizontal_padding();
+ let availWidth = primary.width - parentPadding - this.get_theme_node().get_horizontal_padding();
+
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+ let iconSizes = baseIconSizes.map(s => s * scaleFactor);
+ let iconSize = baseIconSizes[0];
+
+ if (this._items.length > 1) {
+ for (let i = 0; i < baseIconSizes.length; i++) {
+ iconSize = baseIconSizes[i];
+ let height = iconSizes[i] + iconSpacing;
+ let w = height * this._items.length + totalSpacing;
+ if (w <= availWidth)
+ break;
+ }
+ }
+
+ this._iconSize = iconSize;
+
+ for (let i = 0; i < this.icons.length; i++) {
+ if (this.icons[i].icon != null)
+ break;
+ this.icons[i].set_size(iconSize);
+ }
+ }
+
+ vfunc_get_preferred_height(forWidth) {
+ if (!this._iconSize)
+ this._setIconSize();
+
+ return super.vfunc_get_preferred_height(forWidth);
+ }
+
+ vfunc_allocate(box) {
+ // Allocate the main list items
+ super.vfunc_allocate(box);
+
+ let contentBox = this.get_theme_node().get_content_box(box);
+
+ let arrowHeight = Math.floor(this.get_theme_node().get_padding(St.Side.BOTTOM) / 3);
+ let arrowWidth = arrowHeight * 2;
+
+ // Now allocate each arrow underneath its item
+ let childBox = new Clutter.ActorBox();
+ for (let i = 0; i < this._items.length; i++) {
+ let itemBox = this._items[i].allocation;
+ childBox.x1 = contentBox.x1 + Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2);
+ childBox.x2 = childBox.x1 + arrowWidth;
+ childBox.y1 = contentBox.y1 + itemBox.y2 + arrowHeight;
+ childBox.y2 = childBox.y1 + arrowHeight;
+ this._arrows[i].allocate(childBox);
+ }
+ }
+
+ // We override SwitcherList's _onItemMotion method to delay
+ // activation when the thumbnail list is open
+ _onItemMotion(item) {
+ if (item === this._items[this._highlighted] ||
+ item === this._items[this._delayedHighlighted])
+ return Clutter.EVENT_PROPAGATE;
+
+ const index = this._items.indexOf(item);
+
+ if (this._mouseTimeOutId !== 0) {
+ GLib.source_remove(this._mouseTimeOutId);
+ this._delayedHighlighted = -1;
+ this._mouseTimeOutId = 0;
+ }
+
+ if (this._altTabPopup.thumbnailsVisible) {
+ this._delayedHighlighted = index;
+ this._mouseTimeOutId = GLib.timeout_add(
+ GLib.PRIORITY_DEFAULT,
+ APP_ICON_HOVER_TIMEOUT,
+ () => {
+ this._enterItem(index);
+ this._delayedHighlighted = -1;
+ this._mouseTimeOutId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._mouseTimeOutId, '[gnome-shell] this._enterItem');
+ } else {
+ this._itemEntered(index);
+ }
+
+ return Clutter.EVENT_PROPAGATE;
+ }
+
+ _enterItem(index) {
+ let [x, y] = global.get_pointer();
+ let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
+ if (this._items[index].contains(pickedActor))
+ this._itemEntered(index);
+ }
+
+ // We override SwitcherList's highlight() method to also deal with
+ // the AppSwitcher->ThumbnailSwitcher arrows. Apps with only 1 window
+ // will hide their arrows by default, but show them when their
+ // thumbnails are visible (ie, when the app icon is supposed to be
+ // in justOutline mode). Apps with multiple windows will normally
+ // show a dim arrow, but show a bright arrow when they are
+ // highlighted.
+ highlight(n, justOutline) {
+ if (this.icons[this._highlighted]) {
+ if (this.icons[this._highlighted].cachedWindows.length === 1)
+ this._arrows[this._highlighted].hide();
+ else
+ this._arrows[this._highlighted].remove_style_pseudo_class('highlighted');
+ }
+
+ super.highlight(n, justOutline);
+
+ if (this._highlighted !== -1) {
+ if (justOutline && this.icons[this._highlighted].cachedWindows.length === 1)
+ this._arrows[this._highlighted].show();
+ else
+ this._arrows[this._highlighted].add_style_pseudo_class('highlighted');
+ }
+ }
+
+ _addIcon(appIcon) {
+ this.icons.push(appIcon);
+ let item = this.addItem(appIcon, appIcon.label);
+
+ appIcon.app.connectObject('notify::state', app => {
+ if (app.state != Shell.AppState.RUNNING)
+ this._removeIcon(app);
+ }, this);
+
+ let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' });
+ arrow.connect('repaint', () => SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM));
+ this.add_actor(arrow);
+ this._arrows.push(arrow);
+
+ if (appIcon.cachedWindows.length == 1)
+ arrow.hide();
+ else
+ item.add_accessible_state(Atk.StateType.EXPANDABLE);
+ }
+
+ _removeIcon(app) {
+ let index = this.icons.findIndex(icon => {
+ return icon.app == app;
+ });
+ if (index === -1)
+ return;
+
+ this._arrows[index].destroy();
+ this._arrows.splice(index, 1);
+
+ this.icons.splice(index, 1);
+ this.removeItem(index);
+ }
+});
+
+var ThumbnailSwitcher = GObject.registerClass(
+class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
+ _init(windows) {
+ super._init(false);
+
+ this._labels = [];
+ this._thumbnailBins = [];
+ this._clones = [];
+ this._windows = windows;
+
+ for (let i = 0; i < windows.length; i++) {
+ const box = new St.BoxLayout({
+ style_class: 'thumbnail-box',
+ vertical: true,
+ });
+
+ let bin = new St.Bin({ style_class: 'thumbnail' });
+
+ box.add_actor(bin);
+ this._thumbnailBins.push(bin);
+
+ const title = windows[i].get_title();
+ const name = new St.Label({
+ text: title,
+ // St.Label doesn't support text-align
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ this._labels.push(name);
+ box.add_actor(name);
+
+ this.addItem(box, name);
+ }
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ addClones(availHeight) {
+ if (!this._thumbnailBins.length)
+ return;
+ let totalPadding = this._items[0].get_theme_node().get_horizontal_padding() + this._items[0].get_theme_node().get_vertical_padding();
+ totalPadding += this.get_theme_node().get_horizontal_padding() + this.get_theme_node().get_vertical_padding();
+ let [, labelNaturalHeight] = this._labels[0].get_preferred_height(-1);
+ let spacing = this._items[0].child.get_theme_node().get_length('spacing');
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+ let thumbnailSize = THUMBNAIL_DEFAULT_SIZE * scaleFactor;
+
+ availHeight = Math.min(availHeight - labelNaturalHeight - totalPadding - spacing, thumbnailSize);
+ let binHeight = availHeight + this._items[0].get_theme_node().get_vertical_padding() + this.get_theme_node().get_vertical_padding() - spacing;
+ binHeight = Math.min(thumbnailSize, binHeight);
+
+ for (let i = 0; i < this._thumbnailBins.length; i++) {
+ let mutterWindow = this._windows[i].get_compositor_private();
+ if (!mutterWindow)
+ continue;
+
+ let clone = _createWindowClone(mutterWindow, thumbnailSize);
+ this._thumbnailBins[i].set_height(binHeight);
+ this._thumbnailBins[i].add_actor(clone);
+
+ mutterWindow.connectObject('destroy',
+ source => this._removeThumbnail(source, clone), this);
+ this._clones.push(clone);
+ }
+
+ // Make sure we only do this once
+ this._thumbnailBins = [];
+ }
+
+ _removeThumbnail(source, clone) {
+ let index = this._clones.indexOf(clone);
+ if (index === -1)
+ return;
+
+ this._clones.splice(index, 1);
+ this._windows.splice(index, 1);
+ this._labels.splice(index, 1);
+ this.removeItem(index);
+
+ if (this._clones.length > 0)
+ this.highlight(SwitcherPopup.mod(index, this._clones.length));
+ else
+ this.destroy();
+ }
+
+ _onDestroy() {
+ this._clones.forEach(
+ clone => clone?.source.disconnectObject(this));
+ }
+});
+
+var WindowIcon = GObject.registerClass(
+class WindowIcon extends St.BoxLayout {
+ _init(window, mode) {
+ super._init({
+ style_class: 'alt-tab-app',
+ vertical: true,
+ });
+
+ this.window = window;
+
+ this._icon = new St.Widget({ layout_manager: new Clutter.BinLayout() });
+
+ this.add_child(this._icon);
+ this.label = new St.Label({ text: window.get_title() });
+
+ let tracker = Shell.WindowTracker.get_default();
+ this.app = tracker.get_window_app(window);
+
+ let mutterWindow = this.window.get_compositor_private();
+ let size;
+
+ this._icon.destroy_all_children();
+
+ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
+
+ switch (mode) {
+ case AppIconMode.THUMBNAIL_ONLY:
+ size = WINDOW_PREVIEW_SIZE;
+ this._icon.add_actor(_createWindowClone(mutterWindow, size * scaleFactor));
+ break;
+
+ case AppIconMode.BOTH:
+ size = WINDOW_PREVIEW_SIZE;
+ this._icon.add_actor(_createWindowClone(mutterWindow, size * scaleFactor));
+
+ if (this.app) {
+ this._icon.add_actor(this._createAppIcon(this.app,
+ APP_ICON_SIZE_SMALL));
+ }
+ break;
+
+ case AppIconMode.APP_ICON_ONLY:
+ size = APP_ICON_SIZE;
+ this._icon.add_actor(this._createAppIcon(this.app, size));
+ }
+
+ this._icon.set_size(size * scaleFactor, size * scaleFactor);
+ }
+
+ _createAppIcon(app, size) {
+ let appIcon = app
+ ? app.create_icon_texture(size)
+ : new St.Icon({ icon_name: 'icon-missing', icon_size: size });
+ appIcon.x_expand = appIcon.y_expand = true;
+ appIcon.x_align = appIcon.y_align = Clutter.ActorAlign.END;
+
+ return appIcon;
+ }
+});
+
+var WindowSwitcher = GObject.registerClass(
+class WindowSwitcher extends SwitcherPopup.SwitcherList {
+ _init(windows, mode) {
+ super._init(true);
+
+ this._label = new St.Label({
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this.add_actor(this._label);
+
+ this.windows = windows;
+ this.icons = [];
+
+ for (let i = 0; i < windows.length; i++) {
+ let win = windows[i];
+ let icon = new WindowIcon(win, mode);
+
+ this.addItem(icon, icon.label);
+ this.icons.push(icon);
+
+ icon.window.connectObject('unmanaged',
+ window => this._removeWindow(window), this);
+ }
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ this.icons.forEach(
+ icon => icon.window.disconnectObject(this));
+ }
+
+ vfunc_get_preferred_height(forWidth) {
+ let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth);
+
+ let spacing = this.get_theme_node().get_padding(St.Side.BOTTOM);
+ let [labelMin, labelNat] = this._label.get_preferred_height(-1);
+
+ minHeight += labelMin + spacing;
+ natHeight += labelNat + spacing;
+
+ return [minHeight, natHeight];
+ }
+
+ vfunc_allocate(box) {
+ let themeNode = this.get_theme_node();
+ let contentBox = themeNode.get_content_box(box);
+ const labelHeight = this._label.height;
+ const totalLabelHeight =
+ labelHeight + themeNode.get_padding(St.Side.BOTTOM);
+
+ box.y2 -= totalLabelHeight;
+ super.vfunc_allocate(box);
+
+ // Hooking up the parent vfunc will call this.set_allocation() with
+ // the height without the label height, so call it again with the
+ // correct size here.
+ box.y2 += totalLabelHeight;
+ this.set_allocation(box);
+
+ const childBox = new Clutter.ActorBox();
+ childBox.x1 = contentBox.x1;
+ childBox.x2 = contentBox.x2;
+ childBox.y2 = contentBox.y2;
+ childBox.y1 = childBox.y2 - labelHeight;
+ this._label.allocate(childBox);
+ }
+
+ highlight(index, justOutline) {
+ super.highlight(index, justOutline);
+
+ this._label.set_text(index == -1 ? '' : this.icons[index].label.text);
+ }
+
+ _removeWindow(window) {
+ let index = this.icons.findIndex(icon => {
+ return icon.window == window;
+ });
+ if (index === -1)
+ return;
+
+ this.icons.splice(index, 1);
+ this.removeItem(index);
+ }
+});