RaveOS-Gnome/releng/airootfs/usr/share/gnome-shell/extensions/unity-like-appswitcher@gonza.com/extension.js
2025-03-15 15:27:53 +01:00

515 lines
15 KiB
JavaScript

import Atk from "gi://Atk";
import Clutter from "gi://Clutter";
import Meta from "gi://Meta";
import St from "gi://St";
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import GLib from 'gi://GLib';
import Shell from 'gi://Shell';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as AltTab from 'resource:///org/gnome/shell/ui/altTab.js';
import * as SwitcherPopup from 'resource:///org/gnome/shell/ui/switcherPopup.js';
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Utils from './utils.js';
const baseIconSizes = [96, 64, 48, 32, 22];
let injections = {};
let extension = null;
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);
}
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/master/js/ui/altTab.js#L272
function _finish(timestamp) {
this._currentWindow = this._currentWindow < 0 ? 0 : this._currentWindow;
return injections._finish.call(this, timestamp);
}
// https://gitlab.gnome.org/GNOME/gnome-shell/commit/092e1a691d57a3be205100f7d3910534d3c59f84
function _initialSelection(backward, binding) {
if (backward || binding != 'switch-applications'
|| this._items.length == 0 || this._items[0].cachedWindows.length < 2) {
injections._initialSelection.call(this, backward, binding);
return;
}
let ws = global.workspace_manager.get_active_workspace();
let wt = Shell.WindowTracker.get_default();
let tab_list = global.display.get_tab_list(Meta.TabList.NORMAL, ws);
let currentApp = wt.get_window_app(tab_list[0]);
let secondApp = wt.get_window_app(tab_list[1]);
if (currentApp == secondApp) {
this._select(0, 1);
} else {
injections._initialSelection.call(this, backward, binding);
}
}
// from SwitcherPopup.SwitcherList
function highlight2(index, justOutline) {
if (this._items[this._highlighted]) {
this._items[this._highlighted].remove_style_pseudo_class('outlined');
this._items[this._highlighted].remove_style_pseudo_class('selected');
}
if (this._items[index]) {
if (justOutline)
this._items[index].add_style_pseudo_class('outlined');
else
this._items[index].add_style_pseudo_class('selected');
}
this._highlighted = index;
let adjustment = this._scrollView.hscroll.adjustment;
let [value] = adjustment.get_values();
let [absItemX] = this._items[index].get_transformed_position();
let [result_, posX, posY_] = this.transform_stage_point(absItemX, 0);
let [containerWidth] = this.get_transformed_size();
this._scroll(index);
}
function _scroll(index) {
let adjustment = this._scrollView.hscroll.adjustment;
let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();
let n = this._items.length;
let fakeSize = 2;
this._scrollableRight = index !== n - 1;
this._scrollableLeft = index !== 0;
if (upper === pageSize)
return;
let item = this._items[index];
let sizeItem = (item.allocation.x2 - item.allocation.x1);
value = (upper - pageSize + sizeItem) * (index / n);
let maxScrollingAmount = (upper - pageSize);
let percentaje = (index-fakeSize) / (n - 1 - 2*fakeSize);
value = percentaje * maxScrollingAmount;
// special cases
if (index < fakeSize || percentaje <= 0) {
this._scrollableLeft = false;
value = 0;
} else if (index >= n - fakeSize || percentaje >= 1) {
this._scrollableRight = false;
value = maxScrollingAmount;
}
adjustment.ease(value, {
progress_mode: Clutter.AnimationMode.EASE_OUT_EXPO,
duration: 250, // POPUP_SCROLL_TIME,
onComplete: () => {
this.queue_relayout();
},
});
}
function _setIconSize() {
this._iconSize = 96 * 1.5;
for (let i = 0; i < this.icons.length; i++) {
if (this.icons[i].icon != null)
break;
this.icons[i].set_size(this._iconSize);
}
}
function addColours() {
injections.WINDOW_PREVIEW_SIZE = AltTab.WINDOW_PREVIEW_SIZE;
// AltTab.WINDOW_PREVIEW_SIZE = 256;
// injections._setIconSize = AltTab.AppSwitcher.prototype._setIconSize;
// AltTab.AppSwitcher.prototype._setIconSize = _setIconSize;
// injections.highlight = AltTab.AppSwitcher.prototype.highlight;
// AltTab.AppSwitcher.prototype.highlight = highlight;
// injections._addIcon = AltTab.AppSwitcher.prototype._addIcon;
// AltTab.AppSwitcher.prototype._addIcon = _addIcon;
injections._init = AltTab.AppSwitcherPopup.prototype._init;
AltTab.AppSwitcherPopup.prototype._init = _init;
// injections.POPUP_SCROLL_TIME = SwitcherPopup.POPUP_SCROLL_TIME;
// SwitcherPopup.POPUP_SCROLL_TIME = 250;
injections.highlight2 = SwitcherPopup.SwitcherList.prototype.highlight;
SwitcherPopup.SwitcherList.prototype.highlight = highlight2;
injections._scroll = SwitcherPopup.SwitcherList.prototype._scroll;
SwitcherPopup.SwitcherList.prototype._scroll = _scroll;
}
function removeColours() {
// AltTab.WINDOW_PREVIEW_SIZE = injections.WINDOW_PREVIEW_SIZE;
// AltTab.AppSwitcher.prototype._setIconSize = injections._setIconSize;
// AltTab.AppSwitcher.prototype.highlight = injections.highlight;
// AltTab.AppSwitcher.prototype._addIcon = injections._addIcon;
AltTab.AppSwitcherPopup.prototype._init = injections._init;
// SwitcherPopup.POPUP_SCROLL_TIME = injections.POPUP_SCROLL_TIME;
// injections.POPUP_SCROLL_TIME = undefined;
SwitcherPopup.SwitcherList.prototype.highlight = injections.highlight2;
injections.highlight2 = undefined;
SwitcherPopup.SwitcherList.prototype._scroll = injections._scroll; // undefined
injections._scroll = undefined;
}
function setInitialSelection(argument) {
if (!injections._finish) {
injections._finish = AltTab.AppSwitcherPopup.prototype._finish;
AltTab.AppSwitcherPopup.prototype._finish = _finish;
}
if (!injections._initialSelection) {
injections._initialSelection = AltTab.AppSwitcherPopup.prototype._initialSelection;
AltTab.AppSwitcherPopup.prototype._initialSelection = _initialSelection;
}
}
function resetInitialSelection(argument) {
if (injections._finish) {
AltTab.AppSwitcherPopup.prototype._finish = injections._finish;
injections._finish = undefined;
}
if (injections._initialSelection) {
AltTab.AppSwitcherPopup.prototype._initialSelection = injections._initialSelection;
injections._initialSelection = undefined;
}
}
class MyExtension {
constructor(settings) {
this._settings = settings;
this._connectSettings();
this._firstChangeWindowChanged();
}
_connectSettings() {
this._settingsHandlerFirstSwitch = this._settings.connect(
'changed::first-change-window',
this._firstChangeWindowChanged.bind(this)
);
}
_firstChangeWindowChanged() {
this._firstChangeWindow = this._settings.get_boolean('first-change-window');
if (this._firstChangeWindow) {
setInitialSelection();
} else {
resetInitialSelection();
}
}
destroy() {
this._disconnectSettings();
resetInitialSelection();
}
_disconnectSettings() {
this._settings.disconnect(this._settingsHandlerFirstSwitch);
}
}
export default class UnityLikeAppSwitcherExtension extends Extension {
enable() {
const settings = this.getSettings();
extension = new MyExtension(settings);
addColours();
}
disable() {
removeColours();
extension.destroy();
extension = null;
}
}
function _init() {
SwitcherPopup.SwitcherPopup.prototype._init.call(this);
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;
}
const 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 AltTab.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');
}
// mein
let previous = this._items[this._highlighted];
if (previous) {
let st = previous.get_style();
previous.set_style(st.substr(0, st.indexOf(';')+1));
}
let item = this._items[n];
if (item) {
item.set_style(item.get_style() + 'box-shadow: inset 0 0 10px '+ item.colorPalette.lighter + '; border: 2px solid '+ item.colorPalette.lighter + ';');
}
// end mein
// super.highlight(n, justOutline);
highlight2.call(this, n, justOutline);
this._curApp = n;
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);
item.colorPalette = new Utils.DominantColorExtractor(appIcon.app)._getColorPalette();
if (item.colorPalette == null) {
item.colorPalette = {
original: '#888888',
lighter: '#ffffff',
darker: '#000000'
}
}
let hex = item.colorPalette.original;
let rgb = Utils.ColorUtils._hexToRgb(hex);
item.set_style('background: rgba('+ rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.3);');
// item.set_style('background: '+ item.colorPalette.darker);
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(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);
}
});