From 6d649b21029d821f548a1ca7bd77c0f86ac09fba Mon Sep 17 00:00:00 2001 From: Rahul Batra Date: Fri, 17 Jul 2026 23:07:46 +0530 Subject: [PATCH 1/2] Fix GNOME 50 shell crash when closing SearchLight on outside click. Defer show/hide actor-tree changes out of input handlers, unmap search widgets before detaching them, and guard against re-entrant hide() calls so Clutter 18 no longer aborts the session when the overlay is dismissed. Co-authored-by: Cursor --- extension.js | 179 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 135 insertions(+), 44 deletions(-) diff --git a/extension.js b/extension.js index 81d5e0f..71f784e 100644 --- a/extension.js +++ b/extension.js @@ -134,6 +134,9 @@ export default class SearchLightExt extends Extension { this, ); + this._showSession = {}; + this._hiding = false; + this.mainContainer = new SearchLight(); this.mainContainer._delegate = this; this.container = new St.BoxLayout({ @@ -169,9 +172,9 @@ export default class SearchLightExt extends Extension { this._animationSpeed = this._settings.get_double('animation-speed'); Main.overview.connectObject( - 'overview-showing', + 'showing', this._onOverviewShowing.bind(this), - 'overview-hidden', + 'hidden', this._onOverviewHidden.bind(this), this, ); @@ -264,6 +267,13 @@ export default class SearchLightExt extends Extension { this._background = null; } + this._hideClickCatcher(); + + if (this._clickCatcher) { + this._clickCatcher.destroy(); + this._clickCatcher = null; + } + Main.layoutManager.removeChrome(this.mainContainer); this.mainContainer = null; } @@ -281,7 +291,10 @@ export default class SearchLightExt extends Extension { this._indicator.set_child(icon); this._indicator.connectObject( 'button-press-event', - this._toggle_search_light.bind(this), + () => { + this._defer(() => this._toggle_search_light()); + return Clutter.EVENT_STOP; + }, this, ); try { @@ -410,11 +423,16 @@ export default class SearchLightExt extends Extension { let background = new St.Widget({ name: 'searchLightBlurredBackground', layout_manager: new Clutter.BinLayout(), + reactive: true, x: 0, y: 0, width: 20, height: 20, }); + background.connect('button-press-event', () => { + this._deferHide(); + return Clutter.EVENT_STOP; + }); // let image = new St.Widget({ // name: 'searchLightBlurredBackgroundImage', @@ -457,6 +475,7 @@ export default class SearchLightExt extends Extension { this.mainContainer.show(); this.container.show(); + this._showClickCatcher(); this._add_events(); // fixes the background size relative to text - after adjusting font size @@ -488,33 +507,39 @@ export default class SearchLightExt extends Extension { } hide() { - if (this._isDraggingIcon()) { + if (this._hiding || this._isDraggingIcon()) { return; } - this._release_ui(); - this._remove_events(); - - if (this._useAnimations) { - this.mainContainer.ease({ - opacity: 0, - scale_x: 0.9, - scale_y: 0.9, - translation_x: (this.width * 0.1) / 2, - translation_y: (this.height * 0.1) / 2, - duration: this._animationSpeed, - mode: Clutter.AnimationMode.EASE_OUT, - onComplete: () => { - this._visible = false; - this.mainContainer.hide(); - global.compositor.enable_unredirect(); - }, - }); - } else { - this.mainContainer.opacity = 0; - this._visible = false; - this.mainContainer.hide(); - global.compositor.enable_unredirect(); + this._hiding = true; + try { + this._release_ui(); + this._remove_events(); + this._hideClickCatcher(); + + if (this._useAnimations) { + this.mainContainer.ease({ + opacity: 0, + scale_x: 0.9, + scale_y: 0.9, + translation_x: (this.width * 0.1) / 2, + translation_y: (this.height * 0.1) / 2, + duration: this._animationSpeed, + mode: Clutter.AnimationMode.EASE_OUT, + onComplete: () => { + this._visible = false; + this.mainContainer.hide(); + global.compositor.enable_unredirect(); + }, + }); + } else { + this.mainContainer.opacity = 0; + this._visible = false; + this.mainContainer.hide(); + global.compositor.enable_unredirect(); + } + } finally { + this._hiding = false; } // this._hidePopups(); } @@ -751,6 +776,7 @@ export default class SearchLightExt extends Extension { _release_ui() { if (this._entry) { + this._entry.hide(); if (this._entry.get_parent()) { this._entry.get_parent().remove_child(this._entry); } @@ -909,16 +935,83 @@ export default class SearchLightExt extends Extension { this._style.build('custom-search-light', styles); } - _toggle_search_light() { - if (this._inOverview) return; - if (!this._visible) { - this.show(); - if (this._entry) { - global.stage.set_key_focus(this._entry); + _defer(callback) { + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + if (!this.mainContainer) { + return GLib.SOURCE_REMOVE; } - } else { - global.stage.set_key_focus(null); + callback(); + return GLib.SOURCE_REMOVE; + }); + } + + _deferHide() { + this._defer(() => this.hide()); + } + + _setupClickCatcher() { + if (this._clickCatcher) { + return; } + + this._clickCatcher = new St.Widget({ + name: 'searchLightClickCatcher', + reactive: true, + can_focus: false, + }); + this._clickCatcher.connect('button-press-event', () => { + this._deferHide(); + return Clutter.EVENT_STOP; + }); + } + + _showClickCatcher() { + this._setupClickCatcher(); + this._queryDisplay(); + if (!this.monitor) { + return; + } + + this._clickCatcher.set_position(this.monitor.x, this.monitor.y); + this._clickCatcher.set_size(this.monitor.width, this.monitor.height); + + if (!this._clickCatcher.get_parent()) { + Main.layoutManager.addChrome(this._clickCatcher, { + affectsStruts: false, + trackFullscreen: false, + }); + Main.layoutManager.uiGroup.set_child_below_sibling( + this._clickCatcher, + this.mainContainer, + ); + } + + this._clickCatcher.show(); + } + + _hideClickCatcher() { + if (!this._clickCatcher?.get_parent()) { + return; + } + + this._clickCatcher.hide(); + Main.layoutManager.removeChrome(this._clickCatcher); + } + + _toggle_search_light() { + this._defer(() => { + if (this._inOverview) { + return; + } + if (!this._visible) { + this.show(); + if (this._entry) { + global.stage.set_key_focus(this._entry); + } + } else { + global.stage.set_key_focus(null); + } + }); } _add_events() { @@ -927,7 +1020,7 @@ export default class SearchLightExt extends Extension { this._onKeyFocusChanged.bind(this), 'key-press-event', this._onKeyPressed.bind(this), - this, + this._showSession, ); global.display.connectObject( @@ -935,15 +1028,13 @@ export default class SearchLightExt extends Extension { this._onFocusWindow.bind(this), 'in-fullscreen-changed', this._onFullScreen.bind(this), - this, + this._showSession, ); } _remove_events() { - global.display.disconnectObject(this); - global.stage.disconnectObject(this); - Main.overview.disconnectObject(this); - Shell.AppSystem.get_default().disconnectObject(this); + global.display.disconnectObject(this._showSession); + global.stage.disconnectObject(this._showSession); } _onOverviewShowing() { @@ -1009,7 +1100,7 @@ export default class SearchLightExt extends Extension { this._hidePopups(); } - this.hide(); + this._deferHide(); } // hide window immediately when activated @@ -1029,7 +1120,7 @@ export default class SearchLightExt extends Extension { let focus = global.stage.get_key_focus(); if (!focus || !this._entry.contains(focus)) { if (evt.get_key_symbol() === Clutter.KEY_Escape) { - this.hide(); + this._deferHide(); return Clutter.EVENT_STOP; } this._search._text.get_parent().grab_key_focus(); @@ -1039,6 +1130,6 @@ export default class SearchLightExt extends Extension { } _onFullScreen() { - this.hide(); + this._deferHide(); } } From b06b33c8a40a1b0c3f9f270efc18d68c7db75aaa Mon Sep 17 00:00:00 2001 From: Rahul Batra Date: Sat, 18 Jul 2026 13:16:42 +0530 Subject: [PATCH 2/2] Release input capture when SearchLight closes to prevent ghost dead zones. Several close paths only set opacity to zero, leaving reactive chrome and the click catcher intercepting pointer events after the overlay looked hidden. Route those paths through full hide cleanup and release input capture immediately when closing. Co-authored-by: Cursor --- extension.js | 85 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/extension.js b/extension.js index 71f784e..839891c 100644 --- a/extension.js +++ b/extension.js @@ -136,6 +136,7 @@ export default class SearchLightExt extends Extension { this._showSession = {}; this._hiding = false; + this._visible = false; this.mainContainer = new SearchLight(); this.mainContainer._delegate = this; @@ -189,7 +190,7 @@ export default class SearchLightExt extends Extension { 'window-created', (display, win) => { if (this._visible) { - this.mainContainer.opacity = 0; + this._deferHide(); } }, this, @@ -221,6 +222,7 @@ export default class SearchLightExt extends Extension { this._updateProviders(); this._updateWindowEffect(); this._updateBlurredBackground(); + this._ensureHidden(); } disable() { @@ -473,6 +475,7 @@ export default class SearchLightExt extends Extension { global.compositor.disable_unredirect(); + this._restoreInputCapture(); this.mainContainer.show(); this.container.show(); this._showClickCatcher(); @@ -513,9 +516,13 @@ export default class SearchLightExt extends Extension { this._hiding = true; try { + if (this._animSeq) { + this._hiTimer.cancel(this._animSeq); + this._animSeq = null; + } + this._release_ui(); - this._remove_events(); - this._hideClickCatcher(); + this._releaseInputCapture(); if (this._useAnimations) { this.mainContainer.ease({ @@ -527,16 +534,12 @@ export default class SearchLightExt extends Extension { duration: this._animationSpeed, mode: Clutter.AnimationMode.EASE_OUT, onComplete: () => { - this._visible = false; - this.mainContainer.hide(); - global.compositor.enable_unredirect(); + this._finishHide(); }, }); } else { this.mainContainer.opacity = 0; - this._visible = false; - this.mainContainer.hide(); - global.compositor.enable_unredirect(); + this._finishHide(); } } finally { this._hiding = false; @@ -544,6 +547,52 @@ export default class SearchLightExt extends Extension { // this._hidePopups(); } + _finishHide() { + this._visible = false; + if (this.mainContainer) { + this.mainContainer.hide(); + } + global.compositor.enable_unredirect(); + this._ensureHidden(); + } + + _releaseInputCapture() { + this._hideClickCatcher(); + this._remove_events(); + + if (this.container) { + this.container.reactive = false; + this.container.track_hover = false; + } + + if (this._background) { + this._background.reactive = false; + } + } + + _restoreInputCapture() { + if (this.container) { + this.container.reactive = true; + this.container.track_hover = true; + } + + if (this._background) { + this._background.reactive = true; + } + } + + _ensureHidden() { + this._visible = false; + this._hideClickCatcher(); + this._remove_events(); + this._releaseInputCapture(); + + if (this.mainContainer) { + this.mainContainer.opacity = 0; + this.mainContainer.hide(); + } + } + _isDraggingIcon() { // cancel all drag let result = false; @@ -716,7 +765,7 @@ export default class SearchLightExt extends Extension { Main.overview._hide = Main.overview.hide; } Main.overview.hide = () => { - this.mainContainer.opacity = 0; + this._deferHide(); Main.overview._hide(); }; @@ -739,8 +788,7 @@ export default class SearchLightExt extends Extension { this._searchResults.activateDefault; } this._searchResults.activateDefault = () => { - // hide window immediately when activated - this.mainContainer.opacity = 0; + this._deferHide(); this._searchResults._activateDefault(); }; @@ -974,6 +1022,7 @@ export default class SearchLightExt extends Extension { this._clickCatcher.set_position(this.monitor.x, this.monitor.y); this._clickCatcher.set_size(this.monitor.width, this.monitor.height); + this._clickCatcher.reactive = true; if (!this._clickCatcher.get_parent()) { Main.layoutManager.addChrome(this._clickCatcher, { @@ -990,12 +1039,16 @@ export default class SearchLightExt extends Extension { } _hideClickCatcher() { - if (!this._clickCatcher?.get_parent()) { + if (!this._clickCatcher) { return; } + this._clickCatcher.reactive = false; this._clickCatcher.hide(); - Main.layoutManager.removeChrome(this._clickCatcher); + + if (this._clickCatcher.get_parent()) { + Main.layoutManager.removeChrome(this._clickCatcher); + } } _toggle_search_light() { @@ -1048,7 +1101,7 @@ export default class SearchLightExt extends Extension { _onAppStateChanged(st) { this._lastAppState = st; if (this._visible) { - this.mainContainer.opacity = 0; + this._deferHide(); } } @@ -1108,7 +1161,7 @@ export default class SearchLightExt extends Extension { if (!focus._activate) { focus._activate = focus.activate; focus.activate = () => { - this.mainContainer.opacity = 0; + this._deferHide(); focus._activate(); }; }