diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index 61d9029..387dea7 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -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+ 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, diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index d75a1ae..55f4e90 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -1486,6 +1486,14 @@ begin WebBrowser.ExecuteJavaScript( 'if(window.Bridge&&typeof Bridge.onAutofillResult==="function")' + 'Bridge.onAutofillResult(' + BoolToStr(LFillOk, True).ToLower + ')'); + // Window hidden (tray) → the in-app failure toast is invisible; surface + // the block via a tray balloon instead (gated by the tray-notifications + // setting inside ShowBalloon). + if (not LFillOk) and + ((not Self.Visible) or IsIconic(WindowHandleToPlatform(Self.Handle).Wnd)) then + FBridge.ShowBalloon('Autofill blocked', + 'The target window runs as administrator — Windows silently blocks ' + + 'simulated keystrokes. Copy the password instead.', True); // Hide-after (Ctrl+Shift+Q from tray): SendInput is done, the target // already has focus — now we can safely tray ourselves without