fix(window): route every tray restore through one guard-clearing helper

Checking the hook's blast radius surfaced a real interaction: ExecuteAutofill
genuinely minimises the window (SW_MINIMIZE) before typing, which raises the
minimise guard. If the window is then HIDDEN to the tray rather than
un-minimised — the quick-search hide-after flow does exactly that —
MINIMIZEEND may never fire and the guard would stay raised for the rest of the
session, silently freezing the tracked window state.

All six FBridge.RestoreFromTray call sites now go through
RestoreWindowFromTray, which clears the guard first. RestoreFromTray replays
its own saved placement, so nothing is left for the guard to protect there.

MinimizeToTray itself is unaffected: it uses SW_HIDE, which raises no minimise
event at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-25 23:04:37 +01:00
parent 7ae20aac43
commit 2773cc444b
+21 -6
View File
@@ -169,6 +169,9 @@ type
public public
// Called from the WinEvent hook. WINEVENT_OUTOFCONTEXT delivers through // Called from the WinEvent hook. WINEVENT_OUTOFCONTEXT delivers through
// our own message queue, so these already run on the main thread. // our own message queue, so these already run on the main thread.
// Every tray restore goes through here so the minimise guard cannot get
// stuck raised (see the implementation).
procedure RestoreWindowFromTray;
procedure WinStateMinimizeStart; procedure WinStateMinimizeStart;
procedure WinStateMinimizeEnd; procedure WinStateMinimizeEnd;
end; end;
@@ -407,7 +410,7 @@ begin
// it vanishes from the taskbar it came from. Restored-from-iconic just // it vanishes from the taskbar it came from. Restored-from-iconic just
// stays open after the modal closes. // stays open after the modal closes.
LWasHidden := not Self.Visible; LWasHidden := not Self.Visible;
FBridge.RestoreFromTray; RestoreWindowFromTray;
LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower; LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower;
// Second arg forFill=true tells JS to SendInput on pick instead of copy. // Second arg forFill=true tells JS to SendInput on pick instead of copy.
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
@@ -431,7 +434,7 @@ begin
// can paste into the target app without an extra alt-tab. Iconic-only // can paste into the target app without an extra alt-tab. Iconic-only
// (taskbar) does NOT count — see BridgeQuickSearchHotkey. // (taskbar) does NOT count — see BridgeQuickSearchHotkey.
LWasHidden := not Self.Visible; LWasHidden := not Self.Visible;
FBridge.RestoreFromTray; RestoreWindowFromTray;
LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower; LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower;
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&typeof Bridge.openQuickSearch==="function")' + 'if(window.Bridge&&typeof Bridge.openQuickSearch==="function")' +
@@ -444,7 +447,7 @@ var
EscapedTitle: string; EscapedTitle: string;
begin begin
if not FServer.Active then Exit; if not FServer.Active then Exit;
FBridge.RestoreFromTray; RestoreWindowFromTray;
EscapedTitle := StringReplace(AWindowTitle, '\', '\\', [rfReplaceAll]); EscapedTitle := StringReplace(AWindowTitle, '\', '\\', [rfReplaceAll]);
EscapedTitle := StringReplace(EscapedTitle, '"', '\"', [rfReplaceAll]); EscapedTitle := StringReplace(EscapedTitle, '"', '\"', [rfReplaceAll]);
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
@@ -1027,7 +1030,7 @@ begin
// restores it later via ForceForegroundWindow. // restores it later via ForceForegroundWindow.
else if ACmd = 'app/focus' then else if ACmd = 'app/focus' then
begin begin
FBridge.RestoreFromTray; RestoreWindowFromTray;
LogLine('App brought to front (autofill picker)'); LogLine('App brought to front (autofill picker)');
end end
@@ -1519,7 +1522,7 @@ end;
procedure TMainForm.BridgeTrayRestore; procedure TMainForm.BridgeTrayRestore;
begin begin
FBridge.RestoreFromTray; RestoreWindowFromTray;
// Notify the JS layer: the UI may want to reset the auto-lock timer, // Notify the JS layer: the UI may want to reset the auto-lock timer,
// refresh state, or show a "welcome back" toast. // refresh state, or show a "welcome back" toast.
WebBrowser.ExecuteJavaScript( WebBrowser.ExecuteJavaScript(
@@ -1545,7 +1548,7 @@ begin
// Restore the form first so the tray icon goes away and FormDestroy // Restore the form first so the tray icon goes away and FormDestroy
// executes from a normal (non-hidden) state. RestoreFromTray also // executes from a normal (non-hidden) state. RestoreFromTray also
// deletes the tray icon. // deletes the tray icon.
FBridge.RestoreFromTray; RestoreWindowFromTray;
Application.Terminate; Application.Terminate;
end; end;
@@ -1572,6 +1575,18 @@ begin
else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd; else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd;
end; end;
// Single entry point for every tray restore. ExecuteAutofill really minimises
// the window (SW_MINIMIZE) before typing, which raises the guard; if the window
// is then HIDDEN to the tray instead of being un-minimised, MINIMIZEEND may
// never arrive and the guard would stay up forever, silently freezing the
// tracked window state. RestoreFromTray replays its own saved placement, so
// there is nothing left for the guard to protect by then.
procedure TMainForm.RestoreWindowFromTray;
begin
FIgnoreResizes := False;
FBridge.RestoreFromTray;
end;
// The window is going down: from here until the restore settles, every resize // The window is going down: from here until the restore settles, every resize
// FMX fires is part of that transition, never a user gesture. // FMX fires is part of that transition, never a user gesture.
procedure TMainForm.WinStateMinimizeStart; procedure TMainForm.WinStateMinimizeStart;