%PDF- %PDF-
Direktori : /usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/ |
Current File : //usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/altTab.js |
import { Atk, Clutter, Gio, GLib, GObject, Meta, Shell, St } from '../dependencies/gi.js'; import { AltTab, Extension, Main, SwitcherPopup } from '../dependencies/shell.js'; import { baseIconSizes, APP_ICON_HOVER_TIMEOUT } from '../dependencies/unexported/altTab.js'; import { Settings } from '../common.js'; import { TilingWindowManager as Twm } from './tilingWindowManager.js'; /** * Optionally, override GNOME's altTab / appSwitcher to group tileGroups */ export default class AltTabOverride { constructor() { if (Settings.getBoolean(Settings.TILEGROUPS_IN_APP_SWITCHER)) this._overrideNativeAppSwitcher(); this._settingsId = Settings.changed(Settings.TILEGROUPS_IN_APP_SWITCHER, () => { if (Settings.getBoolean(Settings.TILEGROUPS_IN_APP_SWITCHER)) this._overrideNativeAppSwitcher(); else this._restoreNativeAppSwitcher(); }); } destroy() { Settings.disconnect(this._settingsId); this._restoreNativeAppSwitcher(); } _overrideNativeAppSwitcher() { Main.wm.setCustomKeybindingHandler( 'switch-applications', Shell.ActionMode.NORMAL, this._startSwitcher.bind(this) ); } _restoreNativeAppSwitcher() { Main.wm.setCustomKeybindingHandler( 'switch-applications', Shell.ActionMode.NORMAL, Main.wm._startSwitcher.bind(Main.wm) ); } /** * Copy-pasta from windowManager.js. Removed unused stuff... * * @param {*} display - * @param {*} window - * @param {*} binding - */ _startSwitcher(display, window, binding) { if (Main.wm._workspaceSwitcherPopup !== null) Main.wm._workspaceSwitcherPopup.destroy(); const tabPopup = new TilingAppSwitcherPopup(); if (!tabPopup.show(binding.is_reversed(), binding.get_name(), binding.get_mask())) tabPopup.destroy(); } } export const TilingAppSwitcherPopup = GObject.registerClass( class TilingAppSwitcherPopup extends AltTab.AppSwitcherPopup { _init() { SwitcherPopup.SwitcherPopup.prototype._init.call(this); const settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' }); const workspace = settings.get_boolean('current-workspace-only') ? global.workspace_manager.get_active_workspace() : null; const windows = global.display.get_tab_list(Meta.TabList.NORMAL, workspace); this._switcherList = new TilingAppSwitcher(this, windows); this._items = this._switcherList.icons; } // Called when closing an entire app / tileGroup _quitApplication(index) { const item = this._items[index]; if (!item) return; item.cachedWindows.forEach(w => w.delete(global.get_current_time())); item.cachedWindows = []; this._switcherList._removeIcon(item); } // Called when closing a window with the thumbnail switcher // meaning that .cachedWindow of an item was updated via signals _windowRemoved(thumbnailSwitcher, n) { const item = this._items[this._selectedIndex]; if (!item) return; if (item.cachedWindows.length) { const newIndex = Math.min(n, item.cachedWindows.length - 1); this._select(this._selectedIndex, newIndex); } item.updateAppIcons(); } }); export const TilingAppSwitcher = GObject.registerClass( class TilingAppSwitcher extends SwitcherPopup.SwitcherList { _init(altTabPopup, windows) { // Don't make the SwitcherButtons squares since 1 SwitcherButton // may contain multiple AppIcons for a tileGroup. super._init(false); this.icons = []; this._arrows = []; this._apps = []; this._altTabPopup = altTabPopup; this._delayedHighlighted = -1; this._mouseTimeOutId = 0; const winTracker = Shell.WindowTracker.get_default(); let groupedWindows; // Group windows based on their tileGroup, if tileGroup.length > 1. // Otherwise group them based on their respective apps. if (Settings.getBoolean(Settings.TILEGROUPS_IN_APP_SWITCHER)) { groupedWindows = windows.reduce((allGroups, w) => { for (const group of allGroups) { if (w.isTiled && Twm.getTileGroupFor(w).length > 1) { if (Twm.getTileGroupFor(w).includes(group[0])) { group.push(w); return allGroups; } } else if ((!group[0].isTiled || group[0].isTiled && Twm.getTileGroupFor(group[0]).length <= 1) && winTracker.get_window_app(group[0]) === winTracker.get_window_app(w)) { group.push(w); return allGroups; } } const newGroup = [w]; allGroups.push(newGroup); return allGroups; }, []); // Group windows based on apps } else { groupedWindows = windows.reduce((allGroups, w) => { for (const group of allGroups) { if (winTracker.get_window_app(group[0]) === winTracker.get_window_app(w)) { group.push(w); return allGroups; } } const newGroup = [w]; allGroups.push(newGroup); return allGroups; }, []); } // Construct the AppIcons and add them to the popup. groupedWindows.forEach(group => { const item = new AppSwitcherItem(group); item.connect('all-icons-removed', () => this._removeIcon(item)); this._addIcon(item); }); // Listen for the app stop state in case the app got closed outside // of the app switcher along with closing via the app switcher const allApps = windows.map(w => winTracker.get_window_app(w)); this._apps = [...new Set(allApps)]; this._stateChangedIds = this._apps.map(app => app.connect('notify::state', () => { if (app.state !== Shell.AppState.RUNNING) this.icons.forEach(item => item.removeApp(app)); })); this.connect('destroy', this._onDestroy.bind(this)); } _onDestroy() { if (this._mouseTimeOutId !== 0) GLib.source_remove(this._mouseTimeOutId); this._stateChangedIds?.forEach((id, index) => this._apps[index].disconnect(id)); this._stateChangedIds = []; this._apps = []; } _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++) { // eslint-disable-next-line eqeqeq 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_child(arrow); this._arrows.push(arrow); if (appIcon.cachedWindows.length === 1) arrow.hide(); else item.add_accessible_state(Atk.StateType.EXPANDABLE); } _removeIcon(item) { const index = this.icons.findIndex(i => i === item); if (index === -1) return; this._arrows[index].destroy(); this._arrows.splice(index, 1); this.icons.splice(index, 1); this.removeItem(index); } }); /** * Replace AltTab.AppIcon and insert this into the TilingAppSwitcher instead. * This may contain multiple AppIcons to represent a tileGroup with chain icons * between the AppIcons. */ const AppSwitcherItem = GObject.registerClass({ Signals: { 'all-icons-removed': {} } }, class AppSwitcherItem extends St.BoxLayout { _init(windows) { super._init({ vertical: false }); // A tiled window in a tileGroup of length 1, doesn't get a separate // AppSwitcherItem. It gets added to the non-tiled windows' AppSwitcherItem const tileGroup = windows[0].isTiled && Twm.getTileGroupFor(windows[0]); this.isTileGroup = tileGroup && tileGroup.every(w => windows.includes(w)) && tileGroup?.length > 1; this.cachedWindows = windows; this.appIcons = []; this.chainIcons = []; // Compatibility with AltTab.AppIcon this.set_size = size => this.appIcons.forEach(i => i.set_size(size)); this.label = null; this.app = { // Only raise the first window since we split up apps and tileGroups activate_window: (window, timestamp) => { Main.activateWindow(this.cachedWindows[0], timestamp); }, // Listening to the app-stop now happens in the custom _init func // So prevent signal connection. here.. careful in case signal // connection in the future is used for more... connectObject: () => {} }; this.updateAppIcons(); } // Re/Create the AppIcons based on the cached window list updateAppIcons() { this.appIcons.forEach(i => i.destroy()); this.appIcons = []; this.chainIcons.forEach(i => i.destroy()); this.chainIcons = []; const winTracker = Shell.WindowTracker.get_default(); const path = Extension.lookupByURL(import.meta.url) .dir.get_child('media/insert-link-symbolic.svg') .get_path(); const icon = new Gio.FileIcon({ file: Gio.File.new_for_path(path) }); const apps = this.isTileGroup // All apps (even duplicates) ? this.cachedWindows.map(w => winTracker.get_window_app(w)) // Only unique apps : this.cachedWindows.reduce((allApps, w) => { const a = winTracker.get_window_app(w); !allApps.includes(a) && allApps.push(a); return allApps; }, []); apps.forEach((app, idx) => { // AppIcon const appIcon = new AppIcon(app); this.add_child(appIcon); this.appIcons.push(appIcon); // Add chain to the right AppIcon except for the last AppIcon if (idx >= apps.length - 1) return; // Chain const chain = new St.Icon({ gicon: icon, icon_size: 18 }); this.add_child(chain); this.chainIcons.push(chain); }); if (!this.appIcons.length) { this.emit('all-icons-removed'); return; } this.label = this.appIcons[0].label; } // Remove an AppIcon to the corresponding app. // This doesn't update cached window list! removeApp(app) { for (let i = this.appIcons.length - 1; i >= 0; i--) { const appIcon = this.appIcons[i]; if (appIcon.app !== app) continue; this.appIcons.splice(i, 1); appIcon.destroy(); const chain = this.chainIcons.splice(Math.max(0, i - 1), 1)[0]; chain?.destroy(); } if (!this.appIcons.length) this.emit('all-icons-removed'); } }); const AppIcon = GObject.registerClass( class AppIcon extends AltTab.AppIcon { // Don't make the SwitcherButtons squares since 1 SwitcherButton // may contain multiple AppIcons for a tileGroup. vfunc_get_preferred_width() { return this.get_preferred_height(-1); } });