From bc6d5d3f39e373bc82b7f62d6f18407c0f1c00c0 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 10 Jan 2026 02:26:30 -0100 Subject: [PATCH 01/36] gwl: Add initial overflow support through sliding --- .../applet.js | 27 +- .../workspace.js | 248 ++++++++++++++++-- 2 files changed, 253 insertions(+), 22 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 24c944e17c..ad2f4438cd 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -109,7 +109,7 @@ class PinnedFavs { const currentWorkspace = this.params.state.trigger('getCurrentWorkspace'); const newFavorites = []; let refActorFound = false; - currentWorkspace.actor.get_children().forEach( (actor, i) => { + currentWorkspace.container.get_children().forEach( (actor, i) => { const appGroup = currentWorkspace.appGroups.find( appGroup => appGroup.actor === actor ); if (!appGroup) return; const {app, appId, isFavoriteApp} = appGroup.groupState; @@ -800,12 +800,25 @@ class GroupedWindowListApplet extends Applet.Applet { if(this.state.dragging.posList === null){ this.state.dragging.isForeign = !(source instanceof AppGroup); this.state.dragging.posList = []; - currentWorkspace.actor.get_children().forEach( child => { + + let offset = 0; + if (this.state.isHorizontal) { + offset = currentWorkspace.container.translation_x; + if (currentWorkspace.startButton.visible) + offset += currentWorkspace.startButton.width; + } else { + offset = currentWorkspace.container.translation_y; + if (currentWorkspace.startButton.visible) + offset += currentWorkspace.startButton.height; + } + + currentWorkspace.container.get_children().forEach( child => { let childPos; + let box = child.get_allocation_box(); if(rtl_horizontal) - childPos = this.actor.width - child.get_allocation_box()['x1']; + childPos = this.actor.width - (box.x1 + offset); else - childPos = child.get_allocation_box()[axis[1]]; + childPos = box[axis[1]] + offset; this.state.dragging.posList.push(childPos); }); } @@ -834,13 +847,13 @@ class GroupedWindowListApplet extends Applet.Applet { if(this.state.dragging.isForeign) { if (this.state.dragging.dragPlaceholder) - currentWorkspace.actor.set_child_at_index(this.state.dragging.dragPlaceholder.actor, pos); + currentWorkspace.container.set_child_at_index(this.state.dragging.dragPlaceholder.actor, pos); else { const iconSize = this.getPanelIconSize() * global.ui_scale; this.state.dragging.dragPlaceholder = new DND.GenericDragPlaceholderItem(); this.state.dragging.dragPlaceholder.child.width = iconSize; this.state.dragging.dragPlaceholder.child.height = iconSize; - currentWorkspace.actor.insert_child_at_index( + currentWorkspace.container.insert_child_at_index( this.state.dragging.dragPlaceholder.actor, this.state.dragging.pos ); @@ -848,7 +861,7 @@ class GroupedWindowListApplet extends Applet.Applet { } } else - currentWorkspace.actor.set_child_at_index(source.actor, pos); + currentWorkspace.container.set_child_at_index(source.actor, pos); } if(this.state.dragging.isForeign) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 4af591ee8c..5a0e689a02 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -1,4 +1,6 @@ const Clutter = imports.gi.Clutter; +const St = imports.gi.St; +const GLib = imports.gi.GLib; const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; const {unref} = imports.misc.util; @@ -11,7 +13,9 @@ class Workspace { constructor(params) { this.state = params.state; this.state.connect({ - orientation: () => this.on_orientation_changed(false) + orientation: (state) => { + this.on_orientation_changed(state.orientation); + } }); this.workspaceState = createStore({ workspaceIndex: params.index, @@ -28,7 +32,8 @@ class Workspace { if (this.state.willUnmount) { return; } - this.actor.remove_child(actor); + this.container.remove_child(actor); + this.updateScrollVisibility(); }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { @@ -41,33 +46,245 @@ class Workspace { this.signals = new SignalManager(null); this.metaWorkspace = params.metaWorkspace; - const managerOrientation = this.state.isHorizontal ? 'HORIZONTAL' : 'VERTICAL'; - this.manager = new Clutter.BoxLayout({orientation: Clutter.Orientation[managerOrientation]}); - this.actor = new Clutter.Actor({layout_manager: this.manager}); + const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + + this.manager = new Clutter.BoxLayout({orientation: managerOrientation}); + this.container = new Clutter.Actor({layout_manager: this.manager}); + + this.mainLayout = new Clutter.BoxLayout({orientation: managerOrientation}); + this.actor = new Clutter.Actor({ layout_manager: this.mainLayout, reactive: true }); + + // TODO: Move to Cinnamon default CSS styling + const shadeStyle = 'min-width: 15px; min-height: 20px; background-color: rgba(0, 0, 0, 0.25); border: 1px solid rgba(128, 128, 128, 0.2); margin: 0px; padding: 0px;'; + + this.startButton = new St.Bin({ + style_class: 'grouped-window-list-scroll-button-start', + style: shadeStyle, + visible: false, + reactive: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + this.endButton = new St.Bin({ + style_class: 'grouped-window-list-scroll-button-end', + style: shadeStyle, + visible: false, + reactive: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + + // XXX: Use fixed icon size instead of the popup-menu-icon style class? (or maybe set the default in the cinnamon default theme) + this.startIcon = new St.Icon({ + icon_name: 'pan-start-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' + }); + this.endIcon = new St.Icon({ + icon_name: 'pan-end-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' + }); + + this.startButton.set_child(this.startIcon); + this.endButton.set_child(this.endIcon); + + this.signals.connect(this.startButton, 'enter-event', () => this.startSlide(-1)); + this.signals.connect(this.startButton, 'leave-event', this.stopSlide, this); + this.signals.connect(this.endButton, 'enter-event', () => this.startSlide(1)); + this.signals.connect(this.endButton, 'leave-event', this.stopSlide, this); + + this.scrollBox = new Clutter.Actor({ clip_to_allocation: true }); + this.scrollBox.add_child(this.container); + + this.actor.add_child(this.startButton); + this.actor.add_child(this.scrollBox); + this.actor.add_child(this.endButton); + + this.scrollBox.set_x_expand(true); + this.scrollBox.set_y_expand(true); this.appGroups = []; this.lastFocusedApp = null; + this.slideTimerSourceId = 0; // Connect all the signals this.signals.connect(global.display, 'window-workspace-changed', (...args) => this.windowWorkspaceChanged(...args)); // Ugly change: refresh the removed app instances from all workspaces this.signals.connect(this.metaWorkspace, 'window-removed', (...args) => this.windowRemoved(...args)); this.signals.connect(global.window_manager, 'switch-workspace' , (...args) => this.reloadList(...args)); - this.on_orientation_changed(null, true); + this.signals.connect(this.actor, 'allocation-changed', this.updateScrollVisibility, this); + this.signals.connect(this.container, 'allocation-changed', this.updateScrollVisibility, this); + this.signals.connect(this.container, 'notify::translation-x', this.updateScrollVisibility, this); + this.signals.connect(this.container, 'notify::translation-y', this.updateScrollVisibility, this); + this.signals.connect(this.actor, 'scroll-event', (actor, event) => this.onScroll(event)); + + this.on_orientation_changed(this.state.orientation); } - on_orientation_changed() { + on_orientation_changed(orientation) { if (!this.manager) return; - if (!this.state.isHorizontal) { - this.manager.set_orientation(Clutter.Orientation.VERTICAL); - this.actor.set_x_align(Clutter.ActorAlign.CENTER); + const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + + this.manager.set_orientation(managerOrientation); + this.mainLayout.set_orientation(managerOrientation); + + if (this.state.isHorizontal) { + this.actor.set_x_align(Clutter.ActorAlign.FILL); + + this.startIcon.set_icon_name('pan-start-symbolic'); + this.endIcon.set_icon_name('pan-end-symbolic'); + + this.startButton.set_x_expand(false); + this.startButton.set_y_expand(true); + this.startButton.set_y_align(Clutter.ActorAlign.FILL); + this.endButton.set_x_expand(false); + this.endButton.set_y_expand(true); + this.endButton.set_y_align(Clutter.ActorAlign.FILL); } else { - this.manager.set_orientation(Clutter.Orientation.HORIZONTAL); + this.actor.set_x_align(Clutter.ActorAlign.CENTER); + + this.startIcon.set_icon_name('pan-up-symbolic'); + this.endIcon.set_icon_name('pan-down-symbolic'); + + this.startButton.set_x_expand(true); + this.startButton.set_y_expand(false); + this.startButton.set_x_align(Clutter.ActorAlign.FILL); + this.endButton.set_x_expand(true); + this.endButton.set_y_expand(false); + this.endButton.set_x_align(Clutter.ActorAlign.FILL); } this.refreshList(); } + startSlide(direction) { + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + + const scrollFunc = () => { + this.scroll(direction * 5); + if (this.slideTimerSourceId === 0) return GLib.SOURCE_REMOVE; + + // Check if reached bounds to stop timer + let current, min; + if (this.state.isHorizontal) { + current = this.container.translation_x; + min = Math.min(0, this.scrollBox.width - this.container.width); + } else { + current = this.container.translation_y; + min = Math.min(0, this.scrollBox.height - this.container.height); + } + + if (current >= 0 && direction < 0) return GLib.SOURCE_REMOVE; // At start, trying to go start + if (current <= min && direction > 0) return GLib.SOURCE_REMOVE; // At end, trying to go end + + return GLib.SOURCE_CONTINUE; + }; + + this.slideTimerSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, scrollFunc); + } + + stopSlide() { + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + } + + onScroll(event) { + let containerSize, scrollBoxSize; + if (this.state.isHorizontal) { + containerSize = this.container.get_preferred_width(-1)[1]; + scrollBoxSize = this.scrollBox.width; + } else { + containerSize = this.container.get_preferred_height(-1)[1]; + scrollBoxSize = this.scrollBox.height; + } + + if (containerSize <= scrollBoxSize) return Clutter.EVENT_PROPAGATE; + + const direction = event.get_scroll_direction(); + let delta = 0; + + if (direction === Clutter.ScrollDirection.SMOOTH) { + const [dx, dy] = event.get_scroll_delta(); + delta = this.state.isHorizontal ? dx : dy; + delta *= 15; // Scale smooth scroll + } else { + const step = 20; + if (direction === Clutter.ScrollDirection.UP || direction === Clutter.ScrollDirection.LEFT) { + delta = -step; + } else if (direction === Clutter.ScrollDirection.DOWN || direction === Clutter.ScrollDirection.RIGHT) { + delta = step; + } + } + + if (delta !== 0) { + this.scroll(delta); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + scroll(amount) { + let current, min, next; + if (this.state.isHorizontal) { + current = this.container.translation_x; + min = Math.min(0, this.scrollBox.width - this.container.width); + next = current - amount; + } else { + current = this.container.translation_y; + min = Math.min(0, this.scrollBox.height - this.container.height); + next = current - amount; + } + + if (next > 0) next = 0; + if (next < min) next = min; + + if (this.state.isHorizontal) this.container.translation_x = next; + else this.container.translation_y = next; + } + + updateScrollVisibility() { + let containerSize, scrollBoxSize; + + if (this.state.isHorizontal) { + containerSize = this.container.get_preferred_width(-1)[1]; + scrollBoxSize = this.scrollBox.width; + } else { + containerSize = this.container.get_preferred_height(-1)[1]; + scrollBoxSize = this.scrollBox.height; + } + + let minTranslation = Math.min(0, scrollBoxSize - containerSize); + let currentTranslation = this.state.isHorizontal ? this.container.translation_x : this.container.translation_y; + + // Clamp translation if bounds have changed (resizing, etc) + if (currentTranslation < minTranslation) { + currentTranslation = minTranslation; + if (this.state.isHorizontal) this.container.translation_x = currentTranslation; + else this.container.translation_y = currentTranslation; + } + + if (containerSize > scrollBoxSize) { + // Tolerance of 1 pixel to avoid flickering + this.startButton.visible = currentTranslation < -1; + this.endButton.visible = currentTranslation > minTranslation + 1; + } else { + this.startButton.visible = false; + this.endButton.visible = false; + + if (currentTranslation !== 0) { + if (this.state.isHorizontal) this.container.translation_x = 0; + else this.container.translation_y = 0; + } + } + } + getWindowCount(appId) { let windowCount = 0; this.appGroups.forEach( appGroup => { @@ -308,13 +525,14 @@ class Workspace { }); if(idx > -1) { - this.actor.insert_child_at_index(appGroup.actor, idx); + this.container.insert_child_at_index(appGroup.actor, idx); this.appGroups.splice(idx, 0, appGroup); } else { - this.actor.add_child(appGroup.actor); + this.container.add_child(appGroup.actor); this.appGroups.push(appGroup); } + this.updateScrollVisibility(); appGroup.windowAdded(metaWindow); }; @@ -339,7 +557,7 @@ class Workspace { updateAppGroupIndexes() { const newAppGroups = []; - this.actor.get_children().forEach( child => { + this.container.get_children().forEach( child => { const appGroup = this.appGroups.find( appGroup => appGroup.actor === child); if (appGroup) { newAppGroups.push(appGroup); @@ -403,7 +621,7 @@ class Workspace { // in edge case when multiple apps of the same program are favorited, do not move other app if(!otherAppObject.groupState.isFavoriteApp) { this.appGroups.splice(otherApp, 1); - this.actor.set_child_at_index(otherAppObject.actor, refApp); + this.container.set_child_at_index(otherAppObject.actor, refApp); this.appGroups.splice(refApp, 0, otherAppObject); // change previously unpinned app status to pinned From c6d8ccb604d376d9e795bcd63767481043acea0a Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 11 Jan 2026 00:01:20 -0100 Subject: [PATCH 02/36] gwl: Scroll to focused app when a new one is added or when changing workspaces To find the new app position in the container instead of using actor.x we are summing the size of each preceding appGroup until the current app to focus because actor.x would report 0 some times. --- .../constants.js | 1 + .../workspace.js | 71 ++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js index 51b08467e4..65170eaf26 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js @@ -12,6 +12,7 @@ const constants = { FLASH_INTERVAL: 500, FLASH_MAX_COUNT: 4, RESERVE_KEYS: ['willUnmount'], + SCROLL_TO_APP_DEBOUNCE_TIME: 100, TitleDisplay: { None: 1, App: 2, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 5a0e689a02..3f55ac20f1 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -37,7 +37,10 @@ class Workspace { }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { - if (focusedAppId === appGroup.groupState.appId) return; + if (focusedAppId === appGroup.groupState.appId) { + this.scrollToAppGroup(appGroup); + return; + }; appGroup.onFocusChange(false); }); } @@ -249,14 +252,66 @@ class Workspace { else this.container.translation_y = next; } + scrollToAppGroup(appGroup) { + if (this.scrollToAppDebounceTimeoutId) GLib.source_remove(this.scrollToAppDebounceTimeoutId); + this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + this._scrollToAppGroup(appGroup); + return GLib.SOURCE_REMOVE; + }); + } + + _scrollToAppGroup(appGroup) { + if (!appGroup || !appGroup.actor) return; + + const index = this.appGroups.indexOf(appGroup); + if (index === -1) return; + + const isHorizontal = this.state.isHorizontal; + let itemPos = 0; + let itemSize = 0; + + for (let i = 0; i <= index; i++) { + const actor = this.appGroups[i].actor; + if (isHorizontal) { + itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; + } else { + itemSize = actor.height > 0 ? actor.height : actor.get_preferred_height(-1)[1]; + } + itemPos += itemSize; + } + + const boxSize = isHorizontal ? this.scrollBox.width : this.scrollBox.height; + + let containerSize; + if (isHorizontal) { + containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; + } else { + containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; + } + + // Subtract half size to get center. + const targetCenter = itemPos - (itemSize / 2); + // We want targetCenter to be at boxSize / 2 + let newPos = (boxSize / 2) - targetCenter; + + const minPos = Math.min(0, boxSize - containerSize); + newPos = Math.round(Math.max(minPos, Math.min(newPos, 0))); + + if (isHorizontal) { + this.container.translation_x = newPos; + } else { + this.container.translation_y = newPos; + } + } + updateScrollVisibility() { let containerSize, scrollBoxSize; if (this.state.isHorizontal) { - containerSize = this.container.get_preferred_width(-1)[1]; + containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; scrollBoxSize = this.scrollBox.width; } else { - containerSize = this.container.get_preferred_height(-1)[1]; + containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; scrollBoxSize = this.scrollBox.height; } @@ -394,6 +449,7 @@ class Workspace { this.appGroups = []; this.loadFavorites(); this.refreshApps(); + this.scrollToFocusedApp(); } loadFavorites() { @@ -431,6 +487,15 @@ class Workspace { } } + scrollToFocusedApp() { + for (let appGroup of this.appGroups) { + if (appGroup.groupState.lastFocused && appGroup.groupState.lastFocused.has_focus()) { + this.scrollToAppGroup(appGroup); + return; + } + } + } + updateAttentionState(display, window) { this.appGroups.forEach( appGroup => { if (appGroup.groupState.metaWindows) { From f7d01cb86675ac380d4f1b5f543f1a32f07dadc9 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 11 Jan 2026 01:05:31 -0100 Subject: [PATCH 03/36] gwl: Avoid trying to remove slide timer multiple times This was triggering GLib Critical error in the logs. --- .../workspace.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 3f55ac20f1..73fb36dac0 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -7,7 +7,7 @@ const {unref} = imports.misc.util; const createStore = require('./state'); const AppGroup = require('./appGroup'); -const {RESERVE_KEYS} = require('./constants'); +const {RESERVE_KEYS, SCROLL_TO_APP_DEBOUNCE_TIME} = require('./constants'); class Workspace { constructor(params) { @@ -181,8 +181,17 @@ class Workspace { min = Math.min(0, this.scrollBox.height - this.container.height); } - if (current >= 0 && direction < 0) return GLib.SOURCE_REMOVE; // At start, trying to go start - if (current <= min && direction > 0) return GLib.SOURCE_REMOVE; // At end, trying to go end + // At start, trying to go start + if (current >= 0 && direction < 0) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + // At end, trying to go end + if (current <= min && direction > 0) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } return GLib.SOURCE_CONTINUE; }; @@ -254,7 +263,7 @@ class Workspace { scrollToAppGroup(appGroup) { if (this.scrollToAppDebounceTimeoutId) GLib.source_remove(this.scrollToAppDebounceTimeoutId); - this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SCROLL_TO_APP_DEBOUNCE_TIME, () => { this._scrollToAppGroup(appGroup); return GLib.SOURCE_REMOVE; }); From a79bb46bb107ac687c7b3aae1f77a1311161184c Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 11 Jan 2026 01:16:06 -0100 Subject: [PATCH 04/36] gwl: Properly integrate with the scroll-behavior settings --- .../grouped-window-list@cinnamon.org/settings-schema.json | 5 +++-- .../applets/grouped-window-list@cinnamon.org/workspace.js | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index c4f1b49cc2..43fe798fee 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -98,12 +98,13 @@ }, "scroll-behavior": { "type": "combobox", - "default": 1, + "default": 4, "description": "Mouse wheel scroll action", "options": { "None": 1, "Cycle apps": 2, - "Cycle windows": 3 + "Cycle windows": 3, + "Slide app list": 4 } }, "left-click-action": { diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 73fb36dac0..07d58d7eb1 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -207,6 +207,10 @@ class Workspace { } onScroll(event) { + if (this.state.settings.scrollBehavior !== 4) { + return Clutter.EVENT_PROPAGATE; + } + let containerSize, scrollBoxSize; if (this.state.isHorizontal) { containerSize = this.container.get_preferred_width(-1)[1]; From 6f440aa9189af3a73c11363eab22a996552f069f Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 11 Jan 2026 01:26:24 -0100 Subject: [PATCH 05/36] gwl: Avoid trying to remove the scroll to focused app deboucer timer after being removed already --- .../applets/grouped-window-list@cinnamon.org/workspace.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 07d58d7eb1..9847519d68 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -110,6 +110,7 @@ class Workspace { this.appGroups = []; this.lastFocusedApp = null; this.slideTimerSourceId = 0; + this.scrollToAppDebounceTimeoutId = 0; // Connect all the signals this.signals.connect(global.display, 'window-workspace-changed', (...args) => this.windowWorkspaceChanged(...args)); @@ -266,9 +267,10 @@ class Workspace { } scrollToAppGroup(appGroup) { - if (this.scrollToAppDebounceTimeoutId) GLib.source_remove(this.scrollToAppDebounceTimeoutId); + if (this.scrollToAppDebounceTimeoutId > 0) GLib.source_remove(this.scrollToAppDebounceTimeoutId); this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SCROLL_TO_APP_DEBOUNCE_TIME, () => { this._scrollToAppGroup(appGroup); + this.scrollToAppDebounceTimeoutId = 0; return GLib.SOURCE_REMOVE; }); } From 9839b6514ac7f6a738b48ed2dab83dda76c0503d Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 11 Jan 2026 01:58:52 -0100 Subject: [PATCH 06/36] gwl: Listen to the scrollBox allocation signal to update the scroll buttons visibility This avoids issues with improper representation of the current allocation after the buttons are hidden. --- .../applets/grouped-window-list@cinnamon.org/workspace.js | 1 + 1 file changed, 1 insertion(+) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 9847519d68..d7109e387c 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -121,6 +121,7 @@ class Workspace { this.signals.connect(this.container, 'allocation-changed', this.updateScrollVisibility, this); this.signals.connect(this.container, 'notify::translation-x', this.updateScrollVisibility, this); this.signals.connect(this.container, 'notify::translation-y', this.updateScrollVisibility, this); + this.signals.connect(this.scrollBox, 'notify::allocation', this.updateScrollVisibility, this); this.signals.connect(this.actor, 'scroll-event', (actor, event) => this.onScroll(event)); this.on_orientation_changed(this.state.orientation); From c0798a7b1fd56e0c895c6c0c3da10588fab8d37b Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 11 Jan 2026 02:54:45 -0100 Subject: [PATCH 07/36] gwl: Make sure to scroll to the focused app group on workspace switched --- .../workspace.js | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index d7109e387c..8cbb68aa3d 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -15,6 +15,11 @@ class Workspace { this.state.connect({ orientation: (state) => { this.on_orientation_changed(state.orientation); + }, + currentWs: (state) => { + if (this.metaWorkspace && state.currentWs === this.metaWorkspace.index()) { + this.scrollToFocusedApp(); + } } }); this.workspaceState = createStore({ @@ -283,6 +288,18 @@ class Workspace { if (index === -1) return; const isHorizontal = this.state.isHorizontal; + + let containerSize, boxSize; + if (isHorizontal) { + containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; + boxSize = this.scrollBox.width; + } else { + containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; + boxSize = this.scrollBox.height; + } + + if (containerSize <= boxSize) return; + let itemPos = 0; let itemSize = 0; @@ -296,15 +313,6 @@ class Workspace { itemPos += itemSize; } - const boxSize = isHorizontal ? this.scrollBox.width : this.scrollBox.height; - - let containerSize; - if (isHorizontal) { - containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; - } else { - containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; - } - // Subtract half size to get center. const targetCenter = itemPos - (itemSize / 2); // We want targetCenter to be at boxSize / 2 From 9a5efcce733cb667ff82462309bbe1bfca656470 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 17 Jan 2026 08:26:19 -0100 Subject: [PATCH 08/36] gwl: Refactor the scroll logic to its own class --- .../workspace.js | 237 ++++++++++-------- 1 file changed, 138 insertions(+), 99 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 8cbb68aa3d..3223971a74 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -9,56 +9,14 @@ const createStore = require('./state'); const AppGroup = require('./appGroup'); const {RESERVE_KEYS, SCROLL_TO_APP_DEBOUNCE_TIME} = require('./constants'); -class Workspace { - constructor(params) { - this.state = params.state; - this.state.connect({ - orientation: (state) => { - this.on_orientation_changed(state.orientation); - }, - currentWs: (state) => { - if (this.metaWorkspace && state.currentWs === this.metaWorkspace.index()) { - this.scrollToFocusedApp(); - } - } - }); - this.workspaceState = createStore({ - workspaceIndex: params.index, - lastFocusedApp: null - }); - this.workspaceState.connect({ - getWorkspace: () => this.metaWorkspace, - updateAppGroupIndexes: () => this.updateAppGroupIndexes(), - closeAllRightClickMenus: (cb) => this.closeAllRightClickMenus(cb), - closeAllHoverMenus: (cb) => this.closeAllHoverMenus(cb), - windowAdded: (win) => this.windowAdded(this.metaWorkspace, win), - windowRemoved: (win) => this.windowRemoved(this.metaWorkspace, win), - removeChild: (actor) => { - if (this.state.willUnmount) { - return; - } - this.container.remove_child(actor); - this.updateScrollVisibility(); - }, - updateFocusState: (focusedAppId) => { - this.appGroups.forEach( appGroup => { - if (focusedAppId === appGroup.groupState.appId) { - this.scrollToAppGroup(appGroup); - return; - }; - appGroup.onFocusChange(false); - }); - } - }); - +class AppGroupListScrollBox { + constructor(state, container) { + this.state = state; + this.container = container; this.signals = new SignalManager(null); - this.metaWorkspace = params.metaWorkspace; const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - this.manager = new Clutter.BoxLayout({orientation: managerOrientation}); - this.container = new Clutter.Actor({layout_manager: this.manager}); - this.mainLayout = new Clutter.BoxLayout({orientation: managerOrientation}); this.actor = new Clutter.Actor({ layout_manager: this.mainLayout, reactive: true }); @@ -112,16 +70,9 @@ class Workspace { this.scrollBox.set_x_expand(true); this.scrollBox.set_y_expand(true); - this.appGroups = []; - this.lastFocusedApp = null; this.slideTimerSourceId = 0; - this.scrollToAppDebounceTimeoutId = 0; // Connect all the signals - this.signals.connect(global.display, 'window-workspace-changed', (...args) => this.windowWorkspaceChanged(...args)); - // Ugly change: refresh the removed app instances from all workspaces - this.signals.connect(this.metaWorkspace, 'window-removed', (...args) => this.windowRemoved(...args)); - this.signals.connect(global.window_manager, 'switch-workspace' , (...args) => this.reloadList(...args)); this.signals.connect(this.actor, 'allocation-changed', this.updateScrollVisibility, this); this.signals.connect(this.container, 'allocation-changed', this.updateScrollVisibility, this); this.signals.connect(this.container, 'notify::translation-x', this.updateScrollVisibility, this); @@ -133,11 +84,8 @@ class Workspace { } on_orientation_changed(orientation) { - if (!this.manager) return; - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - this.manager.set_orientation(managerOrientation); this.mainLayout.set_orientation(managerOrientation); if (this.state.isHorizontal) { @@ -165,7 +113,7 @@ class Workspace { this.endButton.set_y_expand(false); this.endButton.set_x_align(Clutter.ActorAlign.FILL); } - this.refreshList(); + this.updateScrollVisibility(); } startSlide(direction) { @@ -272,19 +220,47 @@ class Workspace { else this.container.translation_y = next; } - scrollToAppGroup(appGroup) { - if (this.scrollToAppDebounceTimeoutId > 0) GLib.source_remove(this.scrollToAppDebounceTimeoutId); - this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SCROLL_TO_APP_DEBOUNCE_TIME, () => { - this._scrollToAppGroup(appGroup); - this.scrollToAppDebounceTimeoutId = 0; - return GLib.SOURCE_REMOVE; - }); + updateScrollVisibility() { + let containerSize, scrollBoxSize; + + if (this.state.isHorizontal) { + containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; + scrollBoxSize = this.scrollBox.width; + } else { + containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; + scrollBoxSize = this.scrollBox.height; + } + + let minTranslation = Math.min(0, scrollBoxSize - containerSize); + let currentTranslation = this.state.isHorizontal ? this.container.translation_x : this.container.translation_y; + + // Clamp translation if bounds have changed (resizing, etc) + if (currentTranslation < minTranslation) { + currentTranslation = minTranslation; + if (this.state.isHorizontal) this.container.translation_x = currentTranslation; + else this.container.translation_y = currentTranslation; + } + + if (containerSize > scrollBoxSize) { + // Tolerance of 1 pixel to avoid flickering + this.startButton.visible = currentTranslation < -1; + this.endButton.visible = currentTranslation > minTranslation + 1; + } else { + this.startButton.visible = false; + this.endButton.visible = false; + + if (currentTranslation !== 0) { + if (this.state.isHorizontal) this.container.translation_x = 0; + else this.container.translation_y = 0; + } + } } - _scrollToAppGroup(appGroup) { - if (!appGroup || !appGroup.actor) return; + scrollToChild(childActor) { + if (!childActor) return; - const index = this.appGroups.indexOf(appGroup); + const children = this.container.get_children(); + const index = children.indexOf(childActor); if (index === -1) return; const isHorizontal = this.state.isHorizontal; @@ -304,7 +280,7 @@ class Workspace { let itemSize = 0; for (let i = 0; i <= index; i++) { - const actor = this.appGroups[i].actor; + const actor = children[i]; if (isHorizontal) { itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; } else { @@ -328,40 +304,103 @@ class Workspace { } } - updateScrollVisibility() { - let containerSize, scrollBoxSize; - - if (this.state.isHorizontal) { - containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; - scrollBoxSize = this.scrollBox.width; - } else { - containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; - scrollBoxSize = this.scrollBox.height; + destroy() { + this.signals.disconnectAllSignals(); + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; } + this.actor.destroy(); + } +} - let minTranslation = Math.min(0, scrollBoxSize - containerSize); - let currentTranslation = this.state.isHorizontal ? this.container.translation_x : this.container.translation_y; +class Workspace { + constructor(params) { + this.state = params.state; + this.state.connect({ + orientation: (state) => { + this.on_orientation_changed(state.orientation); + }, + currentWs: (state) => { + if (this.metaWorkspace && state.currentWs === this.metaWorkspace.index()) { + this.scrollToFocusedApp(); + } + } + }); + this.workspaceState = createStore({ + workspaceIndex: params.index, + lastFocusedApp: null + }); + this.workspaceState.connect({ + getWorkspace: () => this.metaWorkspace, + updateAppGroupIndexes: () => this.updateAppGroupIndexes(), + closeAllRightClickMenus: (cb) => this.closeAllRightClickMenus(cb), + closeAllHoverMenus: (cb) => this.closeAllHoverMenus(cb), + windowAdded: (win) => this.windowAdded(this.metaWorkspace, win), + windowRemoved: (win) => this.windowRemoved(this.metaWorkspace, win), + removeChild: (actor) => { + if (this.state.willUnmount) { + return; + } + this.container.remove_child(actor); + this.scrollBoxObject.updateScrollVisibility(); + }, + updateFocusState: (focusedAppId) => { + this.appGroups.forEach( appGroup => { + if (focusedAppId === appGroup.groupState.appId) { + this.scrollToAppGroup(appGroup); + return; + }; + appGroup.onFocusChange(false); + }); + } + }); - // Clamp translation if bounds have changed (resizing, etc) - if (currentTranslation < minTranslation) { - currentTranslation = minTranslation; - if (this.state.isHorizontal) this.container.translation_x = currentTranslation; - else this.container.translation_y = currentTranslation; - } + this.signals = new SignalManager(null); + this.metaWorkspace = params.metaWorkspace; - if (containerSize > scrollBoxSize) { - // Tolerance of 1 pixel to avoid flickering - this.startButton.visible = currentTranslation < -1; - this.endButton.visible = currentTranslation > minTranslation + 1; - } else { - this.startButton.visible = false; - this.endButton.visible = false; + const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - if (currentTranslation !== 0) { - if (this.state.isHorizontal) this.container.translation_x = 0; - else this.container.translation_y = 0; - } - } + this.manager = new Clutter.BoxLayout({orientation: managerOrientation}); + this.container = new Clutter.Actor({layout_manager: this.manager}); + + this.scrollBoxObject = new AppGroupListScrollBox(this.state, this.container); + this.actor = this.scrollBoxObject.actor; + + this.appGroups = []; + this.lastFocusedApp = null; + this.scrollToAppDebounceTimeoutId = 0; + + // Connect all the signals + this.signals.connect(global.display, 'window-workspace-changed', (...args) => this.windowWorkspaceChanged(...args)); + // Ugly change: refresh the removed app instances from all workspaces + this.signals.connect(this.metaWorkspace, 'window-removed', (...args) => this.windowRemoved(...args)); + this.signals.connect(global.window_manager, 'switch-workspace' , (...args) => this.reloadList(...args)); + + this.on_orientation_changed(this.state.orientation); + } + + on_orientation_changed(orientation) { + if (!this.manager) return; + + const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + this.manager.set_orientation(managerOrientation); + this.scrollBoxObject.on_orientation_changed(orientation); + this.refreshList(); + } + + scrollToAppGroup(appGroup) { + if (this.scrollToAppDebounceTimeoutId > 0) GLib.source_remove(this.scrollToAppDebounceTimeoutId); + this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SCROLL_TO_APP_DEBOUNCE_TIME, () => { + this._scrollToAppGroup(appGroup); + this.scrollToAppDebounceTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _scrollToAppGroup(appGroup) { + if (!appGroup || !appGroup.actor) return; + this.scrollBoxObject.scrollToChild(appGroup.actor); } getWindowCount(appId) { @@ -621,7 +660,7 @@ class Workspace { this.container.add_child(appGroup.actor); this.appGroups.push(appGroup); } - this.updateScrollVisibility(); + this.scrollBoxObject.updateScrollVisibility(); appGroup.windowAdded(metaWindow); }; @@ -730,8 +769,8 @@ class Workspace { this.signals.disconnectAllSignals(); this.appGroups.forEach( appGroup => appGroup.destroy() ); this.workspaceState.destroy(); + this.scrollBoxObject.destroy(); this.manager = null; - this.actor.destroy(); unref(this, RESERVE_KEYS); } } From a86bdc8c74935829b439b198caf123ad6e6e74ca Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 17 Jan 2026 09:11:20 -0100 Subject: [PATCH 09/36] gwl: Improve scroll to item implementation --- .../workspace.js | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 3223971a74..b9fbce9f7f 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -168,10 +168,10 @@ class AppGroupListScrollBox { let containerSize, scrollBoxSize; if (this.state.isHorizontal) { - containerSize = this.container.get_preferred_width(-1)[1]; + containerSize = this.container.width || this.container.get_preferred_width(-1)[1]; scrollBoxSize = this.scrollBox.width; } else { - containerSize = this.container.get_preferred_height(-1)[1]; + containerSize = this.container.height || this.container.get_preferred_height(-1)[1]; scrollBoxSize = this.scrollBox.height; } @@ -257,11 +257,7 @@ class AppGroupListScrollBox { } scrollToChild(childActor) { - if (!childActor) return; - - const children = this.container.get_children(); - const index = children.indexOf(childActor); - if (index === -1) return; + if (!childActor || childActor.get_parent() !== this.container) return; const isHorizontal = this.state.isHorizontal; @@ -276,21 +272,43 @@ class AppGroupListScrollBox { if (containerSize <= boxSize) return; - let itemPos = 0; - let itemSize = 0; + let targetCenter = 0; + let allocationValid = false; - for (let i = 0; i <= index; i++) { - const actor = children[i]; - if (isHorizontal) { - itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; - } else { - itemSize = actor.height > 0 ? actor.height : actor.get_preferred_height(-1)[1]; + if (childActor.has_allocation()) { + const box = childActor.get_allocation_box(); + const size = isHorizontal ? box.get_width() : box.get_height(); + + if (size > 0) { + targetCenter = (isHorizontal ? box.x1 : box.y1) + (size / 2); + allocationValid = true; + } + } + + if (!allocationValid) { + const children = this.container.get_children(); + const index = children.indexOf(childActor); + + if (index === -1) return; + + let itemPos = 0; + let itemSize = 0; + + for (let i = 0; i <= index; i++) { + const actor = children[i]; + + if (isHorizontal) { + itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; + } else { + itemSize = actor.height > 0 ? actor.height : actor.get_preferred_height(-1)[1]; + } + + itemPos += itemSize; } - itemPos += itemSize; + + targetCenter = itemPos - (itemSize / 2); } - // Subtract half size to get center. - const targetCenter = itemPos - (itemSize / 2); // We want targetCenter to be at boxSize / 2 let newPos = (boxSize / 2) - targetCenter; From 6b737e3ea41867c9994bbd9767a730017c9c2db0 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 17 Jan 2026 09:27:13 -0100 Subject: [PATCH 10/36] gwl: Extract app group scroll box to its own module --- .../scrollBox.js | 330 +++++++++++++++++ .../workspace.js | 337 +----------------- 2 files changed, 338 insertions(+), 329 deletions(-) create mode 100644 files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js new file mode 100644 index 0000000000..7f521f7e4f --- /dev/null +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -0,0 +1,330 @@ +const Clutter = imports.gi.Clutter; +const St = imports.gi.St; +const GLib = imports.gi.GLib; +const { SignalManager } = imports.misc.signalManager; + + +class AppGroupListScrollBox { + constructor(state, container) { + this.state = state; + this.container = container; + this.signals = new SignalManager(null); + + const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + + this.mainLayout = new Clutter.BoxLayout({orientation: managerOrientation}); + this.actor = new Clutter.Actor({ layout_manager: this.mainLayout, reactive: true }); + + // TODO: Move to Cinnamon default CSS styling + const shadeStyle = 'min-width: 15px; min-height: 20px; background-color: rgba(0, 0, 0, 0.25); border: 1px solid rgba(128, 128, 128, 0.2); margin: 0px; padding: 0px;'; + + this.startButton = new St.Bin({ + style_class: 'grouped-window-list-scroll-button-start', + style: shadeStyle, + visible: false, + reactive: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + this.endButton = new St.Bin({ + style_class: 'grouped-window-list-scroll-button-end', + style: shadeStyle, + visible: false, + reactive: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + + // XXX: Use fixed icon size instead of the popup-menu-icon style class? (or maybe set the default in the cinnamon default theme) + this.startIcon = new St.Icon({ + icon_name: 'pan-start-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' + }); + this.endIcon = new St.Icon({ + icon_name: 'pan-end-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' + }); + + this.startButton.set_child(this.startIcon); + this.endButton.set_child(this.endIcon); + + this.signals.connect(this.startButton, 'enter-event', () => this.startSlide(-1)); + this.signals.connect(this.startButton, 'leave-event', this.stopSlide, this); + this.signals.connect(this.endButton, 'enter-event', () => this.startSlide(1)); + this.signals.connect(this.endButton, 'leave-event', this.stopSlide, this); + + this.scrollBox = new Clutter.Actor({ clip_to_allocation: true }); + this.scrollBox.add_child(this.container); + + this.actor.add_child(this.startButton); + this.actor.add_child(this.scrollBox); + this.actor.add_child(this.endButton); + + this.scrollBox.set_x_expand(true); + this.scrollBox.set_y_expand(true); + + this.slideTimerSourceId = 0; + + // Connect all the signals + this.signals.connect(this.actor, 'allocation-changed', this.updateScrollVisibility, this); + this.signals.connect(this.container, 'allocation-changed', this.updateScrollVisibility, this); + this.signals.connect(this.container, 'notify::translation-x', this.updateScrollVisibility, this); + this.signals.connect(this.container, 'notify::translation-y', this.updateScrollVisibility, this); + this.signals.connect(this.scrollBox, 'notify::allocation', this.updateScrollVisibility, this); + this.signals.connect(this.actor, 'scroll-event', (actor, event) => this.onScroll(event)); + + this.on_orientation_changed(this.state.orientation); + } + + on_orientation_changed(orientation) { + const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + + this.mainLayout.set_orientation(managerOrientation); + + if (this.state.isHorizontal) { + this.actor.set_x_align(Clutter.ActorAlign.FILL); + + this.startIcon.set_icon_name('pan-start-symbolic'); + this.endIcon.set_icon_name('pan-end-symbolic'); + + this.startButton.set_x_expand(false); + this.startButton.set_y_expand(true); + this.startButton.set_y_align(Clutter.ActorAlign.FILL); + this.endButton.set_x_expand(false); + this.endButton.set_y_expand(true); + this.endButton.set_y_align(Clutter.ActorAlign.FILL); + } else { + this.actor.set_x_align(Clutter.ActorAlign.CENTER); + + this.startIcon.set_icon_name('pan-up-symbolic'); + this.endIcon.set_icon_name('pan-down-symbolic'); + + this.startButton.set_x_expand(true); + this.startButton.set_y_expand(false); + this.startButton.set_x_align(Clutter.ActorAlign.FILL); + this.endButton.set_x_expand(true); + this.endButton.set_y_expand(false); + this.endButton.set_x_align(Clutter.ActorAlign.FILL); + } + this.updateScrollVisibility(); + } + + startSlide(direction) { + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + + const scrollFunc = () => { + this.scroll(direction * 5); + if (this.slideTimerSourceId === 0) return GLib.SOURCE_REMOVE; + + // Check if reached bounds to stop timer + let current, min; + if (this.state.isHorizontal) { + current = this.container.translation_x; + min = Math.min(0, this.scrollBox.width - this.container.width); + } else { + current = this.container.translation_y; + min = Math.min(0, this.scrollBox.height - this.container.height); + } + + // At start, trying to go start + if (current >= 0 && direction < 0) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + // At end, trying to go end + if (current <= min && direction > 0) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + return GLib.SOURCE_CONTINUE; + }; + + this.slideTimerSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, scrollFunc); + } + + stopSlide() { + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + } + + onScroll(event) { + if (this.state.settings.scrollBehavior !== 4) { + return Clutter.EVENT_PROPAGATE; + } + + let containerSize, scrollBoxSize; + if (this.state.isHorizontal) { + containerSize = this.container.width || this.container.get_preferred_width(-1)[1]; + scrollBoxSize = this.scrollBox.width; + } else { + containerSize = this.container.height || this.container.get_preferred_height(-1)[1]; + scrollBoxSize = this.scrollBox.height; + } + + if (containerSize <= scrollBoxSize) return Clutter.EVENT_PROPAGATE; + + const direction = event.get_scroll_direction(); + let delta = 0; + + if (direction === Clutter.ScrollDirection.SMOOTH) { + const [dx, dy] = event.get_scroll_delta(); + delta = this.state.isHorizontal ? dx : dy; + delta *= 15; // Scale smooth scroll + } else { + const step = 20; + if (direction === Clutter.ScrollDirection.UP || direction === Clutter.ScrollDirection.LEFT) { + delta = -step; + } else if (direction === Clutter.ScrollDirection.DOWN || direction === Clutter.ScrollDirection.RIGHT) { + delta = step; + } + } + + if (delta !== 0) { + this.scroll(delta); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + scroll(amount) { + let current, min, next; + if (this.state.isHorizontal) { + current = this.container.translation_x; + min = Math.min(0, this.scrollBox.width - this.container.width); + next = current - amount; + } else { + current = this.container.translation_y; + min = Math.min(0, this.scrollBox.height - this.container.height); + next = current - amount; + } + + if (next > 0) next = 0; + if (next < min) next = min; + + if (this.state.isHorizontal) this.container.translation_x = next; + else this.container.translation_y = next; + } + + updateScrollVisibility() { + let containerSize, scrollBoxSize; + + if (this.state.isHorizontal) { + containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; + scrollBoxSize = this.scrollBox.width; + } else { + containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; + scrollBoxSize = this.scrollBox.height; + } + + let minTranslation = Math.min(0, scrollBoxSize - containerSize); + let currentTranslation = this.state.isHorizontal ? this.container.translation_x : this.container.translation_y; + + // Clamp translation if bounds have changed (resizing, etc) + if (currentTranslation < minTranslation) { + currentTranslation = minTranslation; + if (this.state.isHorizontal) this.container.translation_x = currentTranslation; + else this.container.translation_y = currentTranslation; + } + + if (containerSize > scrollBoxSize) { + // Tolerance of 1 pixel to avoid flickering + this.startButton.visible = currentTranslation < -1; + this.endButton.visible = currentTranslation > minTranslation + 1; + } else { + this.startButton.visible = false; + this.endButton.visible = false; + + if (currentTranslation !== 0) { + if (this.state.isHorizontal) this.container.translation_x = 0; + else this.container.translation_y = 0; + } + } + } + + scrollToChild(childActor) { + if (!childActor || childActor.get_parent() !== this.container) return; + + const isHorizontal = this.state.isHorizontal; + + let containerSize, boxSize; + if (isHorizontal) { + containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; + boxSize = this.scrollBox.width; + } else { + containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; + boxSize = this.scrollBox.height; + } + + if (containerSize <= boxSize) return; + + let targetCenter = 0; + let allocationValid = false; + + if (childActor.has_allocation()) { + const box = childActor.get_allocation_box(); + const size = isHorizontal ? box.get_width() : box.get_height(); + + if (size > 0) { + targetCenter = (isHorizontal ? box.x1 : box.y1) + (size / 2); + allocationValid = true; + } + } + + if (!allocationValid) { + const children = this.container.get_children(); + const index = children.indexOf(childActor); + + if (index === -1) return; + + let itemPos = 0; + let itemSize = 0; + + for (let i = 0; i <= index; i++) { + const actor = children[i]; + + if (isHorizontal) { + itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; + } else { + itemSize = actor.height > 0 ? actor.height : actor.get_preferred_height(-1)[1]; + } + + itemPos += itemSize; + } + + targetCenter = itemPos - (itemSize / 2); + } + + // We want targetCenter to be at boxSize / 2 + let newPos = (boxSize / 2) - targetCenter; + + const minPos = Math.min(0, boxSize - containerSize); + newPos = Math.round(Math.max(minPos, Math.min(newPos, 0))); + + if (isHorizontal) { + this.container.translation_x = newPos; + } else { + this.container.translation_y = newPos; + } + } + + destroy() { + this.signals.disconnectAllSignals(); + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + this.actor.destroy(); + } +} + +module.exports = AppGroupListScrollBox; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index b9fbce9f7f..9e7bb4332a 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -7,330 +7,9 @@ const {unref} = imports.misc.util; const createStore = require('./state'); const AppGroup = require('./appGroup'); +const AppGroupListScrollBox = require('./scrollBox'); const {RESERVE_KEYS, SCROLL_TO_APP_DEBOUNCE_TIME} = require('./constants'); -class AppGroupListScrollBox { - constructor(state, container) { - this.state = state; - this.container = container; - this.signals = new SignalManager(null); - - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - - this.mainLayout = new Clutter.BoxLayout({orientation: managerOrientation}); - this.actor = new Clutter.Actor({ layout_manager: this.mainLayout, reactive: true }); - - // TODO: Move to Cinnamon default CSS styling - const shadeStyle = 'min-width: 15px; min-height: 20px; background-color: rgba(0, 0, 0, 0.25); border: 1px solid rgba(128, 128, 128, 0.2); margin: 0px; padding: 0px;'; - - this.startButton = new St.Bin({ - style_class: 'grouped-window-list-scroll-button-start', - style: shadeStyle, - visible: false, - reactive: true, - x_align: St.Align.MIDDLE, - y_align: St.Align.MIDDLE - }); - this.endButton = new St.Bin({ - style_class: 'grouped-window-list-scroll-button-end', - style: shadeStyle, - visible: false, - reactive: true, - x_align: St.Align.MIDDLE, - y_align: St.Align.MIDDLE - }); - - // XXX: Use fixed icon size instead of the popup-menu-icon style class? (or maybe set the default in the cinnamon default theme) - this.startIcon = new St.Icon({ - icon_name: 'pan-start-symbolic', - icon_type: St.IconType.SYMBOLIC, - style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' - }); - this.endIcon = new St.Icon({ - icon_name: 'pan-end-symbolic', - icon_type: St.IconType.SYMBOLIC, - style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' - }); - - this.startButton.set_child(this.startIcon); - this.endButton.set_child(this.endIcon); - - this.signals.connect(this.startButton, 'enter-event', () => this.startSlide(-1)); - this.signals.connect(this.startButton, 'leave-event', this.stopSlide, this); - this.signals.connect(this.endButton, 'enter-event', () => this.startSlide(1)); - this.signals.connect(this.endButton, 'leave-event', this.stopSlide, this); - - this.scrollBox = new Clutter.Actor({ clip_to_allocation: true }); - this.scrollBox.add_child(this.container); - - this.actor.add_child(this.startButton); - this.actor.add_child(this.scrollBox); - this.actor.add_child(this.endButton); - - this.scrollBox.set_x_expand(true); - this.scrollBox.set_y_expand(true); - - this.slideTimerSourceId = 0; - - // Connect all the signals - this.signals.connect(this.actor, 'allocation-changed', this.updateScrollVisibility, this); - this.signals.connect(this.container, 'allocation-changed', this.updateScrollVisibility, this); - this.signals.connect(this.container, 'notify::translation-x', this.updateScrollVisibility, this); - this.signals.connect(this.container, 'notify::translation-y', this.updateScrollVisibility, this); - this.signals.connect(this.scrollBox, 'notify::allocation', this.updateScrollVisibility, this); - this.signals.connect(this.actor, 'scroll-event', (actor, event) => this.onScroll(event)); - - this.on_orientation_changed(this.state.orientation); - } - - on_orientation_changed(orientation) { - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - - this.mainLayout.set_orientation(managerOrientation); - - if (this.state.isHorizontal) { - this.actor.set_x_align(Clutter.ActorAlign.FILL); - - this.startIcon.set_icon_name('pan-start-symbolic'); - this.endIcon.set_icon_name('pan-end-symbolic'); - - this.startButton.set_x_expand(false); - this.startButton.set_y_expand(true); - this.startButton.set_y_align(Clutter.ActorAlign.FILL); - this.endButton.set_x_expand(false); - this.endButton.set_y_expand(true); - this.endButton.set_y_align(Clutter.ActorAlign.FILL); - } else { - this.actor.set_x_align(Clutter.ActorAlign.CENTER); - - this.startIcon.set_icon_name('pan-up-symbolic'); - this.endIcon.set_icon_name('pan-down-symbolic'); - - this.startButton.set_x_expand(true); - this.startButton.set_y_expand(false); - this.startButton.set_x_align(Clutter.ActorAlign.FILL); - this.endButton.set_x_expand(true); - this.endButton.set_y_expand(false); - this.endButton.set_x_align(Clutter.ActorAlign.FILL); - } - this.updateScrollVisibility(); - } - - startSlide(direction) { - if (this.slideTimerSourceId > 0) { - GLib.source_remove(this.slideTimerSourceId); - this.slideTimerSourceId = 0; - } - - const scrollFunc = () => { - this.scroll(direction * 5); - if (this.slideTimerSourceId === 0) return GLib.SOURCE_REMOVE; - - // Check if reached bounds to stop timer - let current, min; - if (this.state.isHorizontal) { - current = this.container.translation_x; - min = Math.min(0, this.scrollBox.width - this.container.width); - } else { - current = this.container.translation_y; - min = Math.min(0, this.scrollBox.height - this.container.height); - } - - // At start, trying to go start - if (current >= 0 && direction < 0) { - this.slideTimerSourceId = 0; - return GLib.SOURCE_REMOVE; - } - - // At end, trying to go end - if (current <= min && direction > 0) { - this.slideTimerSourceId = 0; - return GLib.SOURCE_REMOVE; - } - - return GLib.SOURCE_CONTINUE; - }; - - this.slideTimerSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, scrollFunc); - } - - stopSlide() { - if (this.slideTimerSourceId > 0) { - GLib.source_remove(this.slideTimerSourceId); - this.slideTimerSourceId = 0; - } - } - - onScroll(event) { - if (this.state.settings.scrollBehavior !== 4) { - return Clutter.EVENT_PROPAGATE; - } - - let containerSize, scrollBoxSize; - if (this.state.isHorizontal) { - containerSize = this.container.width || this.container.get_preferred_width(-1)[1]; - scrollBoxSize = this.scrollBox.width; - } else { - containerSize = this.container.height || this.container.get_preferred_height(-1)[1]; - scrollBoxSize = this.scrollBox.height; - } - - if (containerSize <= scrollBoxSize) return Clutter.EVENT_PROPAGATE; - - const direction = event.get_scroll_direction(); - let delta = 0; - - if (direction === Clutter.ScrollDirection.SMOOTH) { - const [dx, dy] = event.get_scroll_delta(); - delta = this.state.isHorizontal ? dx : dy; - delta *= 15; // Scale smooth scroll - } else { - const step = 20; - if (direction === Clutter.ScrollDirection.UP || direction === Clutter.ScrollDirection.LEFT) { - delta = -step; - } else if (direction === Clutter.ScrollDirection.DOWN || direction === Clutter.ScrollDirection.RIGHT) { - delta = step; - } - } - - if (delta !== 0) { - this.scroll(delta); - return Clutter.EVENT_STOP; - } - - return Clutter.EVENT_PROPAGATE; - } - - scroll(amount) { - let current, min, next; - if (this.state.isHorizontal) { - current = this.container.translation_x; - min = Math.min(0, this.scrollBox.width - this.container.width); - next = current - amount; - } else { - current = this.container.translation_y; - min = Math.min(0, this.scrollBox.height - this.container.height); - next = current - amount; - } - - if (next > 0) next = 0; - if (next < min) next = min; - - if (this.state.isHorizontal) this.container.translation_x = next; - else this.container.translation_y = next; - } - - updateScrollVisibility() { - let containerSize, scrollBoxSize; - - if (this.state.isHorizontal) { - containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; - scrollBoxSize = this.scrollBox.width; - } else { - containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; - scrollBoxSize = this.scrollBox.height; - } - - let minTranslation = Math.min(0, scrollBoxSize - containerSize); - let currentTranslation = this.state.isHorizontal ? this.container.translation_x : this.container.translation_y; - - // Clamp translation if bounds have changed (resizing, etc) - if (currentTranslation < minTranslation) { - currentTranslation = minTranslation; - if (this.state.isHorizontal) this.container.translation_x = currentTranslation; - else this.container.translation_y = currentTranslation; - } - - if (containerSize > scrollBoxSize) { - // Tolerance of 1 pixel to avoid flickering - this.startButton.visible = currentTranslation < -1; - this.endButton.visible = currentTranslation > minTranslation + 1; - } else { - this.startButton.visible = false; - this.endButton.visible = false; - - if (currentTranslation !== 0) { - if (this.state.isHorizontal) this.container.translation_x = 0; - else this.container.translation_y = 0; - } - } - } - - scrollToChild(childActor) { - if (!childActor || childActor.get_parent() !== this.container) return; - - const isHorizontal = this.state.isHorizontal; - - let containerSize, boxSize; - if (isHorizontal) { - containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; - boxSize = this.scrollBox.width; - } else { - containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; - boxSize = this.scrollBox.height; - } - - if (containerSize <= boxSize) return; - - let targetCenter = 0; - let allocationValid = false; - - if (childActor.has_allocation()) { - const box = childActor.get_allocation_box(); - const size = isHorizontal ? box.get_width() : box.get_height(); - - if (size > 0) { - targetCenter = (isHorizontal ? box.x1 : box.y1) + (size / 2); - allocationValid = true; - } - } - - if (!allocationValid) { - const children = this.container.get_children(); - const index = children.indexOf(childActor); - - if (index === -1) return; - - let itemPos = 0; - let itemSize = 0; - - for (let i = 0; i <= index; i++) { - const actor = children[i]; - - if (isHorizontal) { - itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; - } else { - itemSize = actor.height > 0 ? actor.height : actor.get_preferred_height(-1)[1]; - } - - itemPos += itemSize; - } - - targetCenter = itemPos - (itemSize / 2); - } - - // We want targetCenter to be at boxSize / 2 - let newPos = (boxSize / 2) - targetCenter; - - const minPos = Math.min(0, boxSize - containerSize); - newPos = Math.round(Math.max(minPos, Math.min(newPos, 0))); - - if (isHorizontal) { - this.container.translation_x = newPos; - } else { - this.container.translation_y = newPos; - } - } - - destroy() { - this.signals.disconnectAllSignals(); - if (this.slideTimerSourceId > 0) { - GLib.source_remove(this.slideTimerSourceId); - this.slideTimerSourceId = 0; - } - this.actor.destroy(); - } -} class Workspace { constructor(params) { @@ -361,7 +40,7 @@ class Workspace { return; } this.container.remove_child(actor); - this.scrollBoxObject.updateScrollVisibility(); + this.scrollBox.updateScrollVisibility(); }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { @@ -382,8 +61,8 @@ class Workspace { this.manager = new Clutter.BoxLayout({orientation: managerOrientation}); this.container = new Clutter.Actor({layout_manager: this.manager}); - this.scrollBoxObject = new AppGroupListScrollBox(this.state, this.container); - this.actor = this.scrollBoxObject.actor; + this.scrollBox = new AppGroupListScrollBox(this.state, this.container); + this.actor = this.scrollBox.actor; this.appGroups = []; this.lastFocusedApp = null; @@ -403,7 +82,7 @@ class Workspace { const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; this.manager.set_orientation(managerOrientation); - this.scrollBoxObject.on_orientation_changed(orientation); + this.scrollBox.on_orientation_changed(orientation); this.refreshList(); } @@ -418,7 +97,7 @@ class Workspace { _scrollToAppGroup(appGroup) { if (!appGroup || !appGroup.actor) return; - this.scrollBoxObject.scrollToChild(appGroup.actor); + this.scrollBox.scrollToChild(appGroup.actor); } getWindowCount(appId) { @@ -678,7 +357,7 @@ class Workspace { this.container.add_child(appGroup.actor); this.appGroups.push(appGroup); } - this.scrollBoxObject.updateScrollVisibility(); + this.scrollBox.updateScrollVisibility(); appGroup.windowAdded(metaWindow); }; @@ -787,7 +466,7 @@ class Workspace { this.signals.disconnectAllSignals(); this.appGroups.forEach( appGroup => appGroup.destroy() ); this.workspaceState.destroy(); - this.scrollBoxObject.destroy(); + this.scrollBox.destroy(); this.manager = null; unref(this, RESERVE_KEYS); } From c3039afaa64eecd046c21d570ed8bcf985aa2a1b Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 17 Jan 2026 22:31:35 -0100 Subject: [PATCH 11/36] gwl: Disable actions if panel edit mode is enabled and update classes on orientation changed --- .../applet.js | 8 +++--- .../scrollBox.js | 26 ++++++++++++++----- .../workspace.js | 1 - 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index ad2f4438cd..b473459097 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -804,12 +804,12 @@ class GroupedWindowListApplet extends Applet.Applet { let offset = 0; if (this.state.isHorizontal) { offset = currentWorkspace.container.translation_x; - if (currentWorkspace.startButton.visible) - offset += currentWorkspace.startButton.width; + if (currentWorkspace.scrollBox.startButton.visible) + offset += currentWorkspace.scrollBox.startButton.width; } else { offset = currentWorkspace.container.translation_y; - if (currentWorkspace.startButton.visible) - offset += currentWorkspace.startButton.height; + if (currentWorkspace.scrollBox.startButton.visible) + offset += currentWorkspace.scrollBox.startButton.height; } currentWorkspace.container.get_children().forEach( child => { diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 7f521f7e4f..ed38662d93 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -19,7 +19,7 @@ class AppGroupListScrollBox { const shadeStyle = 'min-width: 15px; min-height: 20px; background-color: rgba(0, 0, 0, 0.25); border: 1px solid rgba(128, 128, 128, 0.2); margin: 0px; padding: 0px;'; this.startButton = new St.Bin({ - style_class: 'grouped-window-list-scroll-button-start', + style_class: 'grouped-window-list-scroll-button', style: shadeStyle, visible: false, reactive: true, @@ -27,7 +27,7 @@ class AppGroupListScrollBox { y_align: St.Align.MIDDLE }); this.endButton = new St.Bin({ - style_class: 'grouped-window-list-scroll-button-end', + style_class: 'grouped-window-list-scroll-button', style: shadeStyle, visible: false, reactive: true, @@ -85,10 +85,12 @@ class AppGroupListScrollBox { if (this.state.isHorizontal) { this.actor.set_x_align(Clutter.ActorAlign.FILL); - + this.startButton.remove_style_class_name('grouped-window-list-scroll-button-top'); + this.endButton.remove_style_class_name('grouped-window-list-scroll-button-down'); + this.startButton.add_style_class_name('grouped-window-list-scroll-button-left'); + this.endButton.add_style_class_name('grouped-window-list-scroll-button-right'); this.startIcon.set_icon_name('pan-start-symbolic'); this.endIcon.set_icon_name('pan-end-symbolic'); - this.startButton.set_x_expand(false); this.startButton.set_y_expand(true); this.startButton.set_y_align(Clutter.ActorAlign.FILL); @@ -97,10 +99,12 @@ class AppGroupListScrollBox { this.endButton.set_y_align(Clutter.ActorAlign.FILL); } else { this.actor.set_x_align(Clutter.ActorAlign.CENTER); - + this.startButton.remove_style_class_name('grouped-window-list-scroll-button-left'); + this.endButton.remove_style_class_name('grouped-window-list-scroll-button-right'); + this.startButton.add_style_class_name('grouped-window-list-scroll-button-top'); + this.endButton.add_style_class_name('grouped-window-list-scroll-button-down'); this.startIcon.set_icon_name('pan-up-symbolic'); this.endIcon.set_icon_name('pan-down-symbolic'); - this.startButton.set_x_expand(true); this.startButton.set_y_expand(false); this.startButton.set_x_align(Clutter.ActorAlign.FILL); @@ -117,6 +121,8 @@ class AppGroupListScrollBox { this.slideTimerSourceId = 0; } + if (this.state.panelEditMode) return; + const scrollFunc = () => { this.scroll(direction * 5); if (this.slideTimerSourceId === 0) return GLib.SOURCE_REMOVE; @@ -157,6 +163,8 @@ class AppGroupListScrollBox { } onScroll(event) { + if (this.state.panelEditMode) return; + if (this.state.settings.scrollBehavior !== 4) { return Clutter.EVENT_PROPAGATE; } @@ -216,6 +224,8 @@ class AppGroupListScrollBox { } updateScrollVisibility() { + if (this.state.panelEditMode) return; + let containerSize, scrollBoxSize; if (this.state.isHorizontal) { @@ -252,6 +262,8 @@ class AppGroupListScrollBox { } scrollToChild(childActor) { + if (this.state.panelEditMode) return; + if (!childActor || childActor.get_parent() !== this.container) return; const isHorizontal = this.state.isHorizontal; @@ -308,7 +320,7 @@ class AppGroupListScrollBox { let newPos = (boxSize / 2) - targetCenter; const minPos = Math.min(0, boxSize - containerSize); - newPos = Math.round(Math.max(minPos, Math.min(newPos, 0))); + newPos = Math.round(Math.max(minPos, Math.min(newPos, 0)) * 100) / 100; if (isHorizontal) { this.container.translation_x = newPos; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 9e7bb4332a..2c22f365d4 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -1,5 +1,4 @@ const Clutter = imports.gi.Clutter; -const St = imports.gi.St; const GLib = imports.gi.GLib; const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; From 82630e97275d4275c55cd8aeee370c9245e0164a Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 18 Jan 2026 00:32:54 -0100 Subject: [PATCH 12/36] gwl: Debounce calls to updateScrollVisibility to avoid it being called excessively --- .../scrollBox.js | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index ed38662d93..8c193a7e18 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -66,6 +66,7 @@ class AppGroupListScrollBox { this.scrollBox.set_y_expand(true); this.slideTimerSourceId = 0; + this.updateScrollVisibilityId = 0; // Connect all the signals this.signals.connect(this.actor, 'allocation-changed', this.updateScrollVisibility, this); @@ -224,6 +225,15 @@ class AppGroupListScrollBox { } updateScrollVisibility() { + if (this.updateScrollVisibilityId > 0) return; + this.updateScrollVisibilityId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { + this._updateScrollVisibility(); + this.updateScrollVisibilityId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _updateScrollVisibility() { if (this.state.panelEditMode) return; let containerSize, scrollBoxSize; @@ -247,9 +257,9 @@ class AppGroupListScrollBox { } if (containerSize > scrollBoxSize) { - // Tolerance of 1 pixel to avoid flickering - this.startButton.visible = currentTranslation < -1; - this.endButton.visible = currentTranslation > minTranslation + 1; + // Some tolerance to avoid flickering + this.startButton.visible = currentTranslation < -0.1; + this.endButton.visible = currentTranslation > minTranslation + 0.1; } else { this.startButton.visible = false; this.endButton.visible = false; @@ -335,6 +345,12 @@ class AppGroupListScrollBox { GLib.source_remove(this.slideTimerSourceId); this.slideTimerSourceId = 0; } + + if (this.updateScrollVisibilityId > 0) { + GLib.source_remove(this.updateScrollVisibilityId); + this.updateScrollVisibilityId = 0; + } + this.actor.destroy(); } } From 051b7ec1a0ce77936b5db237ed94f52f4963d518 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sun, 18 Jan 2026 01:22:44 -0100 Subject: [PATCH 13/36] gwl: Define style in proper cinnamon theme file instead of in-code --- .../cinnamon-sass/widgets/_windowlist.scss | 31 +++++++++++++++++++ .../scrollBox.js | 11 ++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index 93e401c776..c814b5e478 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -107,6 +107,37 @@ &-notifications-badge-label { font-size: 12px; } + + &-scroll-button { + min-width: 15px; + min-height: 20px; + background-color: rgba(0, 0, 0, 0.25); + border: 1px solid rgba(128, 128, 128, 0.2); + margin: 0; + padding: 0; + border-radius: 0; + box-shadow: none; + + &-icon { + icon-size: 1.09em; + } + + &-left { + border-right-width: 0; + } + + &-right { + border-left-width: 0; + } + + &-top { + border-bottom-width: 0; + } + + &-down { + border-top-width: 0; + } + } } // classic window list diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 8c193a7e18..a121571389 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -15,12 +15,8 @@ class AppGroupListScrollBox { this.mainLayout = new Clutter.BoxLayout({orientation: managerOrientation}); this.actor = new Clutter.Actor({ layout_manager: this.mainLayout, reactive: true }); - // TODO: Move to Cinnamon default CSS styling - const shadeStyle = 'min-width: 15px; min-height: 20px; background-color: rgba(0, 0, 0, 0.25); border: 1px solid rgba(128, 128, 128, 0.2); margin: 0px; padding: 0px;'; - this.startButton = new St.Bin({ style_class: 'grouped-window-list-scroll-button', - style: shadeStyle, visible: false, reactive: true, x_align: St.Align.MIDDLE, @@ -28,23 +24,20 @@ class AppGroupListScrollBox { }); this.endButton = new St.Bin({ style_class: 'grouped-window-list-scroll-button', - style: shadeStyle, visible: false, reactive: true, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); - - // XXX: Use fixed icon size instead of the popup-menu-icon style class? (or maybe set the default in the cinnamon default theme) this.startIcon = new St.Icon({ icon_name: 'pan-start-symbolic', icon_type: St.IconType.SYMBOLIC, - style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' + style_class: 'grouped-window-list-scroll-button-icon' }); this.endIcon = new St.Icon({ icon_name: 'pan-end-symbolic', icon_type: St.IconType.SYMBOLIC, - style_class: 'popup-menu-icon grouped-window-list-scroll-button-icon' + style_class: 'grouped-window-list-scroll-button-icon' }); this.startButton.set_child(this.startIcon); From 8895d868bb650ca9439eef56ec9ace6baec235dc Mon Sep 17 00:00:00 2001 From: anaximeno Date: Mon, 19 Jan 2026 05:45:17 -0100 Subject: [PATCH 14/36] gwl: Update scroll button theme --- data/theme/cinnamon-sass/widgets/_windowlist.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index c814b5e478..da8ced9f21 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -109,13 +109,8 @@ } &-scroll-button { - min-width: 15px; - min-height: 20px; background-color: rgba(0, 0, 0, 0.25); border: 1px solid rgba(128, 128, 128, 0.2); - margin: 0; - padding: 0; - border-radius: 0; box-shadow: none; &-icon { From b0bdc3c679163b486680fda3eed841abba9dff29 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Wed, 21 Jan 2026 07:38:58 -0100 Subject: [PATCH 15/36] gwl: Implementation using the native St.ScrollView --- .../cinnamon-sass/widgets/_windowlist.scss | 27 +- .../appGroup.js | 10 +- .../applet.js | 43 +- .../scrollBox.js | 451 +++++++----------- .../workspace.js | 25 +- 5 files changed, 211 insertions(+), 345 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index da8ced9f21..180f752ff4 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -108,30 +108,9 @@ font-size: 12px; } - &-scroll-button { - background-color: rgba(0, 0, 0, 0.25); - border: 1px solid rgba(128, 128, 128, 0.2); - box-shadow: none; - - &-icon { - icon-size: 1.09em; - } - - &-left { - border-right-width: 0; - } - - &-right { - border-left-width: 0; - } - - &-top { - border-bottom-width: 0; - } - - &-down { - border-top-width: 0; - } + &-scrollbox { + &.vfade { -st-vfade-offset: 68px; } + &.hfade { -st-hfade-offset: 68px; } } } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index 8ca0467b56..f6deafab94 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -374,10 +374,6 @@ class AppGroup { getPreferredWidth(actor, forHeight, alloc) { const [iconMinSize, iconNaturalSize] = this.iconBox.get_preferred_width(forHeight); const [labelMinSize, labelNaturalSize] = this.label.get_preferred_width(forHeight); - // The label text starts in the center of the icon, so we should allocate the space - // needed for the icon plus the space needed for(label - icon/2) - alloc.min_size = 1 * global.ui_scale; - const {appId} = this.groupState; const allocateForLabel = this.labelVisiblePref || @@ -395,13 +391,15 @@ class AppGroup { } else { alloc.natural_size = this.state.trigger('getPanelHeight'); } + + alloc.min_size = alloc.natural_size; } getPreferredHeight(actor, forWidth, alloc) { let [iconMinSize, iconNaturalSize] = this.iconBox.get_preferred_height(forWidth); let [labelMinSize, labelNaturalSize] = this.label.get_preferred_height(forWidth); - alloc.min_size = Math.min(iconMinSize, labelMinSize); alloc.natural_size = Math.max(iconNaturalSize, labelNaturalSize); + alloc.min_size = alloc.natural_size; } allocate(actor, box, flags) { @@ -554,7 +552,7 @@ class AppGroup { } onEnter() { - if (this.state.panelEditMode) return false; + if (this.state.panelEditMode || this.state.scrollActive) return false; this.actor.add_style_pseudo_class('hover'); diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index b473459097..f4d980bbba 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -792,42 +792,43 @@ class GroupedWindowListApplet extends Applet.Applet { const rtl_horizontal = this.state.isHorizontal && St.Widget.get_default_direction () === St.TextDirection.RTL; - const axis = this.state.isHorizontal ? [x, 'x2'] : [y, 'y2']; - if(rtl_horizontal) - axis[0] = this.actor.width - axis[0]; + let [containerX, containerY] = currentWorkspace.container.get_transformed_position(); + let offset = this.state.isHorizontal ? containerX : containerY; + let mousePos = this.state.isHorizontal ? x : y; // save data on drag start if(this.state.dragging.posList === null){ this.state.dragging.isForeign = !(source instanceof AppGroup); this.state.dragging.posList = []; - let offset = 0; - if (this.state.isHorizontal) { - offset = currentWorkspace.container.translation_x; - if (currentWorkspace.scrollBox.startButton.visible) - offset += currentWorkspace.scrollBox.startButton.width; - } else { - offset = currentWorkspace.container.translation_y; - if (currentWorkspace.scrollBox.startButton.visible) - offset += currentWorkspace.scrollBox.startButton.height; - } - currentWorkspace.container.get_children().forEach( child => { - let childPos; let box = child.get_allocation_box(); + + let storedVal; if(rtl_horizontal) - childPos = this.actor.width - (box.x1 + offset); + storedVal = box.x1; + else if (this.state.isHorizontal) + storedVal = box.x2; else - childPos = box[axis[1]] + offset; - this.state.dragging.posList.push(childPos); + storedVal = box.y2; + + this.state.dragging.posList.push(storedVal); }); } // get current position let pos = 0; - while(pos < this.state.dragging.posList.length && axis[0] > this.state.dragging.posList[pos]) - pos++; - + while(pos < this.state.dragging.posList.length) { + let splitPoint = this.state.dragging.posList[pos] + offset; + if (rtl_horizontal) { + if (mousePos < splitPoint) pos++; + else break; + } else { + if (mousePos > splitPoint) pos++; + else break; + } + } + let favLength = 0; for (const appGroup of currentWorkspace.appGroups) { if(appGroup.groupState.isFavoriteApp) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index a121571389..d7a8523539 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -1,351 +1,246 @@ const Clutter = imports.gi.Clutter; -const St = imports.gi.St; const GLib = imports.gi.GLib; +const St = imports.gi.St; const { SignalManager } = imports.misc.signalManager; +const EDGE_SCROLL_ZONE_SIZE = 68; +const EDGE_SCROLL_SPEED = 8; +const EDGE_SCROLL_INTERVAL = 16; -class AppGroupListScrollBox { - constructor(state, container) { +class ScrollBox { + constructor(state) { this.state = state; - this.container = container; this.signals = new SignalManager(null); - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - - this.mainLayout = new Clutter.BoxLayout({orientation: managerOrientation}); - this.actor = new Clutter.Actor({ layout_manager: this.mainLayout, reactive: true }); - - this.startButton = new St.Bin({ - style_class: 'grouped-window-list-scroll-button', - visible: false, - reactive: true, - x_align: St.Align.MIDDLE, - y_align: St.Align.MIDDLE - }); - this.endButton = new St.Bin({ - style_class: 'grouped-window-list-scroll-button', - visible: false, - reactive: true, - x_align: St.Align.MIDDLE, - y_align: St.Align.MIDDLE - }); - this.startIcon = new St.Icon({ - icon_name: 'pan-start-symbolic', - icon_type: St.IconType.SYMBOLIC, - style_class: 'grouped-window-list-scroll-button-icon' - }); - this.endIcon = new St.Icon({ - icon_name: 'pan-end-symbolic', - icon_type: St.IconType.SYMBOLIC, - style_class: 'grouped-window-list-scroll-button-icon' + this.actor = new St.ScrollView({ + x_expand: true, + y_expand: true, + reactive: true }); - this.startButton.set_child(this.startIcon); - this.endButton.set_child(this.endIcon); + this.actor.set_auto_scrolling(false); + this.actor.set_mouse_scrolling(true); + this.actor.set_clip_to_allocation(true); + this.actor.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); - this.signals.connect(this.startButton, 'enter-event', () => this.startSlide(-1)); - this.signals.connect(this.startButton, 'leave-event', this.stopSlide, this); - this.signals.connect(this.endButton, 'enter-event', () => this.startSlide(1)); - this.signals.connect(this.endButton, 'leave-event', this.stopSlide, this); + this.box = new St.BoxLayout({ + vertical: !this.state.isHorizontal, + style_class: 'grouped-window-list-scrollbox-container' + }); - this.scrollBox = new Clutter.Actor({ clip_to_allocation: true }); - this.scrollBox.add_child(this.container); + this.actor.add_actor(this.box); - this.actor.add_child(this.startButton); - this.actor.add_child(this.scrollBox); - this.actor.add_child(this.endButton); + this.edgeScrollTimeoutId = 0; + this.edgeScrollDirection = 0; - this.scrollBox.set_x_expand(true); - this.scrollBox.set_y_expand(true); + this.signals.connect(this.actor, 'scroll-event', (actor, event) => this._onScroll(actor, event)); - this.slideTimerSourceId = 0; - this.updateScrollVisibilityId = 0; + this.signals.connect(this.actor, 'motion-event', (actor, event) => this._onMotionEvent(actor, event)); + this.signals.connect(this.actor, 'leave-event', () => this._stopEdgeScroll()); - // Connect all the signals - this.signals.connect(this.actor, 'allocation-changed', this.updateScrollVisibility, this); - this.signals.connect(this.container, 'allocation-changed', this.updateScrollVisibility, this); - this.signals.connect(this.container, 'notify::translation-x', this.updateScrollVisibility, this); - this.signals.connect(this.container, 'notify::translation-y', this.updateScrollVisibility, this); - this.signals.connect(this.scrollBox, 'notify::allocation', this.updateScrollVisibility, this); - this.signals.connect(this.actor, 'scroll-event', (actor, event) => this.onScroll(event)); + this.stateConnectionID = this.state.connect({ + orientation: (state) => this.on_orientation_changed() + }); - this.on_orientation_changed(this.state.orientation); + this.on_orientation_changed(); } - on_orientation_changed(orientation) { - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - - this.mainLayout.set_orientation(managerOrientation); + destroy() { + this._stopEdgeScroll(); + if (this.stateConnectionID) { + this.state.disconnect(this.stateConnectionID); + } + this.signals.disconnectAllSignals(); + this.actor.destroy(); + } + on_orientation_changed() { + this.box.vertical = !this.state.isHorizontal; if (this.state.isHorizontal) { - this.actor.set_x_align(Clutter.ActorAlign.FILL); - this.startButton.remove_style_class_name('grouped-window-list-scroll-button-top'); - this.endButton.remove_style_class_name('grouped-window-list-scroll-button-down'); - this.startButton.add_style_class_name('grouped-window-list-scroll-button-left'); - this.endButton.add_style_class_name('grouped-window-list-scroll-button-right'); - this.startIcon.set_icon_name('pan-start-symbolic'); - this.endIcon.set_icon_name('pan-end-symbolic'); - this.startButton.set_x_expand(false); - this.startButton.set_y_expand(true); - this.startButton.set_y_align(Clutter.ActorAlign.FILL); - this.endButton.set_x_expand(false); - this.endButton.set_y_expand(true); - this.endButton.set_y_align(Clutter.ActorAlign.FILL); + this.actor.style_class = 'grouped-window-list-scrollbox hfade'; } else { - this.actor.set_x_align(Clutter.ActorAlign.CENTER); - this.startButton.remove_style_class_name('grouped-window-list-scroll-button-left'); - this.endButton.remove_style_class_name('grouped-window-list-scroll-button-right'); - this.startButton.add_style_class_name('grouped-window-list-scroll-button-top'); - this.endButton.add_style_class_name('grouped-window-list-scroll-button-down'); - this.startIcon.set_icon_name('pan-up-symbolic'); - this.endIcon.set_icon_name('pan-down-symbolic'); - this.startButton.set_x_expand(true); - this.startButton.set_y_expand(false); - this.startButton.set_x_align(Clutter.ActorAlign.FILL); - this.endButton.set_x_expand(true); - this.endButton.set_y_expand(false); - this.endButton.set_x_align(Clutter.ActorAlign.FILL); + this.actor.style_class = 'grouped-window-list-scrollbox vfade'; } - this.updateScrollVisibility(); } - startSlide(direction) { - if (this.slideTimerSourceId > 0) { - GLib.source_remove(this.slideTimerSourceId); - this.slideTimerSourceId = 0; + scrollToChild(childActor) { + if (!childActor) return; + + // Get allocation of child relative to container + const allocation = childActor.get_allocation_box(); + + // Child coordinates + let c1, c2; + + const isHorizontal = this.state.isHorizontal; + let adjustment; + + if (isHorizontal) { + c1 = allocation.x1; + c2 = allocation.x2; + const hBar = this.actor.get_hscroll_bar(); + if (hBar) adjustment = hBar.get_adjustment(); + } else { + c1 = allocation.y1; + c2 = allocation.y2; + const vBar = this.actor.get_vscroll_bar(); + if (vBar) adjustment = vBar.get_adjustment(); } - if (this.state.panelEditMode) return; + if (adjustment) { + const current = adjustment.value; + const page_size = adjustment.page_size; + const item_size = c2 - c1; - const scrollFunc = () => { - this.scroll(direction * 5); - if (this.slideTimerSourceId === 0) return GLib.SOURCE_REMOVE; + let fade_offset = 30; - // Check if reached bounds to stop timer - let current, min; - if (this.state.isHorizontal) { - current = this.container.translation_x; - min = Math.min(0, this.scrollBox.width - this.container.width); - } else { - current = this.container.translation_y; - min = Math.min(0, this.scrollBox.height - this.container.height); - } + const fade_eff = this.actor.get_effect('fade'); - // At start, trying to go start - if (current >= 0 && direction < 0) { - this.slideTimerSourceId = 0; - return GLib.SOURCE_REMOVE; + if (fade_eff) { + fade_offset = this.state.isHorizontal ? fade_eff.hfade_offset : fade_eff.vfade_offset; } - // At end, trying to go end - if (current <= min && direction > 0) { - this.slideTimerSourceId = 0; - return GLib.SOURCE_REMOVE; + if (c1 < current + fade_offset || c2 > current + page_size - fade_offset) { + const newValue = (c1 + c2) / 2 - page_size / 2; + adjustment.value = Math.max(adjustment.lower, Math.min(newValue, adjustment.upper - page_size)); } + } + } - return GLib.SOURCE_CONTINUE; - }; + _onMotionEvent(actor, event) { + if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; - this.slideTimerSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, scrollFunc); - } + const [x, y] = event.get_coords(); + const [actorX, actorY] = actor.get_transformed_position(); + const [actorWidth, actorHeight] = actor.get_transformed_size(); - stopSlide() { - if (this.slideTimerSourceId > 0) { - GLib.source_remove(this.slideTimerSourceId); - this.slideTimerSourceId = 0; - } - } + // Calculate relative position within the actor + const relX = x - actorX; + const relY = y - actorY; + + let scrollDirection = 0; + const adjustment = this._getScrollAdjustment(); - onScroll(event) { - if (this.state.panelEditMode) return; + if (!adjustment) return Clutter.EVENT_PROPAGATE; - if (this.state.settings.scrollBehavior !== 4) { + // Check if we can scroll (content is larger than view) + const canScroll = adjustment.upper > adjustment.page_size; + if (!canScroll) { + this._stopEdgeScroll(); return Clutter.EVENT_PROPAGATE; } - let containerSize, scrollBoxSize; if (this.state.isHorizontal) { - containerSize = this.container.width || this.container.get_preferred_width(-1)[1]; - scrollBoxSize = this.scrollBox.width; - } else { - containerSize = this.container.height || this.container.get_preferred_height(-1)[1]; - scrollBoxSize = this.scrollBox.height; - } - - if (containerSize <= scrollBoxSize) return Clutter.EVENT_PROPAGATE; - - const direction = event.get_scroll_direction(); - let delta = 0; - - if (direction === Clutter.ScrollDirection.SMOOTH) { - const [dx, dy] = event.get_scroll_delta(); - delta = this.state.isHorizontal ? dx : dy; - delta *= 15; // Scale smooth scroll + // Check left edge + if (relX < EDGE_SCROLL_ZONE_SIZE && adjustment.value > adjustment.lower) { + scrollDirection = -1; + } + // Check right edge + else if (relX > actorWidth - EDGE_SCROLL_ZONE_SIZE && + adjustment.value < adjustment.upper - adjustment.page_size) { + scrollDirection = 1; + } } else { - const step = 20; - if (direction === Clutter.ScrollDirection.UP || direction === Clutter.ScrollDirection.LEFT) { - delta = -step; - } else if (direction === Clutter.ScrollDirection.DOWN || direction === Clutter.ScrollDirection.RIGHT) { - delta = step; + // Check top edge + if (relY < EDGE_SCROLL_ZONE_SIZE && adjustment.value > adjustment.lower) { + scrollDirection = -1; + } + // Check bottom edge + else if (relY > actorHeight - EDGE_SCROLL_ZONE_SIZE && + adjustment.value < adjustment.upper - adjustment.page_size) { + scrollDirection = 1; } } - if (delta !== 0) { - this.scroll(delta); - return Clutter.EVENT_STOP; + if (scrollDirection !== 0 && scrollDirection !== this.edgeScrollDirection) { + this._startEdgeScroll(scrollDirection); + } else if (scrollDirection === 0) { + this._stopEdgeScroll(); } return Clutter.EVENT_PROPAGATE; } - scroll(amount) { - let current, min, next; + _getScrollAdjustment() { if (this.state.isHorizontal) { - current = this.container.translation_x; - min = Math.min(0, this.scrollBox.width - this.container.width); - next = current - amount; + const hBar = this.actor.get_hscroll_bar(); + return hBar ? hBar.get_adjustment() : null; } else { - current = this.container.translation_y; - min = Math.min(0, this.scrollBox.height - this.container.height); - next = current - amount; + const vBar = this.actor.get_vscroll_bar(); + return vBar ? vBar.get_adjustment() : null; } - - if (next > 0) next = 0; - if (next < min) next = min; - - if (this.state.isHorizontal) this.container.translation_x = next; - else this.container.translation_y = next; - } - - updateScrollVisibility() { - if (this.updateScrollVisibilityId > 0) return; - this.updateScrollVisibilityId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { - this._updateScrollVisibility(); - this.updateScrollVisibilityId = 0; - return GLib.SOURCE_REMOVE; - }); } - _updateScrollVisibility() { - if (this.state.panelEditMode) return; - - let containerSize, scrollBoxSize; + _startEdgeScroll(direction) { + this._stopEdgeScroll(); + this.edgeScrollDirection = direction; + this.state.scrollActive = true; - if (this.state.isHorizontal) { - containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; - scrollBoxSize = this.scrollBox.width; - } else { - containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; - scrollBoxSize = this.scrollBox.height; - } - - let minTranslation = Math.min(0, scrollBoxSize - containerSize); - let currentTranslation = this.state.isHorizontal ? this.container.translation_x : this.container.translation_y; - - // Clamp translation if bounds have changed (resizing, etc) - if (currentTranslation < minTranslation) { - currentTranslation = minTranslation; - if (this.state.isHorizontal) this.container.translation_x = currentTranslation; - else this.container.translation_y = currentTranslation; - } - - if (containerSize > scrollBoxSize) { - // Some tolerance to avoid flickering - this.startButton.visible = currentTranslation < -0.1; - this.endButton.visible = currentTranslation > minTranslation + 0.1; - } else { - this.startButton.visible = false; - this.endButton.visible = false; - - if (currentTranslation !== 0) { - if (this.state.isHorizontal) this.container.translation_x = 0; - else this.container.translation_y = 0; + this.edgeScrollTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, EDGE_SCROLL_INTERVAL, () => { + const adjustment = this._getScrollAdjustment(); + if (!adjustment) { + this.edgeScrollTimeoutId = 0; + return GLib.SOURCE_REMOVE; } - } - } - - scrollToChild(childActor) { - if (this.state.panelEditMode) return; - - if (!childActor || childActor.get_parent() !== this.container) return; - - const isHorizontal = this.state.isHorizontal; - - let containerSize, boxSize; - if (isHorizontal) { - containerSize = this.container.width > 0 ? this.container.width : this.container.get_preferred_width(-1)[1]; - boxSize = this.scrollBox.width; - } else { - containerSize = this.container.height > 0 ? this.container.height : this.container.get_preferred_height(-1)[1]; - boxSize = this.scrollBox.height; - } - if (containerSize <= boxSize) return; + const newValue = adjustment.value + (EDGE_SCROLL_SPEED * this.edgeScrollDirection * global.ui_scale); - let targetCenter = 0; - let allocationValid = false; + // Clamp value to valid range + adjustment.value = Math.max(adjustment.lower, + Math.min(newValue, adjustment.upper - adjustment.page_size)); - if (childActor.has_allocation()) { - const box = childActor.get_allocation_box(); - const size = isHorizontal ? box.get_width() : box.get_height(); + return GLib.SOURCE_CONTINUE; + }); + } - if (size > 0) { - targetCenter = (isHorizontal ? box.x1 : box.y1) + (size / 2); - allocationValid = true; - } + _stopEdgeScroll() { + if (this.edgeScrollTimeoutId > 0) { + GLib.source_remove(this.edgeScrollTimeoutId); + this.edgeScrollTimeoutId = 0; } + this.edgeScrollDirection = 0; + this.state.scrollActive = false; + } - if (!allocationValid) { - const children = this.container.get_children(); - const index = children.indexOf(childActor); - - if (index === -1) return; - - let itemPos = 0; - let itemSize = 0; - - for (let i = 0; i <= index; i++) { - const actor = children[i]; + _onScroll(actor, event) { + if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; - if (isHorizontal) { - itemSize = actor.width > 0 ? actor.width : actor.get_preferred_width(-1)[1]; + // Handle horizontal scrolling with vertical wheel + if (this.state.isHorizontal) { + const direction = event.get_scroll_direction(); + let delta = 0; + + const hBar = this.actor.get_hscroll_bar(); + if (!hBar) return Clutter.EVENT_PROPAGATE; + + const adjustment = hBar.get_adjustment(); + if (!adjustment) return Clutter.EVENT_PROPAGATE; + + if (direction === Clutter.ScrollDirection.UP) { + delta = -10 * global.ui_scale; + } else if (direction === Clutter.ScrollDirection.DOWN) { + delta = 10 * global.ui_scale; + } else if (direction === Clutter.ScrollDirection.SMOOTH) { + const [dx, dy] = event.get_scroll_delta(); + // If pure vertical scroll, map to horizontal + if (Math.abs(dy) > Math.abs(dx)) { + delta = dy * (16 * global.ui_scale); // Scale factor } else { - itemSize = actor.height > 0 ? actor.height : actor.get_preferred_height(-1)[1]; + // Let StScrollView handle horizontal smooth scroll naturally if it exists + return Clutter.EVENT_PROPAGATE; } - - itemPos += itemSize; + } else { + return Clutter.EVENT_PROPAGATE; } - targetCenter = itemPos - (itemSize / 2); - } - - // We want targetCenter to be at boxSize / 2 - let newPos = (boxSize / 2) - targetCenter; - - const minPos = Math.min(0, boxSize - containerSize); - newPos = Math.round(Math.max(minPos, Math.min(newPos, 0)) * 100) / 100; - - if (isHorizontal) { - this.container.translation_x = newPos; - } else { - this.container.translation_y = newPos; - } - } - - destroy() { - this.signals.disconnectAllSignals(); - if (this.slideTimerSourceId > 0) { - GLib.source_remove(this.slideTimerSourceId); - this.slideTimerSourceId = 0; - } - - if (this.updateScrollVisibilityId > 0) { - GLib.source_remove(this.updateScrollVisibilityId); - this.updateScrollVisibilityId = 0; + if (delta !== 0) { + // Manually updating adjustment value using property + adjustment.value = adjustment.value + delta; + return Clutter.EVENT_STOP; + } } - - this.actor.destroy(); + return Clutter.EVENT_PROPAGATE; } } -module.exports = AppGroupListScrollBox; +module.exports = ScrollBox; diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 2c22f365d4..64ca6e4005 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -1,13 +1,14 @@ const Clutter = imports.gi.Clutter; const GLib = imports.gi.GLib; +const St = imports.gi.St; const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; const {unref} = imports.misc.util; const createStore = require('./state'); const AppGroup = require('./appGroup'); -const AppGroupListScrollBox = require('./scrollBox'); const {RESERVE_KEYS, SCROLL_TO_APP_DEBOUNCE_TIME} = require('./constants'); +const ScrollBox = require('./scrollBox'); class Workspace { @@ -39,7 +40,6 @@ class Workspace { return; } this.container.remove_child(actor); - this.scrollBox.updateScrollVisibility(); }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { @@ -55,13 +55,9 @@ class Workspace { this.signals = new SignalManager(null); this.metaWorkspace = params.metaWorkspace; - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - - this.manager = new Clutter.BoxLayout({orientation: managerOrientation}); - this.container = new Clutter.Actor({layout_manager: this.manager}); - - this.scrollBox = new AppGroupListScrollBox(this.state, this.container); + this.scrollBox = new ScrollBox(this.state); this.actor = this.scrollBox.actor; + this.container = this.scrollBox.box; this.appGroups = []; this.lastFocusedApp = null; @@ -77,11 +73,6 @@ class Workspace { } on_orientation_changed(orientation) { - if (!this.manager) return; - - const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; - this.manager.set_orientation(managerOrientation); - this.scrollBox.on_orientation_changed(orientation); this.refreshList(); } @@ -356,7 +347,6 @@ class Workspace { this.container.add_child(appGroup.actor); this.appGroups.push(appGroup); } - this.scrollBox.updateScrollVisibility(); appGroup.windowAdded(metaWindow); }; @@ -462,11 +452,14 @@ class Workspace { } destroy() { + this.scrollBox.destroy(); + if (this.scrollToAppDebounceTimeoutId > 0) { + GLib.source_remove(this.scrollToAppDebounceTimeoutId); + this.scrollToAppDebounceTimeoutId = 0; + } this.signals.disconnectAllSignals(); this.appGroups.forEach( appGroup => appGroup.destroy() ); this.workspaceState.destroy(); - this.scrollBox.destroy(); - this.manager = null; unref(this, RESERVE_KEYS); } } From ece5b29777c4289f11ccc2032c5f04369bca8f8b Mon Sep 17 00:00:00 2001 From: anaximeno Date: Wed, 21 Jan 2026 16:30:35 -0100 Subject: [PATCH 16/36] gwl: Update scroll police depending on the orientation and make sure to debounce setting scrollActive state as inactive to avoid triggering the menu during the scroll animation --- .../scrollBox.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index d7a8523539..586c07aaa6 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -32,6 +32,7 @@ class ScrollBox { this.edgeScrollTimeoutId = 0; this.edgeScrollDirection = 0; + this.scrollActiveTimeoutId = 0; this.signals.connect(this.actor, 'scroll-event', (actor, event) => this._onScroll(actor, event)); @@ -57,8 +58,10 @@ class ScrollBox { on_orientation_changed() { this.box.vertical = !this.state.isHorizontal; if (this.state.isHorizontal) { + this.actor.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); this.actor.style_class = 'grouped-window-list-scrollbox hfade'; } else { + this.actor.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); this.actor.style_class = 'grouped-window-list-scrollbox vfade'; } } @@ -90,7 +93,6 @@ class ScrollBox { if (adjustment) { const current = adjustment.value; const page_size = adjustment.page_size; - const item_size = c2 - c1; let fade_offset = 30; @@ -199,7 +201,18 @@ class ScrollBox { this.edgeScrollTimeoutId = 0; } this.edgeScrollDirection = 0; - this.state.scrollActive = false; + + if (this.scrollActiveTimeoutId) { + GLib.source_remove(this.scrollActiveTimeoutId); + } + + this.scrollActiveTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => { + if (this.edgeScrollDirection === 0) { + this.state.scrollActive = false; + } + this.scrollActiveTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); } _onScroll(actor, event) { From d2caa0d5863a7e3348864bd3bc344fe45425a003 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Wed, 21 Jan 2026 18:22:31 -0100 Subject: [PATCH 17/36] gwl: Fix handleDragOver positioning for the scrollview based implementation --- .../applet.js | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index f4d980bbba..033d50cec9 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -792,42 +792,40 @@ class GroupedWindowListApplet extends Applet.Applet { const rtl_horizontal = this.state.isHorizontal && St.Widget.get_default_direction () === St.TextDirection.RTL; - let [containerX, containerY] = currentWorkspace.container.get_transformed_position(); - let offset = this.state.isHorizontal ? containerX : containerY; - let mousePos = this.state.isHorizontal ? x : y; + const axis = this.state.isHorizontal ? [x, 'x2'] : [y, 'y2']; + + let adjustmentValue = 0; + if (currentWorkspace.scrollBox) { + const adjustment = this.state.isHorizontal ? + currentWorkspace.scrollBox.actor.get_hscroll_bar().get_adjustment() : + currentWorkspace.scrollBox.actor.get_vscroll_bar().get_adjustment(); + adjustmentValue = adjustment.get_value(); + } + + // Add scroll position to current coordinate + axis[0] += adjustmentValue; + + if(rtl_horizontal) + axis[0] = this.actor.width - axis[0]; // save data on drag start if(this.state.dragging.posList === null){ this.state.dragging.isForeign = !(source instanceof AppGroup); this.state.dragging.posList = []; - currentWorkspace.container.get_children().forEach( child => { - let box = child.get_allocation_box(); - - let storedVal; + let childPos; if(rtl_horizontal) - storedVal = box.x1; - else if (this.state.isHorizontal) - storedVal = box.x2; + childPos = this.actor.width - child.get_allocation_box()['x1']; else - storedVal = box.y2; - - this.state.dragging.posList.push(storedVal); + childPos = child.get_allocation_box()[axis[1]]; + this.state.dragging.posList.push(childPos); }); } // get current position let pos = 0; - while(pos < this.state.dragging.posList.length) { - let splitPoint = this.state.dragging.posList[pos] + offset; - if (rtl_horizontal) { - if (mousePos < splitPoint) pos++; - else break; - } else { - if (mousePos > splitPoint) pos++; - else break; - } - } + while(pos < this.state.dragging.posList.length && axis[0] > this.state.dragging.posList[pos]) + pos++; let favLength = 0; for (const appGroup of currentWorkspace.appGroups) { From dc16a31c21263f71dadb0d74beae64af1ba6bceb Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 31 Jan 2026 02:51:06 -0100 Subject: [PATCH 18/36] Update scrollbox vfade/hfade theme defaults --- data/theme/cinnamon-sass/widgets/_windowlist.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index 180f752ff4..a4ca503e79 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -109,8 +109,14 @@ } &-scrollbox { - &.vfade { -st-vfade-offset: 68px; } - &.hfade { -st-hfade-offset: 68px; } + &.vfade { + -st-vfade-offset: 68px !important; + -st-hfade-offset: 0 !important; + } + &.hfade { + -st-hfade-offset: 68px !important; + -st-vfade-offset: 0 !important; + } } } From f70951b52c67b669efef22c0f8d27a7bac486f59 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Mon, 9 Feb 2026 20:07:37 -0100 Subject: [PATCH 19/36] gwl: Adjust min_size setting for lateral panels --- .../applets/grouped-window-list@cinnamon.org/appGroup.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index f6deafab94..e9baa7c4da 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -388,11 +388,11 @@ class AppGroup { } else { alloc.natural_size = iconNaturalSize + 6 * global.ui_scale; } + alloc.min_size = alloc.natural_size; } else { alloc.natural_size = this.state.trigger('getPanelHeight'); + alloc.min_size = 1 * global.ui_scale; } - - alloc.min_size = alloc.natural_size; } getPreferredHeight(actor, forWidth, alloc) { From 81985123a9bdfbc592d5e8dfa8994e9b2808907a Mon Sep 17 00:00:00 2001 From: anaximeno Date: Mon, 16 Feb 2026 03:22:11 -0100 Subject: [PATCH 20/36] gwl: Only set appGroup focus if app actually has focus This avoids an issue where during workspace switching some times a minimized window would visually have the focused window style when it shouldn't. --- .../applets/grouped-window-list@cinnamon.org/appGroup.js | 2 +- .../applets/grouped-window-list@cinnamon.org/scrollBox.js | 1 - .../applets/grouped-window-list@cinnamon.org/workspace.js | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index e9baa7c4da..f3e676e9c7 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -639,7 +639,7 @@ class AppGroup { const {appId, metaWindows, lastFocused} = this.groupState; if (hasFocus === undefined) { - hasFocus = this.workspaceState.lastFocusedApp === appId; + hasFocus = this.workspaceState.lastFocusedApp === appId && getFocusState(lastFocused); } // If any of the windows associated with our app have focus, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 586c07aaa6..30ea70a980 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -35,7 +35,6 @@ class ScrollBox { this.scrollActiveTimeoutId = 0; this.signals.connect(this.actor, 'scroll-event', (actor, event) => this._onScroll(actor, event)); - this.signals.connect(this.actor, 'motion-event', (actor, event) => this._onMotionEvent(actor, event)); this.signals.connect(this.actor, 'leave-event', () => this._stopEdgeScroll()); diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 64ca6e4005..d463357de3 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -43,7 +43,8 @@ class Workspace { }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { - if (focusedAppId === appGroup.groupState.appId) { + if (focusedAppId === appGroup.groupState.appId && + (!appGroup.groupState.lastFocused || appGroup.groupState.lastFocused.has_focus())) { this.scrollToAppGroup(appGroup); return; }; From 162eb5867d0d44e063fc577594538dfc07b06c20 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Mon, 16 Feb 2026 03:42:27 -0100 Subject: [PATCH 21/36] gwl: Improve the scroll to last focused app This is useful when switching to a new workspace or the applet is reloaded so it guarantees user will at least be aware of opened windows that could otherwise be hidden if the scroll state is mantained. --- .../constants.js | 1 - .../workspace.js | 22 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js index 65170eaf26..51b08467e4 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/constants.js @@ -12,7 +12,6 @@ const constants = { FLASH_INTERVAL: 500, FLASH_MAX_COUNT: 4, RESERVE_KEYS: ['willUnmount'], - SCROLL_TO_APP_DEBOUNCE_TIME: 100, TitleDisplay: { None: 1, App: 2, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index d463357de3..dbbd56e226 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -7,7 +7,7 @@ const {unref} = imports.misc.util; const createStore = require('./state'); const AppGroup = require('./appGroup'); -const {RESERVE_KEYS, SCROLL_TO_APP_DEBOUNCE_TIME} = require('./constants'); +const {RESERVE_KEYS} = require('./constants'); const ScrollBox = require('./scrollBox'); @@ -20,7 +20,7 @@ class Workspace { }, currentWs: (state) => { if (this.metaWorkspace && state.currentWs === this.metaWorkspace.index()) { - this.scrollToFocusedApp(); + this.scrollToLastFocusedApp(); } } }); @@ -79,7 +79,7 @@ class Workspace { scrollToAppGroup(appGroup) { if (this.scrollToAppDebounceTimeoutId > 0) GLib.source_remove(this.scrollToAppDebounceTimeoutId); - this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SCROLL_TO_APP_DEBOUNCE_TIME, () => { + this.scrollToAppDebounceTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { this._scrollToAppGroup(appGroup); this.scrollToAppDebounceTimeoutId = 0; return GLib.SOURCE_REMOVE; @@ -200,7 +200,7 @@ class Workspace { this.appGroups = []; this.loadFavorites(); this.refreshApps(); - this.scrollToFocusedApp(); + this.scrollToLastFocusedApp(); } loadFavorites() { @@ -238,13 +238,23 @@ class Workspace { } } - scrollToFocusedApp() { + scrollToLastFocusedApp() { + let lastFocusedAppInWorkspace = null; for (let appGroup of this.appGroups) { - if (appGroup.groupState.lastFocused && appGroup.groupState.lastFocused.has_focus()) { + let lastFocusedInAppGroup = appGroup.groupState.lastFocused; + if (lastFocusedInAppGroup && lastFocusedInAppGroup.has_focus()) { + lastFocusedAppInWorkspace = appGroup; this.scrollToAppGroup(appGroup); return; + } else if ((this.workspaceState.lastFocusedApp === appGroup.groupState.appId)) { + lastFocusedAppInWorkspace = appGroup; + } else if (!lastFocusedAppInWorkspace && appGroup.groupState.metaWindows.length > 0) { + lastFocusedAppInWorkspace = appGroup; } } + if (lastFocusedAppInWorkspace) { + this.scrollToAppGroup(lastFocusedAppInWorkspace); + } } updateAttentionState(display, window) { From ed01ce7692e6f57b8ab1c7596fbde7d94f7e6d84 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Tue, 17 Feb 2026 03:51:10 -0100 Subject: [PATCH 22/36] gwl: Remove important attr from the scrollbox style --- data/theme/cinnamon-sass/widgets/_windowlist.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index a4ca503e79..b85aab202a 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -110,12 +110,12 @@ &-scrollbox { &.vfade { - -st-vfade-offset: 68px !important; - -st-hfade-offset: 0 !important; + -st-vfade-offset: 68px; + -st-hfade-offset: 0; } &.hfade { - -st-hfade-offset: 68px !important; - -st-vfade-offset: 0 !important; + -st-hfade-offset: 68px; + -st-vfade-offset: 0; } } } From 7290024f0d35f4b7745a9e1d07a0c7dc8572ab96 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Tue, 17 Feb 2026 14:06:48 -0100 Subject: [PATCH 23/36] gwl: Improve scroll to last focused If there's no opened windows just keep the current scroll state. --- .../applets/grouped-window-list@cinnamon.org/workspace.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index dbbd56e226..c530887227 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -246,13 +246,15 @@ class Workspace { lastFocusedAppInWorkspace = appGroup; this.scrollToAppGroup(appGroup); return; - } else if ((this.workspaceState.lastFocusedApp === appGroup.groupState.appId)) { + } else if (this.workspaceState.lastFocusedApp === appGroup.groupState.appId) { lastFocusedAppInWorkspace = appGroup; - } else if (!lastFocusedAppInWorkspace && appGroup.groupState.metaWindows.length > 0) { + } else if ((!lastFocusedAppInWorkspace || lastFocusedAppInWorkspace.groupState.metaWindows.length === 0) && + appGroup.groupState.metaWindows.length > 0 + ) { lastFocusedAppInWorkspace = appGroup; } } - if (lastFocusedAppInWorkspace) { + if (lastFocusedAppInWorkspace && lastFocusedAppInWorkspace.groupState.metaWindows.length > 0) { this.scrollToAppGroup(lastFocusedAppInWorkspace); } } From 13deb3b98781c62c69c375e5e88cefe86bbe2e32 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Mon, 23 Feb 2026 15:48:51 -0100 Subject: [PATCH 24/36] gwl: Improve scroll handling to work better with previous scroll options I've also decided to change the scroll key name, this with the goal of guaranteeing that the scroll app window list will be set as default for all users, those that want other options, or none, can switch back. --- .../applets/grouped-window-list@cinnamon.org/applet.js | 3 ++- .../applets/grouped-window-list@cinnamon.org/scrollBox.js | 5 +++++ .../grouped-window-list@cinnamon.org/settings-schema.json | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 033d50cec9..45c1047395 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -253,6 +253,7 @@ class GroupedWindowListApplet extends Applet.Applet { removeFavorite: (id) => this.pinnedFavorites.removeFavorite(id), getFavorites: () => this.pinnedFavorites._favorites, cycleWindows: (e, source) => this.handleScroll(e, source), + handleScroll: (e) => this.handleScroll(e), openAbout: () => this.openAbout(), configureApplet: () => this.configureApplet(), removeApplet: (event) => this.confirmRemoveApplet(event) @@ -299,7 +300,7 @@ class GroupedWindowListApplet extends Applet.Applet { bindSettings() { const settingsProps = [ {key: 'group-apps', value: 'groupApps', cb: this.refreshAllWorkspaces}, - {key: 'scroll-behavior', value: 'scrollBehavior', cb: null}, + {key: 'list-scroll-behavior', value: 'scrollBehavior', cb: null}, {key: 'left-click-action', value: 'leftClickAction', cb: null}, {key: 'middle-click-action', value: 'middleClickAction', cb: null}, {key: 'show-all-workspaces', value: 'showAllWorkspaces', cb: this.refreshAllWorkspaces}, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 30ea70a980..4f4cb428ee 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -217,6 +217,11 @@ class ScrollBox { _onScroll(actor, event) { if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; + if (this.state.settings.scrollBehavior !== 4) { + this.state.trigger('handleScroll', event); + return; + } + // Handle horizontal scrolling with vertical wheel if (this.state.isHorizontal) { const direction = event.get_scroll_direction(); diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index 43fe798fee..31511ceb5c 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -27,7 +27,7 @@ "title" : "Behavior", "keys": [ "group-apps", - "scroll-behavior", + "list-scroll-behavior", "left-click-action", "middle-click-action", "show-all-workspaces", @@ -96,7 +96,7 @@ "default": true, "description": "Group windows by application" }, - "scroll-behavior": { + "list-scroll-behavior": { "type": "combobox", "default": 4, "description": "Mouse wheel scroll action", From 929951585da478473476c9c29a1e5df7a3dc6453 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Wed, 25 Feb 2026 12:40:14 -0100 Subject: [PATCH 25/36] gwl: Remove unused import --- .../applets/grouped-window-list@cinnamon.org/workspace.js | 1 - 1 file changed, 1 deletion(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index ce75ad6180..2bd7c08e0c 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -1,6 +1,5 @@ const Clutter = imports.gi.Clutter; const GLib = imports.gi.GLib; -const St = imports.gi.St; const Main = imports.ui.main; const {SignalManager} = imports.misc.signalManager; const {unref} = imports.misc.util; From 1b0f2687e848e724e3128ddaed74e882f9eedb75 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Sat, 7 Mar 2026 05:50:51 -0100 Subject: [PATCH 26/36] gwl: Bring back slider buttons --- .../cinnamon-sass/widgets/_windowlist.scss | 28 +- .../applet.js | 4 +- .../scrollBox.js | 298 +++++++++++++----- 3 files changed, 248 insertions(+), 82 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index b85aab202a..2f977e13e8 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -109,13 +109,29 @@ } &-scrollbox { - &.vfade { - -st-vfade-offset: 68px; - -st-hfade-offset: 0; + &-scrollview { + &.vfade { + -st-vfade-offset: 68px; + -st-hfade-offset: 0; + } + &.hfade { + -st-hfade-offset: 68px; + -st-vfade-offset: 0; + } } - &.hfade { - -st-hfade-offset: 68px; - -st-vfade-offset: 0; + + &-button { + &-start, + &-end, + &-up, + &-down { + margin: 0; + padding: 0; + } + + &-icon { + icon-size: $scalable_icon_size; + } } } } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 7ef8eb7b76..31ee0351e1 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -799,8 +799,8 @@ class GroupedWindowListApplet extends Applet.Applet { let adjustmentValue = 0; if (currentWorkspace.scrollBox) { const adjustment = this.state.isHorizontal ? - currentWorkspace.scrollBox.actor.get_hscroll_bar().get_adjustment() : - currentWorkspace.scrollBox.actor.get_vscroll_bar().get_adjustment(); + currentWorkspace.scrollBox.scrollView.get_hscroll_bar().get_adjustment() : + currentWorkspace.scrollBox.scrollView.get_vscroll_bar().get_adjustment(); adjustmentValue = adjustment.get_value(); } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 6abc91c0d1..f8fcdeee22 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -3,50 +3,114 @@ const GLib = imports.gi.GLib; const St = imports.gi.St; const { SignalManager } = imports.misc.signalManager; -const EDGE_SCROLL_ZONE_SIZE = 68; -const EDGE_SCROLL_SPEED = 8; -const EDGE_SCROLL_INTERVAL = 16; +const EDGE_SCROLL_ZONE_SIZE = 48; +const SLIDE_SPEED = 8; +const SLIDE_INTERVAL = 16; var ScrollBox = class ScrollBox { constructor(state) { this.state = state; this.signals = new SignalManager(null); - this.actor = new St.ScrollView({ + this.scrollView = new St.ScrollView({ + style_class: 'grouped-window-list-scrollbox-scrollview', x_expand: true, y_expand: true, - reactive: true + reactive: true, }); - this.actor.set_auto_scrolling(false); - this.actor.set_mouse_scrolling(true); - this.actor.set_clip_to_allocation(true); - this.actor.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); + this.scrollView.set_auto_scrolling(false); + this.scrollView.set_mouse_scrolling(true); + this.scrollView.set_clip_to_allocation(true); + this.scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); this.box = new St.BoxLayout({ vertical: !this.state.isHorizontal, - style_class: 'grouped-window-list-scrollbox-container' + style_class: 'grouped-window-list-scrollbox-container', }); - this.actor.add_actor(this.box); + this.scrollView.add_actor(this.box); - this.edgeScrollTimeoutId = 0; - this.edgeScrollDirection = 0; + // Slider buttons + const buttonStyle = 'min-width: 15px; min-height: 20px; margin: 0px; padding: 0px;'; + + this.startButton = new St.Bin({ + style_class: 'grouped-window-list-scrollbox-button-start', + style: buttonStyle, + visible: false, + reactive: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + + this.endButton = new St.Bin({ + style_class: 'grouped-window-list-scrollbox-button-end', + style: buttonStyle, + visible: false, + reactive: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE + }); + + this.startIcon = new St.Icon({ + icon_name: 'xsi-go-previous-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' + }); + + this.endIcon = new St.Icon({ + icon_name: 'xsi-go-next-symbolic', + icon_type: St.IconType.SYMBOLIC, + style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' + }); + + this.startButton.set_child(this.startIcon); + this.endButton.set_child(this.endIcon); + + // Wrapper actor: [startButton] [scrollView] [endButton] + const managerOrientation = this.state.isHorizontal + ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + this.mainLayout = new Clutter.BoxLayout({ orientation: managerOrientation }); + this.actor = new Clutter.Actor({ + layout_manager: this.mainLayout, + reactive: true, + x_expand: true, + y_expand: true + }); + + this.actor.add_child(this.startButton); + this.actor.add_child(this.scrollView); + this.actor.add_child(this.endButton); + + this.slideTimerSourceId = 0; + this.slideDirection = 0; this.scrollActiveTimeoutId = 0; - this.signals.connect(this.actor, 'scroll-event', (actor, event) => this._onScroll(actor, event)); - this.signals.connect(this.actor, 'motion-event', (actor, event) => this._onMotionEvent(actor, event)); - this.signals.connect(this.actor, 'leave-event', () => this._stopEdgeScroll()); + // Slider button signals + this.signals.connect(this.startButton, 'enter-event', () => this._startSlide(-1)); + this.signals.connect(this.startButton, 'leave-event', () => this._stopSlide()); + this.signals.connect(this.endButton, 'enter-event', () => this._startSlide(1)); + this.signals.connect(this.endButton, 'leave-event', () => this._stopSlide()); + + // Scroll view signals + this.signals.connect(this.scrollView, 'scroll-event', (actor, event) => this._onScroll(actor, event)); + this.signals.connect(this.scrollView, 'motion-event', (actor, event) => this._onMotionEvent(actor, event)); + this.signals.connect(this.scrollView, 'leave-event', () => this._stopSlide()); + + // Track content size changes + this.signals.connect(this.box, 'allocation-changed', () => this.updateScrollButtonVisibility()); this.stateConnectionID = this.state.connect({ orientation: (state) => this.on_orientation_changed() }); this.on_orientation_changed(); + this._connectAdjustmentSignals(); } destroy() { - this._stopEdgeScroll(); + this._stopSlide(); + this._disconnectAdjustmentSignals(); if (this.stateConnectionID) { this.state.disconnect(this.stateConnectionID); } @@ -54,15 +118,69 @@ var ScrollBox = class ScrollBox { this.actor.destroy(); } + _connectAdjustmentSignals() { + this._disconnectAdjustmentSignals(); + + const adjustment = this._getScrollAdjustment(); + + if (!adjustment) return; + + this._currentAdjustment = adjustment; + this._adjustmentValueSigId = adjustment.connect('notify::value', () => this.updateScrollButtonVisibility()); + this._adjustmentChangedSigId = adjustment.connect('changed', () => this.updateScrollButtonVisibility()); + } + + _disconnectAdjustmentSignals() { + if (this._adjustmentValueSigId && this._currentAdjustment) { + this._currentAdjustment.disconnect(this._adjustmentValueSigId); + this._adjustmentValueSigId = 0; + } + if (this._adjustmentChangedSigId && this._currentAdjustment) { + this._currentAdjustment.disconnect(this._adjustmentChangedSigId); + this._adjustmentChangedSigId = 0; + } + this._currentAdjustment = null; + } + on_orientation_changed() { this.box.vertical = !this.state.isHorizontal; + + const managerOrientation = this.state.isHorizontal + ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; + this.mainLayout.set_orientation(managerOrientation); + if (this.state.isHorizontal) { - this.actor.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); - this.actor.style_class = 'grouped-window-list-scrollbox hfade'; + this.scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); + this.scrollView.remove_style_class_name('vfade'); + this.scrollView.add_style_class_name('hfade'); + + this.startIcon.set_icon_name('xsi-go-previous-symbolic'); + this.endIcon.set_icon_name('xsi-go-next-symbolic'); + + this.startButton.set_x_expand(false); + this.startButton.set_y_expand(true); + this.startButton.set_y_align(Clutter.ActorAlign.FILL); + this.endButton.set_x_expand(false); + this.endButton.set_y_expand(true); + this.endButton.set_y_align(Clutter.ActorAlign.FILL); } else { - this.actor.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); - this.actor.style_class = 'grouped-window-list-scrollbox vfade'; + this.scrollView.set_policy(St.PolicyType.NEVER, St.PolicyType.EXTERNAL); + this.scrollView.remove_style_class_name('hfade'); + this.scrollView.add_style_class_name('vfade'); + + this.startIcon.set_icon_name('xsi-go-up-symbolic'); + this.endIcon.set_icon_name('xsi-go-down-symbolic'); + + this.startButton.set_x_expand(true); + this.startButton.set_y_expand(false); + this.startButton.set_x_align(Clutter.ActorAlign.FILL); + this.endButton.set_x_expand(true); + this.endButton.set_y_expand(false); + this.endButton.set_x_align(Clutter.ActorAlign.FILL); } + + this._connectAdjustmentSignals(); + this.updateScrollButtonVisibility(); } scrollToChild(childActor) { @@ -80,12 +198,12 @@ var ScrollBox = class ScrollBox { if (isHorizontal) { c1 = allocation.x1; c2 = allocation.x2; - const hBar = this.actor.get_hscroll_bar(); + const hBar = this.scrollView.get_hscroll_bar(); if (hBar) adjustment = hBar.get_adjustment(); } else { c1 = allocation.y1; c2 = allocation.y2; - const vBar = this.actor.get_vscroll_bar(); + const vBar = this.scrollView.get_vscroll_bar(); if (vBar) adjustment = vBar.get_adjustment(); } @@ -95,7 +213,7 @@ var ScrollBox = class ScrollBox { let fade_offset = 30; - const fade_eff = this.actor.get_effect('fade'); + const fade_eff = this.scrollView.get_effect('fade'); if (fade_eff) { fade_offset = this.state.isHorizontal ? fade_eff.hfade_offset : fade_eff.vfade_offset; @@ -108,6 +226,77 @@ var ScrollBox = class ScrollBox { } } + updateScrollButtonVisibility() { + const adjustment = this._getScrollAdjustment(); + if (!adjustment) { + this.startButton.visible = false; + this.endButton.visible = false; + return; + } + + const canScroll = adjustment.upper > adjustment.page_size; + if (canScroll) { + // Tolerance of 1 pixel to avoid flickering + this.startButton.visible = adjustment.value > adjustment.lower + 1; + this.endButton.visible = adjustment.value < adjustment.upper - adjustment.page_size - 1; + } else { + this.startButton.visible = false; + this.endButton.visible = false; + } + } + + _startSlide(direction) { + if (direction === this.slideDirection || this.state.panelEditMode) return; + + this._stopSlide(); + this.slideDirection = direction; + this.state.scrollActive = true; + + this.slideTimerSourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SLIDE_INTERVAL, () => { + const adjustment = this._getScrollAdjustment(); + if (!adjustment) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + const newValue = adjustment.value + (SLIDE_SPEED * this.slideDirection * global.ui_scale); + adjustment.value = Math.max(adjustment.lower, + Math.min(newValue, adjustment.upper - adjustment.page_size)); + + // Stop if we've reached the bounds + if (this.slideDirection < 0 && adjustment.value <= adjustment.lower) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + if (this.slideDirection > 0 && adjustment.value >= adjustment.upper - adjustment.page_size) { + this.slideTimerSourceId = 0; + return GLib.SOURCE_REMOVE; + } + + return GLib.SOURCE_CONTINUE; + }); + } + + _stopSlide() { + if (this.slideTimerSourceId > 0) { + GLib.source_remove(this.slideTimerSourceId); + this.slideTimerSourceId = 0; + } + this.slideDirection = 0; + + if (this.scrollActiveTimeoutId) { + GLib.source_remove(this.scrollActiveTimeoutId); + } + + this.scrollActiveTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => { + if (this.slideDirection === 0) { + this.state.scrollActive = false; + } + this.scrollActiveTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } + _onMotionEvent(actor, event) { if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; @@ -126,8 +315,9 @@ var ScrollBox = class ScrollBox { // Check if we can scroll (content is larger than view) const canScroll = adjustment.upper > adjustment.page_size; + if (!canScroll) { - this._stopEdgeScroll(); + this._stopSlide(); return Clutter.EVENT_PROPAGATE; } @@ -153,10 +343,10 @@ var ScrollBox = class ScrollBox { } } - if (scrollDirection !== 0 && scrollDirection !== this.edgeScrollDirection) { - this._startEdgeScroll(scrollDirection); - } else if (scrollDirection === 0) { - this._stopEdgeScroll(); + if (scrollDirection !== 0) { + this._startSlide(scrollDirection); + } else { + this._stopSlide(); } return Clutter.EVENT_PROPAGATE; @@ -164,56 +354,14 @@ var ScrollBox = class ScrollBox { _getScrollAdjustment() { if (this.state.isHorizontal) { - const hBar = this.actor.get_hscroll_bar(); + const hBar = this.scrollView.get_hscroll_bar(); return hBar ? hBar.get_adjustment() : null; } else { - const vBar = this.actor.get_vscroll_bar(); + const vBar = this.scrollView.get_vscroll_bar(); return vBar ? vBar.get_adjustment() : null; } } - _startEdgeScroll(direction) { - this._stopEdgeScroll(); - this.edgeScrollDirection = direction; - this.state.scrollActive = true; - - this.edgeScrollTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, EDGE_SCROLL_INTERVAL, () => { - const adjustment = this._getScrollAdjustment(); - if (!adjustment) { - this.edgeScrollTimeoutId = 0; - return GLib.SOURCE_REMOVE; - } - - const newValue = adjustment.value + (EDGE_SCROLL_SPEED * this.edgeScrollDirection * global.ui_scale); - - // Clamp value to valid range - adjustment.value = Math.max(adjustment.lower, - Math.min(newValue, adjustment.upper - adjustment.page_size)); - - return GLib.SOURCE_CONTINUE; - }); - } - - _stopEdgeScroll() { - if (this.edgeScrollTimeoutId > 0) { - GLib.source_remove(this.edgeScrollTimeoutId); - this.edgeScrollTimeoutId = 0; - } - this.edgeScrollDirection = 0; - - if (this.scrollActiveTimeoutId) { - GLib.source_remove(this.scrollActiveTimeoutId); - } - - this.scrollActiveTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, () => { - if (this.edgeScrollDirection === 0) { - this.state.scrollActive = false; - } - this.scrollActiveTimeoutId = 0; - return GLib.SOURCE_REMOVE; - }); - } - _onScroll(actor, event) { if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; @@ -227,7 +375,7 @@ var ScrollBox = class ScrollBox { const direction = event.get_scroll_direction(); let delta = 0; - const hBar = this.actor.get_hscroll_bar(); + const hBar = this.scrollView.get_hscroll_bar(); if (!hBar) return Clutter.EVENT_PROPAGATE; const adjustment = hBar.get_adjustment(); @@ -254,6 +402,8 @@ var ScrollBox = class ScrollBox { // Manually updating adjustment value using property adjustment.value = adjustment.value + delta; return Clutter.EVENT_STOP; + } else { + this.updateScrollButtonVisibility(); } } return Clutter.EVENT_PROPAGATE; From 9955b7d405a292e7361bcd6b1b5b65db0111686d Mon Sep 17 00:00:00 2001 From: anaximeno Date: Thu, 12 Mar 2026 17:35:35 -0100 Subject: [PATCH 27/36] gwl: Fix scroll handling and workspace scrollbox destroy process --- .../applets/grouped-window-list@cinnamon.org/scrollBox.js | 2 +- .../applets/grouped-window-list@cinnamon.org/workspace.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index f8fcdeee22..764a5def46 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -367,7 +367,7 @@ var ScrollBox = class ScrollBox { if (this.state.settings.scrollBehavior !== 4) { this.state.trigger('handleScroll', event); - return; + return Clutter.EVENT_STOP; } // Handle horizontal scrolling with vertical wheel diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 2bd7c08e0c..d9b7f493c3 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -464,13 +464,13 @@ var Workspace = class Workspace { } destroy() { - this.scrollBox.destroy(); if (this.scrollToAppDebounceTimeoutId > 0) { GLib.source_remove(this.scrollToAppDebounceTimeoutId); this.scrollToAppDebounceTimeoutId = 0; } this.signals.disconnectAllSignals(); this.appGroups.forEach( appGroup => appGroup.destroy() ); + this.scrollBox.destroy(); this.workspaceState.destroy(); unref(this, RESERVE_KEYS); } From 9f894c8e0cbed31538dfa26167f3d1dcac3c2e8d Mon Sep 17 00:00:00 2001 From: anaximeno Date: Thu, 12 Mar 2026 17:49:09 -0100 Subject: [PATCH 28/36] gwl: Improve edge slide area and style handling gwl: Update name from box to container for clarity --- .../cinnamon-sass/widgets/_windowlist.scss | 2 + .../scrollBox.js | 47 +++++++++---------- .../workspace.js | 4 +- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index 2f977e13e8..419ae8ae2c 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -127,6 +127,8 @@ &-down { margin: 0; padding: 0; + min-width: 15px; + min-height: 20px; } &-icon { diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 764a5def46..d69f96eb20 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -24,46 +24,38 @@ var ScrollBox = class ScrollBox { this.scrollView.set_clip_to_allocation(true); this.scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.EXTERNAL); - this.box = new St.BoxLayout({ + this.container = new St.BoxLayout({ vertical: !this.state.isHorizontal, style_class: 'grouped-window-list-scrollbox-container', }); - this.scrollView.add_actor(this.box); + this.scrollView.add_actor(this.container); // Slider buttons - const buttonStyle = 'min-width: 15px; min-height: 20px; margin: 0px; padding: 0px;'; - this.startButton = new St.Bin({ style_class: 'grouped-window-list-scrollbox-button-start', - style: buttonStyle, visible: false, reactive: true, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); - this.endButton = new St.Bin({ style_class: 'grouped-window-list-scrollbox-button-end', - style: buttonStyle, visible: false, reactive: true, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); - this.startIcon = new St.Icon({ icon_name: 'xsi-go-previous-symbolic', icon_type: St.IconType.SYMBOLIC, style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' }); - this.endIcon = new St.Icon({ icon_name: 'xsi-go-next-symbolic', icon_type: St.IconType.SYMBOLIC, style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' }); - this.startButton.set_child(this.startIcon); this.endButton.set_child(this.endIcon); @@ -98,7 +90,7 @@ var ScrollBox = class ScrollBox { this.signals.connect(this.scrollView, 'leave-event', () => this._stopSlide()); // Track content size changes - this.signals.connect(this.box, 'allocation-changed', () => this.updateScrollButtonVisibility()); + this.signals.connect(this.container, 'allocation-changed', () => this.updateScrollButtonVisibility()); this.stateConnectionID = this.state.connect({ orientation: (state) => this.on_orientation_changed() @@ -143,7 +135,7 @@ var ScrollBox = class ScrollBox { } on_orientation_changed() { - this.box.vertical = !this.state.isHorizontal; + this.container.vertical = !this.state.isHorizontal; const managerOrientation = this.state.isHorizontal ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL; @@ -183,8 +175,19 @@ var ScrollBox = class ScrollBox { this.updateScrollButtonVisibility(); } + _getFadeOffset() { + const fade_eff = this.scrollView.get_effect('fade'); + + let fade_offset = 0; + if (fade_eff) { + fade_offset = this.state.isHorizontal ? fade_eff.hfade_offset : fade_eff.vfade_offset; + } + + return fade_offset > 0 ? fade_offset : EDGE_SCROLL_ZONE_SIZE * global.ui_scale; + } + scrollToChild(childActor) { - if (!childActor) return; + if (!childActor || !childActor.has_allocation()) return; // Get allocation of child relative to container const allocation = childActor.get_allocation_box(); @@ -211,13 +214,7 @@ var ScrollBox = class ScrollBox { const current = adjustment.value; const page_size = adjustment.page_size; - let fade_offset = 30; - - const fade_eff = this.scrollView.get_effect('fade'); - - if (fade_eff) { - fade_offset = this.state.isHorizontal ? fade_eff.hfade_offset : fade_eff.vfade_offset; - } + let fade_offset = this._getFadeOffset(); if (c1 < current + fade_offset || c2 > current + page_size - fade_offset) { const newValue = (c1 + c2) / 2 - page_size / 2; @@ -321,23 +318,25 @@ var ScrollBox = class ScrollBox { return Clutter.EVENT_PROPAGATE; } + const fadeOffset = this._getFadeOffset(); + if (this.state.isHorizontal) { // Check left edge - if (relX < EDGE_SCROLL_ZONE_SIZE && adjustment.value > adjustment.lower) { + if (relX < fadeOffset && adjustment.value > adjustment.lower) { scrollDirection = -1; } // Check right edge - else if (relX > actorWidth - EDGE_SCROLL_ZONE_SIZE && + else if (relX > actorWidth - fadeOffset && adjustment.value < adjustment.upper - adjustment.page_size) { scrollDirection = 1; } } else { // Check top edge - if (relY < EDGE_SCROLL_ZONE_SIZE && adjustment.value > adjustment.lower) { + if (relY < fadeOffset && adjustment.value > adjustment.lower) { scrollDirection = -1; } // Check bottom edge - else if (relY > actorHeight - EDGE_SCROLL_ZONE_SIZE && + else if (relY > actorHeight - fadeOffset && adjustment.value < adjustment.upper - adjustment.page_size) { scrollDirection = 1; } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index d9b7f493c3..0f9d212a73 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -46,7 +46,7 @@ var Workspace = class Workspace { (!appGroup.groupState.lastFocused || appGroup.groupState.lastFocused.has_focus())) { this.scrollToAppGroup(appGroup); return; - }; + } appGroup.onFocusChange(false); }); } @@ -57,7 +57,7 @@ var Workspace = class Workspace { this.scrollBox = new ScrollBox(this.state); this.actor = this.scrollBox.actor; - this.container = this.scrollBox.box; + this.container = this.scrollBox.container; this.appGroups = []; this.lastFocusedApp = null; From 37c49329d066a28f9efd4b0bdc8a51cbc263e1d8 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Thu, 12 Mar 2026 18:55:22 -0100 Subject: [PATCH 29/36] gwl: Add option to enable/disable slide to focused app button --- .../applets/grouped-window-list@cinnamon.org/applet.js | 1 + .../settings-schema.json | 9 ++++++++- .../grouped-window-list@cinnamon.org/workspace.js | 7 +++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 31ee0351e1..0118e6f204 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -315,6 +315,7 @@ class GroupedWindowListApplet extends Applet.Applet { {key: 'enable-window-count-badges', value: 'enableWindowCountBadges', cb: this.onEnableWindowCountBadgeChange}, {key: 'enable-notification-badges', value: 'enableNotificationBadges', cb: this.onEnableNotificationsChange}, {key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged}, + {key: 'enable-slide-to-focused-app-button', value: 'enableSlideToFocusedAppButton', cb: null}, {key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null}, {key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState}, {key: 'animate-thumbnails', value: 'animateThumbs', cb: null}, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index 31511ceb5c..58bc3379fa 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -52,7 +52,8 @@ "launcher-animation-effect", "enable-window-count-badges", "enable-notification-badges", - "enable-app-button-dragging" + "enable-app-button-dragging", + "enable-slide-to-focused-app-button" ] }, "thumbnailsSection": { @@ -203,6 +204,12 @@ "default": true, "description": "Enable app button dragging" }, + "enable-slide-to-focused-app-button": { + "type": "checkbox", + "default": true, + "description": "Enable slide to focused app button", + "tooltip": "When enabled, if you have many windows open and the app buttons don't fit in the panel, the view will automatically slide to the app button of the focused window." + }, "thumbnail-scroll-behavior": { "type": "checkbox", "default": false, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 0f9d212a73..11d2c97399 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -18,7 +18,9 @@ var Workspace = class Workspace { this.on_orientation_changed(state.orientation); }, currentWs: (state) => { - if (this.metaWorkspace && state.currentWs === this.metaWorkspace.index()) { + if (this.state.settings.enableSlideToFocusedAppButton && this.metaWorkspace && + state.currentWs === this.metaWorkspace.index()) { + // -- this.scrollToLastFocusedApp(); } } @@ -42,8 +44,9 @@ var Workspace = class Workspace { }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { - if (focusedAppId === appGroup.groupState.appId && + if (this.state.settings.enableSlideToFocusedAppButton && (focusedAppId === appGroup.groupState.appId) && (!appGroup.groupState.lastFocused || appGroup.groupState.lastFocused.has_focus())) { + // -- this.scrollToAppGroup(appGroup); return; } From 473aa16bd598845c5d287e82c0c999db0f986f4d Mon Sep 17 00:00:00 2001 From: anaximeno Date: Thu, 12 Mar 2026 20:11:49 -0100 Subject: [PATCH 30/36] gwl: Use xsi-pan icons instead of the arrow icons Looks better and is consistent with what is used in the alt-tab window switcher for example when it reaches the limits of the screen. --- .../grouped-window-list@cinnamon.org/scrollBox.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index d69f96eb20..a3941cba1d 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -47,12 +47,12 @@ var ScrollBox = class ScrollBox { y_align: St.Align.MIDDLE }); this.startIcon = new St.Icon({ - icon_name: 'xsi-go-previous-symbolic', + icon_name: 'xsi-pan-start-symbolic', icon_type: St.IconType.SYMBOLIC, style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' }); this.endIcon = new St.Icon({ - icon_name: 'xsi-go-next-symbolic', + icon_name: 'xsi-pan-end-symbolic', icon_type: St.IconType.SYMBOLIC, style_class: 'popup-menu-icon grouped-window-list-scrollbox-button-icon' }); @@ -146,8 +146,8 @@ var ScrollBox = class ScrollBox { this.scrollView.remove_style_class_name('vfade'); this.scrollView.add_style_class_name('hfade'); - this.startIcon.set_icon_name('xsi-go-previous-symbolic'); - this.endIcon.set_icon_name('xsi-go-next-symbolic'); + this.startIcon.set_icon_name('xsi-pan-start-symbolic'); + this.endIcon.set_icon_name('xsi-pan-end-symbolic'); this.startButton.set_x_expand(false); this.startButton.set_y_expand(true); @@ -160,8 +160,8 @@ var ScrollBox = class ScrollBox { this.scrollView.remove_style_class_name('hfade'); this.scrollView.add_style_class_name('vfade'); - this.startIcon.set_icon_name('xsi-go-up-symbolic'); - this.endIcon.set_icon_name('xsi-go-down-symbolic'); + this.startIcon.set_icon_name('xsi-pan-up-symbolic'); + this.endIcon.set_icon_name('xsi-pan-down-symbolic'); this.startButton.set_x_expand(true); this.startButton.set_y_expand(false); From 8316441e5533de769418326ff41ea0590adebbcf Mon Sep 17 00:00:00 2001 From: anaximeno Date: Tue, 17 Mar 2026 03:40:41 -0100 Subject: [PATCH 31/36] gwl: Handle possible undefined or null this.state cases --- .../applets/grouped-window-list@cinnamon.org/workspace.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 11d2c97399..48d3c18678 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -18,8 +18,8 @@ var Workspace = class Workspace { this.on_orientation_changed(state.orientation); }, currentWs: (state) => { - if (this.state.settings.enableSlideToFocusedAppButton && this.metaWorkspace && - state.currentWs === this.metaWorkspace.index()) { + if (this.state?.settings?.enableSlideToFocusedAppButton && this.metaWorkspace && + state?.currentWs === this.metaWorkspace.index()) { // -- this.scrollToLastFocusedApp(); } From b09507651e476bbf533f9bb88efcd0b6a9150aac Mon Sep 17 00:00:00 2001 From: anaximeno Date: Tue, 28 Apr 2026 16:41:42 -0100 Subject: [PATCH 32/36] gwl: Add click to slide option --- .../cinnamon-sass/widgets/_windowlist.scss | 8 +++ .../applet.js | 2 +- .../scrollBox.js | 65 +++++++++++++++++-- .../settings-schema.json | 10 +-- .../workspace.js | 6 +- 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index 419ae8ae2c..cb97d4740a 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -134,6 +134,14 @@ &-icon { icon-size: $scalable_icon_size; } + + :hover { + // TODO + } + + :active { + // TODO + } } } } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 0118e6f204..ca63b5669a 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -315,7 +315,7 @@ class GroupedWindowListApplet extends Applet.Applet { {key: 'enable-window-count-badges', value: 'enableWindowCountBadges', cb: this.onEnableWindowCountBadgeChange}, {key: 'enable-notification-badges', value: 'enableNotificationBadges', cb: this.onEnableNotificationsChange}, {key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged}, - {key: 'enable-slide-to-focused-app-button', value: 'enableSlideToFocusedAppButton', cb: null}, + {key: 'enable-click-to-slide', value: 'enableClickToSlide', cb: null}, {key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null}, {key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState}, {key: 'animate-thumbnails', value: 'animateThumbs', cb: null}, diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index a3941cba1d..83791b6e61 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -36,6 +36,8 @@ var ScrollBox = class ScrollBox { style_class: 'grouped-window-list-scrollbox-button-start', visible: false, reactive: true, + can_focus: true, + track_hover: true, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); @@ -43,6 +45,8 @@ var ScrollBox = class ScrollBox { style_class: 'grouped-window-list-scrollbox-button-end', visible: false, reactive: true, + can_focus: true, + track_hover: true, x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE }); @@ -79,10 +83,58 @@ var ScrollBox = class ScrollBox { this.scrollActiveTimeoutId = 0; // Slider button signals - this.signals.connect(this.startButton, 'enter-event', () => this._startSlide(-1)); - this.signals.connect(this.startButton, 'leave-event', () => this._stopSlide()); - this.signals.connect(this.endButton, 'enter-event', () => this._startSlide(1)); - this.signals.connect(this.endButton, 'leave-event', () => this._stopSlide()); + this.signals.connect(this.startButton, 'enter-event', () => { + if (!this.state.settings.enableClickToSlide) + this._startSlide(-1); + }); + this.signals.connect(this.startButton, 'leave-event', () => { + this._stopSlide(); + this.startButton.remove_style_pseudo_class('active'); + }); + this.signals.connect(this.endButton, 'enter-event', () => { + if (!this.state.settings.enableClickToSlide) + this._startSlide(1); + }); + this.signals.connect(this.endButton, 'leave-event', () => { + this._stopSlide(); + this.endButton.remove_style_pseudo_class('active'); + }); + this.signals.connect(this.startButton, 'button-press-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + if (this.state.settings.enableClickToSlide) { + this.startButton.add_style_pseudo_class('active'); + this._startSlide(-1); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this.signals.connect(this.startButton, 'button-release-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + this.startButton.remove_style_pseudo_class('active'); + if (this.state.settings.enableClickToSlide) { + this._stopSlide(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this.signals.connect(this.endButton, 'button-press-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + if (this.state.settings.enableClickToSlide) { + this.endButton.add_style_pseudo_class('active'); + this._startSlide(1); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); + this.signals.connect(this.endButton, 'button-release-event', (actor, event) => { + if (event.get_button() !== 1) return Clutter.EVENT_PROPAGATE; + this.endButton.remove_style_pseudo_class('active'); + if (this.state.settings.enableClickToSlide) { + this._stopSlide(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + }); // Scroll view signals this.signals.connect(this.scrollView, 'scroll-event', (actor, event) => this._onScroll(actor, event)); @@ -214,7 +266,7 @@ var ScrollBox = class ScrollBox { const current = adjustment.value; const page_size = adjustment.page_size; - let fade_offset = this._getFadeOffset(); + let fade_offset = this._getFadeOffset() / 2; if (c1 < current + fade_offset || c2 > current + page_size - fade_offset) { const newValue = (c1 + c2) / 2 - page_size / 2; @@ -295,7 +347,8 @@ var ScrollBox = class ScrollBox { } _onMotionEvent(actor, event) { - if (this.state.panelEditMode) return Clutter.EVENT_PROPAGATE; + if (this.state.panelEditMode || this.state.settings.enableClickToSlide) + return Clutter.EVENT_PROPAGATE; const [x, y] = event.get_coords(); const [actorX, actorY] = actor.get_transformed_position(); diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index 58bc3379fa..989882207a 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -53,7 +53,7 @@ "enable-window-count-badges", "enable-notification-badges", "enable-app-button-dragging", - "enable-slide-to-focused-app-button" + "enable-click-to-slide" ] }, "thumbnailsSection": { @@ -204,11 +204,11 @@ "default": true, "description": "Enable app button dragging" }, - "enable-slide-to-focused-app-button": { + "enable-click-to-slide": { "type": "checkbox", - "default": true, - "description": "Enable slide to focused app button", - "tooltip": "When enabled, if you have many windows open and the app buttons don't fit in the panel, the view will automatically slide to the app button of the focused window." + "default": false, + "description": "Enable slide on click", + "tooltip": "When the sliding view is visible, clicking the button at the edges will slide the app list. If disabled, hovering the mouse on the edge of the app list will slide the app list towards that side." }, "thumbnail-scroll-behavior": { "type": "checkbox", diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 48d3c18678..325907fad1 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -18,9 +18,7 @@ var Workspace = class Workspace { this.on_orientation_changed(state.orientation); }, currentWs: (state) => { - if (this.state?.settings?.enableSlideToFocusedAppButton && this.metaWorkspace && - state?.currentWs === this.metaWorkspace.index()) { - // -- + if (this.metaWorkspace && (state?.currentWs === this.metaWorkspace.index())) { this.scrollToLastFocusedApp(); } } @@ -44,7 +42,7 @@ var Workspace = class Workspace { }, updateFocusState: (focusedAppId) => { this.appGroups.forEach( appGroup => { - if (this.state.settings.enableSlideToFocusedAppButton && (focusedAppId === appGroup.groupState.appId) && + if ((focusedAppId === appGroup.groupState.appId) && (!appGroup.groupState.lastFocused || appGroup.groupState.lastFocused.has_focus())) { // -- this.scrollToAppGroup(appGroup); From 06cb3d777cbf1d74011f6103b30a72f1712db3a1 Mon Sep 17 00:00:00 2001 From: anaximeno Date: Tue, 28 Apr 2026 18:00:14 -0100 Subject: [PATCH 33/36] gwl: Update scroll box buttons classes --- .../cinnamon-sass/widgets/_windowlist.scss | 24 +++++++------------ .../scrollBox.js | 10 ++++++-- .../settings-schema.json | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index cb97d4740a..374ccced9c 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -121,27 +121,19 @@ } &-button { - &-start, - &-end, - &-up, - &-down { - margin: 0; - padding: 0; - min-width: 15px; - min-height: 20px; - } - &-icon { icon-size: $scalable_icon_size; } - :hover { - // TODO - } + &.vertical { } - :active { - // TODO - } + &-start { } + + &-end { } + + :hover { } + + :active { } } } } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 83791b6e61..6b3021cbee 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -33,7 +33,7 @@ var ScrollBox = class ScrollBox { // Slider buttons this.startButton = new St.Bin({ - style_class: 'grouped-window-list-scrollbox-button-start', + style_class: 'grouped-window-list-scrollbox-button grouped-window-list-scrollbox-button-start', visible: false, reactive: true, can_focus: true, @@ -42,7 +42,7 @@ var ScrollBox = class ScrollBox { y_align: St.Align.MIDDLE }); this.endButton = new St.Bin({ - style_class: 'grouped-window-list-scrollbox-button-end', + style_class: 'grouped-window-list-scrollbox-button grouped-window-list-scrollbox-button-end', visible: false, reactive: true, can_focus: true, @@ -201,9 +201,12 @@ var ScrollBox = class ScrollBox { this.startIcon.set_icon_name('xsi-pan-start-symbolic'); this.endIcon.set_icon_name('xsi-pan-end-symbolic'); + this.startButton.remove_style_class_name('vertical'); this.startButton.set_x_expand(false); this.startButton.set_y_expand(true); this.startButton.set_y_align(Clutter.ActorAlign.FILL); + + this.endButton.remove_style_class_name('vertical'); this.endButton.set_x_expand(false); this.endButton.set_y_expand(true); this.endButton.set_y_align(Clutter.ActorAlign.FILL); @@ -215,9 +218,12 @@ var ScrollBox = class ScrollBox { this.startIcon.set_icon_name('xsi-pan-up-symbolic'); this.endIcon.set_icon_name('xsi-pan-down-symbolic'); + this.startButton.add_style_class_name('vertical'); this.startButton.set_x_expand(true); this.startButton.set_y_expand(false); this.startButton.set_x_align(Clutter.ActorAlign.FILL); + + this.endButton.add_style_class_name('vertical'); this.endButton.set_x_expand(true); this.endButton.set_y_expand(false); this.endButton.set_x_align(Clutter.ActorAlign.FILL); diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index 989882207a..182b614b6b 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -206,7 +206,7 @@ }, "enable-click-to-slide": { "type": "checkbox", - "default": false, + "default": true, "description": "Enable slide on click", "tooltip": "When the sliding view is visible, clicking the button at the edges will slide the app list. If disabled, hovering the mouse on the edge of the app list will slide the app list towards that side." }, From 0d1072775c3786ceb140e42a4a5d7432458ca17d Mon Sep 17 00:00:00 2001 From: anaximeno Date: Tue, 28 Apr 2026 22:35:58 -0100 Subject: [PATCH 34/36] gwl: Break to handle lastFocused app instead of handling inside the loop --- .../applets/grouped-window-list@cinnamon.org/workspace.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 325907fad1..98e4c9a9b7 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -244,8 +244,7 @@ var Workspace = class Workspace { let lastFocusedInAppGroup = appGroup.groupState.lastFocused; if (lastFocusedInAppGroup && lastFocusedInAppGroup.has_focus()) { lastFocusedAppInWorkspace = appGroup; - this.scrollToAppGroup(appGroup); - return; + break; } else if (this.workspaceState.lastFocusedApp === appGroup.groupState.appId) { lastFocusedAppInWorkspace = appGroup; } else if ((!lastFocusedAppInWorkspace || lastFocusedAppInWorkspace.groupState.metaWindows.length === 0) && From 43b6b2e9e892609a2d7181a0a3dabe3b807a9a1b Mon Sep 17 00:00:00 2001 From: anaximeno Date: Wed, 29 Apr 2026 18:21:07 -0100 Subject: [PATCH 35/36] gwl: Remove edge scroll zone in favor of the side buttons Considering we are already using the side scroll buttons which gives a clear over area for sliding the app list, having a edge scroll zone there makes it redudant and potentially confusing for the user. --- .../scrollBox.js | 62 +------------------ .../settings-schema.json | 6 +- 2 files changed, 4 insertions(+), 64 deletions(-) diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js index 6b3021cbee..1d96512fb3 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/scrollBox.js @@ -138,8 +138,6 @@ var ScrollBox = class ScrollBox { // Scroll view signals this.signals.connect(this.scrollView, 'scroll-event', (actor, event) => this._onScroll(actor, event)); - this.signals.connect(this.scrollView, 'motion-event', (actor, event) => this._onMotionEvent(actor, event)); - this.signals.connect(this.scrollView, 'leave-event', () => this._stopSlide()); // Track content size changes this.signals.connect(this.container, 'allocation-changed', () => this.updateScrollButtonVisibility()); @@ -272,7 +270,7 @@ var ScrollBox = class ScrollBox { const current = adjustment.value; const page_size = adjustment.page_size; - let fade_offset = this._getFadeOffset() / 2; + let fade_offset = this._getFadeOffset(); if (c1 < current + fade_offset || c2 > current + page_size - fade_offset) { const newValue = (c1 + c2) / 2 - page_size / 2; @@ -352,64 +350,6 @@ var ScrollBox = class ScrollBox { }); } - _onMotionEvent(actor, event) { - if (this.state.panelEditMode || this.state.settings.enableClickToSlide) - return Clutter.EVENT_PROPAGATE; - - const [x, y] = event.get_coords(); - const [actorX, actorY] = actor.get_transformed_position(); - const [actorWidth, actorHeight] = actor.get_transformed_size(); - - // Calculate relative position within the actor - const relX = x - actorX; - const relY = y - actorY; - - let scrollDirection = 0; - const adjustment = this._getScrollAdjustment(); - - if (!adjustment) return Clutter.EVENT_PROPAGATE; - - // Check if we can scroll (content is larger than view) - const canScroll = adjustment.upper > adjustment.page_size; - - if (!canScroll) { - this._stopSlide(); - return Clutter.EVENT_PROPAGATE; - } - - const fadeOffset = this._getFadeOffset(); - - if (this.state.isHorizontal) { - // Check left edge - if (relX < fadeOffset && adjustment.value > adjustment.lower) { - scrollDirection = -1; - } - // Check right edge - else if (relX > actorWidth - fadeOffset && - adjustment.value < adjustment.upper - adjustment.page_size) { - scrollDirection = 1; - } - } else { - // Check top edge - if (relY < fadeOffset && adjustment.value > adjustment.lower) { - scrollDirection = -1; - } - // Check bottom edge - else if (relY > actorHeight - fadeOffset && - adjustment.value < adjustment.upper - adjustment.page_size) { - scrollDirection = 1; - } - } - - if (scrollDirection !== 0) { - this._startSlide(scrollDirection); - } else { - this._stopSlide(); - } - - return Clutter.EVENT_PROPAGATE; - } - _getScrollAdjustment() { if (this.state.isHorizontal) { const hBar = this.scrollView.get_hscroll_bar(); diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index 182b614b6b..a3470b5a52 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -206,9 +206,9 @@ }, "enable-click-to-slide": { "type": "checkbox", - "default": true, - "description": "Enable slide on click", - "tooltip": "When the sliding view is visible, clicking the button at the edges will slide the app list. If disabled, hovering the mouse on the edge of the app list will slide the app list towards that side." + "default": false, + "description": "Enable click to slide when slide buttons are visible", + "tooltip": "When enabled, clicking the slide buttons at either end of the app list will scroll the list in that direction. When disabled, the list will scroll when the mouse pointer hovers over the slide buttons." }, "thumbnail-scroll-behavior": { "type": "checkbox", From e673741efa452b50a193736c7dd4432bfff2adae Mon Sep 17 00:00:00 2001 From: anaximeno Date: Wed, 29 Apr 2026 18:50:52 -0100 Subject: [PATCH 36/36] gwl: Implement proper styling for the slide buttons --- .../cinnamon-sass/widgets/_windowlist.scss | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index 374ccced9c..96f4ff6974 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -121,19 +121,42 @@ } &-button { + transition-duration: 100ms; + &-icon { icon-size: $scalable_icon_size; + color: inherit; } &.vertical { } - &-start { } + &-start { + margin-right: 1px; + + &.vertical { + margin-right: 0; + margin-bottom: 1px; + } + } - &-end { } + &-end { + margin-left: 1px; - :hover { } + &.vertical { + margin-left: 0; + margin-top: 1px; + } + } - :active { } + &:hover { + background-color: $lighter_bg_color; + color: $fg_color; + } + + &:active { + background-color: $lightest_bg_color; + color: $fg_color; + } } } }