fix(autofill): force-release stuck modifiers; balloon when blocked from tray

Root cause of "Ctrl+Shift+P opened the new-entry modal": if the user still
holds Ctrl+Shift when WaitForModifierRelease times out (1s), every password
letter is typed as a Ctrl+Shift+<letter> chord — garbage in the field AND it
fires our own global hotkeys (a password containing 'a' triggers Ctrl+Shift+A
= new entry). ForceReleaseModifiers now injects KEYUP for any still-held
modifier before typing.

Also: when the fill is blocked (elevated target) while the window is hidden
in the tray, the in-app toast is invisible — show a tray balloon instead.
ShowFirstTimeBalloon generalized into ShowBalloon(title, text, warning),
gated by the existing "Show tray notifications" setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-12 05:08:50 +01:00
parent b869effee5
commit aab0b14be4
2 changed files with 61 additions and 12 deletions
+53 -12
View File
@@ -125,6 +125,10 @@ type
procedure PrepareNid;
procedure ShowTrayMenu;
procedure ShowFirstTimeBalloon;
// Tray balloon (gated by the "Show tray notifications" setting). Used
// for messages the user must see while the window is hidden — e.g. an
// autofill blocked by an elevated target.
procedure ShowBalloon(const ATitle, AText: string; AWarning: Boolean = False);
function FindFMXAppWindow: HWND;
public
constructor Create(AMainForm: TForm);
@@ -614,30 +618,36 @@ begin
end;
end;
procedure TPMBridge.ShowFirstTimeBalloon;
procedure TPMBridge.ShowBalloon(const ATitle, AText: string;
AWarning: Boolean = False);
var
LBalloon: TNotifyIconData;
const
BALLOON_TITLE = 'Password Manager';
BALLOON_TEXT = 'Still running in the tray — click the icon to restore, ' +
'right-click for menu.';
begin
// Gated by the "Show tray notifications" user setting.
if not FShowNotifications then Exit;
// Build a separate TNotifyIconData with NIF_INFO set, NIM_MODIFY on the
// same uID. szInfo/szInfoTitle carry the balloon content. NIIF_INFO
// gives the system info icon — no scary warning glyph.
// same uID. szInfo/szInfoTitle carry the balloon content.
FillChar(LBalloon, SizeOf(LBalloon), 0);
LBalloon.cbSize := SizeOf(LBalloon);
LBalloon.Wnd := FMsgWindow;
LBalloon.uID := 1;
LBalloon.uFlags := NIF_INFO;
Move(PChar(BALLOON_TITLE)^, LBalloon.szInfoTitle[0],
Min(Length(BALLOON_TITLE), High(LBalloon.szInfoTitle)) * SizeOf(Char));
Move(PChar(BALLOON_TEXT)^, LBalloon.szInfo[0],
Min(Length(BALLOON_TEXT), High(LBalloon.szInfo)) * SizeOf(Char));
LBalloon.dwInfoFlags := NIIF_INFO;
Move(PChar(ATitle)^, LBalloon.szInfoTitle[0],
Min(Length(ATitle), High(LBalloon.szInfoTitle)) * SizeOf(Char));
Move(PChar(AText)^, LBalloon.szInfo[0],
Min(Length(AText), High(LBalloon.szInfo)) * SizeOf(Char));
if AWarning then LBalloon.dwInfoFlags := NIIF_WARNING
else LBalloon.dwInfoFlags := NIIF_INFO;
Shell_NotifyIcon(NIM_MODIFY, @LBalloon);
end;
procedure TPMBridge.ShowFirstTimeBalloon;
begin
ShowBalloon('Password Manager',
'Still running in the tray — click the icon to restore, ' +
'right-click for menu.');
end;
procedure TPMBridge.RestoreFromTray;
var
LFormHwnd, LAppHwnd: HWND;
@@ -1010,6 +1020,36 @@ begin
end;
end;
// Inject KEYUP for any modifier still physically held after the wait timed
// out. Typing with Ctrl+Shift down turns every password letter into a
// Ctrl+Shift+<letter> chord — which not only types garbage but FIRES OUR OWN
// global hotkeys (a password containing 'a' triggered Ctrl+Shift+A = the
// new-entry modal mid-fill). The user's keys stay physically down but the OS
// modifier state clears until they release and press again.
procedure ForceReleaseModifiers;
const
MODS: array[0..4] of Word = (VK_CONTROL, VK_SHIFT, VK_MENU, VK_LWIN, VK_RWIN);
var
LInputs: array[0..4] of TInput;
I, N: Integer;
begin
N := 0;
for I := 0 to High(MODS) do
if (GetAsyncKeyState(MODS[I]) and $8000) <> 0 then
begin
FillChar(LInputs[N], SizeOf(TInput), 0);
LInputs[N].Itype := INPUT_KEYBOARD;
LInputs[N].ki.wVk := MODS[I];
LInputs[N].ki.dwFlags := KEYEVENTF_KEYUP;
Inc(N);
end;
if N > 0 then
begin
SendInput(N, @LInputs[0], SizeOf(TInput));
Sleep(30);
end;
end;
// Build (and immediately send) a key-down+up pair for each char in AText
// using KEYEVENTF_UNICODE. Returns nothing — best-effort.
procedure SendUnicodeString(const AText: string);
@@ -1231,6 +1271,7 @@ begin
ForceForegroundWindow(ATargetHWND);
WaitForModifierRelease(1000);
ForceReleaseModifiers; // timeout hit with keys still down → clean state
Sleep(FocusSettleDelayMs);
// Never type into our own window: if the target refused the foreground,