diff --git a/0001-Include-top-icons-in-classic-session.patch b/0001-Include-top-icons-in-classic-session.patch new file mode 100644 index 0000000..da92c63 --- /dev/null +++ b/0001-Include-top-icons-in-classic-session.patch @@ -0,0 +1,32 @@ +From 714feb8e1646b8a3e60bfd95683622a8f125bcf5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 23 Feb 2018 16:56:46 +0100 +Subject: [PATCH] Include top-icons in classic session + +--- + meson.build | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/meson.build b/meson.build +index 6a94e4e2..56a4bb57 100644 +--- a/meson.build ++++ b/meson.build +@@ -34,6 +34,7 @@ classic_extensions = [ + 'apps-menu', + 'places-menu', + 'launch-new-instance', ++ 'top-icons', + 'window-list' + ] + +@@ -44,7 +45,6 @@ default_extensions += [ + 'light-style', + 'screenshot-window-sizer', + 'system-monitor', +- 'top-icons', + 'windowsNavigator', + 'workspace-indicator' + ] +-- +2.44.0 + diff --git a/0001-apps-menu-Set-label_actor-of-Category-items.patch b/0001-apps-menu-Set-label_actor-of-Category-items.patch new file mode 100644 index 0000000..558ec54 --- /dev/null +++ b/0001-apps-menu-Set-label_actor-of-Category-items.patch @@ -0,0 +1,30 @@ +From 4d04a035416867caec9d0aa83f90b4156f017ad0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 17 Mar 2016 17:15:38 +0100 +Subject: [PATCH] apps-menu: Set label_actor of Category items + +Category items are based on BaseMenuItem rather than MenuItem, +so the accessible relationship isn't set up automatically for us. +--- + extensions/apps-menu/extension.js | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/extensions/apps-menu/extension.js b/extensions/apps-menu/extension.js +index c32b61aa..17bcf3b5 100644 +--- a/extensions/apps-menu/extension.js ++++ b/extensions/apps-menu/extension.js +@@ -125,7 +125,10 @@ class CategoryMenuItem extends PopupMenu.PopupBaseMenuItem { + else + name = _('Favorites'); + +- this.add_child(new St.Label({text: name})); ++ const label = new St.Label({text: name}); ++ this.add_child(label); ++ this.actor.label_actor = label; ++ + this.connect('motion-event', this._onMotionEvent.bind(this)); + this.connect('notify::active', this._onActiveChanged.bind(this)); + } +-- +2.44.0 + diff --git a/extra-extensions-0001-Add-top-icons-extension.patch b/extra-extensions-0001-Add-top-icons-extension.patch new file mode 100644 index 0000000..41a4ece --- /dev/null +++ b/extra-extensions-0001-Add-top-icons-extension.patch @@ -0,0 +1,158 @@ +From 56da93fbbe675144ae8eb60daedb9f9d3e93be0f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 20 May 2015 17:44:50 +0200 +Subject: [PATCH 1/5] Add top-icons extension + +--- + extensions/top-icons/extension.js | 91 +++++++++++++++++++++++++++ + extensions/top-icons/meson.build | 9 +++ + extensions/top-icons/metadata.json.in | 10 +++ + meson.build | 1 + + 4 files changed, 111 insertions(+) + create mode 100644 extensions/top-icons/extension.js + create mode 100644 extensions/top-icons/meson.build + create mode 100644 extensions/top-icons/metadata.json.in + +diff --git a/extensions/top-icons/extension.js b/extensions/top-icons/extension.js +new file mode 100644 +index 00000000..c28f1386 +--- /dev/null ++++ b/extensions/top-icons/extension.js +@@ -0,0 +1,91 @@ ++// SPDX-FileCopyrightText: 2018 Adel Gadllah ++// SPDX-FileCopyrightText: 2018 Florian Müllner ++// ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++import Clutter from 'gi://Clutter'; ++import Shell from 'gi://Shell'; ++import St from 'gi://St'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++import {Button as PanelButton} from 'resource:///org/gnome/shell/ui/panelMenu.js'; ++ ++const PANEL_ICON_SIZE = 16; ++ ++const STANDARD_TRAY_ICON_IMPLEMENTATIONS = [ ++ 'bluetooth-applet', ++ 'gnome-sound-applet', ++ 'nm-applet', ++ 'gnome-power-manager', ++ 'keyboard', ++ 'a11y-keyboard', ++ 'kbd-scrolllock', ++ 'kbd-numlock', ++ 'kbd-capslock', ++ 'ibus-ui-gtk', ++]; ++ ++export default class SysTray { ++ constructor() { ++ this._icons = new Map(); ++ this._tray = null; ++ } ++ ++ _onTrayIconAdded(o, icon) { ++ let wmClass = icon.wm_class ? icon.wm_class.toLowerCase() : ''; ++ if (STANDARD_TRAY_ICON_IMPLEMENTATIONS.includes(wmClass)) ++ return; ++ ++ let button = new PanelButton(0.5, null, true); ++ ++ let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; ++ let iconSize = PANEL_ICON_SIZE * scaleFactor; ++ ++ icon.set({ ++ width: iconSize, ++ height: iconSize, ++ x_align: Clutter.ActorAlign.CENTER, ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ ++ let iconBin = new St.Widget({ ++ layout_manager: new Clutter.BinLayout(), ++ style_class: 'system-status-icon', ++ }); ++ iconBin.add_child(icon); ++ button.add_child(iconBin); ++ ++ this._icons.set(icon, button); ++ ++ button.connect('button-release-event', ++ (actor, event) => icon.click(event)); ++ button.connect('key-press-event', ++ (actor, event) => icon.click(event)); ++ ++ const role = `${icon}`; ++ Main.panel.addToStatusArea(role, button); ++ } ++ ++ _onTrayIconRemoved(o, icon) { ++ const button = this._icons.get(icon); ++ button?.destroy(); ++ this._icons.delete(icon); ++ } ++ ++ enable() { ++ this._tray = new Shell.TrayManager(); ++ this._tray.connect('tray-icon-added', ++ this._onTrayIconAdded.bind(this)); ++ this._tray.connect('tray-icon-removed', ++ this._onTrayIconRemoved.bind(this)); ++ this._tray.manage_screen(Main.panel); ++ } ++ ++ disable() { ++ this._icons.forEach(button => button.destroy()); ++ this._icons.clear(); ++ ++ this._tray.unmanage_screen(); ++ this._tray = null; ++ } ++} +diff --git a/extensions/top-icons/meson.build b/extensions/top-icons/meson.build +new file mode 100644 +index 00000000..b30272ad +--- /dev/null ++++ b/extensions/top-icons/meson.build +@@ -0,0 +1,9 @@ ++# SPDX-FileCopyrightText: 2018 Florian Müllner ++# ++# SPDX-License-Identifier: GPL-2.0-or-later ++ ++extension_data += configure_file( ++ input: metadata_name + '.in', ++ output: metadata_name, ++ configuration: metadata_conf ++) +diff --git a/extensions/top-icons/metadata.json.in b/extensions/top-icons/metadata.json.in +new file mode 100644 +index 00000000..1d2e0bc2 +--- /dev/null ++++ b/extensions/top-icons/metadata.json.in +@@ -0,0 +1,10 @@ ++{ ++"extension-id": "@extension_id@", ++"uuid": "@uuid@", ++"settings-schema": "@gschemaname@", ++"gettext-domain": "@gettext_domain@", ++"name": "Top Icons", ++"description": "Show legacy tray icons on top", ++"shell-version": [ "@shell_current@" ], ++"url": "@url@" ++} +diff --git a/meson.build b/meson.build +index 6d403512..4d2ca280 100644 +--- a/meson.build ++++ b/meson.build +@@ -43,6 +43,7 @@ default_extensions += [ + 'light-style', + 'screenshot-window-sizer', + 'system-monitor', ++ 'top-icons', + 'windowsNavigator', + 'workspace-indicator' + ] +-- +2.45.0 + diff --git a/extra-extensions-0002-Add-gesture-inhibitor-extension.patch b/extra-extensions-0002-Add-gesture-inhibitor-extension.patch new file mode 100644 index 0000000..24cdf26 --- /dev/null +++ b/extra-extensions-0002-Add-gesture-inhibitor-extension.patch @@ -0,0 +1,181 @@ +From 4ca791c4d7872cb51ebc6cc90f906a9fcbb5b995 Mon Sep 17 00:00:00 2001 +From: Carlos Garnacho +Date: Thu, 28 Jan 2021 00:06:12 +0100 +Subject: [PATCH 2/5] Add gesture-inhibitor extension + +This extension may disable default GNOME Shell gestures. +--- + extensions/gesture-inhibitor/extension.js | 79 +++++++++++++++++++ + extensions/gesture-inhibitor/meson.build | 8 ++ + extensions/gesture-inhibitor/metadata.json.in | 12 +++ + ...l.extensions.gesture-inhibitor.gschema.xml | 25 ++++++ + meson.build | 1 + + 5 files changed, 125 insertions(+) + create mode 100644 extensions/gesture-inhibitor/extension.js + create mode 100644 extensions/gesture-inhibitor/meson.build + create mode 100644 extensions/gesture-inhibitor/metadata.json.in + create mode 100644 extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml + +diff --git a/extensions/gesture-inhibitor/extension.js b/extensions/gesture-inhibitor/extension.js +new file mode 100644 +index 00000000..872020ba +--- /dev/null ++++ b/extensions/gesture-inhibitor/extension.js +@@ -0,0 +1,79 @@ ++// SPDX-FileCopyrightText: 2021 Carlos Garnacho ++// ++// SPDX-License-Identifier: GPL-2.0-or-later ++// ++ ++import Clutter from 'gi://Clutter'; ++import Gio from 'gi://Gio'; ++import St from 'gi://St'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++ ++import {AppSwitchAction} from 'resource:///org/gnome/shell/ui/windowManager.js'; ++import {EdgeDragAction} from 'resource:///org/gnome/shell/ui/edgeDragAction.js'; ++ ++import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; ++ ++export default class GestureInhibitorExtension extends Extension { ++ constructor(metadata) { ++ super(metadata); ++ ++ let actions = global.stage.get_actions(); ++ ++ actions.forEach(a => { ++ if (a instanceof AppSwitchAction) ++ this._appSwitch = a; ++ else if (a instanceof EdgeDragAction && ++ a._side === St.Side.BOTTOM) ++ this._showOsk = a; ++ else if (a instanceof EdgeDragAction && ++ a._side === St.Side.TOP) ++ this._unfullscreen = a; ++ }); ++ ++ this._map = [ ++ {setting: 'overview', action: Main.overview._swipeTracker}, ++ {setting: 'app-switch', action: this._appSwitch}, ++ {setting: 'show-osk', action: this._showOsk}, ++ {setting: 'unfullscreen', action: this._unfullscreen}, ++ {setting: 'workspace-switch', action: Main.wm._workspaceAnimation._swipeTracker}, ++ ]; ++ ++ this._enabledDesc = Object.getOwnPropertyDescriptor( ++ Clutter.ActorMeta.prototype, 'enabled'); ++ } ++ ++ _overrideEnabledSetter(obj, set) { ++ if (!(obj instanceof Clutter.ActorMeta)) ++ return; ++ ++ const desc = set ++ ? {...this._enabledDesc, set} ++ : {...this._enabledDesc}; ++ Object.defineProperty(obj, 'enabled', desc); ++ } ++ ++ enable() { ++ const settings = this.getSettings(); ++ ++ this._map.forEach(m => { ++ settings.bind(m.setting, m.action, 'enabled', ++ Gio.SettingsBindFlags.DEFAULT); ++ ++ this._overrideEnabledSetter(m.action, function (value) { ++ if (settings.get_boolean(m.setting)) { ++ // eslint-disable-next-line no-invalid-this ++ this.set_enabled(value); ++ } ++ }); ++ }); ++ } ++ ++ disable() { ++ this._map.forEach(m => { ++ Gio.Settings.unbind(m.action, 'enabled'); ++ this._overrideEnabledSetter(m.action); ++ m.action.enabled = true; ++ }); ++ } ++} +diff --git a/extensions/gesture-inhibitor/meson.build b/extensions/gesture-inhibitor/meson.build +new file mode 100644 +index 00000000..fdad5cc8 +--- /dev/null ++++ b/extensions/gesture-inhibitor/meson.build +@@ -0,0 +1,8 @@ ++extension_data += configure_file( ++ input: metadata_name + '.in', ++ output: metadata_name, ++ configuration: metadata_conf ++) ++ ++# extension_sources += files('prefs.js') ++extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') +diff --git a/extensions/gesture-inhibitor/metadata.json.in b/extensions/gesture-inhibitor/metadata.json.in +new file mode 100644 +index 00000000..37d6a117 +--- /dev/null ++++ b/extensions/gesture-inhibitor/metadata.json.in +@@ -0,0 +1,12 @@ ++{ ++ "uuid": "@uuid@", ++ "extension-id": "@extension_id@", ++ "settings-schema": "@gschemaname@", ++ "gettext-domain": "@gettext_domain@", ++ "name": "Gesture Inhibitor", ++ "description": "Makes touchscreen gestures optional.", ++ "shell-version": [ "@shell_current@" ], ++ "original-authors": [ "cgarnach@redhat.com" ], ++ "url": "@url@" ++} ++ +diff --git a/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml b/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml +new file mode 100644 +index 00000000..b06d027a +--- /dev/null ++++ b/extensions/gesture-inhibitor/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml +@@ -0,0 +1,25 @@ ++ ++ ++ ++ true ++ Show OSK gesture ++ ++ ++ true ++ Show Overview gesture ++ ++ ++ true ++ Application switch gesture ++ ++ ++ true ++ Workspace switch gesture ++ ++ ++ true ++ Unfullscreen gesture ++ ++ ++ ++ +diff --git a/meson.build b/meson.build +index 4d2ca280..c78d0cc6 100644 +--- a/meson.build ++++ b/meson.build +@@ -51,6 +51,7 @@ default_extensions += [ + all_extensions = default_extensions + all_extensions += [ + 'auto-move-windows', ++ 'gesture-inhibitor', + 'native-window-placement', + 'user-theme' + ] +-- +2.45.0 + diff --git a/extra-extensions-0003-Add-classification-banner.patch b/extra-extensions-0003-Add-classification-banner.patch new file mode 100644 index 0000000..eae17f0 --- /dev/null +++ b/extra-extensions-0003-Add-classification-banner.patch @@ -0,0 +1,688 @@ +From fc39411ecf0431ecc39581bace4217a55ab028e9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 2 Dec 2021 19:39:50 +0100 +Subject: [PATCH 3/5] Add classification-banner + +--- + extensions/classification-banner/adwShim.js | 202 ++++++++++++++++++ + extensions/classification-banner/extension.js | 162 ++++++++++++++ + extensions/classification-banner/meson.build | 9 + + .../classification-banner/metadata.json.in | 11 + + ...tensions.classification-banner.gschema.xml | 29 +++ + extensions/classification-banner/prefs.js | 192 +++++++++++++++++ + .../classification-banner/stylesheet.css | 3 + + meson.build | 1 + + 8 files changed, 609 insertions(+) + create mode 100644 extensions/classification-banner/adwShim.js + create mode 100644 extensions/classification-banner/extension.js + create mode 100644 extensions/classification-banner/meson.build + create mode 100644 extensions/classification-banner/metadata.json.in + create mode 100644 extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml + create mode 100644 extensions/classification-banner/prefs.js + create mode 100644 extensions/classification-banner/stylesheet.css + +diff --git a/extensions/classification-banner/adwShim.js b/extensions/classification-banner/adwShim.js +new file mode 100644 +index 00000000..46a8afca +--- /dev/null ++++ b/extensions/classification-banner/adwShim.js +@@ -0,0 +1,202 @@ ++/* exported init PreferencesPage PreferencesGroup ActionRow ComboRow */ ++const { Gio, GObject, Gtk } = imports.gi; ++ ++function init() { ++} ++ ++var PreferencesGroup = GObject.registerClass( ++class PreferencesGroup extends Gtk.Widget { ++ _init(params) { ++ super._init({ ++ ...params, ++ layout_manager: new Gtk.BinLayout(), ++ }); ++ ++ this._listBox = new Gtk.ListBox({ ++ css_classes: ['rich-list'], ++ show_separators: true, ++ selection_mode: Gtk.SelectionMode.NONE, ++ }); ++ ++ const frame = new Gtk.Frame({ child: this._listBox }); ++ frame.set_parent(this); ++ } ++ ++ add(child) { ++ this._listBox.append(child); ++ } ++}); ++ ++var PreferencesPage = GObject.registerClass( ++class PreferencesPage extends Gtk.Widget { ++ _init(params) { ++ super._init({ ++ ...params, ++ layout_manager: new Gtk.BinLayout(), ++ }); ++ ++ const scrolledWindow = new Gtk.ScrolledWindow({ ++ hscrollbar_policy: Gtk.PolicyType.NEVER, ++ }); ++ scrolledWindow.set_parent(this); ++ ++ this._box = new Gtk.Box({ ++ orientation: Gtk.Orientation.VERTICAL, ++ halign: Gtk.Align.CENTER, ++ spacing: 24, ++ margin_top: 24, ++ margin_bottom: 24, ++ margin_start: 12, ++ margin_end: 12, ++ }); ++ scrolledWindow.set_child(this._box); ++ ++ const provider = new Gtk.CssProvider(); ++ provider.load_from_data('* { min-width: 500px; }'); ++ this._box.get_style_context().add_provider(provider, ++ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); ++ } ++ ++ add(child) { ++ this._box.append(child); ++ } ++}); ++ ++var ActionRow = GObject.registerClass({ ++ Properties: { ++ 'activatable-widget': GObject.ParamSpec.object( ++ 'activatable-widget', 'activatable-widget', 'activatable-widget', ++ GObject.ParamFlags.READWRITE, ++ Gtk.Widget), ++ 'title': GObject.ParamSpec.string( ++ 'title', 'title', 'title', ++ GObject.ParamFlags.READWRITE, ++ null), ++ }, ++}, class ActionRow extends Gtk.ListBoxRow { ++ _init(params) { ++ super._init(params); ++ ++ const box = new Gtk.Box({ ++ spacing: 12, ++ }); ++ this.set_child(box); ++ ++ this._prefixes = new Gtk.Box({ ++ spacing: 12, ++ visible: false, ++ }); ++ box.append(this._prefixes); ++ ++ this._title = new Gtk.Label({ ++ css_classes: ['title'], ++ hexpand: true, ++ xalign: 0, ++ }); ++ box.append(this._title); ++ ++ this._suffixes = new Gtk.Box({ ++ spacing: 12, ++ visible: false, ++ }); ++ box.append(this._suffixes); ++ ++ this.bind_property('title', ++ this._title, 'label', ++ GObject.BindingFlags.SYNC_CREATE); ++ ++ this.connect('notify::parent', () => { ++ const parent = this.get_parent(); ++ parent?.connect('row-activated', (list, row) => { ++ if (row === this) ++ this.activate(); ++ }); ++ }); ++ } ++ ++ vfunc_activate() { ++ this.activatable_widget?.mnemonic_activate(false); ++ } ++ ++ activate() { ++ this.vfunc_activate(); ++ } ++ ++ add_prefix(child) { ++ this._prefixes.append(child); ++ this._prefixes.show(); ++ } ++ ++ add_suffix(child) { ++ this._suffixes.append(child); ++ this._suffixes.show(); ++ } ++}); ++ ++var ComboRow = GObject.registerClass({ ++ Properties: { ++ 'selected-item': GObject.ParamSpec.object( ++ 'selected-item', 'selected-item', 'selected-item', ++ GObject.ParamFlags.READABLE, ++ GObject.Object), ++ 'model': GObject.ParamSpec.object( ++ 'model', 'model', 'model', ++ GObject.ParamFlags.READWRITE, ++ Gio.ListModel), ++ 'list-factory': GObject.ParamSpec.object( ++ 'list-factory', 'list-factory', 'list-factory', ++ GObject.ParamFlags.READWRITE, ++ Gtk.ListItemFactory), ++ 'expression': Gtk.param_spec_expression( ++ 'expression', 'expression', 'expression', ++ GObject.ParamFlags.READWRITE), ++ }, ++}, class ComboRow extends ActionRow { ++ _init(params) { ++ super._init({ ++ ...params, ++ activatable: true, ++ }); ++ ++ const box = new Gtk.Box({ ++ valign: Gtk.Align.CENTER, ++ }); ++ box.append(new Gtk.Image({ ++ icon_name: 'pan-down-symbolic', ++ })); ++ this.add_suffix(box); ++ ++ this._popover = new Gtk.Popover(); ++ this._popover.set_parent(box); ++ ++ this._selection = new Gtk.SingleSelection(); ++ this._selected = -1; ++ ++ this._listView = new Gtk.ListView({ ++ model: this._selection, ++ single_click_activate: true, ++ }); ++ this._popover.set_child(this._listView); ++ ++ this._listView.connect('activate', (view, pos) => { ++ this._selected = pos; ++ this.notify('selected-item'); ++ this._popover.popdown(); ++ }); ++ ++ this.bind_property('model', ++ this._selection, 'model', ++ GObject.BindingFlags.SYNC_CREATE); ++ this.bind_property('list-factory', ++ this._listView, 'factory', ++ GObject.BindingFlags.SYNC_CREATE); ++ } ++ ++ get selected_item() { ++ return this._selection.selected_item; ++ } ++ ++ vfunc_activate() { ++ this._popover.popup(); ++ } ++}); +diff --git a/extensions/classification-banner/extension.js b/extensions/classification-banner/extension.js +new file mode 100644 +index 00000000..e872d57d +--- /dev/null ++++ b/extensions/classification-banner/extension.js +@@ -0,0 +1,162 @@ ++// SPDX-FileCopyrightText: 2021 Florian Müllner ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++import Clutter from 'gi://Clutter'; ++import Gio from 'gi://Gio'; ++import GLib from 'gi://GLib'; ++import GObject from 'gi://GObject'; ++import Shell from 'gi://Shell'; ++import St from 'gi://St'; ++ ++import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++import {MonitorConstraint} from 'resource:///org/gnome/shell/ui/layout.js'; ++ ++class ClassificationBanner extends Clutter.Actor { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ #topBanner; ++ #bottomBanner; ++ #monitorConstraint; ++ #settings; ++ ++ constructor(index, settings) { ++ const constraint = new MonitorConstraint({index}); ++ super({ ++ layout_manager: new Clutter.BinLayout(), ++ constraints: constraint, ++ }); ++ this.#monitorConstraint = constraint; ++ ++ Shell.util_set_hidden_from_pick(this, true); ++ ++ this.#settings = settings; ++ ++ this.#topBanner = new St.BoxLayout({ ++ style_class: 'classification-banner', ++ x_expand: true, ++ y_expand: true, ++ y_align: Clutter.ActorAlign.START, ++ }); ++ this.add_child(this.#topBanner); ++ this.#settings.bind('top-banner', ++ this.#topBanner, 'visible', ++ Gio.SettingsBindFlags.GET); ++ ++ this.#bottomBanner = new St.BoxLayout({ ++ style_class: 'classification-banner', ++ x_expand: true, ++ y_expand: true, ++ y_align: Clutter.ActorAlign.END, ++ }); ++ this.add_child(this.#bottomBanner); ++ this.#settings.bind('bottom-banner', ++ this.#bottomBanner, 'visible', ++ Gio.SettingsBindFlags.GET); ++ ++ for (const banner of [this.#topBanner, this.#bottomBanner]) { ++ const label = new St.Label({ ++ style_class: 'classification-message', ++ x_align: Clutter.ActorAlign.CENTER, ++ x_expand: true, ++ }); ++ banner.add_child(label); ++ ++ this.#settings.bind('message', ++ label, 'text', ++ Gio.SettingsBindFlags.GET); ++ } ++ ++ const hostLabel = new St.Label({ ++ style_class: 'classification-system-info', ++ text: GLib.get_host_name(), ++ }); ++ this.#topBanner.insert_child_at_index(hostLabel, 0); ++ this.#settings.bind('system-info', ++ hostLabel, 'visible', ++ Gio.SettingsBindFlags.GET); ++ ++ const userLabel = new St.Label({ ++ style_class: 'classification-system-info', ++ text: GLib.get_user_name(), ++ }); ++ this.#topBanner.add_child(userLabel); ++ this.#settings.bind('system-info', ++ userLabel, 'visible', ++ Gio.SettingsBindFlags.GET); ++ ++ global.display.connectObject('in-fullscreen-changed', ++ () => this.#updateMonitorConstraint(), this); ++ this.#updateMonitorConstraint(); ++ ++ this.#settings.connectObject( ++ 'changed::color', () => this.#updateStyles(), ++ 'changed::background-color', () => this.#updateStyles(), ++ this); ++ this.#updateStyles(); ++ } ++ ++ #getColorSetting(key) { ++ const str = this.#settings.get_string(key); ++ const [valid, color] = Clutter.Color.from_string(str); ++ if (!valid) ++ return ''; ++ const {red, green, blue, alpha} = color; ++ return `${key}: rgba(${red},${green},${blue},${alpha / 255});`; ++ } ++ ++ #updateMonitorConstraint() { ++ const {index} = this.#monitorConstraint; ++ this.#monitorConstraint.work_area = ++ !global.display.get_monitor_in_fullscreen(index); ++ } ++ ++ #updateStyles() { ++ const bgStyle = this.#getColorSetting('background-color'); ++ const fgStyle = this.#getColorSetting('color'); ++ const style = `${bgStyle}${fgStyle}`; ++ this.#topBanner.set({style}); ++ this.#bottomBanner.set({style}); ++ } ++} ++ ++export default class ClassificationBannerExtension extends Extension { ++ #banners = []; ++ ++ #updateMonitors() { ++ const {monitors, panelBox, primaryIndex} = Main.layoutManager; ++ if (monitors.length !== this.#banners.length) { ++ this.#clearBanners(); ++ ++ const settings = this.getSettings(); ++ for (let i = 0; i < monitors.length; i++) { ++ const banner = new ClassificationBanner(i, settings); ++ Main.uiGroup.add_child(banner); ++ this.#banners.push(banner); ++ } ++ } ++ ++ const primaryBanner = this.#banners[primaryIndex]; ++ if (primaryBanner) ++ Main.uiGroup.set_child_below_sibling(primaryBanner, panelBox); ++ } ++ ++ #clearBanners() { ++ this.#banners.forEach(b => b.destroy()); ++ this.#banners = []; ++ } ++ ++ enable() { ++ Main.layoutManager.connectObject('monitors-changed', ++ () => this.#updateMonitors(), this); ++ this.#updateMonitors(); ++ } ++ ++ disable() { ++ Main.layoutManager.disconnectObject(this); ++ this.#clearBanners(); ++ } ++} +diff --git a/extensions/classification-banner/meson.build b/extensions/classification-banner/meson.build +new file mode 100644 +index 00000000..aa943741 +--- /dev/null ++++ b/extensions/classification-banner/meson.build +@@ -0,0 +1,9 @@ ++extension_data += configure_file( ++ input: metadata_name + '.in', ++ output: metadata_name, ++ configuration: metadata_conf ++) ++extension_data += files('stylesheet.css') ++ ++extension_sources += files('prefs.js') ++extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') +diff --git a/extensions/classification-banner/metadata.json.in b/extensions/classification-banner/metadata.json.in +new file mode 100644 +index 00000000..f93b1a2d +--- /dev/null ++++ b/extensions/classification-banner/metadata.json.in +@@ -0,0 +1,11 @@ ++{ ++"extension-id": "@extension_id@", ++"uuid": "@uuid@", ++"settings-schema": "@gschemaname@", ++"gettext-domain": "@gettext_domain@", ++"name": "Classification Banner", ++"description": "Display classification level banner", ++"shell-version": [ "@shell_current@" ], ++"session-modes": [ "gdm", "unlock-dialog", "user" ], ++"url": "@url@" ++} +diff --git a/extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml b/extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml +new file mode 100644 +index 00000000..0314ef60 +--- /dev/null ++++ b/extensions/classification-banner/org.gnome.shell.extensions.classification-banner.gschema.xml +@@ -0,0 +1,29 @@ ++ ++ ++ ++ true ++ Show a banner at the top ++ ++ ++ true ++ Show a banner at the bottom ++ ++ ++ "UNCLASSIFIED" ++ classification message ++ ++ ++ "#fff" ++ text color ++ ++ ++ "rgba(0,122,51,0.75)" ++ background color ++ ++ ++ false ++ Include system info in top banner ++ ++ ++ +diff --git a/extensions/classification-banner/prefs.js b/extensions/classification-banner/prefs.js +new file mode 100644 +index 00000000..dc73ddae +--- /dev/null ++++ b/extensions/classification-banner/prefs.js +@@ -0,0 +1,192 @@ ++import Adw from 'gi://Adw'; ++import Gdk from 'gi://Gdk'; ++import Gio from 'gi://Gio'; ++import GObject from 'gi://GObject'; ++import Gtk from 'gi://Gtk'; ++ ++import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; ++ ++class GenericPrefs extends Adw.PreferencesGroup { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ #actionGroup = new Gio.SimpleActionGroup(); ++ #settings; ++ ++ constructor(settings) { ++ super(); ++ ++ this.#settings = settings; ++ this.insert_action_group('options', this.#actionGroup); ++ ++ this.#actionGroup.add_action(settings.create_action('top-banner')); ++ this.#actionGroup.add_action(settings.create_action('bottom-banner')); ++ this.#actionGroup.add_action(settings.create_action('system-info')); ++ ++ this.add(new Adw.SwitchRow({ ++ title: _('Top Banner'), ++ action_name: 'options.top-banner', ++ })); ++ ++ this.add(new Adw.SwitchRow({ ++ title: _('Bottom Banner'), ++ action_name: 'options.bottom-banner', ++ })); ++ ++ this.add(new Adw.SwitchRow({ ++ title: _('System Info'), ++ action_name: 'options.system-info', ++ })); ++ } ++} ++ ++class BannerPreset extends GObject.Object { ++ static [GObject.properties] = { ++ 'message': GObject.ParamSpec.string( ++ 'message', 'message', 'message', ++ GObject.ParamFlags.READWRITE, ++ null), ++ 'color': GObject.ParamSpec.string( ++ 'color', 'color', 'color', ++ GObject.ParamFlags.READWRITE, ++ null), ++ 'background-color': GObject.ParamSpec.string( ++ 'background-color', 'background-color', 'background-color', ++ GObject.ParamFlags.READWRITE, ++ null), ++ }; ++ ++ static { ++ GObject.registerClass(this); ++ } ++} ++ ++class AppearancePrefs extends Adw.PreferencesGroup { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ #settings; ++ ++ constructor(settings) { ++ super(); ++ ++ this.#settings = settings; ++ ++ const model = new Gio.ListStore({item_type: BannerPreset.$gtype}); ++ model.append(new BannerPreset({ ++ message: 'UNCLASSIFIED', ++ color: '#fff', ++ background_color: 'rgba(0, 122, 51, 0.75)', ++ })); ++ model.append(new BannerPreset({ ++ message: 'CONFIDENTIAL', ++ color: '#fff', ++ background_color: 'rgba(0, 51, 160, 0.75)', ++ })); ++ model.append(new BannerPreset({ ++ message: 'SECRET', ++ color: '#fff', ++ background_color: 'rgba(200, 16, 46, 0.75)', ++ })); ++ model.append(new BannerPreset({ ++ message: 'TOP SECRET', ++ color: '#fff', ++ background_color: 'rgba(255, 103, 31, 0.75)', ++ })); ++ model.append(new BannerPreset({ ++ message: 'TOP SECRET//SCI', ++ color: '#000', ++ background_color: 'rgba(247, 234, 72, 0.75)', ++ })); ++ ++ let row, activatableWidget; ++ row = this.#createPresetsRow(model); ++ row.connect('notify::selected-item', comboRow => { ++ const {message, color, backgroundColor} = comboRow.selected_item; ++ this.#settings.set_string('message', message); ++ this.#settings.set_string('color', color); ++ this.#settings.set_string('background-color', backgroundColor); ++ }); ++ this.add(row); ++ ++ activatableWidget = new Gtk.Entry({ ++ valign: Gtk.Align.CENTER, ++ }); ++ this.#settings.bind('message', ++ activatableWidget, 'text', ++ Gio.SettingsBindFlags.DEFAULT); ++ row = new Adw.ActionRow({title: _('Message'), activatableWidget}); ++ row.add_suffix(activatableWidget); ++ this.add(row); ++ ++ activatableWidget = this.#createColorButton('background-color', { ++ use_alpha: true, ++ }); ++ row = new Adw.ActionRow({title: _('Background color'), activatableWidget}); ++ row.add_suffix(activatableWidget); ++ this.add(row); ++ ++ activatableWidget = this.#createColorButton('color'); ++ row = new Adw.ActionRow({title: _('Text color'), activatableWidget}); ++ row.add_suffix(activatableWidget); ++ this.add(row); ++ } ++ ++ #createPresetsRow(model) { ++ const listFactory = new Gtk.SignalListItemFactory(); ++ listFactory.connect('setup', ++ (f, item) => item.set_child(new Gtk.Label())); ++ listFactory.connect('bind', (f, listItem) => { ++ const {child, item} = listItem; ++ ++ const provider = new Gtk.CssProvider(); ++ provider.load_from_data(`* { ++ border-radius: 99px; ++ padding: 6px; ++ color: ${item.color}; ++ background-color: ${item.background_color}; ++ }`, -1); ++ child.get_style_context().add_provider(provider, ++ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); ++ child.label = item.message; ++ }); ++ ++ return new Adw.ComboRow({ ++ title: _('Presets'), ++ model, ++ listFactory, ++ expression: Gtk.ConstantExpression.new_for_value(''), ++ }); ++ } ++ ++ #createColorButton(key, params = {}) { ++ const rgba = new Gdk.RGBA(); ++ rgba.parse(this.#settings.get_string(key)); ++ ++ const button = new Gtk.ColorButton({ ++ ...params, ++ rgba, ++ valign: Gtk.Align.CENTER, ++ }); ++ this.#settings.connect(`changed::${key}`, () => { ++ const newRgba = new Gdk.RGBA(); ++ newRgba.parse(this.#settings.get_string(key)); ++ if (!newRgba.equal(button.rgba)) ++ button.set({rgba: newRgba}); ++ }); ++ button.connect('notify::rgba', ++ () => this.#settings.set_string(key, button.rgba.to_string())); ++ return button; ++ } ++} ++ ++export default class ClassificationPrefs extends ExtensionPreferences { ++ getPreferencesWidget() { ++ const page = new Adw.PreferencesPage(); ++ page.add(new AppearancePrefs(this.getSettings())); ++ page.add(new GenericPrefs(this.getSettings())); ++ return page; ++ } ++} +diff --git a/extensions/classification-banner/stylesheet.css b/extensions/classification-banner/stylesheet.css +new file mode 100644 +index 00000000..fb6a697e +--- /dev/null ++++ b/extensions/classification-banner/stylesheet.css +@@ -0,0 +1,3 @@ ++.classification-system-info { padding: 0 24px; } ++.classification-message { font-weight: bold; } ++.classification-banner { font-size: 0.9em; } +diff --git a/meson.build b/meson.build +index c78d0cc6..b3dac8de 100644 +--- a/meson.build ++++ b/meson.build +@@ -51,6 +51,7 @@ default_extensions += [ + all_extensions = default_extensions + all_extensions += [ + 'auto-move-windows', ++ 'classification-banner', + 'gesture-inhibitor', + 'native-window-placement', + 'user-theme' +-- +2.45.0 + diff --git a/extra-extensions-0004-Add-heads-up-display.patch b/extra-extensions-0004-Add-heads-up-display.patch new file mode 100644 index 0000000..587f4db --- /dev/null +++ b/extra-extensions-0004-Add-heads-up-display.patch @@ -0,0 +1,878 @@ +From 94e0261560490bdfe37df5d9f5ff1cbac9533d6a Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Tue, 24 Aug 2021 15:03:57 -0400 +Subject: [PATCH 4/5] Add heads-up-display + +--- + extensions/heads-up-display/extension.js | 404 ++++++++++++++++++ + extensions/heads-up-display/headsUpMessage.js | 166 +++++++ + extensions/heads-up-display/meson.build | 13 + + extensions/heads-up-display/metadata.json.in | 12 + + ...ll.extensions.heads-up-display.gschema.xml | 60 +++ + extensions/heads-up-display/prefs.js | 92 ++++ + extensions/heads-up-display/stylesheet.css | 38 ++ + meson.build | 1 + + po/POTFILES.in | 1 + + 9 files changed, 787 insertions(+) + create mode 100644 extensions/heads-up-display/extension.js + create mode 100644 extensions/heads-up-display/headsUpMessage.js + create mode 100644 extensions/heads-up-display/meson.build + create mode 100644 extensions/heads-up-display/metadata.json.in + create mode 100644 extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml + create mode 100644 extensions/heads-up-display/prefs.js + create mode 100644 extensions/heads-up-display/stylesheet.css + +diff --git a/extensions/heads-up-display/extension.js b/extensions/heads-up-display/extension.js +new file mode 100644 +index 00000000..a71b5925 +--- /dev/null ++++ b/extensions/heads-up-display/extension.js +@@ -0,0 +1,404 @@ ++// SPDX-FileCopyrightText: 2021 Ray Strode ++// ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++import GObject from 'gi://GObject'; ++import Meta from 'gi://Meta'; ++import Mtk from 'gi://Mtk'; ++ ++import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++import {MonitorConstraint} from 'resource:///org/gnome/shell/ui/layout.js'; ++ ++import {HeadsUpMessage} from './headsUpMessage.js'; ++ ++var HeadsUpConstraint = GObject.registerClass({ ++ Properties: { ++ 'offset': GObject.ParamSpec.int( ++ 'offset', 'Offset', 'offset', ++ GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, ++ -1, 0, -1), ++ 'active': GObject.ParamSpec.boolean( ++ 'active', 'Active', 'active', ++ GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, ++ true), ++ }, ++}, class HeadsUpConstraint extends MonitorConstraint { ++ constructor(props) { ++ super(props); ++ this._offset = 0; ++ this._active = true; ++ } ++ ++ get offset() { ++ return this._offset; ++ } ++ ++ set offset(o) { ++ this._offset = o; ++ } ++ ++ get active() { ++ return this._active; ++ } ++ ++ set active(a) { ++ this._active = a; ++ } ++ ++ vfunc_update_allocation(actor, actorBox) { ++ if (!Main.layoutManager.primaryMonitor) ++ return; ++ ++ if (!this.active) ++ return; ++ ++ if (actor.has_allocation()) ++ return; ++ ++ const workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex); ++ actorBox.init_rect(workArea.x, workArea.y + this.offset, workArea.width, workArea.height - this.offset); ++ } ++}); ++ ++export default class HeadsUpDisplayExtension extends Extension { ++ enable() { ++ this._settings = this.getSettings('org.gnome.shell.extensions.heads-up-display'); ++ this._settings.connectObject('changed', ++ () => this._updateMessage(), this); ++ ++ this._idleMonitor = global.backend.get_core_idle_monitor(); ++ this._messageInhibitedUntilIdle = false; ++ global.window_manager.connectObject('map', ++ this._onWindowMap.bind(this), this); ++ ++ if (Main.layoutManager._startingUp) ++ Main.layoutManager.connectObject('startup-complete', () => this._onStartupComplete(), this); ++ else ++ this._onStartupComplete(); ++ } ++ ++ disable() { ++ this._dismissMessage(); ++ ++ this._stopWatchingForIdle(); ++ ++ Main.sessionMode.disconnectObject(this); ++ Main.overview.disconnectObject(this); ++ Main.layoutManager.panelBox.disconnectObject(this); ++ Main.layoutManager.disconnectObject(this); ++ global.window_manager.disconnectObject(this); ++ ++ if (this._screenShieldVisibleId) { ++ Main.screenShield._dialog._clock.disconnect(this._screenShieldVisibleId); ++ this._screenShieldVisibleId = 0; ++ } ++ ++ this._settings.disconnectObject(this); ++ delete this._settings; ++ } ++ ++ _onWindowMap(shellwm, actor) { ++ const windowObject = actor.meta_window; ++ const windowType = windowObject.get_window_type(); ++ ++ if (windowType !== Meta.WindowType.NORMAL) ++ return; ++ ++ if (!this._message || !this._message.visible) ++ return; ++ ++ const messageRect = new Mtk.Rectangle({ ++ x: this._message.x, ++ y: this._message.y, ++ width: this._message.width, ++ height: this._message.height, ++ }); ++ const windowRect = windowObject.get_frame_rect(); ++ ++ if (windowRect.intersect(messageRect)) ++ windowObject.move_frame(false, windowRect.x, this._message.y + this._message.height); ++ } ++ ++ _onStartupComplete() { ++ Main.overview.connectObject( ++ 'showing', () => this._updateMessage(), ++ 'hidden', () => this._updateMessage(), ++ this); ++ Main.layoutManager.panelBox.connectObject('notify::visible', ++ () => this._updateMessage(), this); ++ Main.sessionMode.connectObject('updated', ++ () => this._onSessionModeUpdated(), this); ++ ++ this._updateMessage(); ++ } ++ ++ _onSessionModeUpdated() { ++ if (!Main.sessionMode.hasWindows) ++ this._messageInhibitedUntilIdle = false; ++ ++ const dialog = Main.screenShield._dialog; ++ if (!Main.sessionMode.isGreeter && dialog && !this._screenShieldVisibleId) { ++ this._screenShieldVisibleId = dialog._clock.connect('notify::visible', this._updateMessage.bind(this)); ++ this._screenShieldDestroyId = dialog._clock.connect('destroy', () => { ++ this._screenShieldVisibleId = 0; ++ this._screenShieldDestroyId = 0; ++ }); ++ } ++ this._updateMessage(); ++ } ++ ++ _stopWatchingForIdle() { ++ if (this._idleWatchId) { ++ this._idleMonitor.remove_watch(this._idleWatchId); ++ this._idleWatchId = 0; ++ } ++ ++ if (this._idleTimeoutChangedId) { ++ this._settings.disconnect(this._idleTimeoutChangedId); ++ this._idleTimeoutChangedId = 0; ++ } ++ } ++ ++ _onIdleTimeoutChanged() { ++ this._stopWatchingForIdle(); ++ this._messageInhibitedUntilIdle = false; ++ } ++ ++ _onUserIdle() { ++ this._messageInhibitedUntilIdle = false; ++ this._updateMessage(); ++ } ++ ++ _watchForIdle() { ++ this._stopWatchingForIdle(); ++ ++ const idleTimeout = this._settings.get_uint('idle-timeout'); ++ ++ this._idleTimeoutChangedId = ++ this._settings.connect('changed::idle-timeout', ++ this._onIdleTimeoutChanged.bind(this)); ++ this._idleWatchId = this._idleMonitor.add_idle_watch(idleTimeout * 1000, ++ this._onUserIdle.bind(this)); ++ } ++ ++ _updateMessage() { ++ if (this._messageInhibitedUntilIdle) { ++ if (this._message) ++ this._dismissMessage(); ++ return; ++ } ++ ++ this._stopWatchingForIdle(); ++ ++ if (Main.sessionMode.hasOverview && Main.overview.visible) { ++ this._dismissMessage(); ++ return; ++ } ++ ++ if (!Main.layoutManager.panelBox.visible) { ++ this._dismissMessage(); ++ return; ++ } ++ ++ let supportedModes = []; ++ ++ if (this._settings.get_boolean('show-when-unlocked')) ++ supportedModes.push('user'); ++ ++ if (this._settings.get_boolean('show-when-unlocking') || ++ this._settings.get_boolean('show-when-locked')) ++ supportedModes.push('unlock-dialog'); ++ ++ if (this._settings.get_boolean('show-on-login-screen')) ++ supportedModes.push('gdm'); ++ ++ if (!supportedModes.includes(Main.sessionMode.currentMode) && ++ !supportedModes.includes(Main.sessionMode.parentMode)) { ++ this._dismissMessage(); ++ return; ++ } ++ ++ if (Main.sessionMode.currentMode === 'unlock-dialog') { ++ const dialog = Main.screenShield._dialog; ++ if (!this._settings.get_boolean('show-when-locked')) { ++ if (dialog._clock.visible) { ++ this._dismissMessage(); ++ return; ++ } ++ } ++ ++ if (!this._settings.get_boolean('show-when-unlocking')) { ++ if (!dialog._clock.visible) { ++ this._dismissMessage(); ++ return; ++ } ++ } ++ } ++ ++ const heading = this._settings.get_string('message-heading'); ++ const body = this._settings.get_string('message-body'); ++ ++ if (!heading && !body) { ++ this._dismissMessage(); ++ return; ++ } ++ ++ if (!this._message) { ++ this._message = new HeadsUpMessage(heading, body); ++ ++ this._message.connect('notify::allocation', this._adaptSessionForMessage.bind(this)); ++ this._message.connect('clicked', this._onMessageClicked.bind(this)); ++ } ++ ++ this._message.reactive = true; ++ this._message.track_hover = true; ++ ++ this._message.setHeading(heading); ++ this._message.setBody(body); ++ ++ if (!Main.sessionMode.hasWindows) { ++ this._message.track_hover = false; ++ this._message.reactive = false; ++ } ++ } ++ ++ _onMessageClicked() { ++ if (!Main.sessionMode.hasWindows) ++ return; ++ ++ this._watchForIdle(); ++ this._messageInhibitedUntilIdle = true; ++ this._updateMessage(); ++ } ++ ++ _dismissMessage() { ++ if (!this._message) ++ return; ++ ++ this._message.visible = false; ++ this._message.destroy(); ++ this._message = null; ++ this._resetMessageTray(); ++ this._resetLoginDialog(); ++ } ++ ++ _resetMessageTray() { ++ if (!Main.messageTray) ++ return; ++ ++ if (this._updateMessageTrayId) { ++ global.stage.disconnect(this._updateMessageTrayId); ++ this._updateMessageTrayId = 0; ++ } ++ ++ if (this._messageTrayConstraint) { ++ Main.messageTray.remove_constraint(this._messageTrayConstraint); ++ this._messageTrayConstraint = null; ++ } ++ } ++ ++ _alignMessageTray() { ++ if (!Main.messageTray) ++ return; ++ ++ if (!this._message || !this._message.visible) { ++ this._resetMessageTray(); ++ return; ++ } ++ ++ if (this._updateMessageTrayId) ++ return; ++ ++ this._updateMessageTrayId = global.stage.connect('before-update', () => { ++ if (!this._messageTrayConstraint) { ++ this._messageTrayConstraint = new HeadsUpConstraint({primary: true}); ++ ++ Main.layoutManager.panelBox.bind_property('visible', ++ this._messageTrayConstraint, 'active', ++ GObject.BindingFlags.SYNC_CREATE); ++ ++ Main.messageTray.add_constraint(this._messageTrayConstraint); ++ } ++ ++ const panelBottom = Main.layoutManager.panelBox.y + Main.layoutManager.panelBox.height; ++ const messageBottom = this._message.y + this._message.height; ++ ++ this._messageTrayConstraint.offset = messageBottom - panelBottom; ++ global.stage.disconnect(this._updateMessageTrayId); ++ this._updateMessageTrayId = 0; ++ }); ++ } ++ ++ _resetLoginDialog() { ++ if (!Main.sessionMode.isGreeter) ++ return; ++ ++ if (!Main.screenShield || !Main.screenShield._dialog) ++ return; ++ ++ const dialog = Main.screenShield._dialog; ++ ++ if (this._authPromptAllocatedId) { ++ dialog.disconnect(this._authPromptAllocatedId); ++ this._authPromptAllocatedId = 0; ++ } ++ ++ if (this._updateLoginDialogId) { ++ global.stage.disconnect(this._updateLoginDialogId); ++ this._updateLoginDialogId = 0; ++ } ++ ++ if (this._loginDialogConstraint) { ++ dialog.remove_constraint(this._loginDialogConstraint); ++ this._loginDialogConstraint = null; ++ } ++ } ++ ++ _adaptLoginDialogForMessage() { ++ if (!Main.sessionMode.isGreeter) ++ return; ++ ++ if (!Main.screenShield || !Main.screenShield._dialog) ++ return; ++ ++ if (!this._message || !this._message.visible) { ++ this._resetLoginDialog(); ++ return; ++ } ++ ++ const dialog = Main.screenShield._dialog; ++ ++ if (this._updateLoginDialogId) ++ return; ++ ++ this._updateLoginDialogId = global.stage.connect('before-update', () => { ++ let messageHeight = this._message.y + this._message.height; ++ if (dialog._logoBin.visible) ++ messageHeight -= dialog._logoBin.height; ++ ++ if (!this._logindDialogConstraint) { ++ this._loginDialogConstraint = new HeadsUpConstraint({primary: true}); ++ dialog.add_constraint(this._loginDialogConstraint); ++ } ++ ++ this._loginDialogConstraint.offset = messageHeight; ++ ++ global.stage.disconnect(this._updateLoginDialogId); ++ this._updateLoginDialogId = 0; ++ }); ++ } ++ ++ _adaptSessionForMessage() { ++ this._alignMessageTray(); ++ ++ if (Main.sessionMode.isGreeter) { ++ this._adaptLoginDialogForMessage(); ++ if (!this._authPromptAllocatedId) { ++ const dialog = Main.screenShield._dialog; ++ this._authPromptAllocatedId = dialog._authPrompt.connect('notify::allocation', this._adaptLoginDialogForMessage.bind(this)); ++ } ++ } ++ } ++} +diff --git a/extensions/heads-up-display/headsUpMessage.js b/extensions/heads-up-display/headsUpMessage.js +new file mode 100644 +index 00000000..30298847 +--- /dev/null ++++ b/extensions/heads-up-display/headsUpMessage.js +@@ -0,0 +1,166 @@ ++// SPDX-FileCopyrightText: 2021 Ray Strode ++// ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++import Atk from 'gi://Atk'; ++import Clutter from 'gi://Clutter'; ++import GObject from 'gi://GObject'; ++import St from 'gi://St'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++ ++const HeadsUpMessageBodyLabel = GObject.registerClass({ ++}, class HeadsUpMessageBodyLabel extends St.Label { ++ constructor(params) { ++ super(params); ++ ++ this._widthCoverage = 0.75; ++ this._heightCoverage = 0.25; ++ ++ global.display.connectObject('workareas-changed', ++ () => this._getWorkAreaAndMeasureLineHeight()); ++ } ++ ++ _getWorkAreaAndMeasureLineHeight() { ++ if (!this.get_parent()) ++ return; ++ ++ this._workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex); ++ ++ this.clutter_text.single_line_mode = true; ++ this.clutter_text.line_wrap = false; ++ ++ this._lineHeight = super.vfunc_get_preferred_height(-1)[0]; ++ ++ this.clutter_text.single_line_mode = false; ++ this.clutter_text.line_wrap = true; ++ } ++ ++ vfunc_parent_set() { ++ this._getWorkAreaAndMeasureLineHeight(); ++ } ++ ++ vfunc_get_preferred_width(forHeight) { ++ const maxWidth = this._widthCoverage * this._workArea.width; ++ ++ let [labelMinimumWidth, labelNaturalWidth] = super.vfunc_get_preferred_width(forHeight); ++ ++ labelMinimumWidth = Math.min(labelMinimumWidth, maxWidth); ++ labelNaturalWidth = Math.min(labelNaturalWidth, maxWidth); ++ ++ return [labelMinimumWidth, labelNaturalWidth]; ++ } ++ ++ vfunc_get_preferred_height(forWidth) { ++ const labelHeightUpperBound = this._heightCoverage * this._workArea.height; ++ const numberOfLines = Math.floor(labelHeightUpperBound / this._lineHeight); ++ this._numberOfLines = Math.max(numberOfLines, 1); ++ ++ const maxHeight = this._lineHeight * this._numberOfLines; ++ ++ let [labelMinimumHeight, labelNaturalHeight] = super.vfunc_get_preferred_height(forWidth); ++ ++ labelMinimumHeight = Math.min(labelMinimumHeight, maxHeight); ++ labelNaturalHeight = Math.min(labelNaturalHeight, maxHeight); ++ ++ return [labelMinimumHeight, labelNaturalHeight]; ++ } ++}); ++ ++export const HeadsUpMessage = GObject.registerClass({ ++}, class HeadsUpMessage extends St.Button { ++ constructor(heading, body) { ++ super({ ++ style_class: 'message', ++ accessible_role: Atk.Role.NOTIFICATION, ++ can_focus: false, ++ opacity: 0, ++ }); ++ ++ Main.layoutManager.addChrome(this, {affectsInputRegion: true}); ++ ++ this.add_style_class_name('heads-up-display-message'); ++ ++ this.connect('destroy', () => this._onDestroy()); ++ ++ Main.layoutManager.panelBox.connectObject('notify::allocation', ++ () => this._alignWithPanel()); ++ this.connect('notify::allocation', ++ () => this._alignWithPanel()); ++ ++ const contentsBox = new St.BoxLayout({ ++ style_class: 'heads-up-message-content', ++ vertical: true, ++ x_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(contentsBox); ++ ++ this._headingLabel = new St.Label({ ++ style_class: 'heads-up-message-heading', ++ x_expand: true, ++ x_align: Clutter.ActorAlign.CENTER, ++ }); ++ ++ this.setHeading(heading); ++ contentsBox.add_child(this._headingLabel); ++ ++ this._bodyLabel = new HeadsUpMessageBodyLabel({ ++ style_class: 'heads-up-message-body', ++ x_expand: true, ++ y_expand: true, ++ }); ++ contentsBox.add_child(this._bodyLabel); ++ ++ this.setBody(body); ++ } ++ ++ vfunc_parent_set() { ++ this._alignWithPanel(); ++ } ++ ++ _alignWithPanel() { ++ if (this._beforeUpdateId) ++ return; ++ ++ this._beforeUpdateId = global.stage.connect('before-update', () => { ++ let x = Main.panel.x; ++ let y = Main.panel.y + Main.panel.height; ++ ++ x += Main.panel.width / 2; ++ x -= this.width / 2; ++ x = Math.floor(x); ++ this.set_position(x, y); ++ this.opacity = 255; ++ ++ global.stage.disconnect(this._beforeUpdateId); ++ this._beforeUpdateId = 0; ++ }); ++ } ++ ++ setHeading(text) { ++ if (text) { ++ const heading = text ? text.replace(/\n/g, ' ') : ''; ++ this._headingLabel.text = heading; ++ this._headingLabel.visible = true; ++ } else { ++ this._headingLabel.text = text; ++ this._headingLabel.visible = false; ++ } ++ } ++ ++ setBody(text) { ++ this._bodyLabel.text = text; ++ ++ if (text) ++ this._bodyLabel.visible = true; ++ else ++ this._bodyLabel.visible = false; ++ } ++ ++ _onDestroy() { ++ if (this._beforeUpdateId) { ++ global.stage.disconnect(this._beforeUpdateId); ++ this._beforeUpdateId = 0; ++ } ++ } ++}); +diff --git a/extensions/heads-up-display/meson.build b/extensions/heads-up-display/meson.build +new file mode 100644 +index 00000000..42ce222c +--- /dev/null ++++ b/extensions/heads-up-display/meson.build +@@ -0,0 +1,13 @@ ++# SPDX-FileCopyrightText: 2021 Ray Strode ++# ++# SPDX-License-Identifier: GPL-2.0-or-later ++ ++extension_data += configure_file( ++ input: metadata_name + '.in', ++ output: metadata_name, ++ configuration: metadata_conf ++) ++ ++extension_data += files('stylesheet.css') ++extension_sources += files('headsUpMessage.js', 'prefs.js') ++extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') +diff --git a/extensions/heads-up-display/metadata.json.in b/extensions/heads-up-display/metadata.json.in +new file mode 100644 +index 00000000..01bcd4df +--- /dev/null ++++ b/extensions/heads-up-display/metadata.json.in +@@ -0,0 +1,12 @@ ++{ ++"extension-id": "@extension_id@", ++"uuid": "@uuid@", ++"settings-schema": "@gschemaname@", ++"gettext-domain": "@gettext_domain@", ++"name": "Heads-up Display Message", ++"description": "Add a message to be displayed on screen always above all windows and chrome.", ++"original-authors": [ "rstrode@redhat.com" ], ++"shell-version": [ "@shell_current@" ], ++"url": "@url@", ++"session-modes": [ "gdm", "lock-screen", "unlock-dialog", "user" ] ++} +diff --git a/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml b/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml +new file mode 100644 +index 00000000..1e2119c8 +--- /dev/null ++++ b/extensions/heads-up-display/org.gnome.shell.extensions.heads-up-display.gschema.xml +@@ -0,0 +1,60 @@ ++ ++ ++ ++ ++ ++ 30 ++ Idle Timeout ++ ++ Number of seconds until message is reshown after user goes idle. ++ ++ ++ ++ "" ++ Message to show at top of display ++ ++ The top line of the heads up display message. ++ ++ ++ ++ "" ++ Banner message ++ ++ A message to always show at the top of the screen. ++ ++ ++ ++ true ++ Show on login screen ++ ++ Whether or not the message should display on the login screen ++ ++ ++ ++ false ++ Show on screen shield ++ ++ Whether or not the message should display when the screen is locked ++ ++ ++ ++ false ++ Show on unlock screen ++ ++ Whether or not the message should display on the unlock screen. ++ ++ ++ ++ false ++ Show in user session ++ ++ Whether or not the message should display when the screen is unlocked. ++ ++ ++ ++ +diff --git a/extensions/heads-up-display/prefs.js b/extensions/heads-up-display/prefs.js +new file mode 100644 +index 00000000..304c8813 +--- /dev/null ++++ b/extensions/heads-up-display/prefs.js +@@ -0,0 +1,92 @@ ++// SPDX-FileCopyrightText: 2021 Ray Strode ++// ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++import Adw from 'gi://Adw'; ++import Gio from 'gi://Gio'; ++import GObject from 'gi://GObject'; ++import Gtk from 'gi://Gtk'; ++ ++import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; ++ ++class GeneralGroup extends Adw.PreferencesGroup { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(settings) { ++ super(); ++ ++ const actionGroup = new Gio.SimpleActionGroup(); ++ this.insert_action_group('options', actionGroup); ++ ++ actionGroup.add_action(settings.create_action('show-when-locked')); ++ actionGroup.add_action(settings.create_action('show-when-unlocking')); ++ actionGroup.add_action(settings.create_action('show-when-unlocked')); ++ ++ this.add(new Adw.SwitchRow({ ++ title: _('Show message when screen is locked'), ++ action_name: 'options.show-when-locked', ++ })); ++ this.add(new Adw.SwitchRow({ ++ title: _('Show message on unlock screen'), ++ action_name: 'options.show-when-unlocking', ++ })); ++ this.add(new Adw.SwitchRow({ ++ title: _('Show message when screen is unlocked'), ++ action_name: 'options.show-when-unlocked', ++ })); ++ ++ const spinRow = new Adw.SpinRow({ ++ title: _('Seconds after user goes idle before reshowing message'), ++ adjustment: new Gtk.Adjustment({ ++ lower: 0, ++ upper: 2147483647, ++ step_increment: 1, ++ page_increment: 60, ++ page_size: 60, ++ }), ++ }); ++ settings.bind('idle-timeout', ++ spinRow, 'value', ++ Gio.SettingsBindFlags.DEFAULT); ++ this.add(spinRow); ++ } ++} ++ ++class MessageGroup extends Adw.PreferencesGroup { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(settings) { ++ super({ ++ title: _('Message'), ++ }); ++ ++ const textView = new Gtk.TextView({ ++ accepts_tab: false, ++ wrap_mode: Gtk.WrapMode.WORD, ++ top_margin: 6, ++ bottom_margin: 6, ++ left_margin: 6, ++ right_margin: 6, ++ vexpand: true, ++ }); ++ textView.add_css_class('card'); ++ ++ settings.bind('message-body', ++ textView.get_buffer(), 'text', ++ Gio.SettingsBindFlags.DEFAULT); ++ this.add(textView); ++ } ++} ++ ++export default class HeadsUpDisplayPrefs extends ExtensionPreferences { ++ getPreferencesWidget() { ++ const page = new Adw.PreferencesPage(); ++ page.add(new GeneralGroup(this.getSettings())); ++ page.add(new MessageGroup(this.getSettings())); ++ return page; ++ } ++} +diff --git a/extensions/heads-up-display/stylesheet.css b/extensions/heads-up-display/stylesheet.css +new file mode 100644 +index 00000000..a1a34e3f +--- /dev/null ++++ b/extensions/heads-up-display/stylesheet.css +@@ -0,0 +1,38 @@ ++/* ++ * SPDX-FileCopyrightText: 2021 Ray Strode ++ * ++ * SPDX-License-Identifier: GPL-2.0-or-later ++ */ ++ ++.heads-up-display-message { ++ background-color: rgba(0.24, 0.24, 0.24, 0.80); ++ border: 1px solid black; ++ border-radius: 6px; ++ color: #eeeeec; ++ font-size: 11pt; ++ margin-top: 0.5em; ++ margin-bottom: 0.5em; ++ padding: 0.9em; ++} ++ ++.heads-up-display-message:insensitive { ++ background-color: rgba(0.24, 0.24, 0.24, 0.33); ++} ++ ++.heads-up-display-message:hover { ++ background-color: rgba(0.24, 0.24, 0.24, 0.2); ++ border: 1px solid rgba(0.0, 0.0, 0.0, 0.5); ++ color: #4d4d4d; ++ transition-duration: 250ms; ++} ++ ++.heads-up-message-heading { ++ height: 1.75em; ++ font-size: 1.25em; ++ font-weight: bold; ++ text-align: center; ++} ++ ++.heads-up-message-body { ++ text-align: center; ++} +diff --git a/meson.build b/meson.build +index b3dac8de..b2a5d94d 100644 +--- a/meson.build ++++ b/meson.build +@@ -40,6 +40,7 @@ classic_extensions = [ + default_extensions = classic_extensions + default_extensions += [ + 'drive-menu', ++ 'heads-up-display', + 'light-style', + 'screenshot-window-sizer', + 'system-monitor', +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 4abfcfca..b77f6c21 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -6,6 +6,7 @@ extensions/auto-move-windows/extension.js + extensions/auto-move-windows/org.gnome.shell.extensions.auto-move-windows.gschema.xml + extensions/auto-move-windows/prefs.js + extensions/drive-menu/extension.js ++extensions/heads-up-display/prefs.js + extensions/native-window-placement/extension.js + extensions/native-window-placement/org.gnome.shell.extensions.native-window-placement.gschema.xml + extensions/places-menu/extension.js +-- +2.45.0 + diff --git a/extra-extensions-0005-Add-custom-menu-extension.patch b/extra-extensions-0005-Add-custom-menu-extension.patch new file mode 100644 index 0000000..9f05a68 --- /dev/null +++ b/extra-extensions-0005-Add-custom-menu-extension.patch @@ -0,0 +1,719 @@ +From 2e2bc1032163110993aa3433295b5d8c84978790 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 12 Jan 2023 19:43:52 +0100 +Subject: [PATCH 5/5] Add custom-menu extension + +--- + extensions/custom-menu/config.js | 445 ++++++++++++++++++++++++ + extensions/custom-menu/extension.js | 201 +++++++++++ + extensions/custom-menu/meson.build | 7 + + extensions/custom-menu/metadata.json.in | 10 + + meson.build | 1 + + 5 files changed, 664 insertions(+) + create mode 100644 extensions/custom-menu/config.js + create mode 100644 extensions/custom-menu/extension.js + create mode 100644 extensions/custom-menu/meson.build + create mode 100644 extensions/custom-menu/metadata.json.in + +diff --git a/extensions/custom-menu/config.js b/extensions/custom-menu/config.js +new file mode 100644 +index 00000000..d08e3201 +--- /dev/null ++++ b/extensions/custom-menu/config.js +@@ -0,0 +1,445 @@ ++import Gio from 'gi://Gio'; ++import GLib from 'gi://GLib'; ++import Json from 'gi://Json'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; ++ ++import {getLogger} from './extension.js'; ++ ++class Entry { ++ constructor(prop) { ++ this.type = prop.type; ++ this.title = prop.title || ""; ++ this.__vars = prop.__vars || []; ++ this.updateEnv(prop); ++ } ++ ++ setTitle(text) { ++ this.item.label.get_clutter_text().set_text(text); ++ } ++ ++ updateEnv(prop) { ++ this.__env = {} ++ if (!this.__vars) return; ++ for (let i in this.__vars) { ++ let v = this.__vars[i]; ++ this.__env[v] = prop[v] ? String(prop[v]) : ""; ++ } ++ } ++ ++ // the pulse function should be read as "a pulse arrives" ++ pulse() { ++ } ++ ++ _try_destroy() { ++ try { ++ if (this.item && this.item.destroy) { ++ this.item.destroy(); ++ } ++ } catch(e) { /* Ignore all errors during destory*/ } ++ } ++} ++ ++class DerivedEntry { ++ constructor(prop) { ++ if (!prop.base) { ++ throw new Error("Base entry not specified in type definition."); ++ } ++ this.base = prop.base; ++ this.vars = prop.vars || []; ++ delete prop.base; ++ delete prop.vars; ++ this.prop = prop; ++ } ++ ++ createInstance(addit_prop) { ++ let cls = type_map[this.base]; ++ if (!cls) { ++ throw new Error("Bad base class."); ++ } ++ if (cls.createInstance) { ++ throw new Error("Not allowed to derive from dervied types"); ++ } ++ for (let rp in this.prop) { ++ addit_prop[rp] = this.prop[rp]; ++ } ++ addit_prop.__vars = this.vars; ++ let instance = new cls(addit_prop); ++ return instance; ++ } ++} ++ ++let __pipeOpenQueue = []; ++ ++/* callback: function (stdout, stderr, exit_status) { } */ ++function pipeOpen(cmdline, env, callback) { ++ if (cmdline === undefined || callback === undefined) { ++ return false; ++ } ++ realPipeOpen(cmdline, env, callback); ++ return true; ++} /**/ ++ ++function realPipeOpen(cmdline, env, callback) { ++ let user_cb = callback; ++ let proc; ++ ++ function wait_cb(_, _res) { ++ let stdout_pipe = proc.get_stdout_pipe(); ++ let stderr_pipe = proc.get_stderr_pipe(); ++ let stdout_content; ++ let stderr_content; ++ ++ // Only the first GLib.MAXINT16 characters are fetched for optimization. ++ stdout_pipe.read_bytes_async(GLib.MAXINT16, 0, null, function(osrc, ores) { ++ const decoder = new TextDecoder(); ++ stdout_content = decoder.decode(stdout_pipe.read_bytes_finish(ores).get_data()); ++ stdout_pipe.close(null); ++ stderr_pipe.read_bytes_async(GLib.MAXINT16, 0, null, function(esrc, eres) { ++ stderr_content = decoder.decode(stderr_pipe.read_bytes_finish(eres).get_data()); ++ stderr_pipe.close(null); ++ user_cb(stdout_content, stderr_content, proc.get_exit_status()); ++ }); ++ }); ++ } ++ ++ if (user_cb) { ++ let _pipedLauncher = new Gio.SubprocessLauncher({ ++ flags: ++ Gio.SubprocessFlags.STDERR_PIPE | ++ Gio.SubprocessFlags.STDOUT_PIPE ++ }); ++ for (let key in env) { ++ _pipedLauncher.setenv(key, env[key], true); ++ } ++ proc = _pipedLauncher.spawnv(['bash', '-c', cmdline]); ++ proc.wait_async(null, wait_cb); ++ } else { ++ // Detached launcher is used to spawn commands that we are not concerned about its result. ++ let _detacLauncher = new Gio.SubprocessLauncher(); ++ for (let key in env) { ++ _detacLauncher.setenv(key, env[key], true); ++ } ++ proc = _detacLauncher.spawnv(['bash', '-c', cmdline]); ++ } ++ getLogger().info("Spawned " + cmdline); ++ return proc.get_identifier(); ++} ++ ++function _generalSpawn(command, env, title) { ++ title = title || "Process"; ++ pipeOpen(command, env, function(stdout, stderr, exit_status) { ++ if (exit_status != 0) { ++ log ++ getLogger().warning(stderr); ++ getLogger().notify("proc", title + " exited with status " + exit_status, stderr); ++ } ++ }); ++} ++ ++// Detect menu toggle on startup ++function _toggleDetect(command, env, object) { ++ pipeOpen(command, env, function(stdout, stderr, exit_status) { ++ if (exit_status == 0) { ++ object.item.setToggleState(true); ++ } ++ }); ++} /**/ ++ ++function quoteShellArg(arg) { ++ arg = arg.replace(/'/g, "'\"'\"'"); ++ return "'" + arg + "'"; ++} ++ ++/** ++ * This cache is used to reduce detector cost. ++ * Each time creating an item, it check if the result of this detector is cached, ++ * which prevent the togglers from running detector on each creation. ++ * This is useful especially in search mode. ++ */ ++let _toggler_state_cache = { }; ++ ++class TogglerEntry extends Entry { ++ constructor(prop) { ++ super(prop); ++ this.command_on = prop.command_on || ""; ++ this.command_off = prop.command_off || ""; ++ this.detector = prop.detector || ""; ++ this.auto_on = prop.auto_on || false; ++ this.notify_when = prop.notify_when || []; ++ // if the switch is manually turned off, auto_on is disabled. ++ this._manually_switched_off = false; ++ this.pulse(); // load initial state ++ } ++ ++ createItem() { ++ this._try_destroy(); ++ this.item = new PopupMenu.PopupSwitchMenuItem(this.title, false); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ this.item.connect('toggled', this._onManuallyToggled.bind(this)); ++ this._loadState(); ++ _toggleDetect(this.detector, this.__env, this); ++ return this.item; ++ } ++ ++ _onManuallyToggled(_, state) { ++ // when switched on again, this flag will get cleared. ++ this._manually_switched_off = !state; ++ this._storeState(state); ++ this._onToggled(state); ++ } ++ ++ _onToggled(state) { ++ if (state) { ++ _generalSpawn(this.command_on, this.__env, this.title); ++ } else { ++ _generalSpawn(this.command_off, this.__env, this.title); ++ } ++ } ++ ++ _detect(callback) { ++ // abort detecting if detector is an empty string ++ if (!this.detector) { ++ return; ++ } ++ pipeOpen(this.detector, this.__env, function(out) { ++ out = String(out); ++ callback(!Boolean(out.match(/^\s*$/))); ++ }); ++ } ++ ++ // compare the new state with cached state notify when state is different ++ compareState(new_state) { ++ let old_state = _toggler_state_cache[this.detector]; ++ if (old_state === undefined) return; ++ if (old_state == new_state) return; ++ ++ if (this.notify_when.indexOf(new_state ? "on" : "off") >= 0) { ++ let not_str = this.title + (new_state ? " started." : " stopped."); ++ if (!new_state && this.auto_on) { ++ not_str += " Attempt to restart it now."; ++ } ++ getLogger().notify("state", not_str); ++ } ++ } ++ ++ _storeState(state) { ++ let hash = JSON.stringify({ env: this.__env, detector: this.detector }); ++ _toggler_state_cache[hash] = state; ++ } ++ ++ _loadState() { ++ let hash = JSON.stringify({ env: this.__env, detector: this.detector }); ++ let state = _toggler_state_cache[hash]; ++ if (state !== undefined) { ++ this.item.setToggleState(state); // doesn't emit 'toggled' ++ } ++ } ++ ++ pulse() { ++ this._detect(state => { ++ this.compareState(state); ++ this._storeState(state); ++ this._loadState(); ++ if (!state && !this._manually_switched_off && this.auto_on) { ++ // do not call setToggleState here, because command_on may fail ++ this._onToggled(this.item, true); ++ } ++ }); ++ } ++ ++ perform() { ++ this.item.toggle(); ++ } ++} ++ ++class LauncherEntry extends Entry { ++ constructor(prop) { ++ super(prop); ++ this.command = prop.command || ""; ++ } ++ ++ createItem() { ++ this._try_destroy(); ++ this.item = new PopupMenu.PopupMenuItem(this.title); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ this.item.connect('activate', this._onClicked.bind(this)); ++ return this.item; ++ } ++ ++ _onClicked(_) { ++ _generalSpawn(this.command, this.__env, this.title); ++ } ++ ++ perform() { ++ this.item.emit('activate'); ++ } ++} ++ ++class SubMenuEntry extends Entry { ++ constructor(prop) { ++ super(prop); ++ ++ if (prop.entries == undefined) { ++ throw new Error("Expected entries provided in submenu entry."); ++ } ++ this.entries = []; ++ for (let i in prop.entries) { ++ let entry_prop = prop.entries[i]; ++ let entry = createEntry(entry_prop); ++ this.entries.push(entry); ++ } ++ } ++ ++ createItem() { ++ this._try_destroy(); ++ this.item = new PopupMenu.PopupSubMenuMenuItem(this.title); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ for (let i in this.entries) { ++ let entry = this.entries[i]; ++ this.item.menu.addMenuItem(entry.createItem()); ++ } ++ return this.item; ++ } ++ ++ pulse() { ++ for (let i in this.entries) { ++ let entry = this.entries[i]; ++ entry.pulse(); ++ } ++ } ++} ++ ++class SeparatorEntry extends Entry { ++ createItem() { ++ this._try_destroy(); ++ this.item = new PopupMenu.PopupSeparatorMenuItem(this.title); ++ this.item.label.get_clutter_text().set_use_markup(true); ++ return this.item; ++ } ++} ++ ++let type_map = {}; ++ ++//////////////////////////////////////////////////////////////////////////////// ++// Config Loader loads config from JSON file. ++ ++// convert Json Nodes (GLib based) to native javascript value. ++function convertJson(node) { ++ if (node.get_node_type() == Json.NodeType.VALUE) { ++ return node.get_value(); ++ } ++ if (node.get_node_type() == Json.NodeType.OBJECT) { ++ let obj = {} ++ node.get_object().foreach_member(function(_, k, v_n) { ++ obj[k] = convertJson(v_n); ++ }); ++ return obj; ++ } ++ if (node.get_node_type() == Json.NodeType.ARRAY) { ++ let arr = [] ++ node.get_array().foreach_element(function(_, i, elem) { ++ arr.push(convertJson(elem)); ++ }); ++ return arr; ++ } ++ return null; ++} ++ ++// ++function createEntry(entry_prop) { ++ if (!entry_prop.type) { ++ throw new Error("No type specified in entry."); ++ } ++ let cls = type_map[entry_prop.type]; ++ if (!cls) { ++ throw new Error("Incorrect type '" + entry_prop.type + "'"); ++ } else if (cls.createInstance) { ++ return cls.createInstance(entry_prop); ++ } ++ return new cls(entry_prop); ++} ++ ++export class Loader { ++ constructor(filename) { ++ if (filename) { ++ this.loadConfig(filename); ++ } ++ } ++ ++ loadConfig(filename) { ++ // reset type_map everytime load the config ++ type_map = { ++ launcher: LauncherEntry, ++ toggler: TogglerEntry, ++ submenu: SubMenuEntry, ++ separator: SeparatorEntry ++ }; ++ ++ type_map.systemd = new DerivedEntry({ ++ base: 'toggler', ++ vars: ['unit'], ++ command_on: "pkexec systemctl start ${unit}", ++ command_off: "pkexec systemctl stop ${unit}", ++ detector: "systemctl status ${unit} | grep Active:\\\\s\\*activ[ei]", ++ }); ++ ++ type_map.tmux = new DerivedEntry({ ++ base: 'toggler', ++ vars: ['command', 'session'], ++ command_on: 'tmux new -d -s ${session} bash -c "${command}"', ++ command_off: 'tmux kill-session -t ${session}', ++ detector: 'tmux has -t "${session}" 2>/dev/null && echo yes', ++ }); ++ ++ /* ++ * Refer to README file for detailed config file format. ++ */ ++ this.entries = []; // CAUTION: remove all entries. ++ ++ let config_parser = new Json.Parser(); ++ config_parser.load_from_file(filename); ++ let conf = convertJson(config_parser.get_root()); ++ if (conf.entries == undefined) { ++ throw new Error("Key 'entries' not found."); ++ } ++ if (conf.deftype) { ++ for (let tname in conf.deftype) { ++ if (type_map[tname]) { ++ throw new Error("Type \""+tname+"\" duplicated."); ++ } ++ type_map[tname] = new DerivedEntry(conf.deftype[tname]); ++ } ++ } ++ ++ for (let conf_i in conf.entries) { ++ let entry_prop = conf.entries[conf_i]; ++ this.entries.push(createEntry(entry_prop)); ++ } ++ } ++ ++ saveDefaultConfig(filename) { ++ // Write default config ++ const PERMISSIONS_MODE = 0o640; ++ const jsonString = JSON.stringify({ ++ "_homepage_": "https://github.com/andreabenini/gnome-plugin.custom-menu-panel", ++ "_examples_": "https://github.com/andreabenini/gnome-plugin.custom-menu-panel/tree/main/examples", ++ "entries": [ { ++ "type": "launcher", ++ "title": "Edit menu", ++ "command": "gedit $HOME/.entries.json" ++ } ] ++ }, null, 4); ++ let fileConfig = Gio.File.new_for_path(filename); ++ if (GLib.mkdir_with_parents(fileConfig.get_parent().get_path(), PERMISSIONS_MODE) === 0) { ++ fileConfig.replace_contents(jsonString, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); ++ } ++ // Try to load newly saved file ++ try { ++ this.loadConfig(filename); ++ } catch(e) { ++ Main.notify(_('Cannot create and load file: '+filename)); ++ } ++ } ++} +diff --git a/extensions/custom-menu/extension.js b/extensions/custom-menu/extension.js +new file mode 100644 +index 00000000..9edbc548 +--- /dev/null ++++ b/extensions/custom-menu/extension.js +@@ -0,0 +1,201 @@ ++/* extension.js ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ */ ++ ++/** ++ * @author Ben ++ * @see https://github.com/andreabenini/gnome-plugin.custom-menu-panel ++ */ ++ ++const CONFIGURATION_FILE = '/.entries.json'; ++ ++import Gio from 'gi://Gio'; ++import GLib from 'gi://GLib'; ++import GObject from 'gi://GObject'; ++import St from 'gi://St'; ++ ++import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; ++ ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++import * as BackgroundMenu from 'resource:///org/gnome/shell/ui/backgroundMenu.js'; ++import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; ++import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; ++ ++import * as Config from './config.js'; ++ ++const LOGGER_INFO = 0; ++const LOGGER_WARNING = 1; ++const LOGGER_ERROR = 2; ++ ++const {BackgroundMenu: OriginalBackgroundMenu} = BackgroundMenu; ++ ++ ++class CustomMenu extends PopupMenu.PopupMenu { ++ constructor(sourceActor) { ++ super(sourceActor, 0.0, St.Side.TOP, 0); ++ ++ this._loadSetup(); ++ } ++ ++ /** ++ * LOAD Program settings from .entries.json file ++ */ ++ _loadSetup() { ++ this.removeAll(); ++ // Loading configuration from file ++ this.configLoader = new Config.Loader(); ++ try { ++ this.configLoader.loadConfig(GLib.get_home_dir() + CONFIGURATION_FILE); // $HOME/.entries.json ++ } catch(e) { ++ this.configLoader.saveDefaultConfig(GLib.get_home_dir() + CONFIGURATION_FILE); // create default entries ++ } ++ // Build the menu ++ let i = 0; ++ for (let i in this.configLoader.entries) { ++ let item = this.configLoader.entries[i].createItem(); ++ this.addMenuItem(item); ++ } ++ } /**/ ++} ++ ++class CustomBackgroundMenu extends CustomMenu { ++ constructor(layoutManager) { ++ super(layoutManager.dummyCursor); ++ ++ this.actor.add_style_class_name('background-menu'); ++ ++ layoutManager.uiGroup.add_actor(this.actor); ++ this.actor.hide(); ++ ++ this.connect('open-state-changed', (menu, open) => { ++ if (open) ++ this._updateMaxHeight(); ++ }); ++ } ++ ++ _updateMaxHeight() { ++ const monitor = Main.layoutManager.findMonitorForActor(this.actor); ++ const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index); ++ const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); ++ const vMargins = this.actor.margin_top + this.actor.margin_bottom; ++ const {y: offsetY} = this.sourceActor; ++ ++ const maxHeight = Math.round((monitor.height - offsetY - vMargins) / scaleFactor); ++ this.actor.style = `max-height: ${maxHeight}px;`; ++ } ++} ++ ++const Indicator = GObject.registerClass( ++ class Indicator extends PanelMenu.Button { ++ _init() { ++ super._init(0.0, _('Custom Menu Panel Indicator'), true); ++ this.add_child(new St.Icon({ ++ icon_name: 'view-list-bullet-symbolic', ++ style_class: 'system-status-icon', ++ })); ++ ++ this.setMenu(new CustomMenu(this)); ++ } ++ } ++); /**/ ++ ++ ++class Logger { ++ constructor(log_file) { ++ this._log_file = log_file; ++ // initailize log_backend ++ if (!log_file) { ++ this._initEmptyLog(); ++ } else if(log_file == "gnome-shell") { ++ this._initGnomeLog(); ++ } else { ++ this._initFileLog(); ++ } ++ this.level = LOGGER_WARNING; ++ this.info = function(t) { ++ if (this.level <= LOGGER_INFO) { ++ this.log(t); ++ } ++ }; ++ this.warning = function(t) { ++ if (this.level <= LOGGER_WARNING) { ++ this.log(t); ++ } ++ }; ++ this.error = function(t) { ++ if (this.level <= LOGGER_ERROR) { ++ this.log(t); ++ } ++ }; ++ } ++ ++ _initEmptyLog() { ++ this.log = function(_) { }; ++ } ++ ++ _initGnomeLog() { ++ this.log = function(s) { ++ global.log("custom-menu-panel> " + s); ++ }; ++ } ++ ++ _initFileLog() { ++ this.log = function(s) { ++ // all operations are synchronous: any needs to optimize? ++ if (!this._output_file || !this._output_file.query_exists(null) || !this._fstream || this._fstream.is_closed()) { ++ this._output_file = Gio.File.new_for_path(this._log_file); ++ this._fstream = this._output_file.append_to(Gio.FileCreateFlags.NONE, null); ++ if (!this._fstream instanceof Gio.FileIOStream) { ++ this._initGnomeLog(); ++ this.log("IOError: Failed to append to " + this._log_file + " [Gio.IOErrorEnum:" + this._fstream + "]"); ++ return; ++ } ++ } ++ this._fstream.write(String(new Date())+" "+s+"\n", null); ++ this._fstream.flush(null); ++ } ++ } ++ ++ notify(t, str, details) { ++ this.ncond = this.ncond || ['proc', 'ext', 'state']; ++ if (this.ncond.indexOf(t) < 0) { ++ return; ++ } ++ Main.notify(str, details || ""); ++ } ++} ++ ++// lazy-evaluation ++let logger = null; ++export function getLogger() { ++ if (logger === null) { ++ logger = new Logger("gnome-shell"); ++ } ++ return logger; ++} /**/ ++ ++export default class CustomMenuExtension extends Extension { ++ enable() { ++ BackgroundMenu.BackgroundMenu = CustomBackgroundMenu; ++ Main.layoutManager._updateBackgrounds(); ++ } ++ ++ disable() { ++ BackgroundMenu.BackgroundMenu = OriginalBackgroundMenu; ++ Main.layoutManager._updateBackgrounds(); ++ } ++} /**/ +diff --git a/extensions/custom-menu/meson.build b/extensions/custom-menu/meson.build +new file mode 100644 +index 00000000..92450963 +--- /dev/null ++++ b/extensions/custom-menu/meson.build +@@ -0,0 +1,7 @@ ++extension_data += configure_file( ++ input: metadata_name + '.in', ++ output: metadata_name, ++ configuration: metadata_conf ++) ++ ++extension_sources += files('config.js') +diff --git a/extensions/custom-menu/metadata.json.in b/extensions/custom-menu/metadata.json.in +new file mode 100644 +index 00000000..054f639b +--- /dev/null ++++ b/extensions/custom-menu/metadata.json.in +@@ -0,0 +1,10 @@ ++{ ++"extension-id": "@extension_id@", ++"uuid": "@uuid@", ++"settings-schema": "@gschemaname@", ++"gettext-domain": "@gettext_domain@", ++"name": "Custom menu", ++"description": "Quick custom menu for launching your favorite applications", ++"shell-version": [ "@shell_current@" ], ++"url": "@url@" ++} +diff --git a/meson.build b/meson.build +index b2a5d94d..bbb72889 100644 +--- a/meson.build ++++ b/meson.build +@@ -53,6 +53,7 @@ all_extensions = default_extensions + all_extensions += [ + 'auto-move-windows', + 'classification-banner', ++ 'custom-menu', + 'gesture-inhibitor', + 'native-window-placement', + 'user-theme' +-- +2.45.0 + diff --git a/gnome-shell-extensions.spec b/gnome-shell-extensions.spec index bd94791..f0d21dd 100644 --- a/gnome-shell-extensions.spec +++ b/gnome-shell-extensions.spec @@ -21,6 +21,46 @@ BuildRequires: glib2%{?_isa} Requires: gnome-shell >= %{min_gs_version} BuildArch: noarch + +Patch: extra-extensions-0001-Add-top-icons-extension.patch +Patch: extra-extensions-0002-Add-gesture-inhibitor-extension.patch +Patch: extra-extensions-0003-Add-classification-banner.patch +Patch: extra-extensions-0004-Add-heads-up-display.patch +Patch: extra-extensions-0005-Add-custom-menu-extension.patch + +Patch: 0001-Include-top-icons-in-classic-session.patch +Patch: 0001-apps-menu-Set-label_actor-of-Category-items.patch +Patch: prefer-window-icon.patch + +Patch: more-ws-previews-0001-workspace-indicator-Move-indicator-code-into-separat.patch +Patch: more-ws-previews-0002-workspace-indicator-Use-descendant-style-selectors.patch +Patch: more-ws-previews-0003-window-list-Use-consistent-style-class-prefix.patch +Patch: more-ws-previews-0004-workspace-indicator-Allow-overriding-base-style-clas.patch +Patch: more-ws-previews-0005-window-list-Override-base-style-class.patch +Patch: more-ws-previews-0006-window-list-Externally-adjust-workspace-menu.patch +Patch: more-ws-previews-0007-window-list-Handle-changes-to-workspace-menu.patch +Patch: more-ws-previews-0008-workspace-indicator-Don-t-use-SCHEMA-KEY-constants.patch +Patch: more-ws-previews-0009-workspace-indicator-Use-existing-property.patch +Patch: more-ws-previews-0010-workspace-indicator-Don-t-use-menu-section.patch +Patch: more-ws-previews-0011-workspace-indicator-Support-showing-tooltips-above.patch +Patch: more-ws-previews-0012-workspace-indicator-Only-change-top-bar-redirect-whe.patch +Patch: more-ws-previews-0013-workspace-indicator-Small-cleanup.patch +Patch: more-ws-previews-0014-workspace-indicator-Simplify-getting-status-text.patch +Patch: more-ws-previews-0015-workspace-indicator-Include-n-workspaces-in-status-l.patch +Patch: more-ws-previews-0016-workspace-indicator-Tweak-preview-style.patch +Patch: more-ws-previews-0017-workspace-indicator-Support-light-style.patch +Patch: more-ws-previews-0018-export-zips-Pick-up-non-default-stylesheets.patch +Patch: more-ws-previews-0019-window-list-Use-actual-copy-of-workspace-indicator.patch +Patch: more-ws-previews-0020-workspace-indicator-Simplify-scroll-handling.patch +Patch: more-ws-previews-0021-workspace-indicator-Handle-active-indication-in-thum.patch +Patch: more-ws-previews-0022-workspace-indicator-Split-out-WorkspacePreviews.patch +Patch: more-ws-previews-0023-workspace-indicator-Handle-preview-overflow.patch +Patch: more-ws-previews-0024-workspace-indicator-Support-labels-in-previews.patch +Patch: more-ws-previews-0025-workspace-indicator-Stop-handling-vertical-layouts.patch +Patch: more-ws-previews-0026-workspace-indicator-Also-show-previews-in-menu.patch +Patch: more-ws-previews-0027-workspace-indicator-Make-previews-configurable.patch +Patch: more-ws-previews-0028-window-list-Expose-workspace-preview-option.patch + %description GNOME Shell Extensions is a collection of extensions providing additional and optional functionality to GNOME Shell. @@ -28,13 +68,18 @@ optional functionality to GNOME Shell. Enabled extensions: * apps-menu * auto-move-windows + * classification-banner + * custom-menu * drive-menu + * gesture-inhibitor + * heads-up-display * launch-new-instance * light-style * native-window-placement * places-menu * screenshot-window-sizer * system-monitor + * top-icons * user-theme * window-list * windowsNavigator @@ -101,6 +146,26 @@ workspace can be assigned to each application as soon as it creates a window, in a manner configurable with a GSettings key. +%package -n %{pkg_prefix}-classification-banner +Summary: Display classification level banner in GNOME Shell +Group: User Interface/Desktops +License: GPLv2+ +Requires: %{pkg_prefix}-common = %{version}-%{release} + +%description -n %{pkg_prefix}-classification-banner +This GNOME Shell extension adds a banner that displays the classification level. + + +%package -n %{pkg_prefix}-custom-menu +Summary: Add a custom menu to the desktop +Group: User Interface/Desktops +License: GPLv2+ +Requires: %{pkg_prefix}-common = %{version}-%{release} + +%description -n %{pkg_prefix}-custom-menu +This GNOME Shell extension adds a custom menu to the desktop background. + + %package -n %{pkg_prefix}-drive-menu Summary: Drive status menu for GNOME Shell License: GPL-2.0-or-later @@ -111,6 +176,27 @@ This GNOME Shell extension provides a panel status menu for accessing and unmounting removable devices. +%package -n %{pkg_prefix}-gesture-inhibitor +Summary: Gesture inhibitor +Group: User Interface/Desktops +License: GPLv2+ +Requires: %{pkg_prefix}-common = %{version}-%{release} + +%description -n %{pkg_prefix}-gesture-inhibitor +This GNOME Shell extension allows disabling the default desktop gestures. + + +%package -n %{pkg_prefix}-heads-up-display +Summary: Display persistent on-screen message +Group: User Interface/Desktops +License: GPLv3+ +Requires: %{pkg_prefix}-common = %{version}-%{release} + +%description -n %{pkg_prefix}-heads-up-display +This GNOME Shell extension displays a persistent message in the top middle of the screen. +This message can appear on the login screen, lock screen, or regular user session. + + %package -n %{pkg_prefix}-launch-new-instance Summary: Always launch a new application instance for GNOME Shell License: GPL-2.0-or-later @@ -169,6 +255,15 @@ Requires: %{pkg_prefix}-common = %{version}-%{release} This GNOME Shell extension displays system usage information in the top bar. +%package -n %{pkg_prefix}-top-icons +Summary: Show legacy icons on top +License: GPLv2+ +Requires: %{pkg_prefix}-common = %{version}-%{release} + +%description -n %{pkg_prefix}-top-icons +This GNOME Shell extension moves legacy tray icons into the top bar + + %package -n %{pkg_prefix}-user-theme Summary: Support for custom themes in GNOME Shell License: GPL-2.0-or-later @@ -249,10 +344,29 @@ workspaces. %{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.auto-move-windows.gschema.xml %{_datadir}/gnome-shell/extensions/auto-move-windows*/ +%files -n %{pkg_prefix}-classification-banner +%{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.classification-banner.gschema.xml +%{_datadir}/gnome-shell/extensions/classification-banner*/ + + +%files -n %{pkg_prefix}-custom-menu +%{_datadir}/gnome-shell/extensions/custom-menu*/ + + %files -n %{pkg_prefix}-drive-menu %{_datadir}/gnome-shell/extensions/drive-menu*/ +%files -n %{pkg_prefix}-gesture-inhibitor +%{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.gesture-inhibitor.gschema.xml +%{_datadir}/gnome-shell/extensions/gesture-inhibitor*/ + + +%files -n %{pkg_prefix}-heads-up-display +%{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.heads-up-display.gschema.xml +%{_datadir}/gnome-shell/extensions/heads-up-display*/ + + %files -n %{pkg_prefix}-launch-new-instance %{_datadir}/gnome-shell/extensions/launch-new-instance*/ @@ -280,6 +394,10 @@ workspaces. %{_datadir}/gnome-shell/extensions/system-monitor*/ +%files -n %{pkg_prefix}-top-icons +%{_datadir}/gnome-shell/extensions/top-icons*/ + + %files -n %{pkg_prefix}-user-theme %{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.user-theme.gschema.xml %{_datadir}/gnome-shell/extensions/user-theme*/ @@ -296,6 +414,7 @@ workspaces. %files -n %{pkg_prefix}-workspace-indicator %{_datadir}/gnome-shell/extensions/workspace-indicator*/ +%{_datadir}/glib-2.0/schemas/org.gnome.shell.extensions.workspace-indicator.gschema.xml %changelog diff --git a/more-ws-previews-0001-workspace-indicator-Move-indicator-code-into-separat.patch b/more-ws-previews-0001-workspace-indicator-Move-indicator-code-into-separat.patch new file mode 100644 index 0000000..8a14877 --- /dev/null +++ b/more-ws-previews-0001-workspace-indicator-Move-indicator-code-into-separat.patch @@ -0,0 +1,942 @@ +From 1f0681875eefd09df28630a74aabd1bf47f90dab Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 12:38:33 +0100 +Subject: [PATCH 01/28] workspace-indicator: Move indicator code into separate + file + +Shortly after the window-list extension was added, it gained a +workspace switcher based on the workspace indicator extension. + +Duplicating the code wasn't a big issue while the switcher was +a simple menu, but since it gained previews with a fair bit of +custom styling, syncing changes between the two extensions has +become tedious, in particular as the two copies have slightly +diverged over time. + +In order to allow the two copies to converge again, the indicator +code needs to be separate from the extension boilerplate, so +split out the code into a separate module. + +Part-of: +--- + extensions/workspace-indicator/extension.js | 432 +---------------- + extensions/workspace-indicator/meson.build | 2 +- + .../workspace-indicator/workspaceIndicator.js | 438 ++++++++++++++++++ + po/POTFILES.in | 2 +- + 4 files changed, 442 insertions(+), 432 deletions(-) + create mode 100644 extensions/workspace-indicator/workspaceIndicator.js + +diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js +index f9c7dd9a..b383c919 100644 +--- a/extensions/workspace-indicator/extension.js ++++ b/extensions/workspace-indicator/extension.js +@@ -4,439 +4,11 @@ + // + // SPDX-License-Identifier: GPL-2.0-or-later + +-// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- +-import Clutter from 'gi://Clutter'; +-import Gio from 'gi://Gio'; +-import GObject from 'gi://GObject'; +-import Meta from 'gi://Meta'; +-import St from 'gi://St'; ++import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; + +-import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; +- +-import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; + import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +-import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; +-import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +- +-const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences'; +-const WORKSPACE_KEY = 'workspace-names'; +- +-const TOOLTIP_OFFSET = 6; +-const TOOLTIP_ANIMATION_TIME = 150; +- +-const MAX_THUMBNAILS = 6; +- +-class WindowPreview extends St.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor(window) { +- super({ +- style_class: 'workspace-indicator-window-preview', +- }); +- +- this._delegate = this; +- DND.makeDraggable(this, {restoreOnSuccess: true}); +- +- this._window = window; +- +- this._window.connectObject( +- 'size-changed', () => this._checkRelayout(), +- 'position-changed', () => this._checkRelayout(), +- 'notify::minimized', this._updateVisible.bind(this), +- 'notify::window-type', this._updateVisible.bind(this), +- this); +- this._updateVisible(); +- +- global.display.connectObject('notify::focus-window', +- this._onFocusChanged.bind(this), this); +- this._onFocusChanged(); +- } +- +- // needed for DND +- get metaWindow() { +- return this._window; +- } +- +- _onFocusChanged() { +- if (global.display.focus_window === this._window) +- this.add_style_class_name('active'); +- else +- this.remove_style_class_name('active'); +- } +- +- _checkRelayout() { +- const monitor = Main.layoutManager.findIndexForActor(this); +- const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); +- if (this._window.get_frame_rect().overlap(workArea)) +- this.queue_relayout(); +- } +- +- _updateVisible() { +- this.visible = this._window.window_type !== Meta.WindowType.DESKTOP && +- this._window.showing_on_its_workspace(); +- } +-} +- +-class WorkspaceLayout extends Clutter.LayoutManager { +- static { +- GObject.registerClass(this); +- } +- +- vfunc_get_preferred_width() { +- return [0, 0]; +- } +- +- vfunc_get_preferred_height() { +- return [0, 0]; +- } +- +- vfunc_allocate(container, box) { +- const monitor = Main.layoutManager.findIndexForActor(container); +- const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); +- const hscale = box.get_width() / workArea.width; +- const vscale = box.get_height() / workArea.height; +- +- for (const child of container) { +- const childBox = new Clutter.ActorBox(); +- const frameRect = child.metaWindow.get_frame_rect(); +- childBox.set_size( +- Math.round(Math.min(frameRect.width, workArea.width) * hscale), +- Math.round(Math.min(frameRect.height, workArea.height) * vscale)); +- childBox.set_origin( +- Math.round((frameRect.x - workArea.x) * hscale), +- Math.round((frameRect.y - workArea.y) * vscale)); +- child.allocate(childBox); +- } +- } +-} +- +-class WorkspaceThumbnail extends St.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor(index) { +- super({ +- style_class: 'workspace', +- child: new Clutter.Actor({ +- layout_manager: new WorkspaceLayout(), +- clip_to_allocation: true, +- x_expand: true, +- y_expand: true, +- }), +- }); +- +- this._tooltip = new St.Label({ +- style_class: 'dash-label', +- visible: false, +- }); +- Main.uiGroup.add_child(this._tooltip); +- +- this.connect('destroy', this._onDestroy.bind(this)); +- this.connect('notify::hover', this._syncTooltip.bind(this)); +- +- this._index = index; +- this._delegate = this; // needed for DND +- +- this._windowPreviews = new Map(); +- +- let workspaceManager = global.workspace_manager; +- this._workspace = workspaceManager.get_workspace_by_index(index); +- +- this._workspace.connectObject( +- 'window-added', (ws, window) => this._addWindow(window), +- 'window-removed', (ws, window) => this._removeWindow(window), +- this); +- +- global.display.connectObject('restacked', +- this._onRestacked.bind(this), this); +- +- this._workspace.list_windows().forEach(w => this._addWindow(w)); +- this._onRestacked(); +- } +- +- acceptDrop(source) { +- if (!source.metaWindow) +- return false; +- +- this._moveWindow(source.metaWindow); +- return true; +- } +- +- handleDragOver(source) { +- if (source.metaWindow) +- return DND.DragMotionResult.MOVE_DROP; +- else +- return DND.DragMotionResult.CONTINUE; +- } +- +- _addWindow(window) { +- if (this._windowPreviews.has(window)) +- return; +- +- let preview = new WindowPreview(window); +- preview.connect('clicked', (a, btn) => this.emit('clicked', btn)); +- this._windowPreviews.set(window, preview); +- this.child.add_child(preview); +- } +- +- _removeWindow(window) { +- let preview = this._windowPreviews.get(window); +- if (!preview) +- return; +- +- this._windowPreviews.delete(window); +- preview.destroy(); +- } +- +- _onRestacked() { +- let lastPreview = null; +- let windows = global.get_window_actors().map(a => a.meta_window); +- for (let i = 0; i < windows.length; i++) { +- let preview = this._windowPreviews.get(windows[i]); +- if (!preview) +- continue; +- +- this.child.set_child_above_sibling(preview, lastPreview); +- lastPreview = preview; +- } +- } +- +- _moveWindow(window) { +- let monitorIndex = Main.layoutManager.findIndexForActor(this); +- if (monitorIndex !== window.get_monitor()) +- window.move_to_monitor(monitorIndex); +- window.change_workspace_by_index(this._index, false); +- } +- +- on_clicked() { +- let ws = global.workspace_manager.get_workspace_by_index(this._index); +- if (ws) +- ws.activate(global.get_current_time()); +- } +- +- _syncTooltip() { +- if (this.hover) { +- this._tooltip.set({ +- text: Meta.prefs_get_workspace_name(this._index), +- visible: true, +- opacity: 0, +- }); +- +- const [stageX, stageY] = this.get_transformed_position(); +- const thumbWidth = this.allocation.get_width(); +- const thumbHeight = this.allocation.get_height(); +- const tipWidth = this._tooltip.width; +- const xOffset = Math.floor((thumbWidth - tipWidth) / 2); +- const monitor = Main.layoutManager.findMonitorForActor(this); +- const x = Math.clamp( +- stageX + xOffset, +- monitor.x, +- monitor.x + monitor.width - tipWidth); +- const y = stageY + thumbHeight + TOOLTIP_OFFSET; +- this._tooltip.set_position(x, y); +- } +- +- this._tooltip.ease({ +- opacity: this.hover ? 255 : 0, +- duration: TOOLTIP_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => (this._tooltip.visible = this.hover), +- }); +- } +- +- _onDestroy() { +- this._tooltip.destroy(); +- } +-} +- +-class WorkspaceIndicator extends PanelMenu.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor() { +- super(0.5, _('Workspace Indicator')); +- +- let container = new St.Widget({ +- layout_manager: new Clutter.BinLayout(), +- x_expand: true, +- y_expand: true, +- }); +- this.add_child(container); +- +- let workspaceManager = global.workspace_manager; +- +- this._currentWorkspace = workspaceManager.get_active_workspace_index(); +- this._statusLabel = new St.Label({ +- style_class: 'panel-workspace-indicator', +- y_align: Clutter.ActorAlign.CENTER, +- text: this._labelText(), +- }); +- +- container.add_child(this._statusLabel); +- +- this._thumbnailsBox = new St.BoxLayout({ +- style_class: 'panel-workspace-indicator-box', +- y_expand: true, +- reactive: true, +- }); +- +- container.add_child(this._thumbnailsBox); +- +- this._workspacesItems = []; +- this._workspaceSection = new PopupMenu.PopupMenuSection(); +- this.menu.addMenuItem(this._workspaceSection); +- +- workspaceManager.connectObject( +- 'notify::n-workspaces', this._nWorkspacesChanged.bind(this), GObject.ConnectFlags.AFTER, +- 'workspace-switched', this._onWorkspaceSwitched.bind(this), GObject.ConnectFlags.AFTER, +- 'notify::layout-rows', this._updateThumbnailVisibility.bind(this), +- this); +- +- this.connect('scroll-event', this._onScrollEvent.bind(this)); +- this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this)); +- this._createWorkspacesSection(); +- this._updateThumbnails(); +- this._updateThumbnailVisibility(); +- +- this._settings = new Gio.Settings({schema_id: WORKSPACE_SCHEMA}); +- this._settings.connectObject(`changed::${WORKSPACE_KEY}`, +- this._updateMenuLabels.bind(this), this); +- } +- +- _onDestroy() { +- Main.panel.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); +- +- super._onDestroy(); +- } +- +- _updateThumbnailVisibility() { +- const {workspaceManager} = global; +- const vertical = workspaceManager.layout_rows === -1; +- const useMenu = +- vertical || workspaceManager.n_workspaces > MAX_THUMBNAILS; +- this.reactive = useMenu; +- +- this._statusLabel.visible = useMenu; +- this._thumbnailsBox.visible = !useMenu; +- +- // Disable offscreen-redirect when showing the workspace switcher +- // so that clip-to-allocation works +- Main.panel.set_offscreen_redirect(useMenu +- ? Clutter.OffscreenRedirect.ALWAYS +- : Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY); +- } +- +- _onWorkspaceSwitched() { +- this._currentWorkspace = global.workspace_manager.get_active_workspace_index(); + +- this._updateMenuOrnament(); +- this._updateActiveThumbnail(); +- +- this._statusLabel.set_text(this._labelText()); +- } +- +- _nWorkspacesChanged() { +- this._createWorkspacesSection(); +- this._updateThumbnails(); +- this._updateThumbnailVisibility(); +- } +- +- _updateMenuOrnament() { +- for (let i = 0; i < this._workspacesItems.length; i++) { +- this._workspacesItems[i].setOrnament(i === this._currentWorkspace +- ? PopupMenu.Ornament.DOT +- : PopupMenu.Ornament.NO_DOT); +- } +- } +- +- _updateActiveThumbnail() { +- let thumbs = this._thumbnailsBox.get_children(); +- for (let i = 0; i < thumbs.length; i++) { +- if (i === this._currentWorkspace) +- thumbs[i].add_style_class_name('active'); +- else +- thumbs[i].remove_style_class_name('active'); +- } +- } +- +- _labelText(workspaceIndex) { +- if (workspaceIndex === undefined) { +- workspaceIndex = this._currentWorkspace; +- return (workspaceIndex + 1).toString(); +- } +- return Meta.prefs_get_workspace_name(workspaceIndex); +- } +- +- _updateMenuLabels() { +- for (let i = 0; i < this._workspacesItems.length; i++) +- this._workspacesItems[i].label.text = this._labelText(i); +- } +- +- _createWorkspacesSection() { +- let workspaceManager = global.workspace_manager; +- +- this._workspaceSection.removeAll(); +- this._workspacesItems = []; +- this._currentWorkspace = workspaceManager.get_active_workspace_index(); +- +- let i = 0; +- for (; i < workspaceManager.n_workspaces; i++) { +- this._workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i)); +- this._workspaceSection.addMenuItem(this._workspacesItems[i]); +- this._workspacesItems[i].workspaceId = i; +- this._workspacesItems[i].label_actor = this._statusLabel; +- this._workspacesItems[i].connect('activate', (actor, _event) => { +- this._activate(actor.workspaceId); +- }); +- +- this._workspacesItems[i].setOrnament(i === this._currentWorkspace +- ? PopupMenu.Ornament.DOT +- : PopupMenu.Ornament.NO_DOT); +- } +- +- this._statusLabel.set_text(this._labelText()); +- } +- +- _updateThumbnails() { +- let workspaceManager = global.workspace_manager; +- +- this._thumbnailsBox.destroy_all_children(); +- +- for (let i = 0; i < workspaceManager.n_workspaces; i++) { +- let thumb = new WorkspaceThumbnail(i); +- this._thumbnailsBox.add_child(thumb); +- } +- this._updateActiveThumbnail(); +- } +- +- _activate(index) { +- let workspaceManager = global.workspace_manager; +- +- if (index >= 0 && index < workspaceManager.n_workspaces) { +- let metaWorkspace = workspaceManager.get_workspace_by_index(index); +- metaWorkspace.activate(global.get_current_time()); +- } +- } +- +- _onScrollEvent(actor, event) { +- let direction = event.get_scroll_direction(); +- let diff = 0; +- if (direction === Clutter.ScrollDirection.DOWN) +- diff = 1; +- else if (direction === Clutter.ScrollDirection.UP) +- diff = -1; +- else +- return; +- +- +- let newIndex = global.workspace_manager.get_active_workspace_index() + diff; +- this._activate(newIndex); +- } +-} ++import {WorkspaceIndicator} from './workspaceIndicator.js'; + + export default class WorkspaceIndicatorExtension extends Extension { + enable() { +diff --git a/extensions/workspace-indicator/meson.build b/extensions/workspace-indicator/meson.build +index 36daa535..6dd08dae 100644 +--- a/extensions/workspace-indicator/meson.build ++++ b/extensions/workspace-indicator/meson.build +@@ -9,4 +9,4 @@ extension_data += configure_file( + ) + extension_data += files('stylesheet.css') + +-extension_sources += files('prefs.js') ++extension_sources += files('prefs.js', 'workspaceIndicator.js') +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +new file mode 100644 +index 00000000..6b0903d5 +--- /dev/null ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -0,0 +1,438 @@ ++// SPDX-FileCopyrightText: 2011 Erick Pérez Castellanos ++// SPDX-FileCopyrightText: 2011 Giovanni Campagna ++// SPDX-FileCopyrightText: 2017 Florian Müllner ++// ++// SPDX-License-Identifier: GPL-2.0-or-later ++ ++import Clutter from 'gi://Clutter'; ++import Gio from 'gi://Gio'; ++import GObject from 'gi://GObject'; ++import Meta from 'gi://Meta'; ++import St from 'gi://St'; ++ ++import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; ++ ++import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; ++import * as Main from 'resource:///org/gnome/shell/ui/main.js'; ++import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; ++import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; ++ ++const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences'; ++const WORKSPACE_KEY = 'workspace-names'; ++ ++const TOOLTIP_OFFSET = 6; ++const TOOLTIP_ANIMATION_TIME = 150; ++ ++const MAX_THUMBNAILS = 6; ++ ++class WindowPreview extends St.Button { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(window) { ++ super({ ++ style_class: 'workspace-indicator-window-preview', ++ }); ++ ++ this._delegate = this; ++ DND.makeDraggable(this, {restoreOnSuccess: true}); ++ ++ this._window = window; ++ ++ this._window.connectObject( ++ 'size-changed', () => this._checkRelayout(), ++ 'position-changed', () => this._checkRelayout(), ++ 'notify::minimized', this._updateVisible.bind(this), ++ 'notify::window-type', this._updateVisible.bind(this), ++ this); ++ this._updateVisible(); ++ ++ global.display.connectObject('notify::focus-window', ++ this._onFocusChanged.bind(this), this); ++ this._onFocusChanged(); ++ } ++ ++ // needed for DND ++ get metaWindow() { ++ return this._window; ++ } ++ ++ _onFocusChanged() { ++ if (global.display.focus_window === this._window) ++ this.add_style_class_name('active'); ++ else ++ this.remove_style_class_name('active'); ++ } ++ ++ _checkRelayout() { ++ const monitor = Main.layoutManager.findIndexForActor(this); ++ const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); ++ if (this._window.get_frame_rect().overlap(workArea)) ++ this.queue_relayout(); ++ } ++ ++ _updateVisible() { ++ this.visible = this._window.window_type !== Meta.WindowType.DESKTOP && ++ this._window.showing_on_its_workspace(); ++ } ++} ++ ++class WorkspaceLayout extends Clutter.LayoutManager { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ vfunc_get_preferred_width() { ++ return [0, 0]; ++ } ++ ++ vfunc_get_preferred_height() { ++ return [0, 0]; ++ } ++ ++ vfunc_allocate(container, box) { ++ const monitor = Main.layoutManager.findIndexForActor(container); ++ const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); ++ const hscale = box.get_width() / workArea.width; ++ const vscale = box.get_height() / workArea.height; ++ ++ for (const child of container) { ++ const childBox = new Clutter.ActorBox(); ++ const frameRect = child.metaWindow.get_frame_rect(); ++ childBox.set_size( ++ Math.round(Math.min(frameRect.width, workArea.width) * hscale), ++ Math.round(Math.min(frameRect.height, workArea.height) * vscale)); ++ childBox.set_origin( ++ Math.round((frameRect.x - workArea.x) * hscale), ++ Math.round((frameRect.y - workArea.y) * vscale)); ++ child.allocate(childBox); ++ } ++ } ++} ++ ++class WorkspaceThumbnail extends St.Button { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(index) { ++ super({ ++ style_class: 'workspace', ++ child: new Clutter.Actor({ ++ layout_manager: new WorkspaceLayout(), ++ clip_to_allocation: true, ++ x_expand: true, ++ y_expand: true, ++ }), ++ }); ++ ++ this._tooltip = new St.Label({ ++ style_class: 'dash-label', ++ visible: false, ++ }); ++ Main.uiGroup.add_child(this._tooltip); ++ ++ this.connect('destroy', this._onDestroy.bind(this)); ++ this.connect('notify::hover', this._syncTooltip.bind(this)); ++ ++ this._index = index; ++ this._delegate = this; // needed for DND ++ ++ this._windowPreviews = new Map(); ++ ++ let workspaceManager = global.workspace_manager; ++ this._workspace = workspaceManager.get_workspace_by_index(index); ++ ++ this._workspace.connectObject( ++ 'window-added', (ws, window) => this._addWindow(window), ++ 'window-removed', (ws, window) => this._removeWindow(window), ++ this); ++ ++ global.display.connectObject('restacked', ++ this._onRestacked.bind(this), this); ++ ++ this._workspace.list_windows().forEach(w => this._addWindow(w)); ++ this._onRestacked(); ++ } ++ ++ acceptDrop(source) { ++ if (!source.metaWindow) ++ return false; ++ ++ this._moveWindow(source.metaWindow); ++ return true; ++ } ++ ++ handleDragOver(source) { ++ if (source.metaWindow) ++ return DND.DragMotionResult.MOVE_DROP; ++ else ++ return DND.DragMotionResult.CONTINUE; ++ } ++ ++ _addWindow(window) { ++ if (this._windowPreviews.has(window)) ++ return; ++ ++ let preview = new WindowPreview(window); ++ preview.connect('clicked', (a, btn) => this.emit('clicked', btn)); ++ this._windowPreviews.set(window, preview); ++ this.child.add_child(preview); ++ } ++ ++ _removeWindow(window) { ++ let preview = this._windowPreviews.get(window); ++ if (!preview) ++ return; ++ ++ this._windowPreviews.delete(window); ++ preview.destroy(); ++ } ++ ++ _onRestacked() { ++ let lastPreview = null; ++ let windows = global.get_window_actors().map(a => a.meta_window); ++ for (let i = 0; i < windows.length; i++) { ++ let preview = this._windowPreviews.get(windows[i]); ++ if (!preview) ++ continue; ++ ++ this.child.set_child_above_sibling(preview, lastPreview); ++ lastPreview = preview; ++ } ++ } ++ ++ _moveWindow(window) { ++ let monitorIndex = Main.layoutManager.findIndexForActor(this); ++ if (monitorIndex !== window.get_monitor()) ++ window.move_to_monitor(monitorIndex); ++ window.change_workspace_by_index(this._index, false); ++ } ++ ++ on_clicked() { ++ let ws = global.workspace_manager.get_workspace_by_index(this._index); ++ if (ws) ++ ws.activate(global.get_current_time()); ++ } ++ ++ _syncTooltip() { ++ if (this.hover) { ++ this._tooltip.set({ ++ text: Meta.prefs_get_workspace_name(this._index), ++ visible: true, ++ opacity: 0, ++ }); ++ ++ const [stageX, stageY] = this.get_transformed_position(); ++ const thumbWidth = this.allocation.get_width(); ++ const thumbHeight = this.allocation.get_height(); ++ const tipWidth = this._tooltip.width; ++ const xOffset = Math.floor((thumbWidth - tipWidth) / 2); ++ const monitor = Main.layoutManager.findMonitorForActor(this); ++ const x = Math.clamp( ++ stageX + xOffset, ++ monitor.x, ++ monitor.x + monitor.width - tipWidth); ++ const y = stageY + thumbHeight + TOOLTIP_OFFSET; ++ this._tooltip.set_position(x, y); ++ } ++ ++ this._tooltip.ease({ ++ opacity: this.hover ? 255 : 0, ++ duration: TOOLTIP_ANIMATION_TIME, ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ onComplete: () => (this._tooltip.visible = this.hover), ++ }); ++ } ++ ++ _onDestroy() { ++ this._tooltip.destroy(); ++ } ++} ++ ++export class WorkspaceIndicator extends PanelMenu.Button { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor() { ++ super(0.5, _('Workspace Indicator')); ++ ++ let container = new St.Widget({ ++ layout_manager: new Clutter.BinLayout(), ++ x_expand: true, ++ y_expand: true, ++ }); ++ this.add_child(container); ++ ++ let workspaceManager = global.workspace_manager; ++ ++ this._currentWorkspace = workspaceManager.get_active_workspace_index(); ++ this._statusLabel = new St.Label({ ++ style_class: 'panel-workspace-indicator', ++ y_align: Clutter.ActorAlign.CENTER, ++ text: this._labelText(), ++ }); ++ ++ container.add_child(this._statusLabel); ++ ++ this._thumbnailsBox = new St.BoxLayout({ ++ style_class: 'panel-workspace-indicator-box', ++ y_expand: true, ++ reactive: true, ++ }); ++ ++ container.add_child(this._thumbnailsBox); ++ ++ this._workspacesItems = []; ++ this._workspaceSection = new PopupMenu.PopupMenuSection(); ++ this.menu.addMenuItem(this._workspaceSection); ++ ++ workspaceManager.connectObject( ++ 'notify::n-workspaces', this._nWorkspacesChanged.bind(this), GObject.ConnectFlags.AFTER, ++ 'workspace-switched', this._onWorkspaceSwitched.bind(this), GObject.ConnectFlags.AFTER, ++ 'notify::layout-rows', this._updateThumbnailVisibility.bind(this), ++ this); ++ ++ this.connect('scroll-event', this._onScrollEvent.bind(this)); ++ this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this)); ++ this._createWorkspacesSection(); ++ this._updateThumbnails(); ++ this._updateThumbnailVisibility(); ++ ++ this._settings = new Gio.Settings({schema_id: WORKSPACE_SCHEMA}); ++ this._settings.connectObject(`changed::${WORKSPACE_KEY}`, ++ this._updateMenuLabels.bind(this), this); ++ } ++ ++ _onDestroy() { ++ Main.panel.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); ++ ++ super._onDestroy(); ++ } ++ ++ _updateThumbnailVisibility() { ++ const {workspaceManager} = global; ++ const vertical = workspaceManager.layout_rows === -1; ++ const useMenu = ++ vertical || workspaceManager.n_workspaces > MAX_THUMBNAILS; ++ this.reactive = useMenu; ++ ++ this._statusLabel.visible = useMenu; ++ this._thumbnailsBox.visible = !useMenu; ++ ++ // Disable offscreen-redirect when showing the workspace switcher ++ // so that clip-to-allocation works ++ Main.panel.set_offscreen_redirect(useMenu ++ ? Clutter.OffscreenRedirect.ALWAYS ++ : Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY); ++ } ++ ++ _onWorkspaceSwitched() { ++ this._currentWorkspace = global.workspace_manager.get_active_workspace_index(); ++ ++ this._updateMenuOrnament(); ++ this._updateActiveThumbnail(); ++ ++ this._statusLabel.set_text(this._labelText()); ++ } ++ ++ _nWorkspacesChanged() { ++ this._createWorkspacesSection(); ++ this._updateThumbnails(); ++ this._updateThumbnailVisibility(); ++ } ++ ++ _updateMenuOrnament() { ++ for (let i = 0; i < this._workspacesItems.length; i++) { ++ this._workspacesItems[i].setOrnament(i === this._currentWorkspace ++ ? PopupMenu.Ornament.DOT ++ : PopupMenu.Ornament.NO_DOT); ++ } ++ } ++ ++ _updateActiveThumbnail() { ++ let thumbs = this._thumbnailsBox.get_children(); ++ for (let i = 0; i < thumbs.length; i++) { ++ if (i === this._currentWorkspace) ++ thumbs[i].add_style_class_name('active'); ++ else ++ thumbs[i].remove_style_class_name('active'); ++ } ++ } ++ ++ _labelText(workspaceIndex) { ++ if (workspaceIndex === undefined) { ++ workspaceIndex = this._currentWorkspace; ++ return (workspaceIndex + 1).toString(); ++ } ++ return Meta.prefs_get_workspace_name(workspaceIndex); ++ } ++ ++ _updateMenuLabels() { ++ for (let i = 0; i < this._workspacesItems.length; i++) ++ this._workspacesItems[i].label.text = this._labelText(i); ++ } ++ ++ _createWorkspacesSection() { ++ let workspaceManager = global.workspace_manager; ++ ++ this._workspaceSection.removeAll(); ++ this._workspacesItems = []; ++ this._currentWorkspace = workspaceManager.get_active_workspace_index(); ++ ++ let i = 0; ++ for (; i < workspaceManager.n_workspaces; i++) { ++ this._workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i)); ++ this._workspaceSection.addMenuItem(this._workspacesItems[i]); ++ this._workspacesItems[i].workspaceId = i; ++ this._workspacesItems[i].label_actor = this._statusLabel; ++ this._workspacesItems[i].connect('activate', (actor, _event) => { ++ this._activate(actor.workspaceId); ++ }); ++ ++ this._workspacesItems[i].setOrnament(i === this._currentWorkspace ++ ? PopupMenu.Ornament.DOT ++ : PopupMenu.Ornament.NO_DOT); ++ } ++ ++ this._statusLabel.set_text(this._labelText()); ++ } ++ ++ _updateThumbnails() { ++ let workspaceManager = global.workspace_manager; ++ ++ this._thumbnailsBox.destroy_all_children(); ++ ++ for (let i = 0; i < workspaceManager.n_workspaces; i++) { ++ let thumb = new WorkspaceThumbnail(i); ++ this._thumbnailsBox.add_child(thumb); ++ } ++ this._updateActiveThumbnail(); ++ } ++ ++ _activate(index) { ++ let workspaceManager = global.workspace_manager; ++ ++ if (index >= 0 && index < workspaceManager.n_workspaces) { ++ let metaWorkspace = workspaceManager.get_workspace_by_index(index); ++ metaWorkspace.activate(global.get_current_time()); ++ } ++ } ++ ++ _onScrollEvent(actor, event) { ++ let direction = event.get_scroll_direction(); ++ let diff = 0; ++ if (direction === Clutter.ScrollDirection.DOWN) ++ diff = 1; ++ else if (direction === Clutter.ScrollDirection.UP) ++ diff = -1; ++ else ++ return; ++ ++ ++ let newIndex = global.workspace_manager.get_active_workspace_index() + diff; ++ this._activate(newIndex); ++ } ++} +diff --git a/po/POTFILES.in b/po/POTFILES.in +index b77f6c21..182b2be0 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -20,5 +20,5 @@ extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml + extensions/window-list/prefs.js + extensions/window-list/workspaceIndicator.js + extensions/windowsNavigator/extension.js +-extensions/workspace-indicator/extension.js + extensions/workspace-indicator/prefs.js ++extensions/workspace-indicator/workspaceIndicator.js +-- +2.44.0 + diff --git a/more-ws-previews-0002-workspace-indicator-Use-descendant-style-selectors.patch b/more-ws-previews-0002-workspace-indicator-Use-descendant-style-selectors.patch new file mode 100644 index 0000000..04127f1 --- /dev/null +++ b/more-ws-previews-0002-workspace-indicator-Use-descendant-style-selectors.patch @@ -0,0 +1,81 @@ +From 5bb91b4303bb0696dce4ad7aeb31035e89c1e9ce Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 19:09:38 +0100 +Subject: [PATCH 02/28] workspace-indicator: Use descendant style selectors + +Add a style class to the indicator itself, and only select +descendant elements. This allows using the briefer class names +from the window-list extension without too much risk of conflicts. + +Part-of: +--- + extensions/workspace-indicator/stylesheet.css | 8 ++++---- + extensions/workspace-indicator/workspaceIndicator.js | 6 ++++-- + 2 files changed, 8 insertions(+), 6 deletions(-) + +diff --git a/extensions/workspace-indicator/stylesheet.css b/extensions/workspace-indicator/stylesheet.css +index 7b53a46f..749878c1 100644 +--- a/extensions/workspace-indicator/stylesheet.css ++++ b/extensions/workspace-indicator/stylesheet.css +@@ -5,23 +5,23 @@ + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +-.panel-workspace-indicator { ++.workspace-indicator .status-label { + padding: 0 8px; + } + +-.panel-workspace-indicator-box { ++.workspace-indicator .workspaces-box { + padding: 4px 0; + spacing: 4px; + } + +-.panel-workspace-indicator-box .workspace { ++.workspace-indicator .workspace { + width: 40px; + border: 2px solid #000; + border-radius: 2px; + background-color: #595959; + } + +-.panel-workspace-indicator-box .workspace.active { ++.workspace-indicator .workspace.active { + border-color: #fff; + } + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 6b0903d5..4bf9c0a2 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -259,6 +259,8 @@ export class WorkspaceIndicator extends PanelMenu.Button { + constructor() { + super(0.5, _('Workspace Indicator')); + ++ this.add_style_class_name('workspace-indicator'); ++ + let container = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, +@@ -270,7 +272,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + this._currentWorkspace = workspaceManager.get_active_workspace_index(); + this._statusLabel = new St.Label({ +- style_class: 'panel-workspace-indicator', ++ style_class: 'status-label', + y_align: Clutter.ActorAlign.CENTER, + text: this._labelText(), + }); +@@ -278,7 +280,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + container.add_child(this._statusLabel); + + this._thumbnailsBox = new St.BoxLayout({ +- style_class: 'panel-workspace-indicator-box', ++ style_class: 'workspaces-box', + y_expand: true, + reactive: true, + }); +-- +2.44.0 + diff --git a/more-ws-previews-0003-window-list-Use-consistent-style-class-prefix.patch b/more-ws-previews-0003-window-list-Use-consistent-style-class-prefix.patch new file mode 100644 index 0000000..e36b17c --- /dev/null +++ b/more-ws-previews-0003-window-list-Use-consistent-style-class-prefix.patch @@ -0,0 +1,68 @@ +From 962983e8019817afae63807459eeaf3ff50eab03 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 12:48:43 +0100 +Subject: [PATCH 03/28] window-list: Use consistent style class prefix + +This will eventually allow us to re-use the workspace-indicator +extension without changing anything but the used prefix. + +Part-of: +--- + extensions/window-list/stylesheet-dark.css | 4 ++-- + extensions/window-list/stylesheet-light.css | 4 ++-- + extensions/window-list/workspaceIndicator.js | 2 +- + 3 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/extensions/window-list/stylesheet-dark.css b/extensions/window-list/stylesheet-dark.css +index b4c0a3b4..fbadf4d0 100644 +--- a/extensions/window-list/stylesheet-dark.css ++++ b/extensions/window-list/stylesheet-dark.css +@@ -102,12 +102,12 @@ + background-color: #3f3f3f; + } + +-.window-list-window-preview { ++.window-list-workspace-indicator-window-preview { + background-color: #bebebe; + border-radius: 1px; + } + +-.window-list-window-preview.active { ++.window-list-workspace-indicator-window-preview.active { + background-color: #d4d4d4; + } + +diff --git a/extensions/window-list/stylesheet-light.css b/extensions/window-list/stylesheet-light.css +index e4d3a36c..e9352362 100644 +--- a/extensions/window-list/stylesheet-light.css ++++ b/extensions/window-list/stylesheet-light.css +@@ -61,11 +61,11 @@ + border-color: #888; + } + +-.window-list-window-preview { ++.window-list-workspace-indicator-window-preview { + background-color: #ededed; + border: 1px solid #ccc; + } + +-.window-list-window-preview.active { ++.window-list-workspace-indicator-window-preview.active { + background-color: #f6f5f4; + } +diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js +index 62ebffbd..5b11fe88 100644 +--- a/extensions/window-list/workspaceIndicator.js ++++ b/extensions/window-list/workspaceIndicator.js +@@ -27,7 +27,7 @@ class WindowPreview extends St.Button { + + constructor(window) { + super({ +- style_class: 'window-list-window-preview', ++ style_class: 'window-list-workspace-indicator-window-preview', + }); + + this._delegate = this; +-- +2.44.0 + diff --git a/more-ws-previews-0004-workspace-indicator-Allow-overriding-base-style-clas.patch b/more-ws-previews-0004-workspace-indicator-Allow-overriding-base-style-clas.patch new file mode 100644 index 0000000..a7b6dfe --- /dev/null +++ b/more-ws-previews-0004-workspace-indicator-Allow-overriding-base-style-clas.patch @@ -0,0 +1,57 @@ +From cadf2bb80984ec3b27b2d22ad65ec675230905c4 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 23 Feb 2024 01:59:15 +0100 +Subject: [PATCH 04/28] workspace-indicator: Allow overriding base style class + +This will allow reusing the code from the window-list extension +without limiting the ability to specify styling that only applies +to one of the extensions. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 13 ++++++++++--- + 1 file changed, 10 insertions(+), 3 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 4bf9c0a2..cdcc67fe 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -25,6 +25,8 @@ const TOOLTIP_ANIMATION_TIME = 150; + + const MAX_THUMBNAILS = 6; + ++let baseStyleClassName = ''; ++ + class WindowPreview extends St.Button { + static { + GObject.registerClass(this); +@@ -32,7 +34,7 @@ class WindowPreview extends St.Button { + + constructor(window) { + super({ +- style_class: 'workspace-indicator-window-preview', ++ style_class: `${baseStyleClassName}-window-preview`, + }); + + this._delegate = this; +@@ -256,10 +258,15 @@ export class WorkspaceIndicator extends PanelMenu.Button { + GObject.registerClass(this); + } + +- constructor() { ++ constructor(params = {}) { + super(0.5, _('Workspace Indicator')); + +- this.add_style_class_name('workspace-indicator'); ++ const { ++ baseStyleClass = 'workspace-indicator', ++ } = params; ++ ++ baseStyleClassName = baseStyleClass; ++ this.add_style_class_name(baseStyleClassName); + + let container = new St.Widget({ + layout_manager: new Clutter.BinLayout(), +-- +2.44.0 + diff --git a/more-ws-previews-0005-window-list-Override-base-style-class.patch b/more-ws-previews-0005-window-list-Override-base-style-class.patch new file mode 100644 index 0000000..e0d4bd9 --- /dev/null +++ b/more-ws-previews-0005-window-list-Override-base-style-class.patch @@ -0,0 +1,78 @@ +From a54815b7e6726af56c9300b9c209bb554c3a4a25 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Fri, 23 Feb 2024 01:58:50 +0100 +Subject: [PATCH 05/28] window-list: Override base style class + +Apply the changes from the last commit to the workspace-indicator +copy, and override the base style class from the extension. + +This will eventually allow us to share the exact same code between +the two extensions, but still use individual styling if necessary. + +Part-of: +--- + extensions/window-list/extension.js | 5 ++++- + extensions/window-list/workspaceIndicator.js | 15 ++++++++++++--- + 2 files changed, 16 insertions(+), 4 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 034b72ba..ab5b042c 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -767,7 +767,10 @@ class WindowList extends St.Widget { + let indicatorsBox = new St.BoxLayout({x_align: Clutter.ActorAlign.END}); + box.add_child(indicatorsBox); + +- this._workspaceIndicator = new WorkspaceIndicator(); ++ this._workspaceIndicator = new WorkspaceIndicator({ ++ baseStyleClass: 'window-list-workspace-indicator', ++ }); ++ + indicatorsBox.add_child(this._workspaceIndicator.container); + + this._mutterSettings = new Gio.Settings({schema_id: 'org.gnome.mutter'}); +diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js +index 5b11fe88..2c557e5b 100644 +--- a/extensions/window-list/workspaceIndicator.js ++++ b/extensions/window-list/workspaceIndicator.js +@@ -20,6 +20,8 @@ const TOOLTIP_ANIMATION_TIME = 150; + + const MAX_THUMBNAILS = 6; + ++let baseStyleClassName = ''; ++ + class WindowPreview extends St.Button { + static { + GObject.registerClass(this); +@@ -27,7 +29,7 @@ class WindowPreview extends St.Button { + + constructor(window) { + super({ +- style_class: 'window-list-workspace-indicator-window-preview', ++ style_class: `${baseStyleClassName}-window-preview`, + }); + + this._delegate = this; +@@ -251,10 +253,17 @@ export class WorkspaceIndicator extends PanelMenu.Button { + GObject.registerClass(this); + } + +- constructor() { ++ constructor(params = {}) { + super(0.5, _('Workspace Indicator'), true); ++ ++ const { ++ baseStyleClass = 'workspace-indicator', ++ } = params; ++ ++ baseStyleClassName = baseStyleClass; ++ this.add_style_class_name(baseStyleClassName); ++ + this.setMenu(new PopupMenu.PopupMenu(this, 0.0, St.Side.BOTTOM)); +- this.add_style_class_name('window-list-workspace-indicator'); + this.remove_style_class_name('panel-button'); + this.menu.actor.remove_style_class_name('panel-menu'); + +-- +2.44.0 + diff --git a/more-ws-previews-0006-window-list-Externally-adjust-workspace-menu.patch b/more-ws-previews-0006-window-list-Externally-adjust-workspace-menu.patch new file mode 100644 index 0000000..bfe3724 --- /dev/null +++ b/more-ws-previews-0006-window-list-Externally-adjust-workspace-menu.patch @@ -0,0 +1,92 @@ +From b539608940eb5956bbf1cb9083b7171a9d2d6708 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 12:48:43 +0100 +Subject: [PATCH 06/28] window-list: Externally adjust workspace menu + +In order to use a PanelMenu.Button in the bottom bar, we have +to tweak its menu a bit. + +We currently handle this inside the indicator, but that means the +code diverges from the original code in the workspace-indicator +extension. + +Avoid this by using a small subclass that handles the adjustments. + +Part-of: +--- + extensions/window-list/extension.js | 25 ++++++++++++++++++-- + extensions/window-list/workspaceIndicator.js | 6 +---- + 2 files changed, 24 insertions(+), 7 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index ab5b042c..1bb7cf60 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -767,10 +767,9 @@ class WindowList extends St.Widget { + let indicatorsBox = new St.BoxLayout({x_align: Clutter.ActorAlign.END}); + box.add_child(indicatorsBox); + +- this._workspaceIndicator = new WorkspaceIndicator({ ++ this._workspaceIndicator = new BottomWorkspaceIndicator({ + baseStyleClass: 'window-list-workspace-indicator', + }); +- + indicatorsBox.add_child(this._workspaceIndicator.container); + + this._mutterSettings = new Gio.Settings({schema_id: 'org.gnome.mutter'}); +@@ -1100,6 +1099,28 @@ class WindowList extends St.Widget { + } + } + ++class BottomWorkspaceIndicator extends WorkspaceIndicator { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ super(params); ++ ++ this.remove_style_class_name('panel-button'); ++ } ++ ++ setMenu(menu) { ++ super.setMenu(menu); ++ ++ if (!menu) ++ return; ++ ++ this.menu.actor.updateArrowSide(St.Side.BOTTOM); ++ this.menu.actor.remove_style_class_name('panel-menu'); ++ } ++} ++ + export default class WindowListExtension extends Extension { + constructor(metadata) { + super(metadata); +diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js +index 2c557e5b..69167eb6 100644 +--- a/extensions/window-list/workspaceIndicator.js ++++ b/extensions/window-list/workspaceIndicator.js +@@ -254,7 +254,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + + constructor(params = {}) { +- super(0.5, _('Workspace Indicator'), true); ++ super(0.5, _('Workspace Indicator')); + + const { + baseStyleClass = 'workspace-indicator', +@@ -263,10 +263,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + baseStyleClassName = baseStyleClass; + this.add_style_class_name(baseStyleClassName); + +- this.setMenu(new PopupMenu.PopupMenu(this, 0.0, St.Side.BOTTOM)); +- this.remove_style_class_name('panel-button'); +- this.menu.actor.remove_style_class_name('panel-menu'); +- + let container = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + x_expand: true, +-- +2.44.0 + diff --git a/more-ws-previews-0007-window-list-Handle-changes-to-workspace-menu.patch b/more-ws-previews-0007-window-list-Handle-changes-to-workspace-menu.patch new file mode 100644 index 0000000..7734a9c --- /dev/null +++ b/more-ws-previews-0007-window-list-Handle-changes-to-workspace-menu.patch @@ -0,0 +1,44 @@ +From dca2c8505c770958849fc8dda27715a09ceab3c5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 21 Mar 2024 16:49:35 +0100 +Subject: [PATCH 07/28] window-list: Handle changes to workspace menu + +For now the menu is always set at construction time, however this +will change in the future. Prepare for that by handling the +`menu-set` signal, similar to the top bar. + +Part-of: +--- + extensions/window-list/extension.js | 9 ++++++++- + 1 file changed, 8 insertions(+), 1 deletion(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 1bb7cf60..3950c535 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -782,7 +782,9 @@ class WindowList extends St.Widget { + this._updateWorkspaceIndicatorVisibility(); + + this._menuManager = new PopupMenu.PopupMenuManager(this); +- this._menuManager.addMenu(this._workspaceIndicator.menu); ++ this._workspaceIndicator.connectObject('menu-set', ++ () => this._onWorkspaceMenuSet(), this); ++ this._onWorkspaceMenuSet(); + + Main.layoutManager.addChrome(this, { + affectsStruts: true, +@@ -879,6 +881,11 @@ class WindowList extends St.Widget { + children[newActive].activate(); + } + ++ _onWorkspaceMenuSet() { ++ if (this._workspaceIndicator.menu) ++ this._menuManager.addMenu(this._workspaceIndicator.menu); ++ } ++ + _updatePosition() { + this.set_position( + this._monitor.x, +-- +2.44.0 + diff --git a/more-ws-previews-0008-workspace-indicator-Don-t-use-SCHEMA-KEY-constants.patch b/more-ws-previews-0008-workspace-indicator-Don-t-use-SCHEMA-KEY-constants.patch new file mode 100644 index 0000000..0c235c8 --- /dev/null +++ b/more-ws-previews-0008-workspace-indicator-Don-t-use-SCHEMA-KEY-constants.patch @@ -0,0 +1,47 @@ +From c343e1d65ddc538972640e7da86429cbd4cf2829 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 15:58:39 +0100 +Subject: [PATCH 08/28] workspace-indicator: Don't use SCHEMA/KEY constants + +Each constant is only used once, so all they do is disconnect +the actual value from the code that uses it. + +The copy in the window-list extension just uses the strings directly, +do the same here. + +Part-of: +--- + extensions/workspace-indicator/workspaceIndicator.js | 10 ++++------ + 1 file changed, 4 insertions(+), 6 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index cdcc67fe..fe54b95c 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -17,9 +17,6 @@ import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; + import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +-const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences'; +-const WORKSPACE_KEY = 'workspace-names'; +- + const TOOLTIP_OFFSET = 6; + const TOOLTIP_ANIMATION_TIME = 150; + +@@ -310,9 +307,10 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._updateThumbnails(); + this._updateThumbnailVisibility(); + +- this._settings = new Gio.Settings({schema_id: WORKSPACE_SCHEMA}); +- this._settings.connectObject(`changed::${WORKSPACE_KEY}`, +- this._updateMenuLabels.bind(this), this); ++ const desktopSettings = ++ new Gio.Settings({schema_id: 'org.gnome.desktop.wm.preferences'}); ++ desktopSettings.connectObject('changed::workspace-names', ++ () => this._updateMenuLabels(), this); + } + + _onDestroy() { +-- +2.44.0 + diff --git a/more-ws-previews-0009-workspace-indicator-Use-existing-property.patch b/more-ws-previews-0009-workspace-indicator-Use-existing-property.patch new file mode 100644 index 0000000..8f0a89e --- /dev/null +++ b/more-ws-previews-0009-workspace-indicator-Use-existing-property.patch @@ -0,0 +1,29 @@ +From 14ab607a7695297efe98b97ab023973055189cee Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 18:59:23 +0100 +Subject: [PATCH 09/28] workspace-indicator: Use existing property + +We already track the current workspace index, use that +instead of getting it from the workspace manager again. + +Part-of: +--- + extensions/workspace-indicator/workspaceIndicator.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index fe54b95c..26c9d7ee 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -439,7 +439,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + return; + + +- let newIndex = global.workspace_manager.get_active_workspace_index() + diff; ++ const newIndex = this._currentWorkspace + diff; + this._activate(newIndex); + } + } +-- +2.44.0 + diff --git a/more-ws-previews-0010-workspace-indicator-Don-t-use-menu-section.patch b/more-ws-previews-0010-workspace-indicator-Don-t-use-menu-section.patch new file mode 100644 index 0000000..57d8768 --- /dev/null +++ b/more-ws-previews-0010-workspace-indicator-Don-t-use-menu-section.patch @@ -0,0 +1,70 @@ +From c83fd93d2e7cae124a5fddcb87f415688b3a63b6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 16:14:24 +0100 +Subject: [PATCH 10/28] workspace-indicator: Don't use menu section + +We never added anything else to the menu, so we can just operate +on the entire menu instead of an intermediate section. + +This removes another difference with the window-list copy. + +Part-of: +--- + extensions/workspace-indicator/workspaceIndicator.js | 12 +++++------- + 1 file changed, 5 insertions(+), 7 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 26c9d7ee..117c7a65 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -292,8 +292,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + container.add_child(this._thumbnailsBox); + + this._workspacesItems = []; +- this._workspaceSection = new PopupMenu.PopupMenuSection(); +- this.menu.addMenuItem(this._workspaceSection); + + workspaceManager.connectObject( + 'notify::n-workspaces', this._nWorkspacesChanged.bind(this), GObject.ConnectFlags.AFTER, +@@ -303,7 +301,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + this.connect('scroll-event', this._onScrollEvent.bind(this)); + this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this)); +- this._createWorkspacesSection(); ++ this._updateMenu(); + this._updateThumbnails(); + this._updateThumbnailVisibility(); + +@@ -346,7 +344,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + + _nWorkspacesChanged() { +- this._createWorkspacesSection(); ++ this._updateMenu(); + this._updateThumbnails(); + this._updateThumbnailVisibility(); + } +@@ -382,17 +380,17 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._workspacesItems[i].label.text = this._labelText(i); + } + +- _createWorkspacesSection() { ++ _updateMenu() { + let workspaceManager = global.workspace_manager; + +- this._workspaceSection.removeAll(); ++ this.menu.removeAll(); + this._workspacesItems = []; + this._currentWorkspace = workspaceManager.get_active_workspace_index(); + + let i = 0; + for (; i < workspaceManager.n_workspaces; i++) { + this._workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i)); +- this._workspaceSection.addMenuItem(this._workspacesItems[i]); ++ this.menu.addMenuItem(this._workspacesItems[i]); + this._workspacesItems[i].workspaceId = i; + this._workspacesItems[i].label_actor = this._statusLabel; + this._workspacesItems[i].connect('activate', (actor, _event) => { +-- +2.44.0 + diff --git a/more-ws-previews-0011-workspace-indicator-Support-showing-tooltips-above.patch b/more-ws-previews-0011-workspace-indicator-Support-showing-tooltips-above.patch new file mode 100644 index 0000000..b9711bd --- /dev/null +++ b/more-ws-previews-0011-workspace-indicator-Support-showing-tooltips-above.patch @@ -0,0 +1,44 @@ +From 8105cdf701db7af78bc2e17da6e01172ce7362c3 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 13:05:15 +0100 +Subject: [PATCH 11/28] workspace-indicator: Support showing tooltips above + +The indicator is located in the top bar, so tooltips are always +shown below the previews. However supporting showing tooltips +above previews when space permits allows the same code to be +used in the copy that is included with the window-list extension. + +Part-of: +--- + extensions/workspace-indicator/workspaceIndicator.js | 9 +++++---- + 1 file changed, 5 insertions(+), 4 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 117c7a65..9a1055aa 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -224,16 +224,17 @@ class WorkspaceThumbnail extends St.Button { + }); + + const [stageX, stageY] = this.get_transformed_position(); +- const thumbWidth = this.allocation.get_width(); +- const thumbHeight = this.allocation.get_height(); +- const tipWidth = this._tooltip.width; ++ const [thumbWidth, thumbHeight] = this.allocation.get_size(); ++ const [tipWidth, tipHeight] = this._tooltip.get_size(); + const xOffset = Math.floor((thumbWidth - tipWidth) / 2); + const monitor = Main.layoutManager.findMonitorForActor(this); + const x = Math.clamp( + stageX + xOffset, + monitor.x, + monitor.x + monitor.width - tipWidth); +- const y = stageY + thumbHeight + TOOLTIP_OFFSET; ++ const y = stageY - monitor.y > thumbHeight + TOOLTIP_OFFSET ++ ? stageY - tipHeight - TOOLTIP_OFFSET // show above ++ : stageY + thumbHeight + TOOLTIP_OFFSET; // show below + this._tooltip.set_position(x, y); + } + +-- +2.44.0 + diff --git a/more-ws-previews-0012-workspace-indicator-Only-change-top-bar-redirect-whe.patch b/more-ws-previews-0012-workspace-indicator-Only-change-top-bar-redirect-whe.patch new file mode 100644 index 0000000..3f4af0b --- /dev/null +++ b/more-ws-previews-0012-workspace-indicator-Only-change-top-bar-redirect-whe.patch @@ -0,0 +1,67 @@ +From d595d168ed0fccf57b1995a4d36169d978820a7b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 17:37:16 +0100 +Subject: [PATCH 12/28] workspace-indicator: Only change top bar redirect when + in top bar + +While this is always the case for the workspace indicator, adding +the check will allow to use the same code in the window list. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 23 +++++++++++++++++-- + 1 file changed, 21 insertions(+), 2 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 9a1055aa..f118654e 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -302,6 +302,16 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + this.connect('scroll-event', this._onScrollEvent.bind(this)); + this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this)); ++ ++ this._inTopBar = false; ++ this.connect('notify::realized', () => { ++ if (!this.realized) ++ return; ++ ++ this._inTopBar = Main.panel.contains(this); ++ this._updateTopBarRedirect(); ++ }); ++ + this._updateMenu(); + this._updateThumbnails(); + this._updateThumbnailVisibility(); +@@ -313,7 +323,9 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + + _onDestroy() { +- Main.panel.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); ++ if (this._inTopBar) ++ Main.panel.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS); ++ this._inTopBar = false; + + super._onDestroy(); + } +@@ -328,9 +340,16 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._statusLabel.visible = useMenu; + this._thumbnailsBox.visible = !useMenu; + ++ this._updateTopBarRedirect(); ++ } ++ ++ _updateTopBarRedirect() { ++ if (!this._inTopBar) ++ return; ++ + // Disable offscreen-redirect when showing the workspace switcher + // so that clip-to-allocation works +- Main.panel.set_offscreen_redirect(useMenu ++ Main.panel.set_offscreen_redirect(this._thumbnailsBox.visible + ? Clutter.OffscreenRedirect.ALWAYS + : Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY); + } +-- +2.44.0 + diff --git a/more-ws-previews-0013-workspace-indicator-Small-cleanup.patch b/more-ws-previews-0013-workspace-indicator-Small-cleanup.patch new file mode 100644 index 0000000..a3532d1 --- /dev/null +++ b/more-ws-previews-0013-workspace-indicator-Small-cleanup.patch @@ -0,0 +1,49 @@ +From 49bfa46b5981e94b240780240115fcdabd46a178 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 16:13:00 +0100 +Subject: [PATCH 13/28] workspace-indicator: Small cleanup + +The code to update the menu labels is a bit cleaner in the +window-list extension, so use that. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 19 +++++++++---------- + 1 file changed, 9 insertions(+), 10 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index f118654e..e04e93a4 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -407,19 +407,18 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._workspacesItems = []; + this._currentWorkspace = workspaceManager.get_active_workspace_index(); + +- let i = 0; +- for (; i < workspaceManager.n_workspaces; i++) { +- this._workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i)); +- this.menu.addMenuItem(this._workspacesItems[i]); +- this._workspacesItems[i].workspaceId = i; +- this._workspacesItems[i].label_actor = this._statusLabel; +- this._workspacesItems[i].connect('activate', (actor, _event) => { +- this._activate(actor.workspaceId); +- }); ++ for (let i = 0; i < workspaceManager.n_workspaces; i++) { ++ const item = new PopupMenu.PopupMenuItem(this._labelText(i)); + +- this._workspacesItems[i].setOrnament(i === this._currentWorkspace ++ item.connect('activate', ++ () => this._activate(i)); ++ ++ item.setOrnament(i === this._currentWorkspace + ? PopupMenu.Ornament.DOT + : PopupMenu.Ornament.NO_DOT); ++ ++ this.menu.addMenuItem(item); ++ this._workspacesItems[i] = item; + } + + this._statusLabel.set_text(this._labelText()); +-- +2.44.0 + diff --git a/more-ws-previews-0014-workspace-indicator-Simplify-getting-status-text.patch b/more-ws-previews-0014-workspace-indicator-Simplify-getting-status-text.patch new file mode 100644 index 0000000..95897c9 --- /dev/null +++ b/more-ws-previews-0014-workspace-indicator-Simplify-getting-status-text.patch @@ -0,0 +1,87 @@ +From 5455a97a04c6a2925f5113a85c2e09cff215941d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 16:13:00 +0100 +Subject: [PATCH 14/28] workspace-indicator: Simplify getting status text + +Currently the same method is used to get the label text for the +indicator itself and for the menu items. + +A method that behaves significantly different depending on whether +a parameter is passed is confusing, so only deal with the indicator +label and directly use the mutter API to get the workspace names +for menu items. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 24 +++++++++---------- + 1 file changed, 12 insertions(+), 12 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index e04e93a4..0538e6b2 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -279,7 +279,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._statusLabel = new St.Label({ + style_class: 'status-label', + y_align: Clutter.ActorAlign.CENTER, +- text: this._labelText(), ++ text: this._getStatusText(), + }); + + container.add_child(this._statusLabel); +@@ -360,7 +360,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._updateMenuOrnament(); + this._updateActiveThumbnail(); + +- this._statusLabel.set_text(this._labelText()); ++ this._statusLabel.set_text(this._getStatusText()); + } + + _nWorkspacesChanged() { +@@ -387,17 +387,16 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + } + +- _labelText(workspaceIndex) { +- if (workspaceIndex === undefined) { +- workspaceIndex = this._currentWorkspace; +- return (workspaceIndex + 1).toString(); +- } +- return Meta.prefs_get_workspace_name(workspaceIndex); ++ _getStatusText() { ++ const current = this._currentWorkspace + 1; ++ return `${current}`; + } + + _updateMenuLabels() { +- for (let i = 0; i < this._workspacesItems.length; i++) +- this._workspacesItems[i].label.text = this._labelText(i); ++ for (let i = 0; i < this._workspacesItems.length; i++) { ++ const item = this._workspacesItems[i]; ++ item.label.text = Meta.prefs_get_workspace_name(i); ++ } + } + + _updateMenu() { +@@ -408,7 +407,8 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._currentWorkspace = workspaceManager.get_active_workspace_index(); + + for (let i = 0; i < workspaceManager.n_workspaces; i++) { +- const item = new PopupMenu.PopupMenuItem(this._labelText(i)); ++ const name = Meta.prefs_get_workspace_name(i); ++ const item = new PopupMenu.PopupMenuItem(name); + + item.connect('activate', + () => this._activate(i)); +@@ -421,7 +421,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._workspacesItems[i] = item; + } + +- this._statusLabel.set_text(this._labelText()); ++ this._statusLabel.set_text(this._getStatusText()); + } + + _updateThumbnails() { +-- +2.44.0 + diff --git a/more-ws-previews-0015-workspace-indicator-Include-n-workspaces-in-status-l.patch b/more-ws-previews-0015-workspace-indicator-Include-n-workspaces-in-status-l.patch new file mode 100644 index 0000000..777c43e --- /dev/null +++ b/more-ws-previews-0015-workspace-indicator-Include-n-workspaces-in-status-l.patch @@ -0,0 +1,37 @@ +From f14ae4e2725b4a98ec2e11151c4162e7a6494604 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 16:35:09 +0100 +Subject: [PATCH 15/28] workspace-indicator: Include n-workspaces in status + label + +The two extensions currently use a slightly different label +in menu mode: +The workspace indicator uses the plain workspace number ("2"), +while the window list includes the number of workspaces ("2 / 4"). + +The additional information seem useful, as well as the slightly +bigger click/touch target, so copy the window-list behavior. + +Part-of: +--- + extensions/workspace-indicator/workspaceIndicator.js | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 0538e6b2..594a9e51 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -388,8 +388,9 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + + _getStatusText() { ++ const {nWorkspaces} = global.workspace_manager; + const current = this._currentWorkspace + 1; +- return `${current}`; ++ return `${current} / ${nWorkspaces}`; + } + + _updateMenuLabels() { +-- +2.44.0 + diff --git a/more-ws-previews-0016-workspace-indicator-Tweak-preview-style.patch b/more-ws-previews-0016-workspace-indicator-Tweak-preview-style.patch new file mode 100644 index 0000000..eea70c3 --- /dev/null +++ b/more-ws-previews-0016-workspace-indicator-Tweak-preview-style.patch @@ -0,0 +1,56 @@ +From aca00d6e3a8b7d1fc59b19e9855685989127d59f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 22 Feb 2024 04:45:23 +0100 +Subject: [PATCH 16/28] workspace-indicator: Tweak preview style + +Sync sizes and padding with the window-list previews. + +Tone down the colors a bit, but less then the current window-list +style where workspaces blend too much into the background and +the selection is unclear. + +Part-of: +--- + extensions/workspace-indicator/stylesheet.css | 15 ++++++++------- + 1 file changed, 8 insertions(+), 7 deletions(-) + +diff --git a/extensions/workspace-indicator/stylesheet.css b/extensions/workspace-indicator/stylesheet.css +index 749878c1..3e2ba67f 100644 +--- a/extensions/workspace-indicator/stylesheet.css ++++ b/extensions/workspace-indicator/stylesheet.css +@@ -10,24 +10,25 @@ + } + + .workspace-indicator .workspaces-box { +- padding: 4px 0; +- spacing: 4px; ++ padding: 5px; ++ spacing: 3px; + } + + .workspace-indicator .workspace { +- width: 40px; +- border: 2px solid #000; +- border-radius: 2px; +- background-color: #595959; ++ width: 52px; ++ border: 2px solid transparent; ++ border-radius: 4px; ++ background-color: #3f3f3f; + } + + .workspace-indicator .workspace.active { +- border-color: #fff; ++ border-color: #9f9f9f; + } + + .workspace-indicator-window-preview { + background-color: #bebebe; + border: 1px solid #828282; ++ border-radius: 1px; + } + + .workspace-indicator-window-preview.active { +-- +2.44.0 + diff --git a/more-ws-previews-0017-workspace-indicator-Support-light-style.patch b/more-ws-previews-0017-workspace-indicator-Support-light-style.patch new file mode 100644 index 0000000..528cb70 --- /dev/null +++ b/more-ws-previews-0017-workspace-indicator-Support-light-style.patch @@ -0,0 +1,71 @@ +From 5d7cd70f55a3da9454bffaf0bbcb32dacc8e1971 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 23:22:58 +0100 +Subject: [PATCH 17/28] workspace-indicator: Support light style + +The window-list extension already includes light styling for +its copy of the workspace indicator. Just copy that over to +support the light variant here as well. + +Part-of: +--- + extensions/workspace-indicator/meson.build | 5 +++- + .../{stylesheet.css => stylesheet-dark.css} | 0 + .../workspace-indicator/stylesheet-light.css | 25 +++++++++++++++++++ + 3 files changed, 29 insertions(+), 1 deletion(-) + rename extensions/workspace-indicator/{stylesheet.css => stylesheet-dark.css} (100%) + create mode 100644 extensions/workspace-indicator/stylesheet-light.css + +diff --git a/extensions/workspace-indicator/meson.build b/extensions/workspace-indicator/meson.build +index 6dd08dae..dada5408 100644 +--- a/extensions/workspace-indicator/meson.build ++++ b/extensions/workspace-indicator/meson.build +@@ -7,6 +7,9 @@ extension_data += configure_file( + output: metadata_name, + configuration: metadata_conf + ) +-extension_data += files('stylesheet.css') ++extension_data += files( ++ 'stylesheet-dark.css', ++ 'stylesheet-light.css', ++) + + extension_sources += files('prefs.js', 'workspaceIndicator.js') +diff --git a/extensions/workspace-indicator/stylesheet.css b/extensions/workspace-indicator/stylesheet-dark.css +similarity index 100% +rename from extensions/workspace-indicator/stylesheet.css +rename to extensions/workspace-indicator/stylesheet-dark.css +diff --git a/extensions/workspace-indicator/stylesheet-light.css b/extensions/workspace-indicator/stylesheet-light.css +new file mode 100644 +index 00000000..049b6a38 +--- /dev/null ++++ b/extensions/workspace-indicator/stylesheet-light.css +@@ -0,0 +1,25 @@ ++/* ++ * SPDX-FileCopyrightText: 2013 Florian Müllner ++ * SPDX-FileCopyrightText: 2015 Jakub Steiner ++ * ++ * SPDX-License-Identifier: GPL-2.0-or-later ++ */ ++ ++@import url("stylesheet-dark.css"); ++ ++.workspace-indicator .workspace { ++ background-color: #ccc; ++} ++ ++.workspace-indicator .workspace.active { ++ border-color: #888; ++} ++ ++.workspace-indicator-window-preview { ++ background-color: #ededed; ++ border: 1px solid #ccc; ++} ++ ++.workspace-indicator-window-preview.active { ++ background-color: #f6f5f4; ++} +-- +2.44.0 + diff --git a/more-ws-previews-0018-export-zips-Pick-up-non-default-stylesheets.patch b/more-ws-previews-0018-export-zips-Pick-up-non-default-stylesheets.patch new file mode 100644 index 0000000..f0e504d --- /dev/null +++ b/more-ws-previews-0018-export-zips-Pick-up-non-default-stylesheets.patch @@ -0,0 +1,30 @@ +From d0730c0d9314963bb784ebce408c29b8c2610fc4 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 15 Apr 2024 20:18:34 +0200 +Subject: [PATCH 18/28] export-zips: Pick up non-default stylesheets + +The window-list extension is about to import the workspace-indicator +stylesheet. Explicitly pack css files, so the stylesheet is included +in the bundle. + +Part-of: +--- + export-zips.sh | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/export-zips.sh b/export-zips.sh +index 57c62860..2d7383c3 100755 +--- a/export-zips.sh ++++ b/export-zips.sh +@@ -39,7 +39,7 @@ for f in $extensiondir/*; do + fi + + cp $srcdir/NEWS $srcdir/COPYING $f +- sources=(NEWS COPYING $(cd $f; ls *.js)) ++ sources=(NEWS COPYING $(cd $f; ls *.js *.css 2>/dev/null)) + + [ -d $f/icons ] && sources+=(icons) + +-- +2.44.0 + diff --git a/more-ws-previews-0019-window-list-Use-actual-copy-of-workspace-indicator.patch b/more-ws-previews-0019-window-list-Use-actual-copy-of-workspace-indicator.patch new file mode 100644 index 0000000..3278996 --- /dev/null +++ b/more-ws-previews-0019-window-list-Use-actual-copy-of-workspace-indicator.patch @@ -0,0 +1,589 @@ +From b15212a2d62ff93c1652ef717f778dda3a70f5e8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 21 Feb 2024 13:08:52 +0100 +Subject: [PATCH 19/28] window-list: Use actual copy of workspace-indicator + +We are now at a point where the code from the workspace-indicator +extension is usable from the window-list. + +However instead of updating the copy, go one step further and +remove it altogether, and copy the required files at build time. + +This ensures that future changes are picked up by both extensions +without duplicating any work. + +Part-of: +--- + extensions/window-list/meson.build | 29 +- + extensions/window-list/stylesheet-dark.css | 31 +- + extensions/window-list/stylesheet-light.css | 20 +- + extensions/window-list/workspaceIndicator.js | 435 ------------------- + 4 files changed, 30 insertions(+), 485 deletions(-) + delete mode 100644 extensions/window-list/workspaceIndicator.js + +diff --git a/extensions/window-list/meson.build b/extensions/window-list/meson.build +index 718cdf7a..6fd17007 100644 +--- a/extensions/window-list/meson.build ++++ b/extensions/window-list/meson.build +@@ -12,5 +12,32 @@ extension_data += files( + 'stylesheet-light.css' + ) + +-extension_sources += files('prefs.js', 'workspaceIndicator.js') ++transform_stylesheet = [ ++ 'sed', '-E', ++ '-e', 's:^\.(workspace-indicator):.window-list-\\1:', ++ '-e', '/^@import/d', ++ '@INPUT@', ++ ] ++ ++workspaceIndicatorSources = [ ++ configure_file( ++ input: '../workspace-indicator/workspaceIndicator.js', ++ output: '@PLAINNAME@', ++ copy: true, ++ ), ++ configure_file( ++ input: '../workspace-indicator/stylesheet-dark.css', ++ output: 'stylesheet-workspace-switcher-dark.css', ++ command: transform_stylesheet, ++ capture: true, ++ ), ++ configure_file( ++ input: '../workspace-indicator/stylesheet-light.css', ++ output: 'stylesheet-workspace-switcher-light.css', ++ command: transform_stylesheet, ++ capture: true, ++ ), ++] ++ ++extension_sources += files('prefs.js') + workspaceIndicatorSources + extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml') +diff --git a/extensions/window-list/stylesheet-dark.css b/extensions/window-list/stylesheet-dark.css +index fbadf4d0..9e024f2c 100644 +--- a/extensions/window-list/stylesheet-dark.css ++++ b/extensions/window-list/stylesheet-dark.css +@@ -4,6 +4,7 @@ + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ ++@import url("stylesheet-workspace-switcher-dark.css"); + + .window-list { + spacing: 2px; +@@ -81,36 +82,6 @@ + height: 24px; + } + +-.window-list-workspace-indicator .status-label-bin { +- background-color: rgba(200, 200, 200, 0.3); +- padding: 5px; +- margin: 3px; +-} +- +-.window-list-workspace-indicator .workspaces-box { +- spacing: 3px; +- padding: 5px; +-} +- +-.window-list-workspace-indicator .workspace { +- width: 52px; +- border-radius: 4px; +- background-color: #1e1e1e; +-} +- +-.window-list-workspace-indicator .workspace.active { +- background-color: #3f3f3f; +-} +- +-.window-list-workspace-indicator-window-preview { +- background-color: #bebebe; +- border-radius: 1px; +-} +- +-.window-list-workspace-indicator-window-preview.active { +- background-color: #d4d4d4; +-} +- + .notification { + font-weight: normal; + } +diff --git a/extensions/window-list/stylesheet-light.css b/extensions/window-list/stylesheet-light.css +index e9352362..93a96581 100644 +--- a/extensions/window-list/stylesheet-light.css ++++ b/extensions/window-list/stylesheet-light.css +@@ -6,6 +6,7 @@ + */ + + @import url("stylesheet-dark.css"); ++@import url("stylesheet-workspace-switcher-light.css"); + + #panel.bottom-panel { + border-top-width: 1px; +@@ -50,22 +51,3 @@ + color: #888; + box-shadow: none; + } +- +-/* workspace switcher */ +-.window-list-workspace-indicator .workspace { +- border: 2px solid #f6f5f4; +- background-color: #ccc; +-} +- +-.window-list-workspace-indicator .workspace.active { +- border-color: #888; +-} +- +-.window-list-workspace-indicator-window-preview { +- background-color: #ededed; +- border: 1px solid #ccc; +-} +- +-.window-list-workspace-indicator-window-preview.active { +- background-color: #f6f5f4; +-} +diff --git a/extensions/window-list/workspaceIndicator.js b/extensions/window-list/workspaceIndicator.js +deleted file mode 100644 +index 69167eb6..00000000 +--- a/extensions/window-list/workspaceIndicator.js ++++ /dev/null +@@ -1,435 +0,0 @@ +-// SPDX-FileCopyrightText: 2019 Florian Müllner +-// +-// SPDX-License-Identifier: GPL-2.0-or-later +- +-import Clutter from 'gi://Clutter'; +-import Gio from 'gi://Gio'; +-import GObject from 'gi://GObject'; +-import Meta from 'gi://Meta'; +-import St from 'gi://St'; +- +-import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; +- +-import * as DND from 'resource:///org/gnome/shell/ui/dnd.js'; +-import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +-import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; +-import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +- +-const TOOLTIP_OFFSET = 6; +-const TOOLTIP_ANIMATION_TIME = 150; +- +-const MAX_THUMBNAILS = 6; +- +-let baseStyleClassName = ''; +- +-class WindowPreview extends St.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor(window) { +- super({ +- style_class: `${baseStyleClassName}-window-preview`, +- }); +- +- this._delegate = this; +- DND.makeDraggable(this, {restoreOnSuccess: true}); +- +- this._window = window; +- +- this._window.connectObject( +- 'size-changed', () => this._checkRelayout(), +- 'position-changed', () => this._checkRelayout(), +- 'notify::minimized', this._updateVisible.bind(this), +- 'notify::window-type', this._updateVisible.bind(this), +- this); +- this._updateVisible(); +- +- global.display.connectObject('notify::focus-window', +- this._onFocusChanged.bind(this), this); +- this._onFocusChanged(); +- } +- +- // needed for DND +- get metaWindow() { +- return this._window; +- } +- +- _onFocusChanged() { +- if (global.display.focus_window === this._window) +- this.add_style_class_name('active'); +- else +- this.remove_style_class_name('active'); +- } +- +- _checkRelayout() { +- const monitor = Main.layoutManager.findIndexForActor(this); +- const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); +- if (this._window.get_frame_rect().overlap(workArea)) +- this.queue_relayout(); +- } +- +- _updateVisible() { +- this.visible = this._window.window_type !== Meta.WindowType.DESKTOP && +- this._window.showing_on_its_workspace(); +- } +-} +- +-class WorkspaceLayout extends Clutter.LayoutManager { +- static { +- GObject.registerClass(this); +- } +- +- vfunc_get_preferred_width() { +- return [0, 0]; +- } +- +- vfunc_get_preferred_height() { +- return [0, 0]; +- } +- +- vfunc_allocate(container, box) { +- const monitor = Main.layoutManager.findIndexForActor(container); +- const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor); +- const hscale = box.get_width() / workArea.width; +- const vscale = box.get_height() / workArea.height; +- +- for (const child of container) { +- const childBox = new Clutter.ActorBox(); +- const frameRect = child.metaWindow.get_frame_rect(); +- childBox.set_size( +- Math.round(Math.min(frameRect.width, workArea.width) * hscale), +- Math.round(Math.min(frameRect.height, workArea.height) * vscale)); +- childBox.set_origin( +- Math.round((frameRect.x - workArea.x) * hscale), +- Math.round((frameRect.y - workArea.y) * vscale)); +- child.allocate(childBox); +- } +- } +-} +- +-class WorkspaceThumbnail extends St.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor(index) { +- super({ +- style_class: 'workspace', +- child: new Clutter.Actor({ +- layout_manager: new WorkspaceLayout(), +- clip_to_allocation: true, +- x_expand: true, +- y_expand: true, +- }), +- }); +- +- this._tooltip = new St.Label({ +- style_class: 'dash-label', +- visible: false, +- }); +- Main.uiGroup.add_child(this._tooltip); +- +- this.connect('destroy', this._onDestroy.bind(this)); +- this.connect('notify::hover', this._syncTooltip.bind(this)); +- +- this._index = index; +- this._delegate = this; // needed for DND +- +- this._windowPreviews = new Map(); +- +- let workspaceManager = global.workspace_manager; +- this._workspace = workspaceManager.get_workspace_by_index(index); +- +- this._workspace.connectObject( +- 'window-added', (ws, window) => this._addWindow(window), +- 'window-removed', (ws, window) => this._removeWindow(window), +- this); +- +- global.display.connectObject('restacked', +- this._onRestacked.bind(this), this); +- +- this._workspace.list_windows().forEach(w => this._addWindow(w)); +- this._onRestacked(); +- } +- +- acceptDrop(source) { +- if (!source.metaWindow) +- return false; +- +- this._moveWindow(source.metaWindow); +- return true; +- } +- +- handleDragOver(source) { +- if (source.metaWindow) +- return DND.DragMotionResult.MOVE_DROP; +- else +- return DND.DragMotionResult.CONTINUE; +- } +- +- _addWindow(window) { +- if (this._windowPreviews.has(window)) +- return; +- +- let preview = new WindowPreview(window); +- preview.connect('clicked', (a, btn) => this.emit('clicked', btn)); +- this._windowPreviews.set(window, preview); +- this.child.add_child(preview); +- } +- +- _removeWindow(window) { +- let preview = this._windowPreviews.get(window); +- if (!preview) +- return; +- +- this._windowPreviews.delete(window); +- preview.destroy(); +- } +- +- _onRestacked() { +- let lastPreview = null; +- let windows = global.get_window_actors().map(a => a.meta_window); +- for (let i = 0; i < windows.length; i++) { +- let preview = this._windowPreviews.get(windows[i]); +- if (!preview) +- continue; +- +- this.child.set_child_above_sibling(preview, lastPreview); +- lastPreview = preview; +- } +- } +- +- _moveWindow(window) { +- let monitorIndex = Main.layoutManager.findIndexForActor(this); +- if (monitorIndex !== window.get_monitor()) +- window.move_to_monitor(monitorIndex); +- window.change_workspace_by_index(this._index, false); +- } +- +- on_clicked() { +- let ws = global.workspace_manager.get_workspace_by_index(this._index); +- if (ws) +- ws.activate(global.get_current_time()); +- } +- +- _syncTooltip() { +- if (this.hover) { +- this._tooltip.set({ +- text: Meta.prefs_get_workspace_name(this._index), +- visible: true, +- opacity: 0, +- }); +- +- const [stageX, stageY] = this.get_transformed_position(); +- const thumbWidth = this.allocation.get_width(); +- const tipWidth = this._tooltip.width; +- const tipHeight = this._tooltip.height; +- const xOffset = Math.floor((thumbWidth - tipWidth) / 2); +- const monitor = Main.layoutManager.findMonitorForActor(this); +- const x = Math.clamp( +- stageX + xOffset, +- monitor.x, +- monitor.x + monitor.width - tipWidth); +- const y = stageY - tipHeight - TOOLTIP_OFFSET; +- this._tooltip.set_position(x, y); +- } +- +- this._tooltip.ease({ +- opacity: this.hover ? 255 : 0, +- duration: TOOLTIP_ANIMATION_TIME, +- mode: Clutter.AnimationMode.EASE_OUT_QUAD, +- onComplete: () => (this._tooltip.visible = this.hover), +- }); +- } +- +- _onDestroy() { +- this._tooltip.destroy(); +- } +-} +- +-export class WorkspaceIndicator extends PanelMenu.Button { +- static { +- GObject.registerClass(this); +- } +- +- constructor(params = {}) { +- super(0.5, _('Workspace Indicator')); +- +- const { +- baseStyleClass = 'workspace-indicator', +- } = params; +- +- baseStyleClassName = baseStyleClass; +- this.add_style_class_name(baseStyleClassName); +- +- let container = new St.Widget({ +- layout_manager: new Clutter.BinLayout(), +- x_expand: true, +- y_expand: true, +- }); +- this.add_child(container); +- +- let workspaceManager = global.workspace_manager; +- +- this._currentWorkspace = workspaceManager.get_active_workspace_index(); +- this._statusLabel = new St.Label({text: this._getStatusText()}); +- +- this._statusBin = new St.Bin({ +- style_class: 'status-label-bin', +- x_expand: true, +- y_expand: true, +- child: this._statusLabel, +- }); +- container.add_child(this._statusBin); +- +- this._thumbnailsBox = new St.BoxLayout({ +- style_class: 'workspaces-box', +- y_expand: true, +- reactive: true, +- }); +- this._thumbnailsBox.connect('scroll-event', +- this._onScrollEvent.bind(this)); +- container.add_child(this._thumbnailsBox); +- +- this._workspacesItems = []; +- +- workspaceManager.connectObject( +- 'notify::n-workspaces', this._nWorkspacesChanged.bind(this), GObject.ConnectFlags.AFTER, +- 'workspace-switched', this._onWorkspaceSwitched.bind(this), GObject.ConnectFlags.AFTER, +- 'notify::layout-rows', this._updateThumbnailVisibility.bind(this), +- this); +- +- this.connect('scroll-event', this._onScrollEvent.bind(this)); +- this._updateMenu(); +- this._updateThumbnails(); +- this._updateThumbnailVisibility(); +- +- this._settings = new Gio.Settings({schema_id: 'org.gnome.desktop.wm.preferences'}); +- this._settings.connectObject('changed::workspace-names', +- () => this._updateMenuLabels(), this); +- } +- +- _updateThumbnailVisibility() { +- const {workspaceManager} = global; +- const vertical = workspaceManager.layout_rows === -1; +- const useMenu = +- vertical || workspaceManager.n_workspaces > MAX_THUMBNAILS; +- this.reactive = useMenu; +- +- this._statusBin.visible = useMenu; +- this._thumbnailsBox.visible = !useMenu; +- } +- +- _onWorkspaceSwitched() { +- let workspaceManager = global.workspace_manager; +- this._currentWorkspace = workspaceManager.get_active_workspace_index(); +- +- this._updateMenuOrnament(); +- this._updateActiveThumbnail(); +- +- this._statusLabel.set_text(this._getStatusText()); +- } +- +- _nWorkspacesChanged() { +- this._updateMenu(); +- this._updateThumbnails(); +- this._updateThumbnailVisibility(); +- } +- +- _updateMenuOrnament() { +- for (let i = 0; i < this._workspacesItems.length; i++) { +- this._workspacesItems[i].setOrnament(i === this._currentWorkspace +- ? PopupMenu.Ornament.DOT +- : PopupMenu.Ornament.NO_DOT); +- } +- } +- +- _updateActiveThumbnail() { +- let thumbs = this._thumbnailsBox.get_children(); +- for (let i = 0; i < thumbs.length; i++) { +- if (i === this._currentWorkspace) +- thumbs[i].add_style_class_name('active'); +- else +- thumbs[i].remove_style_class_name('active'); +- } +- } +- +- _getStatusText() { +- let workspaceManager = global.workspace_manager; +- let current = workspaceManager.get_active_workspace_index(); +- let total = workspaceManager.n_workspaces; +- +- return '%d / %d'.format(current + 1, total); +- } +- +- _updateMenuLabels() { +- for (let i = 0; i < this._workspacesItems.length; i++) { +- let item = this._workspacesItems[i]; +- let name = Meta.prefs_get_workspace_name(i); +- item.label.text = name; +- } +- } +- +- _updateMenu() { +- let workspaceManager = global.workspace_manager; +- +- this.menu.removeAll(); +- this._workspacesItems = []; +- this._currentWorkspace = workspaceManager.get_active_workspace_index(); +- +- for (let i = 0; i < workspaceManager.n_workspaces; i++) { +- let name = Meta.prefs_get_workspace_name(i); +- let item = new PopupMenu.PopupMenuItem(name); +- item.workspaceId = i; +- +- item.connect('activate', () => { +- this._activate(item.workspaceId); +- }); +- +- item.setOrnament(i === this._currentWorkspace +- ? PopupMenu.Ornament.DOT +- : PopupMenu.Ornament.NO_DOT); +- +- this.menu.addMenuItem(item); +- this._workspacesItems[i] = item; +- } +- +- this._statusLabel.set_text(this._getStatusText()); +- } +- +- _updateThumbnails() { +- let workspaceManager = global.workspace_manager; +- +- this._thumbnailsBox.destroy_all_children(); +- +- for (let i = 0; i < workspaceManager.n_workspaces; i++) { +- let thumb = new WorkspaceThumbnail(i); +- this._thumbnailsBox.add_child(thumb); +- } +- this._updateActiveThumbnail(); +- } +- +- _activate(index) { +- let workspaceManager = global.workspace_manager; +- +- if (index >= 0 && index < workspaceManager.n_workspaces) { +- let metaWorkspace = workspaceManager.get_workspace_by_index(index); +- metaWorkspace.activate(global.get_current_time()); +- } +- } +- +- _onScrollEvent(actor, event) { +- let direction = event.get_scroll_direction(); +- let diff = 0; +- if (direction === Clutter.ScrollDirection.DOWN) +- diff = 1; +- else if (direction === Clutter.ScrollDirection.UP) +- diff = -1; +- else +- return; +- +- let newIndex = this._currentWorkspace + diff; +- this._activate(newIndex); +- } +-} +-- +2.44.0 + diff --git a/more-ws-previews-0020-workspace-indicator-Simplify-scroll-handling.patch b/more-ws-previews-0020-workspace-indicator-Simplify-scroll-handling.patch new file mode 100644 index 0000000..4ec8a5f --- /dev/null +++ b/more-ws-previews-0020-workspace-indicator-Simplify-scroll-handling.patch @@ -0,0 +1,53 @@ +From 3c1638195b33f9dfdd3df7847e88fab97188520a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 20 Feb 2024 17:39:49 +0100 +Subject: [PATCH 20/28] workspace-indicator: Simplify scroll handling + +gnome-shell already includes a method for switching workspaces +via scroll events. Use that instead of implementing our own. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 21 ++++--------------- + 1 file changed, 4 insertions(+), 17 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 594a9e51..14dd81d0 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -300,8 +300,10 @@ export class WorkspaceIndicator extends PanelMenu.Button { + 'notify::layout-rows', this._updateThumbnailVisibility.bind(this), + this); + +- this.connect('scroll-event', this._onScrollEvent.bind(this)); +- this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this)); ++ this.connect('scroll-event', ++ (a, event) => Main.wm.handleWorkspaceScroll(event)); ++ this._thumbnailsBox.connect('scroll-event', ++ (a, event) => Main.wm.handleWorkspaceScroll(event)); + + this._inTopBar = false; + this.connect('notify::realized', () => { +@@ -445,19 +447,4 @@ export class WorkspaceIndicator extends PanelMenu.Button { + metaWorkspace.activate(global.get_current_time()); + } + } +- +- _onScrollEvent(actor, event) { +- let direction = event.get_scroll_direction(); +- let diff = 0; +- if (direction === Clutter.ScrollDirection.DOWN) +- diff = 1; +- else if (direction === Clutter.ScrollDirection.UP) +- diff = -1; +- else +- return; +- +- +- const newIndex = this._currentWorkspace + diff; +- this._activate(newIndex); +- } + } +-- +2.44.0 + diff --git a/more-ws-previews-0021-workspace-indicator-Handle-active-indication-in-thum.patch b/more-ws-previews-0021-workspace-indicator-Handle-active-indication-in-thum.patch new file mode 100644 index 0000000..c51ff72 --- /dev/null +++ b/more-ws-previews-0021-workspace-indicator-Handle-active-indication-in-thum.patch @@ -0,0 +1,99 @@ +From 13dce7fcc1013a3cbb3a1e521e123a5d4ede75c5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 27 Feb 2024 21:20:45 +0100 +Subject: [PATCH 21/28] workspace-indicator: Handle active indication in + thumbnail + +Meta.Workspace has had an `active` property for a while now, so +we can use a property binding instead of tracking the active +workspace ourselves. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 35 ++++++++++++------- + 1 file changed, 23 insertions(+), 12 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 14dd81d0..bf6511a0 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -111,6 +111,13 @@ class WorkspaceLayout extends Clutter.LayoutManager { + } + + class WorkspaceThumbnail extends St.Button { ++ static [GObject.properties] = { ++ 'active': GObject.ParamSpec.boolean( ++ 'active', '', '', ++ GObject.ParamFlags.READWRITE, ++ false), ++ }; ++ + static { + GObject.registerClass(this); + } +@@ -143,6 +150,10 @@ class WorkspaceThumbnail extends St.Button { + let workspaceManager = global.workspace_manager; + this._workspace = workspaceManager.get_workspace_by_index(index); + ++ this._workspace.bind_property('active', ++ this, 'active', ++ GObject.BindingFlags.SYNC_CREATE); ++ + this._workspace.connectObject( + 'window-added', (ws, window) => this._addWindow(window), + 'window-removed', (ws, window) => this._removeWindow(window), +@@ -155,6 +166,18 @@ class WorkspaceThumbnail extends St.Button { + this._onRestacked(); + } + ++ get active() { ++ return this.has_style_class_name('active'); ++ } ++ ++ set active(active) { ++ if (active) ++ this.add_style_class_name('active'); ++ else ++ this.remove_style_class_name('active'); ++ this.notify('active'); ++ } ++ + acceptDrop(source) { + if (!source.metaWindow) + return false; +@@ -360,7 +383,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._currentWorkspace = global.workspace_manager.get_active_workspace_index(); + + this._updateMenuOrnament(); +- this._updateActiveThumbnail(); + + this._statusLabel.set_text(this._getStatusText()); + } +@@ -379,16 +401,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + } + +- _updateActiveThumbnail() { +- let thumbs = this._thumbnailsBox.get_children(); +- for (let i = 0; i < thumbs.length; i++) { +- if (i === this._currentWorkspace) +- thumbs[i].add_style_class_name('active'); +- else +- thumbs[i].remove_style_class_name('active'); +- } +- } +- + _getStatusText() { + const {nWorkspaces} = global.workspace_manager; + const current = this._currentWorkspace + 1; +@@ -436,7 +448,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + let thumb = new WorkspaceThumbnail(i); + this._thumbnailsBox.add_child(thumb); + } +- this._updateActiveThumbnail(); + } + + _activate(index) { +-- +2.44.0 + diff --git a/more-ws-previews-0022-workspace-indicator-Split-out-WorkspacePreviews.patch b/more-ws-previews-0022-workspace-indicator-Split-out-WorkspacePreviews.patch new file mode 100644 index 0000000..f04f509 --- /dev/null +++ b/more-ws-previews-0022-workspace-indicator-Split-out-WorkspacePreviews.patch @@ -0,0 +1,150 @@ +From 6b508c92c4996771cf79eb4d81e5d285b598fe96 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 20 Feb 2024 17:27:57 +0100 +Subject: [PATCH 22/28] workspace-indicator: Split out WorkspacePreviews + +The previews will become a bit more complex soon, so spit them out +into a dedicated class. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 72 ++++++++++++------- + 1 file changed, 47 insertions(+), 25 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index bf6511a0..73ebca6f 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -274,6 +274,49 @@ class WorkspaceThumbnail extends St.Button { + } + } + ++class WorkspacePreviews extends Clutter.Actor { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(params) { ++ super({ ++ ...params, ++ layout_manager: new Clutter.BinLayout(), ++ reactive: true, ++ y_expand: true, ++ }); ++ ++ this.connect('scroll-event', ++ (a, event) => Main.wm.handleWorkspaceScroll(event)); ++ ++ const {workspaceManager} = global; ++ ++ workspaceManager.connectObject( ++ 'notify::n-workspaces', () => this._updateThumbnails(), GObject.ConnectFlags.AFTER, ++ this); ++ ++ this._thumbnailsBox = new St.BoxLayout({ ++ style_class: 'workspaces-box', ++ y_expand: true, ++ }); ++ this.add_child(this._thumbnailsBox); ++ ++ this._updateThumbnails(); ++ } ++ ++ _updateThumbnails() { ++ const {nWorkspaces} = global.workspace_manager; ++ ++ this._thumbnailsBox.destroy_all_children(); ++ ++ for (let i = 0; i < nWorkspaces; i++) { ++ const thumb = new WorkspaceThumbnail(i); ++ this._thumbnailsBox.add_child(thumb); ++ } ++ } ++} ++ + export class WorkspaceIndicator extends PanelMenu.Button { + static { + GObject.registerClass(this); +@@ -304,16 +347,10 @@ export class WorkspaceIndicator extends PanelMenu.Button { + y_align: Clutter.ActorAlign.CENTER, + text: this._getStatusText(), + }); +- + container.add_child(this._statusLabel); + +- this._thumbnailsBox = new St.BoxLayout({ +- style_class: 'workspaces-box', +- y_expand: true, +- reactive: true, +- }); +- +- container.add_child(this._thumbnailsBox); ++ this._thumbnails = new WorkspacePreviews(); ++ container.add_child(this._thumbnails); + + this._workspacesItems = []; + +@@ -325,8 +362,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + this.connect('scroll-event', + (a, event) => Main.wm.handleWorkspaceScroll(event)); +- this._thumbnailsBox.connect('scroll-event', +- (a, event) => Main.wm.handleWorkspaceScroll(event)); + + this._inTopBar = false; + this.connect('notify::realized', () => { +@@ -338,7 +373,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + }); + + this._updateMenu(); +- this._updateThumbnails(); + this._updateThumbnailVisibility(); + + const desktopSettings = +@@ -363,7 +397,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this.reactive = useMenu; + + this._statusLabel.visible = useMenu; +- this._thumbnailsBox.visible = !useMenu; ++ this._thumbnails.visible = !useMenu; + + this._updateTopBarRedirect(); + } +@@ -374,7 +408,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + // Disable offscreen-redirect when showing the workspace switcher + // so that clip-to-allocation works +- Main.panel.set_offscreen_redirect(this._thumbnailsBox.visible ++ Main.panel.set_offscreen_redirect(this._thumbnails.visible + ? Clutter.OffscreenRedirect.ALWAYS + : Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY); + } +@@ -389,7 +423,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + _nWorkspacesChanged() { + this._updateMenu(); +- this._updateThumbnails(); + this._updateThumbnailVisibility(); + } + +@@ -439,17 +472,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._statusLabel.set_text(this._getStatusText()); + } + +- _updateThumbnails() { +- let workspaceManager = global.workspace_manager; +- +- this._thumbnailsBox.destroy_all_children(); +- +- for (let i = 0; i < workspaceManager.n_workspaces; i++) { +- let thumb = new WorkspaceThumbnail(i); +- this._thumbnailsBox.add_child(thumb); +- } +- } +- + _activate(index) { + let workspaceManager = global.workspace_manager; + +-- +2.44.0 + diff --git a/more-ws-previews-0023-workspace-indicator-Handle-preview-overflow.patch b/more-ws-previews-0023-workspace-indicator-Handle-preview-overflow.patch new file mode 100644 index 0000000..156e7da --- /dev/null +++ b/more-ws-previews-0023-workspace-indicator-Handle-preview-overflow.patch @@ -0,0 +1,133 @@ +From 8d3d9ef8d8688999d959f1062a62e9f3b7f489fe Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 19 Feb 2024 14:42:04 +0100 +Subject: [PATCH 23/28] workspace-indicator: Handle preview overflow + +We currently avoid previews from overflowing in most setups by +artificially limiting them to a maximum of six workspaces. + +Add some proper handling to also cover cases where space is more +limited, and to allow removing the restriction in the future. + +For that, wrap the previews in an auto-scrolling scroll view +and add overflow indicators on each side. + +Part-of: +--- + .../workspace-indicator/stylesheet-dark.css | 4 ++ + .../workspace-indicator/workspaceIndicator.js | 64 ++++++++++++++++++- + 2 files changed, 67 insertions(+), 1 deletion(-) + +diff --git a/extensions/workspace-indicator/stylesheet-dark.css b/extensions/workspace-indicator/stylesheet-dark.css +index 3e2ba67f..22d13370 100644 +--- a/extensions/workspace-indicator/stylesheet-dark.css ++++ b/extensions/workspace-indicator/stylesheet-dark.css +@@ -9,6 +9,10 @@ + padding: 0 8px; + } + ++.workspace-indicator .workspaces-view.hfade { ++ -st-hfade-offset: 20px; ++} ++ + .workspace-indicator .workspaces-box { + padding: 5px; + spacing: 3px; +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 73ebca6f..314b9f45 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -20,6 +20,8 @@ import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + const TOOLTIP_OFFSET = 6; + const TOOLTIP_ANIMATION_TIME = 150; + ++const SCROLL_TIME = 100; ++ + const MAX_THUMBNAILS = 6; + + let baseStyleClassName = ''; +@@ -294,13 +296,29 @@ class WorkspacePreviews extends Clutter.Actor { + + workspaceManager.connectObject( + 'notify::n-workspaces', () => this._updateThumbnails(), GObject.ConnectFlags.AFTER, ++ 'workspace-switched', () => this._updateScrollPosition(), + this); + ++ this.connect('notify::mapped', () => { ++ if (this.mapped) ++ this._updateScrollPosition(); ++ }); ++ + this._thumbnailsBox = new St.BoxLayout({ + style_class: 'workspaces-box', + y_expand: true, + }); +- this.add_child(this._thumbnailsBox); ++ ++ this._scrollView = new St.ScrollView({ ++ style_class: 'workspaces-view hfade', ++ enable_mouse_scrolling: false, ++ hscrollbar_policy: St.PolicyType.EXTERNAL, ++ vscrollbar_policy: St.PolicyType.NEVER, ++ y_expand: true, ++ child: this._thumbnailsBox, ++ }); ++ ++ this.add_child(this._scrollView); + + this._updateThumbnails(); + } +@@ -314,6 +332,50 @@ class WorkspacePreviews extends Clutter.Actor { + const thumb = new WorkspaceThumbnail(i); + this._thumbnailsBox.add_child(thumb); + } ++ ++ if (this.mapped) ++ this._updateScrollPosition(); ++ } ++ ++ _updateScrollPosition() { ++ const adjustment = this._scrollView.hadjustment; ++ const {upper, pageSize} = adjustment; ++ let {value} = adjustment; ++ ++ const activeWorkspace = ++ [...this._thumbnailsBox].find(a => a.active); ++ ++ if (!activeWorkspace) ++ return; ++ ++ let offset = 0; ++ const hfade = this._scrollView.get_effect('fade'); ++ if (hfade) ++ offset = hfade.fade_margins.left; ++ ++ let {x1, x2} = activeWorkspace.get_allocation_box(); ++ let parent = activeWorkspace.get_parent(); ++ while (parent !== this._scrollView) { ++ if (!parent) ++ throw new Error('actor not in scroll view'); ++ ++ const box = parent.get_allocation_box(); ++ x1 += box.x1; ++ x2 += box.x1; ++ parent = parent.get_parent(); ++ } ++ ++ if (x1 < value + offset) ++ value = Math.max(0, x1 - offset); ++ else if (x2 > value + pageSize - offset) ++ value = Math.min(upper, x2 + offset - pageSize); ++ else ++ return; ++ ++ adjustment.ease(value, { ++ mode: Clutter.AnimationMode.EASE_OUT_QUAD, ++ duration: SCROLL_TIME, ++ }); + } + } + +-- +2.44.0 + diff --git a/more-ws-previews-0024-workspace-indicator-Support-labels-in-previews.patch b/more-ws-previews-0024-workspace-indicator-Support-labels-in-previews.patch new file mode 100644 index 0000000..8d1fe6d --- /dev/null +++ b/more-ws-previews-0024-workspace-indicator-Support-labels-in-previews.patch @@ -0,0 +1,157 @@ +From 3affa2e422de26862b4e473cfeeb89aea638df66 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sun, 3 Mar 2024 15:05:23 +0100 +Subject: [PATCH 24/28] workspace-indicator: Support labels in previews + +The space in the top bar is too limited to include the workspace +names. However we'll soon replace the textual menu with a preview +popover. We can use bigger previews there, so we can include the +names to not lose functionality with regards to the current menu. + +Part-of: +--- + .../workspace-indicator/workspaceIndicator.js | 56 +++++++++++++++++-- + 1 file changed, 50 insertions(+), 6 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 314b9f45..e6aa68bf 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -118,6 +118,10 @@ class WorkspaceThumbnail extends St.Button { + 'active', '', '', + GObject.ParamFlags.READWRITE, + false), ++ 'show-label': GObject.ParamSpec.boolean( ++ 'show-label', '', '', ++ GObject.ParamFlags.READWRITE, ++ false), + }; + + static { +@@ -125,7 +129,16 @@ class WorkspaceThumbnail extends St.Button { + } + + constructor(index) { +- super({ ++ super(); ++ ++ const box = new St.BoxLayout({ ++ style_class: 'workspace-box', ++ y_expand: true, ++ vertical: true, ++ }); ++ this.set_child(box); ++ ++ this._preview = new St.Bin({ + style_class: 'workspace', + child: new Clutter.Actor({ + layout_manager: new WorkspaceLayout(), +@@ -133,7 +146,15 @@ class WorkspaceThumbnail extends St.Button { + x_expand: true, + y_expand: true, + }), ++ y_expand: true, ++ }); ++ box.add_child(this._preview); ++ ++ this._label = new St.Label({ ++ x_align: Clutter.ActorAlign.CENTER, ++ text: Meta.prefs_get_workspace_name(index), + }); ++ box.add_child(this._label); + + this._tooltip = new St.Label({ + style_class: 'dash-label', +@@ -141,9 +162,19 @@ class WorkspaceThumbnail extends St.Button { + }); + Main.uiGroup.add_child(this._tooltip); + ++ this.bind_property('show-label', ++ this._label, 'visible', ++ GObject.BindingFlags.SYNC_CREATE); ++ + this.connect('destroy', this._onDestroy.bind(this)); + this.connect('notify::hover', this._syncTooltip.bind(this)); + ++ const desktopSettings = ++ new Gio.Settings({schema_id: 'org.gnome.desktop.wm.preferences'}); ++ desktopSettings.connectObject('changed::workspace-names', () => { ++ this._label.text = Meta.prefs_get_workspace_name(index); ++ }, this); ++ + this._index = index; + this._delegate = this; // needed for DND + +@@ -169,14 +200,14 @@ class WorkspaceThumbnail extends St.Button { + } + + get active() { +- return this.has_style_class_name('active'); ++ return this._preview.has_style_class_name('active'); + } + + set active(active) { + if (active) +- this.add_style_class_name('active'); ++ this._preview.add_style_class_name('active'); + else +- this.remove_style_class_name('active'); ++ this._preview.remove_style_class_name('active'); + this.notify('active'); + } + +@@ -202,7 +233,7 @@ class WorkspaceThumbnail extends St.Button { + let preview = new WindowPreview(window); + preview.connect('clicked', (a, btn) => this.emit('clicked', btn)); + this._windowPreviews.set(window, preview); +- this.child.add_child(preview); ++ this._preview.child.add_child(preview); + } + + _removeWindow(window) { +@@ -222,7 +253,7 @@ class WorkspaceThumbnail extends St.Button { + if (!preview) + continue; + +- this.child.set_child_above_sibling(preview, lastPreview); ++ this._preview.child.set_child_above_sibling(preview, lastPreview); + lastPreview = preview; + } + } +@@ -241,6 +272,9 @@ class WorkspaceThumbnail extends St.Button { + } + + _syncTooltip() { ++ if (this.showLabel) ++ return; ++ + if (this.hover) { + this._tooltip.set({ + text: Meta.prefs_get_workspace_name(this._index), +@@ -277,6 +311,13 @@ class WorkspaceThumbnail extends St.Button { + } + + class WorkspacePreviews extends Clutter.Actor { ++ static [GObject.properties] = { ++ 'show-labels': GObject.ParamSpec.boolean( ++ 'show-labels', '', '', ++ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, ++ false), ++ }; ++ + static { + GObject.registerClass(this); + } +@@ -330,6 +371,9 @@ class WorkspacePreviews extends Clutter.Actor { + + for (let i = 0; i < nWorkspaces; i++) { + const thumb = new WorkspaceThumbnail(i); ++ this.bind_property('show-labels', ++ thumb, 'show-label', ++ GObject.BindingFlags.SYNC_CREATE); + this._thumbnailsBox.add_child(thumb); + } + +-- +2.44.0 + diff --git a/more-ws-previews-0025-workspace-indicator-Stop-handling-vertical-layouts.patch b/more-ws-previews-0025-workspace-indicator-Stop-handling-vertical-layouts.patch new file mode 100644 index 0000000..1edefe6 --- /dev/null +++ b/more-ws-previews-0025-workspace-indicator-Stop-handling-vertical-layouts.patch @@ -0,0 +1,44 @@ +From 1805cceb598d1ed6fd2039453242b28e44e079e0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 20 Feb 2024 21:43:55 +0100 +Subject: [PATCH 25/28] workspace-indicator: Stop handling vertical layouts + +Both the regular session and GNOME classic use a horizontal layout +nowadays, so it doesn't seem worth to specifically handle vertical +layouts anymore. + +The extension will still work when the layout is changed (by some +other extension), there will simply be a mismatch between horizontal +previews and the actual layout. + +Part-of: +--- + extensions/workspace-indicator/workspaceIndicator.js | 5 +---- + 1 file changed, 1 insertion(+), 4 deletions(-) + +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index e6aa68bf..087d2d89 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -463,7 +463,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + workspaceManager.connectObject( + 'notify::n-workspaces', this._nWorkspacesChanged.bind(this), GObject.ConnectFlags.AFTER, + 'workspace-switched', this._onWorkspaceSwitched.bind(this), GObject.ConnectFlags.AFTER, +- 'notify::layout-rows', this._updateThumbnailVisibility.bind(this), + this); + + this.connect('scroll-event', +@@ -497,9 +496,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + _updateThumbnailVisibility() { + const {workspaceManager} = global; +- const vertical = workspaceManager.layout_rows === -1; +- const useMenu = +- vertical || workspaceManager.n_workspaces > MAX_THUMBNAILS; ++ const useMenu = workspaceManager.n_workspaces > MAX_THUMBNAILS; + this.reactive = useMenu; + + this._statusLabel.visible = useMenu; +-- +2.44.0 + diff --git a/more-ws-previews-0026-workspace-indicator-Also-show-previews-in-menu.patch b/more-ws-previews-0026-workspace-indicator-Also-show-previews-in-menu.patch new file mode 100644 index 0000000..fcff075 --- /dev/null +++ b/more-ws-previews-0026-workspace-indicator-Also-show-previews-in-menu.patch @@ -0,0 +1,190 @@ +From f72c6ed223c3d348bdf32c25b54b6c44a826eb7d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sun, 3 Mar 2024 15:05:23 +0100 +Subject: [PATCH 26/28] workspace-indicator: Also show previews in menu + +Since the regular session also switched to horizontal workspaces, +using a vertical menu has been a bit awkward. + +Now that our previews have become more flexible, we can use them +in the collapsed state as well as when embedded into the top bar. + +Part-of: +--- + .../workspace-indicator/stylesheet-dark.css | 25 ++++++- + .../workspace-indicator/workspaceIndicator.js | 74 +++---------------- + 2 files changed, 36 insertions(+), 63 deletions(-) + +diff --git a/extensions/workspace-indicator/stylesheet-dark.css b/extensions/workspace-indicator/stylesheet-dark.css +index 22d13370..b4a716b8 100644 +--- a/extensions/workspace-indicator/stylesheet-dark.css ++++ b/extensions/workspace-indicator/stylesheet-dark.css +@@ -13,18 +13,41 @@ + -st-hfade-offset: 20px; + } + ++.workspace-indicator-menu .workspaces-view { ++ max-width: 480px; ++} ++ + .workspace-indicator .workspaces-box { + padding: 5px; + spacing: 3px; + } + ++.workspace-indicator-menu .workspaces-box { ++ padding: 5px; ++ spacing: 6px; ++} ++ ++.workspace-indicator-menu .workspace-box { ++ spacing: 6px; ++} ++ ++.workspace-indicator-menu .workspace, + .workspace-indicator .workspace { +- width: 52px; + border: 2px solid transparent; + border-radius: 4px; + background-color: #3f3f3f; + } + ++.workspace-indicator .workspace { ++ width: 52px; ++} ++ ++.workspace-indicator-menu .workspace { ++ height: 80px; ++ width: 160px; ++} ++ ++.workspace-indicator-menu .workspace.active, + .workspace-indicator .workspace.active { + border-color: #9f9f9f; + } +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index 087d2d89..a4d3bbee 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -429,7 +429,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + + constructor(params = {}) { +- super(0.5, _('Workspace Indicator')); ++ super(0.5, _('Workspace Indicator'), true); + + const { + baseStyleClass = 'workspace-indicator', +@@ -461,7 +461,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._workspacesItems = []; + + workspaceManager.connectObject( +- 'notify::n-workspaces', this._nWorkspacesChanged.bind(this), GObject.ConnectFlags.AFTER, ++ 'notify::n-workspaces', this._updateThumbnailVisibility.bind(this), GObject.ConnectFlags.AFTER, + 'workspace-switched', this._onWorkspaceSwitched.bind(this), GObject.ConnectFlags.AFTER, + this); + +@@ -477,13 +477,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._updateTopBarRedirect(); + }); + +- this._updateMenu(); + this._updateThumbnailVisibility(); +- +- const desktopSettings = +- new Gio.Settings({schema_id: 'org.gnome.desktop.wm.preferences'}); +- desktopSettings.connectObject('changed::workspace-names', +- () => this._updateMenuLabels(), this); + } + + _onDestroy() { +@@ -502,6 +496,10 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._statusLabel.visible = useMenu; + this._thumbnails.visible = !useMenu; + ++ this.setMenu(useMenu ++ ? this._createPreviewMenu() ++ : null); ++ + this._updateTopBarRedirect(); + } + +@@ -518,69 +516,21 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + _onWorkspaceSwitched() { + this._currentWorkspace = global.workspace_manager.get_active_workspace_index(); +- +- this._updateMenuOrnament(); +- + this._statusLabel.set_text(this._getStatusText()); + } + +- _nWorkspacesChanged() { +- this._updateMenu(); +- this._updateThumbnailVisibility(); +- } +- +- _updateMenuOrnament() { +- for (let i = 0; i < this._workspacesItems.length; i++) { +- this._workspacesItems[i].setOrnament(i === this._currentWorkspace +- ? PopupMenu.Ornament.DOT +- : PopupMenu.Ornament.NO_DOT); +- } +- } +- + _getStatusText() { + const {nWorkspaces} = global.workspace_manager; + const current = this._currentWorkspace + 1; + return `${current} / ${nWorkspaces}`; + } + +- _updateMenuLabels() { +- for (let i = 0; i < this._workspacesItems.length; i++) { +- const item = this._workspacesItems[i]; +- item.label.text = Meta.prefs_get_workspace_name(i); +- } +- } +- +- _updateMenu() { +- let workspaceManager = global.workspace_manager; +- +- this.menu.removeAll(); +- this._workspacesItems = []; +- this._currentWorkspace = workspaceManager.get_active_workspace_index(); ++ _createPreviewMenu() { ++ const menu = new PopupMenu.PopupMenu(this, 0.5, St.Side.TOP); + +- for (let i = 0; i < workspaceManager.n_workspaces; i++) { +- const name = Meta.prefs_get_workspace_name(i); +- const item = new PopupMenu.PopupMenuItem(name); +- +- item.connect('activate', +- () => this._activate(i)); +- +- item.setOrnament(i === this._currentWorkspace +- ? PopupMenu.Ornament.DOT +- : PopupMenu.Ornament.NO_DOT); +- +- this.menu.addMenuItem(item); +- this._workspacesItems[i] = item; +- } +- +- this._statusLabel.set_text(this._getStatusText()); +- } +- +- _activate(index) { +- let workspaceManager = global.workspace_manager; +- +- if (index >= 0 && index < workspaceManager.n_workspaces) { +- let metaWorkspace = workspaceManager.get_workspace_by_index(index); +- metaWorkspace.activate(global.get_current_time()); +- } ++ const previews = new WorkspacePreviews({show_labels: true}); ++ menu.box.add_child(previews); ++ menu.actor.add_style_class_name(`${baseStyleClassName}-menu`); ++ return menu; + } + } +-- +2.44.0 + diff --git a/more-ws-previews-0027-workspace-indicator-Make-previews-configurable.patch b/more-ws-previews-0027-workspace-indicator-Make-previews-configurable.patch new file mode 100644 index 0000000..466620c --- /dev/null +++ b/more-ws-previews-0027-workspace-indicator-Make-previews-configurable.patch @@ -0,0 +1,215 @@ +From 8d2b24290204be98423b3a952939895133bdc036 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 20 Feb 2024 22:00:57 +0100 +Subject: [PATCH 27/28] workspace-indicator: Make previews configurable + +Now that previews scroll when there are too many workspaces, +there is no longer a reason for the 6-workspace limit. + +However some users do prefer the menu, so rather than drop it, +turn it into a proper preference. + +Closes +https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/issues/336 + +Part-of: +--- + extensions/window-list/extension.js | 1 + + ...e.shell.extensions.window-list.gschema.xml | 4 +++ + extensions/workspace-indicator/extension.js | 4 ++- + extensions/workspace-indicator/meson.build | 1 + + extensions/workspace-indicator/prefs.js | 26 +++++++++++++++++-- + ...extensions.workspace-indicator.gschema.xml | 15 +++++++++++ + .../workspace-indicator/workspaceIndicator.js | 11 ++++---- + po/POTFILES.in | 1 + + 8 files changed, 55 insertions(+), 8 deletions(-) + create mode 100644 extensions/workspace-indicator/schemas/org.gnome.shell.extensions.workspace-indicator.gschema.xml + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 3950c535..227625e5 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -769,6 +769,7 @@ class WindowList extends St.Widget { + + this._workspaceIndicator = new BottomWorkspaceIndicator({ + baseStyleClass: 'window-list-workspace-indicator', ++ settings, + }); + indicatorsBox.add_child(this._workspaceIndicator.container); + +diff --git a/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml b/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml +index 2ed680a5..46ff25cb 100644 +--- a/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml ++++ b/extensions/window-list/org.gnome.shell.extensions.window-list.gschema.xml +@@ -36,5 +36,9 @@ SPDX-License-Identifier: GPL-2.0-or-later + only on the primary one. + + ++ ++ true ++ Show workspace previews in window list ++ + + +diff --git a/extensions/workspace-indicator/extension.js b/extensions/workspace-indicator/extension.js +index b383c919..ef24a750 100644 +--- a/extensions/workspace-indicator/extension.js ++++ b/extensions/workspace-indicator/extension.js +@@ -12,7 +12,9 @@ import {WorkspaceIndicator} from './workspaceIndicator.js'; + + export default class WorkspaceIndicatorExtension extends Extension { + enable() { +- this._indicator = new WorkspaceIndicator(); ++ this._indicator = new WorkspaceIndicator({ ++ settings: this.getSettings(), ++ }); + Main.panel.addToStatusArea('workspace-indicator', this._indicator); + } + +diff --git a/extensions/workspace-indicator/meson.build b/extensions/workspace-indicator/meson.build +index dada5408..9388085c 100644 +--- a/extensions/workspace-indicator/meson.build ++++ b/extensions/workspace-indicator/meson.build +@@ -11,5 +11,6 @@ extension_data += files( + 'stylesheet-dark.css', + 'stylesheet-light.css', + ) ++extension_schemas += files('schemas/' + metadata_conf.get('gschemaname') + '.gschema.xml') + + extension_sources += files('prefs.js', 'workspaceIndicator.js') +diff --git a/extensions/workspace-indicator/prefs.js b/extensions/workspace-indicator/prefs.js +index ea0546bf..b828ab8f 100644 +--- a/extensions/workspace-indicator/prefs.js ++++ b/extensions/workspace-indicator/prefs.js +@@ -18,6 +18,25 @@ const N_ = e => e; + const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences'; + const WORKSPACE_KEY = 'workspace-names'; + ++class GeneralGroup extends Adw.PreferencesGroup { ++ static { ++ GObject.registerClass(this); ++ } ++ ++ constructor(settings) { ++ super(); ++ ++ const row = new Adw.SwitchRow({ ++ title: _('Show Previews In Top Bar'), ++ }); ++ this.add(row); ++ ++ settings.bind('embed-previews', ++ row, 'active', ++ Gio.SettingsBindFlags.DEFAULT); ++ } ++} ++ + class NewItem extends GObject.Object {} + GObject.registerClass(NewItem); + +@@ -119,7 +138,7 @@ class WorkspacesList extends GObject.Object { + } + } + +-class WorkspaceSettingsWidget extends Adw.PreferencesGroup { ++class WorkspacesGroup extends Adw.PreferencesGroup { + static { + GObject.registerClass(this); + +@@ -265,6 +284,9 @@ class NewWorkspaceRow extends Adw.PreferencesRow { + + export default class WorkspaceIndicatorPrefs extends ExtensionPreferences { + getPreferencesWidget() { +- return new WorkspaceSettingsWidget(); ++ const page = new Adw.PreferencesPage(); ++ page.add(new GeneralGroup(this.getSettings())); ++ page.add(new WorkspacesGroup()); ++ return page; + } + } +diff --git a/extensions/workspace-indicator/schemas/org.gnome.shell.extensions.workspace-indicator.gschema.xml b/extensions/workspace-indicator/schemas/org.gnome.shell.extensions.workspace-indicator.gschema.xml +new file mode 100644 +index 00000000..c7c634ca +--- /dev/null ++++ b/extensions/workspace-indicator/schemas/org.gnome.shell.extensions.workspace-indicator.gschema.xml +@@ -0,0 +1,15 @@ ++ ++ ++ ++ ++ ++ true ++ Show workspace previews in top bar ++ ++ ++ +diff --git a/extensions/workspace-indicator/workspaceIndicator.js b/extensions/workspace-indicator/workspaceIndicator.js +index a4d3bbee..20d4caa2 100644 +--- a/extensions/workspace-indicator/workspaceIndicator.js ++++ b/extensions/workspace-indicator/workspaceIndicator.js +@@ -22,8 +22,6 @@ const TOOLTIP_ANIMATION_TIME = 150; + + const SCROLL_TIME = 100; + +-const MAX_THUMBNAILS = 6; +- + let baseStyleClassName = ''; + + class WindowPreview extends St.Button { +@@ -433,8 +431,11 @@ export class WorkspaceIndicator extends PanelMenu.Button { + + const { + baseStyleClass = 'workspace-indicator', ++ settings, + } = params; + ++ this._settings = settings; ++ + baseStyleClassName = baseStyleClass; + this.add_style_class_name(baseStyleClassName); + +@@ -461,7 +462,6 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._workspacesItems = []; + + workspaceManager.connectObject( +- 'notify::n-workspaces', this._updateThumbnailVisibility.bind(this), GObject.ConnectFlags.AFTER, + 'workspace-switched', this._onWorkspaceSwitched.bind(this), GObject.ConnectFlags.AFTER, + this); + +@@ -477,6 +477,8 @@ export class WorkspaceIndicator extends PanelMenu.Button { + this._updateTopBarRedirect(); + }); + ++ this._settings.connect('changed::embed-previews', ++ () => this._updateThumbnailVisibility()); + this._updateThumbnailVisibility(); + } + +@@ -489,8 +491,7 @@ export class WorkspaceIndicator extends PanelMenu.Button { + } + + _updateThumbnailVisibility() { +- const {workspaceManager} = global; +- const useMenu = workspaceManager.n_workspaces > MAX_THUMBNAILS; ++ const useMenu = !this._settings.get_boolean('embed-previews'); + this.reactive = useMenu; + + this._statusLabel.visible = useMenu; +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 182b2be0..e6e76039 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -21,4 +21,5 @@ extensions/window-list/prefs.js + extensions/window-list/workspaceIndicator.js + extensions/windowsNavigator/extension.js + extensions/workspace-indicator/prefs.js ++extensions/workspace-indicator/schemas/org.gnome.shell.extensions.workspace-indicator.gschema.xml + extensions/workspace-indicator/workspaceIndicator.js +-- +2.44.0 + diff --git a/more-ws-previews-0028-window-list-Expose-workspace-preview-option.patch b/more-ws-previews-0028-window-list-Expose-workspace-preview-option.patch new file mode 100644 index 0000000..fa6851b --- /dev/null +++ b/more-ws-previews-0028-window-list-Expose-workspace-preview-option.patch @@ -0,0 +1,40 @@ +From 2e7aa8ccd266b66c9641b7e7239e45e7317ff431 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 21 Mar 2024 17:27:09 +0100 +Subject: [PATCH 28/28] window-list: Expose workspace preview option + +Now that we have the option, the window-list should expose it +in its preference window like the workspace-indicator. + +Part-of: +--- + extensions/window-list/prefs.js | 13 +++++++++++++ + 1 file changed, 13 insertions(+) + +diff --git a/extensions/window-list/prefs.js b/extensions/window-list/prefs.js +index 6b2d5958..194d1f9d 100644 +--- a/extensions/window-list/prefs.js ++++ b/extensions/window-list/prefs.js +@@ -81,6 +81,19 @@ class WindowListPrefsWidget extends Adw.PreferencesPage { + }); + row.add_suffix(toggle); + miscGroup.add(row); ++ ++ toggle = new Gtk.Switch({ ++ action_name: 'window-list.embed-previews', ++ valign: Gtk.Align.CENTER, ++ }); ++ this._settings.bind('embed-previews', ++ toggle, 'active', Gio.SettingsBindFlags.DEFAULT); ++ row = new Adw.ActionRow({ ++ title: _('Show workspace previews'), ++ activatable_widget: toggle, ++ }); ++ row.add_suffix(toggle); ++ miscGroup.add(row); + } + } + +-- +2.44.0 + diff --git a/prefer-window-icon.patch b/prefer-window-icon.patch new file mode 100644 index 0000000..a0f4685 --- /dev/null +++ b/prefer-window-icon.patch @@ -0,0 +1,72 @@ +From a344c1599edb64ffc4ad2b59de88616c6509bce8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 19 Mar 2024 13:16:50 +0100 +Subject: [PATCH 1/2] window-list: Use more appropriate fallback icon + +'icon-missing' is not an actual icon name. It somewhat works +because an invalid icon name will fallback to the correct +'image-missing', however for apps the generic app icon is +a better fallback. + +Part-of: +--- + extensions/window-list/extension.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 90bf34cd..c3ffe92f 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -165,7 +165,7 @@ class WindowTitle extends St.BoxLayout { + this._icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE); + } else { + this._icon.child = new St.Icon({ +- icon_name: 'icon-missing', ++ icon_name: 'application-x-executable', + icon_size: ICON_TEXTURE_SIZE, + }); + } +-- +2.44.0 + + +From c0cccebbdf543d25851872abdfdf119a5a9657aa Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 19 Mar 2024 14:07:12 +0100 +Subject: [PATCH 2/2] window-list: Override with window icon if available + +--- + extensions/window-list/extension.js | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index c3ffe92f..034b72ba 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -169,6 +169,23 @@ class WindowTitle extends St.BoxLayout { + icon_size: ICON_TEXTURE_SIZE, + }); + } ++ ++ // Override with window icon if available ++ if (this._hasWindowIcon()) { ++ const textureCache = St.TextureCache.get_default(); ++ this._icon.child.gicon = textureCache.bind_cairo_surface_property( ++ this._metaWindow, 'icon'); ++ } ++ } ++ ++ _hasWindowIcon() { ++ // HACK: GI cannot handle CairoSurface, so this ++ // will throw if the icon property is null ++ try { ++ return this._metaWindow.icon !== null; ++ } catch (e) { ++ return true; ++ } + } + } + +-- +2.44.0 +