From f8572954058868b27d5cbc9b09ef4b9cff971158 Mon Sep 17 00:00:00 2001 From: mattchristenson Date: Tue, 29 Sep 2026 22:54:57 -0500 Subject: [PATCH 1/2] fix: disconnect the settings handlers when Forge is disabled The window manager and the quick settings indicator connected "changed" handlers to Forge's settings and never disconnected them. After Forge was disabled (as on the lock screen), every settings change still ran them, and they failed on the settings the extension had dropped ("this.ext.settings is null"); after enabling again, the old handlers kept running next to the new ones. Keep the handler ids and disconnect them in _removeSignals() and when the indicator is destroyed. Co-Authored-By: Claude Opus 5.5 --- lib/extension/indicator.js | 12 ++++++++++-- lib/extension/window.js | 11 ++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/extension/indicator.js b/lib/extension/indicator.js index 309dcf20..06a0d448 100644 --- a/lib/extension/indicator.js +++ b/lib/extension/indicator.js @@ -118,12 +118,20 @@ export class FeatureIndicator extends SystemIndicator { this._indicator.visible = tilingModeEnabled && quickSettingsEnabled; - this.extension.settings.connect("changed", (_, name) => { + this._settings = this.extension.settings; + this._settingsChangedId = this._settings.connect("changed", (_, name) => { switch (name) { case "tiling-mode-enabled": case "quick-settings-enabled": - this._indicator.visible = this.extension.settings.get_boolean(name); + this._indicator.visible = this._settings.get_boolean(name); } }); } + + destroy() { + // the handler would outlive the indicator, and the extension's settings (#469's log) + if (this._settingsChangedId) this._settings.disconnect(this._settingsChangedId); + this._settingsChangedId = 0; + super.destroy(); + } } diff --git a/lib/extension/window.js b/lib/extension/window.js index 61820e1e..53d7be13 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -289,7 +289,10 @@ export class WindowManager extends GObject.Object { let settings = this.ext.settings; - settings.connect("changed", (_, settingName) => { + // (kept to disconnect in _removeSignals: the extension drops its settings on disable, and + // a handler left connected failed on every later change) + this._settings = settings; + this._settingsChangedId = settings.connect("changed", (_, settingName) => { switch (settingName) { case "window-overrides-reload-trigger": // Reload window overrides when triggered by preferences @@ -1042,6 +1045,12 @@ export class WindowManager extends GObject.Object { _removeSignals() { if (!this._signalsBound) return; + if (this._settingsChangedId) { + this._settings.disconnect(this._settingsChangedId); + this._settingsChangedId = 0; + this._settings = null; + } + if (this._displaySignals) { for (const displaySignal of this._displaySignals) { global.display.disconnect(displaySignal); From 47ba5b7b5af621129e4d3fa8e28f99b1bd4d07b0 Mon Sep 17 00:00:00 2001 From: mattchristenson Date: Tue, 29 Sep 2026 23:25:13 -0500 Subject: [PATCH 2/2] fix: release a closing window's handlers too when Forge is disabled A window that is closing has left the window list, but its actor keeps Forge's destroy handler and borders until the close animation ends. _removeSignals() only went over the listed windows, so when Forge was disabled during a close animation, windowDestroy() still ran afterwards and queued a render, which failed on the settings Forge had dropped ("this.ext.settings is null"). Keep the actors Forge connected to in a set and release them all. Co-Authored-By: Claude Opus 5.5 --- lib/extension/window.js | 46 +++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/extension/window.js b/lib/extension/window.js index 53d7be13..750b9f4c 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -1091,6 +1091,10 @@ export class WindowManager extends GObject.Object { } let allWindows = this.windowsAllWorkspaces; + // A window that is closing has left the window list, but its actor keeps its handlers and + // borders until the close animation ends: release those too + const windowActors = new Set(this._windowActors); + this._windowActors?.clear(); if (allWindows) { for (let metaWindow of allWindows) { @@ -1103,29 +1107,33 @@ export class WindowManager extends GObject.Object { } let windowActor = metaWindow.get_compositor_private(); - if (windowActor && windowActor.actorSignals) { - for (const actorSignal of windowActor.actorSignals) { - windowActor.disconnect(actorSignal); - } - windowActor.actorSignals.length = 0; - windowActor.actorSignals = undefined; + if (windowActor) windowActors.add(windowActor); + } + } + + for (const windowActor of windowActors) { + if (windowActor.actorSignals) { + for (const actorSignal of windowActor.actorSignals) { + windowActor.disconnect(actorSignal); } + windowActor.actorSignals.length = 0; + windowActor.actorSignals = undefined; + } - if (windowActor && windowActor.border) { - windowActor.border.hide(); - if (global.window_group) { - global.window_group.remove_child(windowActor.border); - } - windowActor.border = undefined; + if (windowActor.border) { + windowActor.border.hide(); + if (global.window_group) { + global.window_group.remove_child(windowActor.border); } + windowActor.border = undefined; + } - if (windowActor && windowActor.splitBorder) { - windowActor.splitBorder.hide(); - if (global.window_group) { - global.window_group.remove_child(windowActor.splitBorder); - } - windowActor.splitBorder = undefined; + if (windowActor.splitBorder) { + windowActor.splitBorder.hide(); + if (global.window_group) { + global.window_group.remove_child(windowActor.splitBorder); } + windowActor.splitBorder = undefined; } } @@ -1534,6 +1542,7 @@ export class WindowManager extends GObject.Object { if (!windowActor.actorSignals) { let actorSignals = [windowActor.connect("destroy", this.windowDestroy.bind(this))]; windowActor.actorSignals = actorSignals; + (this._windowActors ??= new Set()).add(windowActor); } if (!windowActor.border) { @@ -1680,6 +1689,7 @@ export class WindowManager extends GObject.Object { } windowDestroy(actor) { + this._windowActors?.delete(actor); // Release any resources on the window let border = actor.border; if (border) {