Skip to content

Grouped window list: a rebuild loses the button order of ungrouped windows (drag on one workspace reorders the others) #14085

Closed

Description

@greencardamom

Distribution

Linux Mint 22 (wilma)

Package version

cinnamon 6.2.10 - the same code is on master (checked at d67b361)

Frequency

Always

Bug description

With Group windows by application off, you can drag window buttons into any order. That order
is thrown away whenever the button list is rebuilt. On a rebuild, each app's windows come back
most-recently-used first, so several windows of the same app (for example, a row of terminals) end
up scrambled.

The common trigger is a drag-and-drop on another workspace. Dropping a button rebuilds every
other workspace's list.

A monitor change rebuilds the current workspace's list the same way, as do a pin change or a theme
change. The order is also lost whenever the applet reloads.

Steps to reproduce

  1. Turn off Group windows by application in the Grouped window list applet. Keep the default of
    showing only the current workspace's windows.
  2. On workspace 2, open about five windows of one app (gnome-terminal is easiest), in order. Give them
    titles such as 1 to 5, so the order is easy to see.
  3. Click a few of them in a random order, so the order they were last used in differs from their
    panel order.
  4. Switch to workspace 1 and drag any panel button to a new position.
  5. Switch back to workspace 2.

Actual: the buttons on workspace 2 are in most-recently-used order.

Expected behavior

The buttons on workspace 2 are in the same order as before.

Additional information

Cause

References are to master.

  • applet.js:931: after a drop, moveLauncher() calls refreshAllWorkspaces({ exceptCurrentOne: true }).
    Other triggers call refreshCurrentWorkspace() or refreshAllWorkspaces(): _onMonitorsChanged() via
    _updateState() (:375), onFavoritesChange() (:82), and theme-set (:291).
  • workspace.js:195: refreshList() destroys every app group and rebuilds the list with loadFavorites()
    and refreshApps().
  • workspace.js:213: loadFavorites() adds a pinned app's windows in app.get_windows() order, which is
    most recently used first. refreshApps() adds the rest in list_windows() order. Neither looks at the
    order the buttons were in before.

The drag case looks unintended. The refresh is there to keep pinned favourites consistent across
workspaces, but it also undoes the user's manual ordering on those workspaces.

Suggested fix

Have refreshList() remember the window order before it destroys the groups. After rebuilding, put each
app's window buttons back in that order, within the slots the rebuild gave that app. Pinned launchers,
other apps and grouped mode are unaffected. Windows not seen before follow, in the order they were
opened, so a fresh start shows windows in the order they were opened rather than most-recently-used
order.

I've been running this as a local patch on 6.2.10 and it fixes the problem. A drag on another
workspace, a monitor change and a forced refreshList() all leave the order intact. The 6.2.10 patch
(against appList.js) is below. Ported to master's names (appGroups, container), it would be
roughly as follows; I haven't run this version:

refreshList() {
    const prevOrder = [];
    this.appGroups.forEach(appGroup => prevOrder.push(...appGroup.groupState.metaWindows));

    this.appGroups.forEach( appGroup => {
        appGroup.destroy();
        appGroup = null;
    });
    this.appGroups = [];
    this.loadFavorites();
    this.refreshApps();
    this.restoreOrder(prevOrder);
    this.scrollToLastFocusedApp();
}

// Put each app's window buttons back in their previous order, within the slots that app holds.
// Windows not seen before follow in the order they were opened.
restoreOrder(prevOrder) {
    const rank = appGroup => {
        const metaWindow = appGroup.groupState.metaWindows[0];
        const i = prevOrder.indexOf(metaWindow);
        return i > -1 ? i : prevOrder.length + metaWindow.get_stable_sequence();
    };
    const slotsByApp = new Map();
    this.appGroups.forEach((appGroup, slot) => {
        if (appGroup.groupState.metaWindows.length === 0) return;
        const appId = appGroup.groupState.appId;
        if (!slotsByApp.has(appId)) slotsByApp.set(appId, []);
        slotsByApp.get(appId).push(slot);
    });
    const newAppGroups = this.appGroups.slice();
    slotsByApp.forEach(slots => {
        const groups = slots.map(slot => this.appGroups[slot]).sort((a, b) => rank(a) - rank(b));
        slots.forEach((slot, n) => { newAppGroups[slot] = groups[n]; });
    });
    this.appGroups = newAppGroups;
    this.appGroups.forEach((appGroup, slot) => this.container.set_child_at_index(appGroup.actor, slot));
}

In grouped mode each app has a single slot, so restoreOrder() changes nothing there.

Related

On 6.2.10 there was a second symptom on rebuilds in ungrouped mode: every button of the last focused app
got the focus style. Master already fixes this in onFocusChange()
(lastFocusedApp === appId && getFocusState(lastFocused)), so it isn't part of this report. The 6.2.10
patch below has a small workaround for it at the end of restoreOrder(), which master doesn't need.

The 6.2.10 patch (against appList.js)
--- a/appList.js
+++ b/appList.js
@@ -170,6 +170,11 @@
     }
 
     refreshList() {
+        // LOCAL PATCH (~/scripts/cinnamon-gwl): remember the button order so a rebuild keeps it
+        const prevOrder = [];
+        for (let i = 0, len = this.appList.length; i < len; i++) {
+            prevOrder.push(...this.appList[i].groupState.metaWindows);
+        }
         for (let i = 0, len = this.appList.length; i < len; i++) {
             this.appList[i].destroy();
             this.appList[i] = null;
@@ -177,6 +182,45 @@
         this.appList = [];
         this.loadFavorites();
         this.refreshApps();
+        this.restoreOrder(prevOrder);
+    }
+
+    // LOCAL PATCH: a rebuild adds each app's windows most-recently-used first. Put each app's
+    // window buttons back in their previous order, within the slots that app already holds.
+    // Windows not seen before follow, in the order they were opened.
+    restoreOrder(prevOrder) {
+        const rank = appGroup => {
+            const metaWindow = appGroup.groupState.metaWindows[0];
+            const i = prevOrder.indexOf(metaWindow);
+            return i > -1 ? i : prevOrder.length + metaWindow.get_stable_sequence();
+        };
+        const slotsByApp = new Map();
+        this.appList.forEach((appGroup, slot) => {
+            if (appGroup.groupState.metaWindows.length === 0) return;
+            const appId = appGroup.groupState.appId;
+            if (!slotsByApp.has(appId)) slotsByApp.set(appId, []);
+            slotsByApp.get(appId).push(slot);
+        });
+        const newAppList = this.appList.slice();
+        slotsByApp.forEach(slots => {
+            const groups = slots.map(slot => this.appList[slot]).sort((a, b) => rank(a) - rank(b));
+            slots.forEach((slot, n) => { newAppList[slot] = groups[n]; });
+        });
+        this.appList = newAppList;
+        this.appList.forEach((appGroup, slot) => this.actor.set_child_at_index(appGroup.actor, slot));
+
+        // onFocusChange() marks every button of the workspace's last focused app. With one button
+        // per window that greys them all - keep the mark on that app's most recently used window only
+        if (this.state.settings.groupApps) return;
+        const appId = this.listState.lastFocusedApp;
+        const appGroups = this.appList.filter(
+            appGroup => appGroup.groupState.appId === appId && appGroup.groupState.metaWindows.length > 0
+        );
+        const userTime = appGroup => appGroup.groupState.metaWindows[0].get_user_time();
+        const recent = appGroups.reduce((a, b) => (userTime(b) > userTime(a) ? b : a), appGroups[0]);
+        appGroups.forEach(appGroup => {
+            if (appGroup !== recent) appGroup.actor.remove_style_pseudo_class('focus');
+        });
     }
 
     loadFavorites() {

Activity

  1. greencardamom commented on Nov 4, 2026

    @greencardamom
    Author

    Thanks @jkramer5103 for the fix, and @clefebvre for merging it so quickly. Much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions