fix(hotkeys): never touch the window while a native modal owns it

Ctrl+Shift+Q and Ctrl+Shift+A call RestoreWindowFromTray before JS can refuse, so a JS guard is too late by construction. Restoring the owner of a Save As dialog stranded it behind everything and the app looked frozen while it was only disabled.

NativeModalActive tests IsWindowEnabled - Windows disables the owner for the lifetime of the modal - and brings the dialog back to front via GetLastActivePopup instead of merely refusing.
This commit is contained in:
r-zakarya
2026-08-01 18:40:45 +01:00
parent 42d2d6fda5
commit 1c0423680d
2 changed files with 31 additions and 0 deletions
+28
View File
@@ -178,6 +178,8 @@ type
// stuck raised (see the implementation).
// One-shot: hides the window the first time FMX shows it on a --tray
// launch, then unhooks itself.
// True when a native modal dialog owns us — see the implementation.
function NativeModalActive: Boolean;
procedure RestoreWindowFromTray;
procedure WinStateMinimizeStart;
procedure WinStateMinimizeEnd;
@@ -470,6 +472,7 @@ begin
// cmd://autofill/execute (fired after the user picks an entry) sends
// the password into it. Then pop the modal in fill mode.
if not FServer.Active then Exit;
if NativeModalActive then Exit;
FAutofillTargetHWND := ATargetHWND;
// Tray-hidden ONLY (Visible=false). A window merely minimised to the
// taskbar must NOT count: hide-after would send it to the TRAY on close —
@@ -494,6 +497,28 @@ begin
[IntToHex(ATargetHWND, 8), ATitle]));
end;
// A native modal dialog (Save As, folder picker) DISABLES its owner window for
// as long as it is up. Restoring / focusing that owner then strands the dialog
// behind everything: the app looks frozen, when it is merely disabled by a modal
// the user can no longer find. Every hotkey entry point checks this first — and
// rather than just refusing, it surfaces the dialog, which is what the user
// actually needs.
function TMainForm.NativeModalActive: Boolean;
var
LHwnd, LPopup: HWND;
begin
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
Result := (LHwnd <> 0) and (not IsWindowEnabled(LHwnd));
if not Result then Exit;
LPopup := GetLastActivePopup(LHwnd);
if (LPopup <> 0) and (LPopup <> LHwnd) and IsWindow(LPopup) then
begin
ShowWindow(LPopup, SW_SHOW);
SetForegroundWindow(LPopup);
LogLine('Hotkey ignored — a native dialog is open; brought it to front.');
end;
end;
procedure TMainForm.BridgeQuickSearchRequest;
var
LWasHidden: Boolean;
@@ -503,6 +528,7 @@ begin
// see the modal, then ask JS to open it. JS handles the locked-vault
// case (shows the auth screen with master-pw focused instead).
if not FServer.Active then Exit;
if NativeModalActive then Exit;
// Remember whether the window was TRAY-hidden BEFORE we restore — after
// the user picks an entry the JS layer asks us to minimize back so they
// can paste into the target app without an extra alt-tab. Iconic-only
@@ -522,6 +548,7 @@ var
EscapedTitle: string;
begin
if not FServer.Active then Exit;
if NativeModalActive then Exit;
RestoreWindowFromTray;
EscapedTitle := StringReplace(AWindowTitle, '\', '\\', [rfReplaceAll]);
EscapedTitle := StringReplace(EscapedTitle, '"', '\"', [rfReplaceAll]);
@@ -1804,6 +1831,7 @@ var
LTitle, LKind: string;
begin
if not FServer.Active then Exit;
if NativeModalActive then Exit;
FAutofillTargetHWND := ATargetHWND;
// Tray-hidden only, like the quick-search path: a window merely minimised to
// the taskbar must not come back as a tray icon it never was.