Re-apply downstream patches

Re-apply rebased and updated version of the RHEL 9 downstream
patches, with some exceptions:

Branding is still TBD, so has been left out for now.

The desktop-icons extension will be replaced by an upstreamed
version of desktop-icons-ng, which is still work-in-progress.

Both dash-to-dock and dash-to-panel will be moved to separate
packages, based on the existing Fedora package.

It was decided to drop the panel-favorites and updates-dialog
extensions.

Resolves: RHEL-34255
This commit is contained in:
Florian Müllner 2024-04-25 19:25:30 +02:00
parent 26d9bf818a
commit e6414d8afc
No known key found for this signature in database
37 changed files with 6496 additions and 0 deletions

View File

@ -0,0 +1,32 @@
From 714feb8e1646b8a3e60bfd95683622a8f125bcf5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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

View File

@ -0,0 +1,30 @@
From 4d04a035416867caec9d0aa83f90b4156f017ad0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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

View File

@ -0,0 +1,158 @@
From 56da93fbbe675144ae8eb60daedb9f9d3e93be0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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 <adel.gadllah@gmail.com>
+// SPDX-FileCopyrightText: 2018 Florian Müllner <fmuellner@gnome.org>
+//
+// 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 <fmuellner@gnome.org>
+#
+# 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

View File

@ -0,0 +1,181 @@
From 4ca791c4d7872cb51ebc6cc90f906a9fcbb5b995 Mon Sep 17 00:00:00 2001
From: Carlos Garnacho <carlosg@gnome.org>
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 <carlosg@gnome.org>
+//
+// 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 @@
+<schemalist>
+ <schema id="org.gnome.shell.extensions.gesture-inhibitor" path="/org/gnome/shell/extensions/gesture-inhibitor/">
+ <key name="show-osk" type="b">
+ <default>true</default>
+ <summary>Show OSK gesture</summary>
+ </key>
+ <key name="overview" type="b">
+ <default>true</default>
+ <summary>Show Overview gesture</summary>
+ </key>
+ <key name="app-switch" type="b">
+ <default>true</default>
+ <summary>Application switch gesture</summary>
+ </key>
+ <key name="workspace-switch" type="b">
+ <default>true</default>
+ <summary>Workspace switch gesture</summary>
+ </key>
+ <key name="unfullscreen" type="b">
+ <default>true</default>
+ <summary>Unfullscreen gesture</summary>
+ </key>
+ </schema>
+</schemalist>
+
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

View File

@ -0,0 +1,688 @@
From fc39411ecf0431ecc39581bace4217a55ab028e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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 <fmuellner@gnome.org>
+// 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 @@
+<schemalist gettext-domain="gnome-shell-extensions">
+ <schema id="org.gnome.shell.extensions.classification-banner"
+ path="/org/gnome/shell/extensions/classification-banner/">
+ <key name="top-banner" type="b">
+ <default>true</default>
+ <summary>Show a banner at the top</summary>
+ </key>
+ <key name="bottom-banner" type="b">
+ <default>true</default>
+ <summary>Show a banner at the bottom</summary>
+ </key>
+ <key name="message" type="s">
+ <default>"UNCLASSIFIED"</default>
+ <summary>classification message</summary>
+ </key>
+ <key name="color" type="s">
+ <default>"#fff"</default>
+ <summary>text color</summary>
+ </key>
+ <key name="background-color" type="s">
+ <default>"rgba(0,122,51,0.75)"</default>
+ <summary>background color</summary>
+ </key>
+ <key name="system-info" type="b">
+ <default>false</default>
+ <summary>Include system info in top banner</summary>
+ </key>
+ </schema>
+</schemalist>
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

View File

@ -0,0 +1,878 @@
From 94e0261560490bdfe37df5d9f5ff1cbac9533d6a Mon Sep 17 00:00:00 2001
From: Ray Strode <rstrode@redhat.com>
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 <rstrode@redhat.com>
+//
+// 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 <rstrode@redhat.com>
+//
+// 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 <rstrode@redhat.com>
+#
+# 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 @@
+<!--
+SPDX-FileCopyrightText: 2021 Ray Strode <rstrode@redhat.com>
+
+SPDX-License-Identifier: GPL-2.0-or-later
+-->
+
+<schemalist gettext-domain="gnome-shell-extensions">
+ <schema id="org.gnome.shell.extensions.heads-up-display"
+ path="/org/gnome/shell/extensions/heads-up-display/">
+ <key name="idle-timeout" type="u">
+ <default>30</default>
+ <summary>Idle Timeout</summary>
+ <description>
+ Number of seconds until message is reshown after user goes idle.
+ </description>
+ </key>
+ <key name="message-heading" type="s">
+ <default>""</default>
+ <summary>Message to show at top of display</summary>
+ <description>
+ The top line of the heads up display message.
+ </description>
+ </key>
+ <key name="message-body" type="s">
+ <default>""</default>
+ <summary>Banner message</summary>
+ <description>
+ A message to always show at the top of the screen.
+ </description>
+ </key>
+ <key name="show-on-login-screen" type="b">
+ <default>true</default>
+ <summary>Show on login screen</summary>
+ <description>
+ Whether or not the message should display on the login screen
+ </description>
+ </key>
+ <key name="show-when-locked" type="b">
+ <default>false</default>
+ <summary>Show on screen shield</summary>
+ <description>
+ Whether or not the message should display when the screen is locked
+ </description>
+ </key>
+ <key name="show-when-unlocking" type="b">
+ <default>false</default>
+ <summary>Show on unlock screen</summary>
+ <description>
+ Whether or not the message should display on the unlock screen.
+ </description>
+ </key>
+ <key name="show-when-unlocked" type="b">
+ <default>false</default>
+ <summary>Show in user session</summary>
+ <description>
+ Whether or not the message should display when the screen is unlocked.
+ </description>
+ </key>
+ </schema>
+</schemalist>
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 <rstrode@redhat.com>
+//
+// 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 <rstrode@redhat.com>
+ *
+ * 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

View File

@ -0,0 +1,719 @@
From 2e2bc1032163110993aa3433295b5d8c84978790 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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 <http://www.gnu.org/licenses/>.
+ *
+ * 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

View File

@ -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

View File

@ -0,0 +1,942 @@
From 1f0681875eefd09df28630a74aabd1bf47f90dab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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 <erick.red@gmail.com>
+// SPDX-FileCopyrightText: 2011 Giovanni Campagna <gcampagna@src.gnome.org>
+// SPDX-FileCopyrightText: 2017 Florian Müllner <fmuellner@gnome.org>
+//
+// 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

View File

@ -0,0 +1,81 @@
From 5bb91b4303bb0696dce4ad7aeb31035e89c1e9ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,68 @@
From 962983e8019817afae63807459eeaf3ff50eab03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,57 @@
From cadf2bb80984ec3b27b2d22ad65ec675230905c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
.../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

View File

@ -0,0 +1,78 @@
From a54815b7e6726af56c9300b9c209bb554c3a4a25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,92 @@
From b539608940eb5956bbf1cb9083b7171a9d2d6708 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,44 @@
From dca2c8505c770958849fc8dda27715a09ceab3c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,47 @@
From c343e1d65ddc538972640e7da86429cbd4cf2829 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,29 @@
From 14ab607a7695297efe98b97ab023973055189cee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,70 @@
From c83fd93d2e7cae124a5fddcb87f415688b3a63b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,44 @@
From 8105cdf701db7af78bc2e17da6e01172ce7362c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,67 @@
From d595d168ed0fccf57b1995a4d36169d978820a7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
.../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

View File

@ -0,0 +1,49 @@
From 49bfa46b5981e94b240780240115fcdabd46a178 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
.../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

View File

@ -0,0 +1,87 @@
From 5455a97a04c6a2925f5113a85c2e09cff215941d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
.../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

View File

@ -0,0 +1,37 @@
From f14ae4e2725b4a98ec2e11151c4162e7a6494604 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,56 @@
From aca00d6e3a8b7d1fc59b19e9855685989127d59f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,71 @@
From 5d7cd70f55a3da9454bffaf0bbcb32dacc8e1971 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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 <fmuellner@gnome.org>
+ * SPDX-FileCopyrightText: 2015 Jakub Steiner <jimmac@gmail.com>
+ *
+ * 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

View File

@ -0,0 +1,30 @@
From d0730c0d9314963bb784ebce408c29b8c2610fc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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

View File

@ -0,0 +1,589 @@
From b15212a2d62ff93c1652ef717f778dda3a70f5e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/307>
---
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 <fmuellner@gnome.org>
-//
-// 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

View File

@ -0,0 +1,53 @@
From 3c1638195b33f9dfdd3df7847e88fab97188520a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
.../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

View File

@ -0,0 +1,99 @@
From 13dce7fcc1013a3cbb3a1e521e123a5d4ede75c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
.../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

View File

@ -0,0 +1,150 @@
From 6b508c92c4996771cf79eb4d81e5d285b598fe96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
.../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

View File

@ -0,0 +1,133 @@
From 8d3d9ef8d8688999d959f1062a62e9f3b7f489fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
.../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

View File

@ -0,0 +1,157 @@
From 3affa2e422de26862b4e473cfeeb89aea638df66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
.../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

View File

@ -0,0 +1,44 @@
From 1805cceb598d1ed6fd2039453242b28e44e079e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
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

View File

@ -0,0 +1,190 @@
From f72c6ed223c3d348bdf32c25b54b6c44a826eb7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
.../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

View File

@ -0,0 +1,215 @@
From 8d2b24290204be98423b3a952939895133bdc036 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
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.
</description>
</key>
+ <key name="embed-previews" type="b">
+ <default>true</default>
+ <summary>Show workspace previews in window list</summary>
+ </key>
</schema>
</schemalist>
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 @@
+<!--
+SPDX-FileCopyrightText: 2024 Florian Müllner <fmuellner@gnome.org>
+
+SPDX-License-Identifier: GPL-2.0-or-later
+-->
+
+<schemalist gettext-domain="gnome-shell-extensions">
+ <schema id="org.gnome.shell.extensions.workspace-indicator"
+ path="/org/gnome/shell/extensions/workspace-indicator/">
+ <key name="embed-previews" type="b">
+ <default>true</default>
+ <summary>Show workspace previews in top bar</summary>
+ </key>
+ </schema>
+</schemalist>
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

View File

@ -0,0 +1,40 @@
From 2e7aa8ccd266b66c9641b7e7239e45e7317ff431 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/316>
---
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

72
prefer-window-icon.patch Normal file
View File

@ -0,0 +1,72 @@
From a344c1599edb64ffc4ad2b59de88616c6509bce8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=BCllner?= <fmuellner@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/315>
---
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?= <fmuellner@gnome.org>
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