From 7f719ed1e2fd41c0c99554fb376999bf833b4bd5 Mon Sep 17 00:00:00 2001 From: IlyaVrum Date: Sun, 23 Aug 2026 23:41:38 +0200 Subject: [PATCH] screenshot: guard against double _ungrab in area/color/window pickers A second BUTTON_RELEASE (or BUTTON_PRESS for SelectWindow) arriving within the 100ms fade-out animation scheduled a second _ungrab(), so Main.popModal() ran twice for the same actor. The second call takes the 'incorrect pop' error path in main.js, which first tears down the stage input mode and the X grab and only then throws - the exception aborts _ungrab() before this._group.destroy(), so the fullscreen selection overlay stays on screen: clicks land on the overlay and the panel appears frozen until the shell is restarted. Reproducible with an area screenshot by clicking once more right after releasing the selection (e.g. xdotool ... mouseup 1 click 1). Finish the selection on the first release only, ignore extra input during the fade-out, and make _ungrab() idempotent in all three pickers. Co-Authored-By: Claude Fable 5 --- js/ui/screenshot.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js index 7893456520..a36428bf01 100644 --- a/js/ui/screenshot.js +++ b/js/ui/screenshot.js @@ -405,6 +405,18 @@ class SelectArea { } _onButtonRelease(actor, event) { + // The selection is finished on the first release. Ignore any further + // press/release pairs arriving during the 100ms fade-out - each of + // them used to overwrite the result and schedule another _ungrab(), + // and the second popModal() would take the 'incorrect pop' error path + // after tearing down the X grab, aborting _ungrab() before + // this._group.destroy(): the fullscreen overlay stayed on screen and + // the panel appeared frozen. + if (!this.active || this._finishing) + return Clutter.EVENT_PROPAGATE; + + this._finishing = true; + this._result = this._getGeometry(); this._group.ease({ opacity: 0, @@ -419,6 +431,12 @@ class SelectArea { } _ungrab() { + // One-shot guard, NOT this.active: Escape legitimately calls + // _ungrab() before any selection started (active is still false). + if (this._ungrabbed) + return; + this._ungrabbed = true; + this.active = false; if (this.stage_event_id > 0) { @@ -522,6 +540,18 @@ class PickColor { } _onButtonRelease(actor, event) { + // The selection is finished on the first release. Ignore any further + // press/release pairs arriving during the 100ms fade-out - each of + // them used to overwrite the result and schedule another _ungrab(), + // and the second popModal() would take the 'incorrect pop' error path + // after tearing down the X grab, aborting _ungrab() before + // this._group.destroy(): the fullscreen overlay stayed on screen and + // the panel appeared frozen. + if (!this.active || this._finishing) + return Clutter.EVENT_PROPAGATE; + + this._finishing = true; + this._result = this._getGeometry(); this._group.ease({ opacity: 0, @@ -535,6 +565,12 @@ class PickColor { } _ungrab() { + // One-shot guard, NOT this.active: Escape legitimately calls + // _ungrab() before any selection started (active is still false). + if (this._ungrabbed) + return; + this._ungrabbed = true; + this.active = false; if (this.stage_event_id > 0) { @@ -657,6 +693,10 @@ class SelectWindow { if (window === null) return Clutter.EVENT_STOP; + if (this._finishing) + return Clutter.EVENT_STOP; + this._finishing = true; + this._result = window; this._group.ease({ opacity: 0, @@ -671,6 +711,10 @@ class SelectWindow { } _ungrab() { + if (this._ungrabbed) + return; + this._ungrabbed = true; + Main.popModal(this._group); global.unset_cursor(); this.emit('finished', this._result);