diff --git a/gnome-shell-extensions.spec b/gnome-shell-extensions.spec index 2b90dcc..a9b115c 100644 --- a/gnome-shell-extensions.spec +++ b/gnome-shell-extensions.spec @@ -7,7 +7,7 @@ Name: gnome-shell-extensions Version: 40.7 -Release: 19%{?dist} +Release: 20%{?dist} Summary: Modify and extend GNOME Shell functionality and behavior License: GPLv2+ @@ -49,6 +49,7 @@ Patch026: 0001-desktop-icons-Handle-touch-events.patch Patch027: more-ws-previews.patch Patch028: 0001-Add-move-clock-extension.patch Patch029: 0001-workspace-indicator-Re-fittsify-workspace-previews.patch +Patch030: window-list-reordering.patch %description GNOME Shell Extensions is a collection of extensions providing additional and @@ -467,6 +468,10 @@ workspaces. %changelog +* Thu Sep 26 2024 Florian Müllner - 40.7-20 +- Allow reordering items in window-list + Resolves: RHEL-22692 + * Tue Jul 02 2024 Florian Müllner - 40.7-19 - Extend workspace buttons to screen edge Resolves: RHEL-43545 diff --git a/window-list-reordering.patch b/window-list-reordering.patch new file mode 100644 index 0000000..17687c5 --- /dev/null +++ b/window-list-reordering.patch @@ -0,0 +1,2058 @@ +From 8d134cdb3d2b43485c55463b34ac07bd6b98fda2 Mon Sep 17 00:00:00 2001 +From: Jakub Steiner +Date: Tue, 16 Jul 2024 09:40:53 +0200 +Subject: [PATCH 01/24] window-list: Update styling + +- Contemporary look. Fewer borders, thinner outlines for workspace indicators +- Lacks the designed unfocused window separators. +- Relies on https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/merge_requests/328 + +Fixes https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/issues/421 + +Part-of: +--- + extensions/window-list/classic.css | 68 ++++++++----- + extensions/window-list/stylesheet.css | 96 +++++++------------ + .../workspace-indicator/stylesheet-dark.css | 4 +- + 3 files changed, 78 insertions(+), 90 deletions(-) + +diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css +index d7ceb062..088c9478 100644 +--- a/extensions/window-list/classic.css ++++ b/extensions/window-list/classic.css +@@ -1,22 +1,24 @@ ++/* ++ * SPDX-FileCopyrightText: 2013 Florian Müllner ++ * SPDX-FileCopyrightText: 2015 Jakub Steiner ++ * ++ * SPDX-License-Identifier: GPL-2.0-or-later ++ */ ++ + @import url("stylesheet.css"); + @import url("stylesheet-workspace-switcher-light.css"); + + #panel.bottom-panel { + border-top-width: 1px; + border-bottom-width: 0px; +- height: 2.25em ; +- padding: 2px; ++ height: 2.5em; + } + +- .bottom-panel .window-button > StWidget, +- .bottom-panel .window-picker-toggle > StWidget { +- color: #2e3436; +- background-color: #eee; ++ .bottom-panel .window-button > StWidget { + border-radius: 3px; + padding: 3px 6px 1px; + box-shadow: none; + text-shadow: none; +- border: 1px solid rgba(0,0,0,0.2); + } + + .bottom-panel .window-button > StWidget { +@@ -24,27 +26,43 @@ + max-width: 18.75em; + } + +- .bottom-panel .window-button:hover > StWidget, +- .bottom-panel .window-picker-toggle:hover > StWidget { +- background-color: #f9f9f9; +- } ++ .window-button > StWidget { ++ color: #000; ++ background-color: transparent; ++} + +- .bottom-panel .window-button:active > StWidget, +- .bottom-panel .window-button:focus > StWidget { +- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); +- } ++.window-button > StWidget { ++ -st-natural-width: 18.75em; ++ max-width: 18.75em; ++} + +- .bottom-panel .window-button.focused > StWidget, +- .bottom-panel .window-picker-toggle:checked > StWidget { +- background-color: #ccc; +- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); +- } ++.window-button:hover > StWidget { ++ background-color: st-darken(#eee,5%); ++} + +- .bottom-panel .window-button.focused:hover > StWidget { +- background-color: #e9e9e9; ++.window-button:active > StWidget, ++.window-button:focus > StWidget { ++ background-color: st-darken(#eee, 10%); ++} ++ ++.window-button.focused > StWidget { ++ background-color: st-darken(#eee,15%); ++} ++ ++ .window-button.focused:hover > StWidget { ++ background-color: st-darken(#eee, 20%); + } + +- .bottom-panel .window-button.minimized > StWidget { +- color: #888; +- box-shadow: none; ++ .window-button.focused:active > StWidget { ++ background-color: st-darken(#eee, 25%); + } ++ ++.window-button.minimized > StWidget { ++ color: #aaa; ++ background-color: #f9f9f9; ++} ++ ++.window-button.minimized:active > StWidget { ++ color: #aaa; ++ background-color: #f9f9f9; ++} +diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css +index 4ba47f07..b9087971 100644 +--- a/extensions/window-list/stylesheet.css ++++ b/extensions/window-list/stylesheet.css +@@ -1,3 +1,9 @@ ++/* ++ * SPDX-FileCopyrightText: 2012 Florian Müllner ++ * SPDX-FileCopyrightText: 2013 Giovanni Campagna ++ * ++ * SPDX-License-Identifier: GPL-2.0-or-later ++ */ + @import url("stylesheet-workspace-switcher-dark.css"); + + .window-list { +@@ -5,8 +11,14 @@ + font-size: 10pt; + } + ++.bottom-panel { ++ background-color: #000000; ++ border-top-width: 0px; ++ height: 2.45em; ++} ++ + .window-button { +- padding: 1px; ++ padding: 4px, 3px; + } + + .window-button:first-child:ltr { +@@ -21,22 +33,12 @@ + spacing: 4px; + } + +-.window-button > StWidget, +-.window-picker-toggle > StWidget { +- color: #bbb; +- background-color: black; +- border-radius: 2px; ++.window-button > StWidget { ++ color: #fff; ++ background-color: transparent; ++ border-radius: 4px; + padding: 3px 6px 1px; +- box-shadow: inset 1px 1px 4px rgba(255,255,255,0.5); +- text-shadow: 1px 1px 4px rgba(0,0,0,0.8); +-} +- +-.window-picker-toggle { +- padding: 3px; +-} +- +-.window-picker-toggle > StWidet { +- border: 1px solid rgba(255,255,255,0.3); ++ transition: 100ms ease; + } + + .window-button > StWidget { +@@ -44,35 +46,35 @@ + max-width: 18.75em; + } + +-.window-button:hover > StWidget, +-.window-picker-toggle:hover > StWidget { +- color: white; +- background-color: #1f1f1f; ++.window-button:hover > StWidget { ++ background-color: #303030; + } + + .window-button:active > StWidget, + .window-button:focus > StWidget { +- box-shadow: inset 2px 2px 4px rgba(255,255,255,0.5); ++ background-color: st-lighten(#303030, 5%); + } + +-.window-button.focused > StWidget, +-.window-picker-toggle:checked > StWidget { +- color: white; +- box-shadow: inset 1px 1px 4px rgba(255,255,255,0.7); ++.window-button.focused > StWidget { ++ background-color: #5b5b5b; + } + +-.window-button.focused:active > StWidget, +-.window-picker-toggle:checked:active > StWidget { +- box-shadow: inset 2px 2px 4px rgba(255,255,255,0.7); +-} ++ .window-button.focused:hover > StWidget { ++ background-color: st-lighten(#5b5b5b, 5%); ++ } ++ ++ .window-button.focused:active > StWidget { ++ background-color: st-lighten(#5b5b5b, 10%); ++ } + + .window-button.minimized > StWidget { + color: #666; +- box-shadow: inset -1px -1px 4px rgba(255,255,255,0.5); ++ background-color: #161616; + } + + .window-button.minimized:active > StWidget { +- box-shadow: inset -2px -2px 4px rgba(255,255,255,0.5); ++ color: #666; ++ background-color: #161616; + } + + .window-button-icon { +@@ -80,38 +82,6 @@ + height: 24px; + } + +-.window-list-workspace-indicator .status-label-bin { +- background-color: rgba(200, 200, 200, .3); +- border: 1px solid #cccccc; +- padding: 0 3px; +- margin: 3px; +-} +- +-.window-list-workspace-indicator .workspaces-box { +- spacing: 3px; +- padding: 3px; +-} +- +-.window-list-workspace-indicator .workspace { +- border: 2px solid #000; +- width: 52px; +- border-radius: 4px; +- background-color: #595959; +-} +- +-.window-list-workspace-indicator .workspace.active { +- border-color: #fff; +-} +- +-.window-list-workspace-indicator-window-preview { +- background-color: #bebebe; +- border: 1px solid #828282; +-} +- +-.window-list-workspace-indicator-window-preview.active { +- background-color: #d4d4d4; +-} +- + .notification { + font-weight: normal; + } +diff --git a/extensions/workspace-indicator/stylesheet-dark.css b/extensions/workspace-indicator/stylesheet-dark.css +index 017d844a..872c6afc 100644 +--- a/extensions/workspace-indicator/stylesheet-dark.css ++++ b/extensions/workspace-indicator/stylesheet-dark.css +@@ -39,7 +39,7 @@ + + .workspace-indicator-menu .workspace, + .workspace-indicator .workspace { +- border: 2px solid transparent; ++ border: 1px solid transparent; + border-radius: 4px; + background-color: #3f3f3f; + } +@@ -55,7 +55,7 @@ + + .workspace-indicator-menu .workspace.active, + .workspace-indicator .workspace.active { +- border-color: #9f9f9f; ++ border-color: #fff; + } + + .workspace-indicator-window-preview { +-- +2.47.0 + + +From a4c5998a98149633e480f9360fdb14f4f745dffb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 25 Sep 2024 03:36:08 +0200 +Subject: [PATCH 02/24] window-list: Small stylesheet cleanup + +The light stylesheet duplicates some declarations, and the +last occurrence matches what we already inherit from the +dark stylesheet. + +Part-of: +--- + extensions/window-list/classic.css | 10 ---------- + 1 file changed, 10 deletions(-) + +diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css +index 088c9478..3a6ffd02 100644 +--- a/extensions/window-list/classic.css ++++ b/extensions/window-list/classic.css +@@ -21,21 +21,11 @@ + text-shadow: none; + } + +- .bottom-panel .window-button > StWidget { +- -st-natural-width: 18.7em; +- max-width: 18.75em; +- } +- + .window-button > StWidget { + color: #000; + background-color: transparent; + } + +-.window-button > StWidget { +- -st-natural-width: 18.75em; +- max-width: 18.75em; +-} +- + .window-button:hover > StWidget { + background-color: st-darken(#eee,5%); + } +-- +2.47.0 + + +From 48d2c5e97563eaf7d7f190938b25d6dbac44f053 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 25 Jun 2024 19:24:35 +0200 +Subject: [PATCH 03/24] window-list: Don't use homogeneous layout + +We want all buttons in the window list to have the same size, +but that's already achieved via max/natural-width in the CSS. + +Not enforcing the equal size via the layout manager will allow +buttons to temporarily have a different size when we start +animating additions and removals. + +Part-of: +--- + extensions/window-list/extension.js | 9 +-------- + 1 file changed, 1 insertion(+), 8 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 688ca761..a59307e2 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -753,22 +753,15 @@ class WindowList extends St.Widget { + toggle.connect('notify::checked', + this._updateWindowListVisibility.bind(this)); + +- let layout = new Clutter.BoxLayout({ homogeneous: true }); +- this._windowList = new St.Widget({ ++ this._windowList = new St.BoxLayout({ + style_class: 'window-list', + reactive: true, +- layout_manager: layout, + x_align: Clutter.ActorAlign.START, + x_expand: true, + y_expand: true, + }); + box.add_child(this._windowList); + +- this._windowList.connect('style-changed', () => { +- let node = this._windowList.get_theme_node(); +- let spacing = node.get_length('spacing'); +- this._windowList.layout_manager.spacing = spacing; +- }); + this._windowList.connect('scroll-event', this._onScrollEvent.bind(this)); + + let indicatorsBox = new St.BoxLayout({ x_align: Clutter.ActorAlign.END }); +-- +2.47.0 + + +From f048566e534bfdb2180cd8a41be8fe32b22d6e93 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 18 Jun 2024 18:55:05 +0200 +Subject: [PATCH 04/24] window-list: Don't hide window button while unmanaging + +This will allow to animate the transition. + +Part-of: +--- + extensions/window-list/extension.js | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index a59307e2..8ac59dd0 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -417,6 +417,10 @@ class WindowButton extends BaseButton { + }); + this._updateVisibility(); + ++ this._unmanaging = false; ++ this._unmanagingId = metaWindow.connect('unmanaging', ++ () => (this._unmanaging = true)); ++ + this._windowTitle = new WindowTitle(this.metaWindow); + this.set_child(this._windowTitle); + this.label_actor = this._windowTitle.label_actor; +@@ -466,6 +470,9 @@ class WindowButton extends BaseButton { + } + + _updateVisibility() { ++ if (this._unmanaging) ++ return; ++ + this.visible = this._isWindowVisible(this.metaWindow); + } + +@@ -476,6 +483,7 @@ class WindowButton extends BaseButton { + _onDestroy() { + super._onDestroy(); + this.metaWindow.disconnect(this._skipTaskbarId); ++ this.metaWindow.disconnect(this._unmanagingId); + this.metaWindow.disconnect(this._workspaceChangedId); + global.display.disconnect(this._notifyFocusId); + this._contextMenu.destroy(); +-- +2.47.0 + + +From 2606ddee95a7eea5c4c1aab024bf696a13e86845 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 18 Jun 2024 18:59:38 +0200 +Subject: [PATCH 05/24] window-list: Animate buttons in and out + +Buttons are currently added and removed from the list without +any transitions, which gives the list a "jumpy" feel. Instead, +do what we do elsewhere and smoothly animate additions and +removals by re-using the dash's ItemContainer class. + +Part-of: +--- + extensions/window-list/extension.js | 34 ++++++++++++++++------------- + 1 file changed, 19 insertions(+), 15 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 8ac59dd0..cb9e7160 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -6,6 +6,7 @@ const ExtensionUtils = imports.misc.extensionUtils; + const Main = imports.ui.main; + const Overview = imports.ui.overview; + const PopupMenu = imports.ui.popupMenu; ++const { DashItemContainer } = imports.ui.dash; + + const Me = ExtensionUtils.getCurrentExtension(); + const { WindowPicker, WindowPickerToggle } = Me.imports.windowPicker; +@@ -229,22 +230,25 @@ const BaseButton = GObject.registerClass({ + GObject.ParamFlags.READWRITE, + false), + }, +-}, class BaseButton extends St.Button { ++}, class BaseButton extends DashItemContainer { + _init(perMonitor, monitorIndex) { + this._perMonitor = perMonitor; + this._monitorIndex = monitorIndex; + this._ignoreWorkspace = false; + +- super._init({ ++ super._init(); ++ ++ this._button = new St.Button({ + style_class: 'window-button', + can_focus: true, + x_expand: true, + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + }); ++ this.setChild(this._button); + + this.connect('notify::allocation', + this._updateIconGeometry.bind(this)); +- this.connect('clicked', this._onClicked.bind(this)); ++ this._button.connect('clicked', this._onClicked.bind(this)); + this.connect('destroy', this._onDestroy.bind(this)); + this.connect('popup-menu', this._onPopupMenu.bind(this)); + +@@ -422,7 +426,7 @@ class WindowButton extends BaseButton { + () => (this._unmanaging = true)); + + this._windowTitle = new WindowTitle(this.metaWindow); +- this.set_child(this._windowTitle); ++ this._button.set_child(this._windowTitle); + this.label_actor = this._windowTitle.label_actor; + + this._contextMenu = new WindowContextMenu(this, this.metaWindow); +@@ -558,7 +562,7 @@ class AppButton extends BaseButton { + this._updateVisibility(); + + let stack = new St.Widget({ layout_manager: new Clutter.BinLayout() }); +- this.set_child(stack); ++ this._button.set_child(stack); + + this._singleWindowTitle = new St.Bin({ + x_expand: true, +@@ -851,7 +855,7 @@ class WindowList extends St.Widget { + + this._windowSignals = new Map(); + this._windowCreatedId = global.display.connect( +- 'window-created', (dsp, win) => this._addWindow(win)); ++ 'window-created', (dsp, win) => this._addWindow(win, true)); + + this._dragBeginId = Main.xdndHandler.connect('drag-begin', + this._monitorDrag.bind(this)); +@@ -980,14 +984,14 @@ class WindowList extends St.Widget { + w2.metaWindow.get_stable_sequence(); + }); + for (let i = 0; i < windows.length; i++) +- this._addWindow(windows[i].metaWindow); ++ this._addWindow(windows[i].metaWindow, false); + } else { + let apps = this._appSystem.get_running().sort((a1, a2) => { + return _getAppStableSequence(a1) - + _getAppStableSequence(a2); + }); + for (let i = 0; i < apps.length; i++) +- this._addApp(apps[i]); ++ this._addApp(apps[i], false); + } + } + +@@ -1001,26 +1005,26 @@ class WindowList extends St.Widget { + return; + + if (app.state === Shell.AppState.RUNNING) +- this._addApp(app); ++ this._addApp(app, true); + else if (app.state === Shell.AppState.STOPPED) + this._removeApp(app); + } + +- _addApp(app) { ++ _addApp(app, animate) { + let button = new AppButton(app, this._perMonitor, this._monitor.index); + this._settings.bind('display-all-workspaces', + button, 'ignore-workspace', Gio.SettingsBindFlags.GET); + this._windowList.add_child(button); ++ button.show(animate); + } + + _removeApp(app) { + let children = this._windowList.get_children(); + let child = children.find(c => c.app === app); +- if (child) +- child.destroy(); ++ child?.animateOutAndDestroy(); + } + +- _addWindow(win) { ++ _addWindow(win, animate) { + if (!this._grouped) + this._checkGrouping(); + +@@ -1038,6 +1042,7 @@ class WindowList extends St.Widget { + this._settings.bind('display-all-workspaces', + button, 'ignore-workspace', Gio.SettingsBindFlags.GET); + this._windowList.add_child(button); ++ button.show(animate); + } + + _removeWindow(win) { +@@ -1054,8 +1059,7 @@ class WindowList extends St.Widget { + + let children = this._windowList.get_children(); + let child = children.find(c => c.metaWindow === win); +- if (child) +- child.destroy(); ++ child?.animateOutAndDestroy(); + } + + _monitorDrag() { +-- +2.47.0 + + +From 03d90de7a681d53674f11a3118ae49b6162f268c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 18 Jun 2024 20:08:56 +0200 +Subject: [PATCH 06/24] window-list: Replace custom tooltip implementation + +DashItemContainer already has support for showing a tooltip-like +label, so now that we use that for animating items, we can use +it for tooltips as well. + +Part-of: +--- + extensions/window-list/extension.js | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index cb9e7160..0dd3775e 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -246,6 +246,13 @@ const BaseButton = GObject.registerClass({ + }); + this.setChild(this._button); + ++ this._button.connect('notify::hover', () => { ++ if (this._button.hover) ++ this.showLabel(); ++ else ++ this.hideLabel(); ++ }); ++ + this.connect('notify::allocation', + this._updateIconGeometry.bind(this)); + this._button.connect('clicked', this._onClicked.bind(this)); +@@ -287,6 +294,18 @@ const BaseButton = GObject.registerClass({ + this._updateVisibility(); + } + ++ showLabel() { ++ const [, , preferredTitleWidth] = this.label_actor.get_preferred_size(); ++ const maxTitleWidth = this.label_actor.allocation.get_width(); ++ const isTitleFullyShown = preferredTitleWidth <= maxTitleWidth; ++ ++ const labelText = isTitleFullyShown ++ ? '' : this.label_actor.text; ++ ++ this.setLabelText(labelText); ++ super.showLabel(); ++ } ++ + _setLongPressTimeout() { + if (this._longPressTimeoutId) + return; +-- +2.47.0 + + +From 576b45df3cadd3398d350ac060a858c1b4881fd8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Sat, 13 Jul 2024 00:11:19 +0200 +Subject: [PATCH 07/24] window-list: Fix .focused styling + +Commit 039c66e7b7c wrapped the button in a container to +animate transitions, but didn't adjust the `.focused` +styling to still apply to the button (where it is +expected) rather than the wrapper. + +Fix that. + +Part-of: +--- + extensions/window-list/extension.js | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 0dd3775e..ec37f50f 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -378,9 +378,9 @@ const BaseButton = GObject.registerClass({ + + _updateStyle() { + if (this._isFocused()) +- this.add_style_class_name('focused'); ++ this._button.add_style_class_name('focused'); + else +- this.remove_style_class_name('focused'); ++ this._button.remove_style_class_name('focused'); + } + + _windowEnteredOrLeftMonitor(_metaDisplay, _monitorIndex, _metaWindow) { +-- +2.47.0 + + +From 44ea9aa790bd66e5d56c805b2d20117b64eaa46e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 25 Sep 2024 02:23:41 +0200 +Subject: [PATCH 08/24] window-list: Split out AppTitle class + +Even though it's just a box with icon and label, it's cleaner to +have a dedicated class. + +Part-of: +--- + extensions/window-list/extension.js | 65 +++++++++++++++++++---------- + 1 file changed, 42 insertions(+), 23 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index ec37f50f..9ced0940 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -221,6 +221,47 @@ class WindowTitle extends St.BoxLayout { + } + }); + ++const AppTitle = GObject.registerClass( ++class AppTitle extends St.BoxLayout { ++ _init(app) { ++ super._init({ ++ style_class: 'window-button-box', ++ x_expand: true, ++ y_expand: true, ++ }); ++ ++ this._app = app; ++ ++ const icon = new St.Bin({ ++ style_class: 'window-button-icon', ++ child: app.create_icon_texture(ICON_TEXTURE_SIZE), ++ }); ++ this.add_child(icon); ++ ++ let label = new St.Label({ ++ text: app.get_name(), ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(label); ++ this.label_actor = label; ++ ++ this._textureCache = St.TextureCache.get_default(); ++ this._iconThemeChangedId = ++ this._textureCache.connect('icon-theme-changed', () => { ++ icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE); ++ }); ++ ++ this.connect('destroy', this._onDestroy.bind(this)); ++ } ++ ++ _onDestroy() { ++ if (this._iconThemeChangedId) ++ this._textureCache.disconnect(this._iconThemeChangedId); ++ this._iconThemeChangedId = 0; ++ this._textureCache = null; ++ } ++}); ++ + + const BaseButton = GObject.registerClass({ + GTypeFlags: GObject.TypeFlags.ABSTRACT, +@@ -588,25 +629,9 @@ class AppButton extends BaseButton { + }); + stack.add_actor(this._singleWindowTitle); + +- this._multiWindowTitle = new St.BoxLayout({ +- style_class: 'window-button-box', +- x_expand: true, +- }); ++ this._multiWindowTitle = new AppTitle(app); + stack.add_actor(this._multiWindowTitle); + +- this._icon = new St.Bin({ +- style_class: 'window-button-icon', +- child: app.create_icon_texture(ICON_TEXTURE_SIZE), +- }); +- this._multiWindowTitle.add(this._icon); +- +- let label = new St.Label({ +- text: app.get_name(), +- y_align: Clutter.ActorAlign.CENTER, +- }); +- this._multiWindowTitle.add(label); +- this._multiWindowTitle.label_actor = label; +- + this._menuManager = new PopupMenu.PopupMenuManager(this); + this._menu = new PopupMenu.PopupMenu(this, 0.5, St.Side.BOTTOM); + this._menu.connect('open-state-changed', _onMenuStateChanged); +@@ -620,12 +645,6 @@ class AppButton extends BaseButton { + this._appContextMenu.actor.hide(); + Main.uiGroup.add_actor(this._appContextMenu.actor); + +- this._textureCache = St.TextureCache.get_default(); +- this._iconThemeChangedId = +- this._textureCache.connect('icon-theme-changed', () => { +- this._icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE); +- }); +- + this._windowsChangedId = this.app.connect( + 'windows-changed', this._windowsChanged.bind(this)); + this._windowsChanged(); +-- +2.47.0 + + +From 9539d42745825d219d7a6059c505817734b55182 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 25 Sep 2024 02:55:14 +0200 +Subject: [PATCH 09/24] window-list: Simplify app button + +Depending on the number of windows, the button either shows the +title of the lone window, or the app title for multiple windows. + +While we always recreate the single-window title, we only create +the app title once and hide it as necessary. Avoiding re-creating +a simple actor 50% of mode transitions isn't worth the additional +complexity, so just handle both single- and multi-window titles +the same way. + +Part-of: +--- + extensions/window-list/extension.js | 70 +++++++++++------------------ + 1 file changed, 25 insertions(+), 45 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 9ced0940..468cd641 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -621,17 +621,6 @@ class AppButton extends BaseButton { + this.app = app; + this._updateVisibility(); + +- let stack = new St.Widget({ layout_manager: new Clutter.BinLayout() }); +- this._button.set_child(stack); +- +- this._singleWindowTitle = new St.Bin({ +- x_expand: true, +- }); +- stack.add_actor(this._singleWindowTitle); +- +- this._multiWindowTitle = new AppTitle(app); +- stack.add_actor(this._multiWindowTitle); +- + this._menuManager = new PopupMenu.PopupMenuManager(this); + this._menu = new PopupMenu.PopupMenu(this, 0.5, St.Side.BOTTOM); + this._menu.connect('open-state-changed', _onMenuStateChanged); +@@ -640,11 +629,6 @@ class AppButton extends BaseButton { + this._menuManager.addMenu(this._menu); + Main.uiGroup.add_actor(this._menu.actor); + +- this._appContextMenu = new AppContextMenu(this); +- this._appContextMenu.connect('open-state-changed', _onMenuStateChanged); +- this._appContextMenu.actor.hide(); +- Main.uiGroup.add_actor(this._appContextMenu.actor); +- + this._windowsChangedId = this.app.connect( + 'windows-changed', this._windowsChanged.bind(this)); + this._windowsChanged(); +@@ -691,37 +675,33 @@ class AppButton extends BaseButton { + } + + _windowsChanged() { +- let windows = this.getWindowList(); +- this._singleWindowTitle.visible = windows.length === 1; +- this._multiWindowTitle.visible = !this._singleWindowTitle.visible; +- +- if (this._singleWindowTitle.visible) { +- if (!this._windowTitle) { +- this.metaWindow = windows[0]; +- this._windowTitle = new WindowTitle(this.metaWindow); +- this._singleWindowTitle.child = this._windowTitle; +- this._windowContextMenu = new WindowContextMenu(this, this.metaWindow); +- this._windowContextMenu.connect( +- 'open-state-changed', _onMenuStateChanged); +- Main.uiGroup.add_actor(this._windowContextMenu.actor); +- this._windowContextMenu.actor.hide(); +- this._contextMenuManager.addMenu(this._windowContextMenu); +- } +- this._contextMenuManager.removeMenu(this._appContextMenu); +- this._contextMenu = this._windowContextMenu; +- this.label_actor = this._windowTitle.label_actor; ++ const windows = this.getWindowList(); ++ const singleWindowMode = windows.length === 1; ++ ++ if (this._singleWindowMode === singleWindowMode) ++ return; ++ ++ this._singleWindowMode = singleWindowMode; ++ ++ this._button.child?.destroy(); ++ this._contextMenu?.destroy(); ++ ++ if (this._singleWindowMode) { ++ const [window] = windows; ++ this._button.child = new WindowTitle(window); ++ this._contextMenu = new WindowContextMenu(this, window); + } else { +- if (this._windowTitle) { +- this.metaWindow = null; +- this._singleWindowTitle.child = null; +- this._windowTitle = null; +- this._windowContextMenu.destroy(); +- this._windowContextMenu = null; +- } +- this._contextMenu = this._appContextMenu; +- this._contextMenuManager.addMenu(this._appContextMenu); +- this.label_actor = this._multiWindowTitle.label_actor; ++ this._button.child = new AppTitle(this.app); ++ this._contextMenu = new AppContextMenu(this); + } ++ ++ this.label_actor = this._button.child.label_actor; ++ ++ this._contextMenu.connect( ++ 'open-state-changed', this._onMenuStateChanged.bind(this)); ++ Main.uiGroup.add_child(this._contextMenu.actor); ++ this._contextMenu.actor.hide(); ++ this._contextMenuManager.addMenu(this._contextMenu); + } + + _onClicked(actor, button) { +-- +2.47.0 + + +From 62440713ed43132f3f4c098fb857fe8f86fba89f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 7 Oct 2024 17:22:04 +0200 +Subject: [PATCH 10/24] window-list: Fix minimized styling + +Commit 039c66e7b7c wrapped the button in a container to +animate transitions, but didn't adjust the `.minimized` +styling to still apply to the button (where it is +expected) rather than the wrapper. + +Fix this just like commit c72b8b21 did for the +`.focused` styling. + +Part-of: +--- + extensions/window-list/extension.js | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 468cd641..0d458022 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -523,9 +523,9 @@ class WindowButton extends BaseButton { + super._updateStyle(); + + if (this.metaWindow.minimized) +- this.add_style_class_name('minimized'); ++ this._button.add_style_class_name('minimized'); + else +- this.remove_style_class_name('minimized'); ++ this._button.remove_style_class_name('minimized'); + } + + _windowEnteredOrLeftMonitor(metaDisplay, monitorIndex, metaWindow) { +-- +2.47.0 + + +From e38db28576ae5c8d954580f4e5b5cf899d98b856 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Mon, 7 Oct 2024 17:10:43 +0200 +Subject: [PATCH 11/24] window-list: Fix active state + +Commit c72b8b21 fixed the styling of the active window's button, +but missed that the `active` property uses the style information +as well. + +Adjust it to use the correct actor when checking for the style class. + +Closes https://gitlab.gnome.org/GNOME/gnome-shell-extensions/-/issues/529 + +Part-of: +--- + extensions/window-list/extension.js | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 0d458022..44917b6d 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -316,7 +316,7 @@ const BaseButton = GObject.registerClass({ + } + + get active() { +- return this.has_style_class_name('focused'); ++ return this._button.has_style_class_name('focused'); + } + + // eslint-disable-next-line camelcase +-- +2.47.0 + + +From 579eea32c6bf226a9ef405863fbee103f0738a23 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 15 Oct 2024 17:48:52 +0200 +Subject: [PATCH 12/24] window-list: Remove outdated style + +A long time ago, the window list used to embed the bottom message +tray, which caused notifications to inherit the window-list's +font style. + +Since that's no longer the case, we have no business in messing +with notification styling, so stop doing that. +--- + extensions/window-list/stylesheet.css | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css +index b9087971..f02fca60 100644 +--- a/extensions/window-list/stylesheet.css ++++ b/extensions/window-list/stylesheet.css +@@ -81,7 +81,3 @@ + width: 24px; + height: 24px; + } +- +-.notification { +- font-weight: normal; +-} +-- +2.47.0 + + +From ab4e4d2aa6dcada7c22e3ce76844b080ff33b310 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 26 Sep 2024 19:07:11 +0200 +Subject: [PATCH 13/24] window-list: Split out some common code + +Adding an app button and adding a window button involves some +shared steps, move those to a shared `_addButton()` method. +--- + extensions/window-list/extension.js | 15 ++++++++------- + 1 file changed, 8 insertions(+), 7 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 44917b6d..ef813f97 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -1028,14 +1028,18 @@ class WindowList extends St.Widget { + this._removeApp(app); + } + +- _addApp(app, animate) { +- let button = new AppButton(app, this._perMonitor, this._monitor.index); ++ _addButton(button, animate) { + this._settings.bind('display-all-workspaces', + button, 'ignore-workspace', Gio.SettingsBindFlags.GET); + this._windowList.add_child(button); + button.show(animate); + } + ++ _addApp(app, animate) { ++ const button = new AppButton(app, this._perMonitor, this._monitor.index); ++ this._addButton(button, animate); ++ } ++ + _removeApp(app) { + let children = this._windowList.get_children(); + let child = children.find(c => c.app === app); +@@ -1056,11 +1060,8 @@ class WindowList extends St.Widget { + this._windowSignals.set( + win, win.connect('unmanaged', () => this._removeWindow(win))); + +- let button = new WindowButton(win, this._perMonitor, this._monitor.index); +- this._settings.bind('display-all-workspaces', +- button, 'ignore-workspace', Gio.SettingsBindFlags.GET); +- this._windowList.add_child(button); +- button.show(animate); ++ const button = new WindowButton(win, this._perMonitor, this._monitor.index); ++ this._addButton(button, animate); + } + + _removeWindow(win) { +-- +2.47.0 + + +From 014569ec46788789bda198fc3c7b70eaa245dc4c Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 3 Oct 2024 17:19:31 +0200 +Subject: [PATCH 14/24] window-list: Split out common TitleWidget class + +Both app- and window title use the same structure, so add a shared +base class. +--- + extensions/window-list/extension.js | 61 ++++++++++++++--------------- + 1 file changed, 30 insertions(+), 31 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index ef813f97..96f94312 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -133,22 +133,35 @@ class WindowContextMenu extends PopupMenu.PopupMenu { + } + } + +-const WindowTitle = GObject.registerClass( +-class WindowTitle extends St.BoxLayout { +- _init(metaWindow) { +- this._metaWindow = metaWindow; +- ++const TitleWidget = GObject.registerClass({ ++ GTypeFlags: GObject.TypeFlags.ABSTRACT, ++}, class TitleWidget extends St.BoxLayout { ++ _init() { + super._init({ + style_class: 'window-button-box', + x_expand: true, + y_expand: true, + }); + +- this._icon = new St.Bin({ style_class: 'window-button-icon' }); +- this.add(this._icon); +- this.label_actor = new St.Label({ y_align: Clutter.ActorAlign.CENTER }); +- this.label_actor.clutter_text.single_line_mode = true; +- this.add(this.label_actor); ++ this._icon = new St.Bin({ ++ style_class: 'window-button-icon', ++ }); ++ this.add_child(this._icon); ++ ++ this._label = new St.Label({ ++ y_align: Clutter.ActorAlign.CENTER, ++ }); ++ this.add_child(this._label); ++ this.label_actor = this._label; ++ } ++}); ++ ++const WindowTitle = GObject.registerClass( ++class WindowTitle extends TitleWidget { ++ _init(metaWindow) { ++ super._init(); ++ ++ this._metaWindow = metaWindow; + + this._textureCache = St.TextureCache.get_default(); + this._iconThemeChangedId = this._textureCache.connect( +@@ -178,9 +191,9 @@ class WindowTitle extends St.BoxLayout { + return; + + if (this._metaWindow.minimized) +- this.label_actor.text = '[%s]'.format(this._metaWindow.title); ++ this._label.text = '[%s]'.format(this._metaWindow.title); + else +- this.label_actor.text = this._metaWindow.title; ++ this._label.text = this._metaWindow.title; + } + + _updateIcon() { +@@ -222,33 +235,19 @@ class WindowTitle extends St.BoxLayout { + }); + + const AppTitle = GObject.registerClass( +-class AppTitle extends St.BoxLayout { ++class AppTitle extends TitleWidget { + _init(app) { +- super._init({ +- style_class: 'window-button-box', +- x_expand: true, +- y_expand: true, +- }); ++ super._init(); + + this._app = app; + +- const icon = new St.Bin({ +- style_class: 'window-button-icon', +- child: app.create_icon_texture(ICON_TEXTURE_SIZE), +- }); +- this.add_child(icon); +- +- let label = new St.Label({ +- text: app.get_name(), +- y_align: Clutter.ActorAlign.CENTER, +- }); +- this.add_child(label); +- this.label_actor = label; ++ this._icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE); ++ this._label.text = app.get_name(); + + this._textureCache = St.TextureCache.get_default(); + this._iconThemeChangedId = + this._textureCache.connect('icon-theme-changed', () => { +- icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE); ++ this._icon.child = app.create_icon_texture(ICON_TEXTURE_SIZE); + }); + + this.connect('destroy', this._onDestroy.bind(this)); +-- +2.47.0 + + +From 8b24c06f6f1b0c4aecdfd4f2f5e320665cff94a2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 3 Oct 2024 17:27:57 +0200 +Subject: [PATCH 15/24] window-list: Add TitleWidget:abstract-label property + +When true, the real label is replaced by a more abstract +representation. When used as drag actor, the focus is not +on identifying the window/app, but about picking a drop +location, and the reduced style helps with that. +--- + extensions/window-list/extension.js | 22 ++++++++++++++++++++++ + extensions/window-list/stylesheet.css | 6 ++++++ + 2 files changed, 28 insertions(+) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 96f94312..e03ab8d5 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -135,6 +135,12 @@ class WindowContextMenu extends PopupMenu.PopupMenu { + + const TitleWidget = GObject.registerClass({ + GTypeFlags: GObject.TypeFlags.ABSTRACT, ++ Properties: { ++ 'abstract-label': GObject.ParamSpec.boolean( ++ 'abstract-label', '', '', ++ GObject.ParamFlags.READWRITE, ++ false), ++ }, + }, class TitleWidget extends St.BoxLayout { + _init() { + super._init({ +@@ -153,6 +159,22 @@ const TitleWidget = GObject.registerClass({ + }); + this.add_child(this._label); + this.label_actor = this._label; ++ ++ this.bind_property('abstract-label', ++ this._label, 'visible', ++ GObject.BindingFlags.SYNC_CREATE | ++ GObject.BindingFlags.INVERT_BOOLEAN); ++ ++ this._abstractLabel = new St.Widget({ ++ style_class: 'window-button-abstract-label', ++ x_expand: true, ++ y_expand: true, ++ }); ++ this.add_child(this._abstractLabel); ++ ++ this.bind_property('abstract-label', ++ this._abstractLabel, 'visible', ++ GObject.BindingFlags.SYNC_CREATE); + } + }); + +diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css +index f02fca60..fce6bcc5 100644 +--- a/extensions/window-list/stylesheet.css ++++ b/extensions/window-list/stylesheet.css +@@ -81,3 +81,9 @@ + width: 24px; + height: 24px; + } ++ ++.window-button-abstract-label { ++ background-color: #888; ++ border-radius: 99px; ++ margin: 6px; ++} +-- +2.47.0 + + +From 7fa8938371db1aba1e467a9923b502351378edf2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 25 Sep 2024 03:20:52 +0200 +Subject: [PATCH 16/24] window-list: Split out `_createTitleActor()` hook + +This will allow creating a suitable drag actor that matches the +current title. In particular this allows for a drag actor that +isn't based on `ClutterClone`, and therefore doesn't inherit +focus/active/minimize/etc. styles that don't make sense outside +the actual window list. +--- + extensions/window-list/extension.js | 23 ++++++++++++++++++++--- + 1 file changed, 20 insertions(+), 3 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index e03ab8d5..bb35b1a4 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -418,6 +418,11 @@ const BaseButton = GObject.registerClass({ + this._onClicked(this, 1); + } + ++ _createTitleActor() { ++ throw new GObject.NotImplementedError( ++ `_createTitleActor in ${this.constructor.name}`); ++ } ++ + _onClicked(_actor, _button) { + throw new GObject.NotImplementedError( + `_onClicked in ${this.constructor.name}`); +@@ -506,7 +511,7 @@ class WindowButton extends BaseButton { + this._unmanagingId = metaWindow.connect('unmanaging', + () => (this._unmanaging = true)); + +- this._windowTitle = new WindowTitle(this.metaWindow); ++ this._windowTitle = this._createTitleActor(); + this._button.set_child(this._windowTitle); + this.label_actor = this._windowTitle.label_actor; + +@@ -524,6 +529,10 @@ class WindowButton extends BaseButton { + this._updateStyle(); + } + ++ _createTitleActor() { ++ return new WindowTitle(this.metaWindow); ++ } ++ + _onClicked(actor, button) { + if (this._contextMenu.isOpen) { + this._contextMenu.close(); +@@ -709,13 +718,12 @@ class AppButton extends BaseButton { + + if (this._singleWindowMode) { + const [window] = windows; +- this._button.child = new WindowTitle(window); + this._contextMenu = new WindowContextMenu(this, window); + } else { +- this._button.child = new AppTitle(this.app); + this._contextMenu = new AppContextMenu(this); + } + ++ this._button.child = this._createTitleActor(); + this.label_actor = this._button.child.label_actor; + + this._contextMenu.connect( +@@ -725,6 +733,15 @@ class AppButton extends BaseButton { + this._contextMenuManager.addMenu(this._contextMenu); + } + ++ _createTitleActor() { ++ if (this._singleWindowMode) { ++ const [window] = this.getWindowList(); ++ return new WindowTitle(window); ++ } else { ++ return new AppTitle(this.app); ++ } ++ } ++ + _onClicked(actor, button) { + let menuWasOpen = this._menu.isOpen; + if (menuWasOpen) +-- +2.47.0 + + +From 4ab013bb782f01991ccb78b0afc0b8442599d89f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 19 Jun 2024 13:01:37 +0200 +Subject: [PATCH 17/24] window-list: Rename XDND related methods and props + +The window list buttons themselves will become draggable, so +include "xdnd" in the existing drag handling to disambiguate +it. +--- + extensions/window-list/extension.js | 20 ++++++++++---------- + 1 file changed, 10 insertions(+), 10 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index bb35b1a4..cd073234 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -914,11 +914,11 @@ class WindowList extends St.Widget { + 'window-created', (dsp, win) => this._addWindow(win, true)); + + this._dragBeginId = Main.xdndHandler.connect('drag-begin', +- this._monitorDrag.bind(this)); ++ this._monitorXdndDrag.bind(this)); + this._dragEndId = Main.xdndHandler.connect('drag-end', +- this._stopMonitoringDrag.bind(this)); +- this._dragMonitor = { +- dragMotion: this._onDragMotion.bind(this), ++ this._stopMonitoringXdndDrag.bind(this)); ++ this._xdndDragMonitor = { ++ dragMotion: this._onXdndDragMotion.bind(this), + }; + + this._dndTimeoutId = 0; +@@ -1119,16 +1119,16 @@ class WindowList extends St.Widget { + child?.animateOutAndDestroy(); + } + +- _monitorDrag() { +- DND.addDragMonitor(this._dragMonitor); ++ _monitorXdndDrag() { ++ DND.addDragMonitor(this._xdndDragMonitor); + } + +- _stopMonitoringDrag() { +- DND.removeDragMonitor(this._dragMonitor); ++ _stopMonitoringXdndDrag() { ++ DND.removeDragMonitor(this._xdndDragMonitor); + this._removeActivateTimeout(); + } + +- _onDragMotion(dragEvent) { ++ _onXdndDragMotion(dragEvent) { + if (Main.overview.visible || + !this.contains(dragEvent.targetActor)) { + this._removeActivateTimeout(); +@@ -1197,7 +1197,7 @@ class WindowList extends St.Widget { + global.display.disconnect(this._fullscreenChangedId); + global.display.disconnect(this._windowCreatedId); + +- this._stopMonitoringDrag(); ++ this._stopMonitoringXdndDrag(); + Main.xdndHandler.disconnect(this._dragBeginId); + Main.xdndHandler.disconnect(this._dragEndId); + +-- +2.47.0 + + +From 260c2487e77ee819751d52c1d2a835babe2958c9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 25 Sep 2024 04:13:25 +0200 +Subject: [PATCH 18/24] window-list: Allow rearranging window buttons + +We currently sort buttons by the stable sequence to get a persistent +and predictable order. However some users want to customize that +order, and rearrange the buttons as they see fit. + +Support that use case by implementing drag-and-drop behavior based +on the overview's dash. + +Closes https://gitlab.gnome.org/GNOME/gnome-shell-extensions/issues/4 +--- + extensions/window-list/classic.css | 5 + + extensions/window-list/extension.js | 140 ++++++++++++++++++++++++++ + extensions/window-list/stylesheet.css | 14 ++- + 3 files changed, 157 insertions(+), 2 deletions(-) + +diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css +index 3a6ffd02..74945657 100644 +--- a/extensions/window-list/classic.css ++++ b/extensions/window-list/classic.css +@@ -56,3 +56,8 @@ + color: #aaa; + background-color: #f9f9f9; + } ++ ++.window-button-drag-actor { ++ background-color: #ddd; ++ border-color: #888; ++} +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index cd073234..a47ba07a 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -18,6 +18,8 @@ const _ = Gettext.gettext; + const ICON_TEXTURE_SIZE = 24; + const DND_ACTIVATE_TIMEOUT = 500; + ++const MIN_DRAG_UPDATE_INTERVAL = 500 * GLib.TIME_SPAN_MILLISECOND; ++ + const GroupingMode = { + NEVER: 0, + AUTO: 1, +@@ -25,6 +27,14 @@ const GroupingMode = { + }; + + ++const DragPlaceholderItem = GObject.registerClass( ++class DragPlaceholderItem extends DashItemContainer { ++ _init() { ++ super._init(); ++ this.setChild(new St.Bin({ style_class: 'placeholder' })); ++ } ++}); ++ + function _minimizeOrActivateWindow(window) { + let focusWindow = global.display.focus_window; + if (focusWindow === window || +@@ -283,6 +293,18 @@ class AppTitle extends TitleWidget { + } + }); + ++const DragActor = GObject.registerClass( ++class DragActor extends St.Bin { ++ _init(source, titleActor) { ++ super._init({ ++ style_class: 'window-button-drag-actor', ++ child: titleActor, ++ width: source.width, ++ }); ++ ++ this.source = source; ++ } ++}); + + const BaseButton = GObject.registerClass({ + GTypeFlags: GObject.TypeFlags.ABSTRACT, +@@ -292,6 +314,10 @@ const BaseButton = GObject.registerClass({ + GObject.ParamFlags.READWRITE, + false), + }, ++ Signals: { ++ 'drag-begin': {}, ++ 'drag-end': {}, ++ }, + }, class BaseButton extends DashItemContainer { + _init(perMonitor, monitorIndex) { + this._perMonitor = perMonitor; +@@ -334,6 +360,15 @@ const BaseButton = GObject.registerClass({ + 'window-left-monitor', + this._windowEnteredOrLeftMonitor.bind(this)); + } ++ ++ this._button._delegate = this; ++ this._draggable = DND.makeDraggable(this._button); ++ this._draggable.connect('drag-begin', () => { ++ this._removeLongPressTimeout(); ++ this.emit('drag-begin'); ++ }); ++ this._draggable.connect('drag-cancelled', () => this.emit('drag-end')); ++ this._draggable.connect('drag-end', () => this.emit('drag-end')); + } + + get active() { +@@ -418,6 +453,17 @@ const BaseButton = GObject.registerClass({ + this._onClicked(this, 1); + } + ++ getDragActor() { ++ const titleActor = this._createTitleActor(); ++ titleActor.set({ abstractLabel: true }); ++ ++ return new DragActor(this, titleActor); ++ } ++ ++ getDragActorSource() { ++ return this; ++ } ++ + _createTitleActor() { + throw new GObject.NotImplementedError( + `_createTitleActor in ${this.constructor.name}`); +@@ -921,9 +967,19 @@ class WindowList extends St.Widget { + dragMotion: this._onXdndDragMotion.bind(this), + }; + ++ this._itemDragMonitor = { ++ dragMotion: this._onItemDragMotion.bind(this), ++ }; ++ + this._dndTimeoutId = 0; + this._dndWindow = null; + ++ this._dragPlaceholder = null; ++ this._dragPlaceholderPos = -1; ++ this._lastPlaceholderUpdate = 0; ++ ++ this._delegate = this; ++ + this._settings = ExtensionUtils.getSettings(); + this._settings.connect('changed::grouping-mode', + () => this._groupingModeChanged()); +@@ -1069,6 +1125,14 @@ class WindowList extends St.Widget { + _addButton(button, animate) { + this._settings.bind('display-all-workspaces', + button, 'ignore-workspace', Gio.SettingsBindFlags.GET); ++ ++ button.connect('drag-begin', ++ () => this._monitorItemDrag()); ++ button.connect('drag-end', () => { ++ this._stopMonitoringItemDrag(); ++ this._clearDragPlaceholder(); ++ }); ++ + this._windowList.add_child(button); + button.show(animate); + } +@@ -1119,6 +1183,82 @@ class WindowList extends St.Widget { + child?.animateOutAndDestroy(); + } + ++ _clearDragPlaceholder() { ++ this._dragPlaceholder?.animateOutAndDestroy(); ++ this._dragPlaceholder = null; ++ this._dragPlaceholderPos = -1; ++ } ++ ++ handleDragOver(source, _actor, x, _y, _time) { ++ if (!(source instanceof BaseButton)) ++ return DND.DragMotionResult.NO_DROP; ++ ++ const buttons = this._windowList.get_children().filter(c => c instanceof BaseButton); ++ const buttonPos = buttons.indexOf(source); ++ const numButtons = buttons.length; ++ let boxWidth = this._windowList.width; ++ ++ // Transform to window list coordinates for index calculation ++ // (mostly relevant for RTL to discard workspace indicator etc.) ++ x -= this._windowList.x; ++ ++ const rtl = this.text_direction === Clutter.TextDirection.RTL; ++ let pos = rtl ++ ? numButtons - Math.round(x * numButtons / boxWidth) ++ : Math.round(x * numButtons / boxWidth); ++ ++ pos = Math.clamp(pos, 0, numButtons); ++ ++ const timeDelta = ++ GLib.get_monotonic_time() - this._lastPlaceholderUpdate; ++ ++ if (pos !== this._dragPlaceholderPos && timeDelta >= MIN_DRAG_UPDATE_INTERVAL) { ++ this._clearDragPlaceholder(); ++ this._dragPlaceholderPos = pos; ++ ++ this._lastPlaceholderUpdate = GLib.get_monotonic_time(); ++ ++ // Don't allow positioning before or after self ++ if (pos === buttonPos || pos === buttonPos + 1) ++ return DND.DragMotionResult.CONTINUE; ++ ++ this._dragPlaceholder = new DragPlaceholderItem(); ++ const sibling = buttons[pos]; ++ if (sibling) ++ this._windowList.insert_child_below(this._dragPlaceholder, sibling); ++ else ++ this._windowList.insert_child_above(this._dragPlaceholder, null); ++ this._dragPlaceholder.show(true); ++ } ++ ++ return this._dragPlaceholder ++ ? DND.DragMotionResult.MOVE_DROP ++ : DND.DragMotionResult.NO_DROP; ++ } ++ ++ acceptDrop(source, _actor, _x, _y, _time) { ++ if (this._dragPlaceholderPos >= 0) ++ this._windowList.set_child_at_index(source, this._dragPlaceholderPos); ++ ++ this._clearDragPlaceholder(); ++ ++ return true; ++ } ++ ++ _monitorItemDrag() { ++ DND.addDragMonitor(this._itemDragMonitor); ++ } ++ ++ _stopMonitoringItemDrag() { ++ DND.removeDragMonitor(this._itemDragMonitor); ++ } ++ ++ _onItemDragMotion(dragEvent) { ++ if (!this._windowList.contains(dragEvent.targetActor)) ++ this._clearDragPlaceholder(); ++ return DND.DragMotionResult.CONTINUE; ++ } ++ + _monitorXdndDrag() { + DND.addDragMonitor(this._xdndDragMonitor); + } +diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css +index fce6bcc5..c92081d2 100644 +--- a/extensions/window-list/stylesheet.css ++++ b/extensions/window-list/stylesheet.css +@@ -17,10 +17,19 @@ + height: 2.45em; + } + +-.window-button { ++.window-button, ++.window-button-drag-actor { + padding: 4px, 3px; + } + ++.window-button-drag-actor { ++ background-color: #444; ++ border-radius: 7px; ++ border-width: 2px; ++ border-color: #fff; ++ box-shadow: 0 1px 2px rgba(0,0,0,0.1); ++} ++ + .window-button:first-child:ltr { + padding-left: 2px; + } +@@ -41,7 +50,8 @@ + transition: 100ms ease; + } + +-.window-button > StWidget { ++.window-button > StWidget, ++.window-list .placeholder { + -st-natural-width: 18.75em; + max-width: 18.75em; + } +-- +2.47.0 + + +From f56f84b6c22c5ce11a1a5a99db7c6a4b56f3ddce Mon Sep 17 00:00:00 2001 +From: Jakub Steiner +Date: Thu, 3 Oct 2024 14:18:32 +0200 +Subject: [PATCH 19/24] window-list: Indicate drop target more prominently + +The drop target is the main focus of the drag operation, so make +its styling more prominent. +--- + extensions/window-list/classic.css | 5 ++++- + extensions/window-list/stylesheet.css | 6 ++++++ + 2 files changed, 10 insertions(+), 1 deletion(-) + +diff --git a/extensions/window-list/classic.css b/extensions/window-list/classic.css +index 74945657..1147984c 100644 +--- a/extensions/window-list/classic.css ++++ b/extensions/window-list/classic.css +@@ -23,7 +23,6 @@ + + .window-button > StWidget { + color: #000; +- background-color: transparent; + } + + .window-button:hover > StWidget { +@@ -61,3 +60,7 @@ + background-color: #ddd; + border-color: #888; + } ++ ++.window-list .placeholder { ++ border-color: rgba(0,0,0,0.5); ++} +diff --git a/extensions/window-list/stylesheet.css b/extensions/window-list/stylesheet.css +index c92081d2..4c06ebc0 100644 +--- a/extensions/window-list/stylesheet.css ++++ b/extensions/window-list/stylesheet.css +@@ -56,6 +56,12 @@ + max-width: 18.75em; + } + ++.window-list .placeholder { ++ border: 1px solid rgba(255,255,255,0.4); ++ border-radius: 7px; ++ margin: 4px; ++} ++ + .window-button:hover > StWidget { + background-color: #303030; + } +-- +2.47.0 + + +From 1fd9fc35ec64e44524368da26356749b769e69eb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Thu, 3 Oct 2024 17:05:42 +0200 +Subject: [PATCH 20/24] window-list: Fade out drag source during drag + +During a drag operation, the focus is on the where to drop the dragged +item, not to identify it or its origin. +--- + extensions/window-list/extension.js | 18 ++++++++++++++++-- + 1 file changed, 16 insertions(+), 2 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index a47ba07a..bd1fc36a 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -20,6 +20,9 @@ const DND_ACTIVATE_TIMEOUT = 500; + + const MIN_DRAG_UPDATE_INTERVAL = 500 * GLib.TIME_SPAN_MILLISECOND; + ++const DRAG_OPACITY = 0.3; ++const DRAG_FADE_DURATION = 200; ++ + const GroupingMode = { + NEVER: 0, + AUTO: 1, +@@ -1126,9 +1129,20 @@ class WindowList extends St.Widget { + this._settings.bind('display-all-workspaces', + button, 'ignore-workspace', Gio.SettingsBindFlags.GET); + +- button.connect('drag-begin', +- () => this._monitorItemDrag()); ++ button.connect('drag-begin', () => { ++ button.ease({ ++ opacity: 255 * DRAG_OPACITY, ++ duration: DRAG_FADE_DURATION, ++ }); ++ ++ this._monitorItemDrag(); ++ }); + button.connect('drag-end', () => { ++ button.ease({ ++ opacity: 255, ++ duration: DRAG_FADE_DURATION, ++ }); ++ + this._stopMonitoringItemDrag(); + this._clearDragPlaceholder(); + }); +-- +2.47.0 + + +From c23e617112883e5589a18a96eadbe00ba940f79e Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 9 Oct 2024 19:15:16 +0200 +Subject: [PATCH 21/24] window-list: Shrink drag-actor size during drags + +Like the previous commit, this helps with putting the focus on +the target location instead of the dragged item. +--- + extensions/window-list/extension.js | 32 +++++++++++++++++++++++++++-- + 1 file changed, 30 insertions(+), 2 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index bd1fc36a..ac17e051 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -23,6 +23,8 @@ const MIN_DRAG_UPDATE_INTERVAL = 500 * GLib.TIME_SPAN_MILLISECOND; + const DRAG_OPACITY = 0.3; + const DRAG_FADE_DURATION = 200; + ++const DRAG_RESIZE_DURATION = 400; ++ + const GroupingMode = { + NEVER: 0, + AUTO: 1, +@@ -307,6 +309,23 @@ class DragActor extends St.Bin { + + this.source = source; + } ++ ++ setTargetWidth(width) { ++ const currentWidth = this.width; ++ ++ // set width immediately so shell's DND code uses correct values ++ this.set({ width }); ++ ++ // then transition from the original to the new width ++ Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { ++ this.set({ width: currentWidth }); ++ this.ease({ ++ width, ++ duration: DRAG_RESIZE_DURATION, ++ }); ++ return GLib.SOURCE_REMOVE; ++ }); ++ } + }); + + const BaseButton = GObject.registerClass({ +@@ -370,7 +389,10 @@ const BaseButton = GObject.registerClass({ + this._removeLongPressTimeout(); + this.emit('drag-begin'); + }); +- this._draggable.connect('drag-cancelled', () => this.emit('drag-end')); ++ this._draggable.connect('drag-cancelled', () => { ++ this._draggable._dragActor?.setTargetWidth(this.width); ++ this.emit('drag-end'); ++ }); + this._draggable.connect('drag-end', () => this.emit('drag-end')); + } + +@@ -460,7 +482,13 @@ const BaseButton = GObject.registerClass({ + const titleActor = this._createTitleActor(); + titleActor.set({ abstractLabel: true }); + +- return new DragActor(this, titleActor); ++ const dragActor = new DragActor(this, titleActor); ++ ++ const [, natWidth] = this.get_preferred_width(-1); ++ const targetWidth = Math.min(natWidth / 2, this.width); ++ dragActor.setTargetWidth(targetWidth); ++ ++ return dragActor; + } + + getDragActorSource() { +-- +2.47.0 + + +From a02a7c398d11586f379d1ac78695c37a4c1976e0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 8 Oct 2024 19:25:53 +0200 +Subject: [PATCH 22/24] window-list: Handle DND events near the drop target + +Even with the previous change, the dragged actor has the tendency +of obscuring the possible drop target. To alleviate this, handle +DND events near drop targets as if they occurred on the target. +--- + extensions/window-list/extension.js | 26 ++++++++++++++++++++++++-- + 1 file changed, 24 insertions(+), 2 deletions(-) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index ac17e051..f3fc0d21 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -25,6 +25,8 @@ const DRAG_FADE_DURATION = 200; + + const DRAG_RESIZE_DURATION = 400; + ++const DRAG_PROXIMITY_THRESHOLD = 30; ++ + const GroupingMode = { + NEVER: 0, + AUTO: 1, +@@ -1000,6 +1002,7 @@ class WindowList extends St.Widget { + + this._itemDragMonitor = { + dragMotion: this._onItemDragMotion.bind(this), ++ dragDrop: this._onItemDragDrop.bind(this), + }; + + this._dndTimeoutId = 0; +@@ -1296,11 +1299,30 @@ class WindowList extends St.Widget { + } + + _onItemDragMotion(dragEvent) { +- if (!this._windowList.contains(dragEvent.targetActor)) +- this._clearDragPlaceholder(); ++ const { source, targetActor, dragActor, x, y } = dragEvent; ++ ++ const hasTarget = this._windowList.contains(targetActor); ++ const isNear = Math.abs(y - this.y) < DRAG_PROXIMITY_THRESHOLD; ++ ++ if (hasTarget || isNear) ++ return this.handleDragOver(source, dragActor, x, y); ++ ++ this._clearDragPlaceholder(); + return DND.DragMotionResult.CONTINUE; + } + ++ _onItemDragDrop(dropEvent) { ++ if (this._dragPlaceholderPos < 0) ++ return DND.DragDropResult.CONTINUE; ++ ++ const { source } = dropEvent.dropActor; ++ this.acceptDrop(source); ++ dropEvent.dropActor.destroy(); ++ // HACK: SUCESS would make more sense, but results in gnome-shell ++ // skipping all drag-end code ++ return DND.DragDropResult.CONTINUE; ++ } ++ + _monitorXdndDrag() { + DND.addDragMonitor(this._xdndDragMonitor); + } +-- +2.47.0 + + +From dd64e5d972246fbfeaf24dec8a6997242b465982 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Wed, 26 Jun 2024 00:58:18 +0200 +Subject: [PATCH 23/24] window-list: Add `id` property to buttons + +A string ID that uniquely identifies a button will allow to +serialize/deserialize the positions in the next commit. +--- + extensions/window-list/extension.js | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index f3fc0d21..2b2c092c 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -608,6 +608,10 @@ class WindowButton extends BaseButton { + this._updateStyle(); + } + ++ get id() { ++ return `window:${this.metaWindow.get_id()}`; ++ } ++ + _createTitleActor() { + return new WindowTitle(this.metaWindow); + } +@@ -748,6 +752,10 @@ class AppButton extends BaseButton { + this._updateStyle(); + } + ++ get id() { ++ return `app:${this.app.get_id()}`; ++ } ++ + _windowEnteredOrLeftMonitor(metaDisplay, monitorIndex, metaWindow) { + if (this._windowTracker.get_window_app(metaWindow) === this.app && + monitorIndex === this._monitorIndex) { +-- +2.47.0 + + +From 89e8ead8eeee6807e917e9252c310dfff58a1c01 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Florian=20M=C3=BCllner?= +Date: Tue, 24 Sep 2024 20:31:06 +0200 +Subject: [PATCH 24/24] window-list: Save and restore positions as runtime + state + +While it doesn't make sense for window list positions to be truly +persistent like dash items, some persistence is desirable. + +Otherwise any manually set position is lost when the extension +is disabled, for example when locking the screen. + +To address this, serialize the positions as runtime state on drop, +and restore them when populating the list. +--- + extensions/window-list/extension.js | 28 ++++++++++++++++++++++++++++ + 1 file changed, 28 insertions(+) + +diff --git a/extensions/window-list/extension.js b/extensions/window-list/extension.js +index 2b2c092c..814f714e 100644 +--- a/extensions/window-list/extension.js ++++ b/extensions/window-list/extension.js +@@ -27,6 +27,8 @@ const DRAG_RESIZE_DURATION = 400; + + const DRAG_PROXIMITY_THRESHOLD = 30; + ++const SAVED_POSITIONS_KEY = 'window-list-positions'; ++ + const GroupingMode = { + NEVER: 0, + AUTO: 1, +@@ -1147,6 +1149,8 @@ class WindowList extends St.Widget { + for (let i = 0; i < apps.length; i++) + this._addApp(apps[i], false); + } ++ ++ this._restorePositions(); + } + + _updateKeyboardAnchor() { +@@ -1295,9 +1299,33 @@ class WindowList extends St.Widget { + + this._clearDragPlaceholder(); + ++ this._savePositions(); ++ + return true; + } + ++ _getPositionStateKey() { ++ return `${SAVED_POSITIONS_KEY}:${this._monitor.index}`; ++ } ++ ++ _savePositions() { ++ const buttons = this._windowList.get_children() ++ .filter(b => b instanceof BaseButton); ++ global.set_runtime_state(this._getPositionStateKey(), ++ new GLib.Variant('as', buttons.map(b => b.id))); ++ } ++ ++ _restorePositions() { ++ const positions = global.get_runtime_state('as', ++ this._getPositionStateKey())?.deepUnpack() ?? []; ++ ++ for (const button of this._windowList.get_children()) { ++ const pos = positions.indexOf(button.id); ++ if (pos > -1) ++ this._windowList.set_child_at_index(button, pos); ++ } ++ } ++ + _monitorItemDrag() { + DND.addDragMonitor(this._itemDragMonitor); + } +-- +2.47.0 +