1309868f27
Le passage NOTOPMOST puis TOPMOST la faisait descendre sous les fenetres ordinaires un instant - un flash a chaque battement ou elle se croyait recouverte, donc souvent quand elle est posee sur la barre des taches. Et il ne gagnait rien contre la barre des taches active, mesure faite. TOPMOST seul suffit contre une fenetre TOPMOST ordinaire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2350 lines
93 KiB
ObjectPascal
2350 lines
93 KiB
ObjectPascal
unit PM.Bridge;
|
||
|
||
{
|
||
PM.Bridge — JS↔Delphi native capability bridge.
|
||
|
||
Three features exposed to the embedded WebView2 via cmd:// URLs:
|
||
|
||
1. TSecureClipboard
|
||
Sets text on the Windows clipboard alongside the
|
||
ExcludeClipboardContentFromMonitorProcessing format, which prevents
|
||
Win+V clipboard history from recording the password. Auto-clears
|
||
after a configurable delay via TTimer.
|
||
|
||
2. Tray icon (TPMBridge.MinimizeToTray / RestoreFromTray)
|
||
Shell_NotifyIcon-based. The main window hides; a tray icon appears.
|
||
Single-click or double-click on the tray icon restores the window.
|
||
OnTrayRestore is called on the main thread so the caller can Show/BringToFront.
|
||
|
||
3. Windows session-lock detection
|
||
WTSRegisterSessionNotification on a dedicated message-only window.
|
||
On WTS_SESSION_LOCK the bridge fires OnSystemLock (main thread) so
|
||
the Delphi host can inject lockVault() into the WebView2.
|
||
|
||
Both tray icon messages and WTS notifications are routed through a
|
||
single message-only window created with AllocateHWnd, avoiding any
|
||
subclassing of the FMX main window.
|
||
}
|
||
|
||
interface
|
||
|
||
uses
|
||
System.SysUtils, System.Classes, System.Math, System.Types,
|
||
System.Diagnostics,
|
||
FMX.Types, FMX.Forms,
|
||
Winapi.Windows, Winapi.ShellAPI, Winapi.Messages, Winapi.MultiMon,
|
||
PM.SingleInstance, PM.UserPrefs;
|
||
|
||
type
|
||
// -------------------------------------------------------------------------
|
||
// TSecureClipboard
|
||
// -------------------------------------------------------------------------
|
||
TSecureClipboard = class
|
||
private
|
||
FClearTimer: TTimer;
|
||
// Clipboard sequence number right after OUR SetText. If it moved, the
|
||
// user copied something else since — that content is theirs, not ours.
|
||
FSetSequence: DWORD;
|
||
procedure ClearTimerTick(Sender: TObject);
|
||
public
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
function ReadText: string;
|
||
// Copy AText to the clipboard, excluding it from Win+V history.
|
||
// AClearAfterMs = 0 disables auto-clear; default is 30 seconds.
|
||
procedure SetText(const AText: string; AClearAfterMs: Integer = 30000);
|
||
procedure Clear;
|
||
// Clear only if the clipboard still holds what WE put there (sequence
|
||
// unchanged). Auto-clear timer + minimize-to-tray use this so they never
|
||
// wipe something the user copied from another app in the meantime.
|
||
procedure ClearIfOurs;
|
||
end;
|
||
|
||
// Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the
|
||
// "password only" hotkey (Ctrl+Shift+P). The host decides what to type
|
||
// based on this kind.
|
||
// akTotp reuses the password-only typing path: JS sends the generated
|
||
// 6-digit code in the password slot with an empty username, so Delphi
|
||
// types just the code into the focused 2FA field.
|
||
TAutofillKind = (akFull, akPasswordOnly, akTotp);
|
||
|
||
// Fired when an autofill hotkey is pressed. Args are the foreground
|
||
// window HWND and its title (captured before any focus change), plus
|
||
// the kind of fill requested.
|
||
// Declared as a method pointer (not TProc<>) because Delphi has no implicit
|
||
// conversion from "procedure of object" to "reference to procedure" — the
|
||
// host wires this with a regular form method (BridgeAutofillRequest).
|
||
TAutofillRequestEvent = procedure(AKind: TAutofillKind;
|
||
ATargetHWND: HWND; const ATitle: string) of object;
|
||
|
||
TNewEntryHotkeyEvent = procedure(const AWindowTitle: string) of object;
|
||
|
||
// -------------------------------------------------------------------------
|
||
// TPMBridge
|
||
// -------------------------------------------------------------------------
|
||
TPMBridge = class
|
||
private
|
||
FMainForm: TForm;
|
||
FMsgWindow: HWND;
|
||
FTrayAdded: Boolean;
|
||
FIconOwned: Boolean; // true = we must call DestroyIcon on FIconHandle
|
||
FIconHandle: HICON;
|
||
FNid: TNotifyIconData;
|
||
FPowerNotify: THandle; // registration handle from PowerRegisterSuspendResumeNotification
|
||
FSecureClipboard: TSecureClipboard;
|
||
FBalloonShown: Boolean;
|
||
// User setting: gates Shell_NotifyIcon NIF_INFO balloons (currently
|
||
// only the "running in the tray" first-time popup, future tray
|
||
// notifications would honour the same flag).
|
||
FShowNotifications: Boolean;
|
||
// Window placement captured at MinimizeToTray time. Replayed on
|
||
// RestoreFromTray so the window comes back in the same state
|
||
// (maximised / normal + position + size) as before hiding.
|
||
FSavedPlacement: TWindowPlacement;
|
||
FHasSavedPlacement: Boolean;
|
||
// Window that owned the foreground just before we came out of the tray, so
|
||
// hiding again can give it back. Windows does that for a minimise, but a
|
||
// hide leaves the previously active application dark.
|
||
FPrevForeground: HWND;
|
||
// WinEvent hook that watches every foreground change in the session.
|
||
FForegroundHook: THandle;
|
||
FOnSystemLock: TProc;
|
||
// Set TRUE the moment Windows tells us the session is ending
|
||
// (WM_QUERYENDSESSION / WM_ENDSESSION). FormCloseQuery checks this
|
||
// to bypass the "minimize to tray" intercept so the form closes
|
||
// normally and the DB connection is checkpointed instead of being
|
||
// force-killed (which leaves -shm / -wal files behind).
|
||
FShutdownPending: Boolean;
|
||
FOnTrayRestore: TProc;
|
||
FOnFillBarSettings: TProc;
|
||
FOnFillBarChanged: TProc;
|
||
FOnLockRequest: TProc;
|
||
FOnQuit: TProc;
|
||
FOnQuickSearchRequest: TProc;
|
||
// Autofill: global hotkeys → inject credentials into browser. Combos
|
||
// are user-configurable from Settings; defaults are Ctrl+Shift+L /
|
||
// Ctrl+Shift+P. We track which IDs are actually live so unregister
|
||
// doesn't blindly call UnregisterHotKey on unregistered IDs (which
|
||
// would set GetLastError noise during shutdown).
|
||
FAutofillRegistered: Boolean;
|
||
FAutofillFullActive: Boolean;
|
||
FAutofillPwdActive: Boolean;
|
||
FAutofillTotpActive: Boolean;
|
||
FOnAutofillRequest: TAutofillRequestEvent;
|
||
FDebugHotkeyRegistered: Boolean;
|
||
FOnDebugHotkey: TProc;
|
||
FNewEntryHotkeyRegistered: Boolean;
|
||
// Current new-entry combo (user-configurable, default Ctrl+Shift+A). Kept
|
||
// so ExecuteAutofill can re-register it after unregistering during a fill.
|
||
FNewEntryHotkeyMods: Word;
|
||
FNewEntryHotkeyVk: Word;
|
||
FOnNewEntryHotkey: TNewEntryHotkeyEvent;
|
||
// Ctrl+Shift+Q — "Quick search + autofill": open the JS quick-search
|
||
// modal, the user picks an entry, the password is SendInput'd into the
|
||
// window that had focus at hotkey time (captured into FAutofillTargetHWND
|
||
// by the host).
|
||
FQuickSearchHotkeyRegistered: Boolean;
|
||
FOnQuickSearchHotkey: TAutofillRequestEvent;
|
||
// "Lazy mode": a two-button strip floating above every other window, doing
|
||
// with the mouse what the autofill hotkeys do with the keyboard. 0 when off.
|
||
//
|
||
// WS_EX_NOACTIVATE IS THE WHOLE TRICK. A normal window would take the
|
||
// foreground on click, and the field the user just clicked in would lose
|
||
// its caret before we could type into it - GetForegroundWindow would then
|
||
// name US as the target. NOACTIVATE means the click never moves the
|
||
// foreground: the browser stays active, its field stays focused, and the
|
||
// capture below sees exactly what the hotkey path sees.
|
||
FLazyWnd: HWND;
|
||
// Dernier theme annonce par la page. La barrette est peinte a la main, donc
|
||
// elle a besoin de savoir - un bouton Win32 standard reste gris clair meme
|
||
// sous SetPreferredAppMode(ForceDark), qui ne themise que ce que le systeme
|
||
// dessine lui-meme (menus, ascenseurs, infobulles).
|
||
FDarkTheme: Boolean;
|
||
// Opacite AU REPOS, en pourcentage - la barrette redevient pleine des que
|
||
// la souris entre. 100 = jamais transparente. Vit en pref DPAPI et non dans
|
||
// settings_json : elle peut etre allumee coffre ferme, quand aucune session
|
||
// n'existe pour lire les reglages du serveur.
|
||
FLazyAlphaPct: Integer;
|
||
// Voir les proprietes LastFillWaits et LastRequestFromBar.
|
||
FLastFillWaits: string;
|
||
FLastRequestFromBar: Boolean;
|
||
// Dernier etat connu du survol, pour ne reposer l'opacite que sur un
|
||
// changement - SetLayeredWindowAttributes force un repaint a chaque appel.
|
||
FLazyHover: Boolean;
|
||
// Barrette en icones (police Segoe MDL2 Assets) plutot qu'en mots, et sa
|
||
// largeur reelle - elle depend des boutons choisis dans les reglages.
|
||
FLazyIcons: Boolean;
|
||
// Fond de la barrette retire (cle de couleur) : seuls les boutons restent.
|
||
FLazyClear: Boolean;
|
||
FLazyW: Integer;
|
||
procedure LazyWindowHandler(var AMsg: TMessage);
|
||
// Corps extraits des raccourcis Ctrl+Shift+A et Ctrl+Shift+Q : la barrette
|
||
// les declenche aussi, meme capture de la fenetre cible.
|
||
procedure FireNewEntryRequest;
|
||
procedure FireQuickSearchRequest;
|
||
/// Pose l'opacite qui convient a l'etat de survol donne.
|
||
procedure ApplyLazyAlpha(AHover: Boolean);
|
||
/// Menu du clic droit sur la barrette : le choix de l'opacite au repos.
|
||
procedure ShowLazyMenu;
|
||
/// Peint un bouton de la barrette (BS_OWNERDRAW). Le systeme garde le suivi
|
||
/// du clic et l'etat enfonce ; nous ne reprenons que les couleurs.
|
||
procedure DrawLazyButton(ADraw: PDrawItemStruct);
|
||
// Capture the foreground window and hand it to the host. Shared by the
|
||
// autofill hotkeys and the lazy strip - same target, same title matching,
|
||
// same picker. Only the gesture differs.
|
||
procedure FireAutofillRequest(AKind: TAutofillKind;
|
||
AFromBar: Boolean = False);
|
||
procedure MsgWindowHandler(var AMsg: TMessage);
|
||
procedure PrepareNid;
|
||
procedure ShowTrayMenu;
|
||
procedure ShowFirstTimeBalloon;
|
||
public
|
||
// The FMX taskbar proxy (TFMAppClass) — a SEPARATE top-level window from
|
||
// the form, and the one that owns the taskbar entry. Public so the host
|
||
// can hide it during a --tray launch, before MinimizeToTray gets to run.
|
||
function FindFMXAppWindow: HWND;
|
||
// Tray balloon (caller applies its own gating 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);
|
||
constructor Create(AMainForm: TForm);
|
||
destructor Destroy; override;
|
||
// Hide main window and show tray icon. AClearClipboard defaults to
|
||
// True (a manual minimise wipes any copied password immediately);
|
||
// the quick-search "copy then hide so I can paste" flow passes False
|
||
// so it doesn't nuke the password it just placed on the clipboard.
|
||
// AReturnForeground: give the foreground back to the application that had
|
||
// it before we came out of the tray. False for callers that already decided
|
||
// who should be focused (post-autofill hands it to the filled window).
|
||
procedure MinimizeToTray(AClearClipboard: Boolean = True;
|
||
AReturnForeground: Boolean = True; ANotify: Boolean = True);
|
||
// Record the application currently in the foreground as the one to restore
|
||
// focus to when we go back to the tray. Ignores our own and shell windows.
|
||
procedure CaptureForegroundOwner;
|
||
// Restore main window and remove tray icon.
|
||
procedure RestoreFromTray;
|
||
// Apply Windows dark-mode title bar to the main form. Win10 19044+
|
||
// / Win11 only — no-op on older builds. Safe to call repeatedly.
|
||
procedure ApplyTitleBarTheme(ADark: Boolean);
|
||
|
||
// Register the two autofill global hotkeys (full + password-only) with
|
||
// the given Win32 modifier flags (MOD_CONTROL/MOD_SHIFT/MOD_ALT/MOD_WIN
|
||
// bitmask) and virtual-key codes. Replaces any prior registration —
|
||
// safe to call repeatedly to swap combos at runtime.
|
||
// Returns True if both hotkeys registered successfully. If one or both
|
||
// failed (clash with another app), best-effort: whichever succeeded
|
||
// stays active.
|
||
// ATotpVk = 0 means "no 2FA hotkey configured" — it is then skipped and
|
||
// does NOT count against the returned all-registered flag.
|
||
function SetAutofillHotkeys(AFullMods, AFullVk,
|
||
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
|
||
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
|
||
// user-chosen combo. True on success.
|
||
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
||
// Re-register the new-entry hotkey with a user-chosen combo (default
|
||
// Ctrl+Shift+A). Stored so ExecuteAutofill can restore it after the fill.
|
||
function SetNewEntryHotkey(AMods, AVk: Word): Boolean;
|
||
// Convenience wrapper: register the historical defaults (Ctrl+Shift+L
|
||
// and Ctrl+Shift+P). Used by the host on first start; runtime changes
|
||
// go through SetAutofillHotkeys.
|
||
procedure RegisterAutofillHotkey;
|
||
// Unregister both autofill hotkeys.
|
||
procedure UnregisterAutofillHotkey;
|
||
// Simulate username + Tab + password keystrokes into ATargetHWND.
|
||
// ATargetHWND = 0 → type into whatever window has focus.
|
||
// If AUsername is empty, only the password is typed (no Tab) — matches
|
||
// the password-only hotkey path AND avoids spurious Tab on entries
|
||
// without a stored username.
|
||
// AUsernameOnly = True → type ONLY the username (no Tab, no password);
|
||
// used by the quick-search "autofill username" action.
|
||
// ARestoreAfter = True → the window was open before the hotkey: keep it
|
||
// visible (no minimize at all — the foreground process is allowed to hand
|
||
// focus to the target). False (tray-origin) → minimize out of the way,
|
||
// the caller trays it after the fill.
|
||
// Returns False when nothing was typed: elevated target (UIPI would
|
||
// silently drop the keystrokes) or focus never left our own window.
|
||
// AClearFirst: send Ctrl+A + Del before each field (user setting).
|
||
function ExecuteAutofill(ATargetHWND: HWND;
|
||
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
|
||
ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean;
|
||
/// Le detail des deux attentes du dernier remplissage, pour le journal de
|
||
/// l'hote. Une chaine plutot qu'un evenement : ExecuteAutofill s'execute
|
||
/// sur le thread principal, juste avant que l'hote journalise.
|
||
property LastFillWaits: string read FLastFillWaits;
|
||
/// Le dernier remplissage venait-il de la barrette plutot que d'un
|
||
/// raccourci ? Les deux chemins se rejoignent des FireAutofillRequest.
|
||
property LastRequestFromBar: Boolean read FLastRequestFromBar;
|
||
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
||
property TrayAdded: Boolean read FTrayAdded;
|
||
// Show or hide the lazy strip. Idempotent, and remembered across launches.
|
||
procedure SetLazyBar(AOn: Boolean);
|
||
// Re-allume la barrette si elle l'etait au dernier arret. A appeler une
|
||
// fois l'hote pret : le bouton Open a besoin de OnTrayRestore, et la
|
||
// peinture a besoin du theme.
|
||
procedure RestoreLazyBar;
|
||
function LazyBarVisible: Boolean;
|
||
/// Reconstruit la barrette apres un changement de ses reglages (boutons,
|
||
/// icones). POSTE : l'appel peut venir de la barrette elle-meme, qu'on ne
|
||
/// detruit pas depuis son propre gestionnaire.
|
||
procedure ReloadLazyBar;
|
||
property AutofillRegistered: Boolean read FAutofillRegistered;
|
||
// Per-slot outcome of the last SetAutofillHotkeys — lets the host name
|
||
// exactly which combo lost the race with another app's global hotkey
|
||
// instead of a vague "one of them failed".
|
||
property AutofillFullActive: Boolean read FAutofillFullActive;
|
||
property AutofillPwdActive: Boolean read FAutofillPwdActive;
|
||
property AutofillTotpActive: Boolean read FAutofillTotpActive;
|
||
// Fired on main thread when Windows locks the session (WTS_SESSION_LOCK).
|
||
property OnSystemLock: TProc read FOnSystemLock write FOnSystemLock;
|
||
// True once WM_QUERYENDSESSION (or WM_ENDSESSION) has been received.
|
||
// FormCloseQuery uses this to allow normal close during shutdown.
|
||
property ShutdownPending: Boolean read FShutdownPending;
|
||
// Fired on main thread when the user clicks the tray icon.
|
||
property OnTrayRestore: TProc read FOnTrayRestore write FOnTrayRestore;
|
||
// Clic droit sur la barrette -> "Fill bar settings..." : la fenetre revient
|
||
// (OnTrayRestore) puis l'hote ouvre l'onglet Fill bar des reglages.
|
||
property OnFillBarSettings: TProc read FOnFillBarSettings write FOnFillBarSettings;
|
||
// La barrette a change hors des reglages (clic droit, menu du tray) : l'hote
|
||
// rafraichit l'onglet Fill bar s'il est affiche.
|
||
property OnFillBarChanged: TProc read FOnFillBarChanged write FOnFillBarChanged;
|
||
// Fired when the user picks "Lock vault" from the tray menu. Handler
|
||
// should trigger the JS lockVault() (typically via ExecuteJavaScript).
|
||
property OnLockRequest: TProc read FOnLockRequest write FOnLockRequest;
|
||
// Fired when the user picks "Quit" from the tray menu. Handler must
|
||
// actually terminate the app (Application.Terminate or similar) — the
|
||
// bridge does not call it itself, so the host stays in control of
|
||
// shutdown order (server stop, save state, etc.).
|
||
property OnQuit: TProc read FOnQuit write FOnQuit;
|
||
// Fired when the user picks "Quick search…" from the tray menu.
|
||
// Handler typically restores the window and pops a JS-side modal
|
||
// (Bridge.openQuickSearch) so the user can type to find an entry
|
||
// and copy its password without restoring the whole vault UI.
|
||
property OnQuickSearchRequest: TProc
|
||
read FOnQuickSearchRequest write FOnQuickSearchRequest;
|
||
// True (default) = show tray balloon notifications. Set False to keep
|
||
// the tray icon mute. Configured from the JS settings panel.
|
||
property ShowNotifications: Boolean
|
||
read FShowNotifications write FShowNotifications;
|
||
// Already-shown flag for the one-time "still running in the tray"
|
||
// balloon. Exposed so the host can pre-mark it true on autostart
|
||
// launches (the user didn't actively minimise — no need to inform them).
|
||
property BalloonShown: Boolean
|
||
read FBalloonShown write FBalloonShown;
|
||
// Fired on main thread when the autofill hotkey fires.
|
||
// Args: (ATargetHWND, AWindowTitle). Handler calls ExecuteJavaScript
|
||
// to let JS match the title against vault entries.
|
||
property OnAutofillRequest: TAutofillRequestEvent
|
||
read FOnAutofillRequest write FOnAutofillRequest;
|
||
property OnDebugHotkey: TProc
|
||
read FOnDebugHotkey write FOnDebugHotkey;
|
||
// Fires on Ctrl+Shift+A. Arg = foreground window title (stripped of
|
||
// browser suffix by the JS layer before pre-fill).
|
||
property OnNewEntryHotkey: TNewEntryHotkeyEvent
|
||
read FOnNewEntryHotkey write FOnNewEntryHotkey;
|
||
// Fires on Ctrl+Shift+Q — quick-search-and-fill. Args mirror the
|
||
// regular autofill hotkey so the host can save the target HWND and
|
||
// pop the JS modal. Kind is always akPasswordOnly (no Tab).
|
||
property OnQuickSearchHotkey: TAutofillRequestEvent
|
||
read FOnQuickSearchHotkey write FOnQuickSearchHotkey;
|
||
end;
|
||
|
||
implementation
|
||
|
||
uses
|
||
FMX.Platform.Win, Winapi.CommCtrl;
|
||
|
||
// Foreground window at process start — captured in this unit's initialization,
|
||
// which runs before any of our windows exist. Every later opportunity is
|
||
// circular: once we own the foreground (and a tray-hidden window keeps owning
|
||
// it), asking who is in front only ever answers "us", so we would never learn
|
||
// which application to give focus back to.
|
||
var
|
||
GStartupForeground: HWND = 0;
|
||
// Last window, in any other process, that genuinely held the foreground.
|
||
// Maintained by the hook below rather than sampled when we need it: by then
|
||
// the user's application is two foreground changes behind (their app → the
|
||
// taskbar they clicked → us), so it can no longer be observed.
|
||
GLastForeground: HWND = 0;
|
||
|
||
// ---- WinEvent hook plumbing (foreground tracking) ------------------------
|
||
// Not in Winapi.Windows; UMainForm declares its own copy for the minimise hook.
|
||
const
|
||
EVENT_SYSTEM_FOREGROUND = $0003;
|
||
WINEVENT_OUTOFCONTEXT = $0000;
|
||
WINEVENT_SKIPOWNPROCESS = $0002;
|
||
OBJID_WINDOW = 0;
|
||
|
||
function SetWinEventHook(eventMin, eventMax: DWORD; hmodWinEventProc: HMODULE;
|
||
lpfnWinEventProc: Pointer; idProcess, idThread, dwFlags: DWORD): THandle;
|
||
stdcall; external 'user32.dll' name 'SetWinEventHook';
|
||
function UnhookWinEvent(hWinEventHook: THandle): BOOL;
|
||
stdcall; external 'user32.dll' name 'UnhookWinEvent';
|
||
|
||
// Defined further down but needed by the constructor / MinimizeToTray.
|
||
function IsOwnProcessWindow(AHwnd: HWND): Boolean; forward;
|
||
function IsShellWindow(AHwnd: HWND): Boolean; forward;
|
||
function ForceForegroundWindow(ATargetHwnd: HWND): Boolean; forward;
|
||
procedure ForegroundEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
||
|
||
// Win32 format name that suppresses Win+V clipboard history recording.
|
||
// Introduced in Windows 10 1809 (build 17763). Silently ignored on older builds.
|
||
const
|
||
CLIPBOARD_EXCLUDE_FORMAT = 'ExcludeClipboardContentFromMonitorProcessing';
|
||
|
||
// Tray callback message routed to our message-only window.
|
||
const
|
||
WM_TRAY_ICON = WM_APP + 1;
|
||
// Fermeture de la barrette, POSTEE a la fenetre message-only plutot que
|
||
// traitee sur place : detruire une fenetre depuis son propre gestionnaire
|
||
// libererait, en plein envoi, l'instance qui sert de window proc.
|
||
WM_LAZY_CLOSE = WM_APP + 2;
|
||
WM_LAZY_RELOAD = WM_APP + 3;
|
||
|
||
// WTS session change message and state constants (declared here to avoid
|
||
// a hard dependency on Winapi.WtsApi32 which varies across Delphi versions).
|
||
const
|
||
WM_WTSSESSION_CHANGE = $02B1;
|
||
WTS_SESSION_LOCK = 7;
|
||
NOTIFY_FOR_THIS_SESSION = 0;
|
||
|
||
// Power management broadcast — sent to all top-level windows when the
|
||
// system is about to sleep / hibernate or has just resumed. No explicit
|
||
// registration needed (unlike WTS).
|
||
// PBT_APMSUSPEND ($04) : "system is suspending operation" — fires once,
|
||
// right before sleep/hibernate. This is our lock trigger.
|
||
// PBT_APMRESUMEAUTOMATIC ($12) : system resumed (we don't need to act).
|
||
// PBT_APMRESUMESUSPEND ($07) : system resumed with user interaction.
|
||
const
|
||
WM_POWERBROADCAST = $0218;
|
||
PBT_APMSUSPEND = $0004;
|
||
PBT_APMRESUMEAUTOMATIC = $0012;
|
||
PBT_APMRESUMESUSPEND = $0007;
|
||
|
||
// Autofill hotkeys — Ctrl+Shift+L (full) and Ctrl+Shift+P (password only).
|
||
// IDs must not clash with other RegisterHotKey calls in this process;
|
||
// 42-43 are arbitrary and well outside the range used by FMX internals.
|
||
const
|
||
AUTOFILL_HOTKEY_ID_FULL = 42; // Ctrl+Shift+L → user + Tab + password
|
||
AUTOFILL_HOTKEY_ID_PWDONLY = 43; // Ctrl+Shift+P → password only
|
||
DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel
|
||
NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title
|
||
QUICK_SEARCH_HOTKEY_ID = 46; // Ctrl+Shift+Q → quick-search-and-fill
|
||
AUTOFILL_HOTKEY_ID_TOTP = 47; // Ctrl+Shift+T → type the 2FA code
|
||
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
|
||
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
|
||
|
||
// Lazy strip - child button IDs and geometry. Deliberately small: it sits on
|
||
// top of whatever the user is reading, so it has to cost as little screen as
|
||
// two words allow.
|
||
const
|
||
LAZY_ID_FULL = 101;
|
||
LAZY_ID_PWD = 102;
|
||
LAZY_ID_TOTP = 103;
|
||
LAZY_ID_CLOSE = 104;
|
||
LAZY_ID_OPEN = 105;
|
||
LAZY_ID_ADD = 106; // nouvelle entree depuis la fenetre en avant (Ctrl+Shift+A)
|
||
LAZY_ID_SEARCH = 107; // recherche rapide (Ctrl+Shift+Q)
|
||
LAZY_ID_SETTINGS = 108; // reglages, onglet Fill bar
|
||
LAZY_MENU_ICONS = 1; // hors des valeurs d'opacite (25..100) du meme menu
|
||
LAZY_MENU_SETTINGS = 2;
|
||
LAZY_MENU_CLEAR = 3;
|
||
// LE SONDAGE PLUTOT QUE TrackMouseEvent. La barrette a quatre fenetres
|
||
// enfants : passer d'elle a un bouton envoie deja un WM_MOUSELEAVE au parent,
|
||
// et il faudrait donc suivre la souris sur les cinq fenetres pour recoller
|
||
// les morceaux. Comparer le curseur au rectangle cinq fois par seconde ne
|
||
// connait pas ce piege, et ne coute rien de mesurable.
|
||
LAZY_TIMER_ID = 1;
|
||
LAZY_POLL_MS = 200;
|
||
// Cle de la pref, et opacite par defaut. 60 % laisse lire ce qu'il y a
|
||
// derriere sans faire disparaitre la barrette du regard.
|
||
LAZY_ALPHA_KEY = 'lazyAlphaPct';
|
||
LAZY_ALPHA_DEFAULT = 60;
|
||
// Coin haut-gauche retenu, 'x,y'. Meme raison que l'opacite d'etre une pref
|
||
// DPAPI : la barrette s'allume parfois coffre ferme.
|
||
LAZY_POS_KEY = 'lazyPos';
|
||
// Barrette allumee ou non au dernier arret. Pref DPAPI comme les deux autres :
|
||
// elle doit revenir AVANT toute ouverture de coffre, et localStorage est vide
|
||
// a chaque lancement (le port ephemere change d'origine).
|
||
LAZY_ON_KEY = 'lazyOn';
|
||
// Boutons affiches, dans l'ordre, et le choix icones/texte. Le X est toujours
|
||
// la : c'est la seule facon de fermer la barrette la ou elle est.
|
||
LAZY_BTNS_KEY = 'lazyButtons';
|
||
LAZY_BTNS_DEFAULT = 'full,pwd,totp,add,search,open,settings';
|
||
LAZY_ICONS_KEY = 'lazyIcons';
|
||
// FOND TRANSPARENT : peint dans une couleur qu'aucun theme n'emploie, puis
|
||
// declaree cle (LWA_COLORKEY) - ces pixels ne sont plus dessines du tout, et
|
||
// les clics y TRAVERSENT. On traine donc la barrette par ses boutons
|
||
// (DragDetect), le fond n'etant plus la pour etre saisi.
|
||
LAZY_CLEAR_KEY = 'lazyClear';
|
||
LAZY_KEY_COLOR = $00FF00FF;
|
||
LAZY_H = 40;
|
||
|
||
// Palette de la barrette, en COLORREF ($00BBGGRR - les octets sont INVERSES
|
||
// par rapport a l'hexadecimal CSS). Reprise des variables de css/style.css pour
|
||
// que la barrette et la fenetre principale ne soient pas deux gris differents.
|
||
const
|
||
LAZY_DARK_BG = $1C1515; // --bg #15151c
|
||
LAZY_DARK_FACE = $322929; // --bg-elev-2 #292932
|
||
LAZY_DARK_DOWN = $3F3434; // --bg-elev-3 #34343f
|
||
LAZY_DARK_EDGE = $413535; // --border #353541
|
||
LAZY_DARK_INK = $F5F1F1; // --text #f1f1f5
|
||
LAZY_LITE_BG = $F5F7F7; // --bg #f7f7f5
|
||
LAZY_LITE_FACE = $FFFFFF; // --bg-elev #ffffff
|
||
LAZY_LITE_DOWN = $DDE3E3; // --bg-elev-3 #e3e3dd
|
||
LAZY_LITE_EDGE = $D7DCDC; // --border #dcdcd7
|
||
LAZY_LITE_INK = $1F1A1A; // --text #1a1a1f
|
||
// Survol : l'accent de l'app, comme les boutons de la fenetre principale.
|
||
LAZY_DARK_ACCENT = $D4B606; // --accent #06b6d4
|
||
LAZY_LITE_ACCENT = $B29108; // --accent #0891b2
|
||
|
||
// Dynamic WTS function pointers — wtsapi32.dll is not guaranteed on all
|
||
// Windows SKUs (e.g. minimal Server Core without Session Services), so
|
||
// we load it at runtime and tolerate absence gracefully.
|
||
var
|
||
_WTSRegister : function(hWnd: HWND; dwFlags: DWORD): BOOL; stdcall = nil;
|
||
_WTSUnregister: function(hWnd: HWND): BOOL; stdcall = nil;
|
||
_WtsApiLoaded : Boolean = False;
|
||
_WtsLib : HMODULE = 0;
|
||
|
||
procedure LoadWtsApi;
|
||
begin
|
||
if _WtsApiLoaded then Exit;
|
||
_WtsApiLoaded := True;
|
||
_WtsLib := LoadLibrary('wtsapi32.dll');
|
||
if _WtsLib = 0 then Exit;
|
||
_WTSRegister := GetProcAddress(_WtsLib, 'WTSRegisterSessionNotification');
|
||
_WTSUnregister := GetProcAddress(_WtsLib, 'WTSUnRegisterSessionNotification');
|
||
end;
|
||
|
||
// Power notification registration (Windows 8+). Forces delivery of
|
||
// WM_POWERBROADCAST to a specific HWND, including non-top-level / hidden
|
||
// utility windows that Windows might otherwise skip. Exported from user32.
|
||
const
|
||
DEVICE_NOTIFY_WINDOW_HANDLE = 0;
|
||
|
||
var
|
||
_PowerRegister : function(Flags: DWORD; Recipient: THandle;
|
||
out RegistrationHandle: THandle): DWORD; stdcall = nil;
|
||
_PowerUnregister: function(RegistrationHandle: THandle): DWORD; stdcall = nil;
|
||
_PowerApiLoaded : Boolean = False;
|
||
|
||
procedure LoadPowerApi;
|
||
var
|
||
LLib: HMODULE;
|
||
begin
|
||
if _PowerApiLoaded then Exit;
|
||
_PowerApiLoaded := True;
|
||
// Functions live in user32.dll despite the "Power" prefix.
|
||
LLib := GetModuleHandle('user32.dll');
|
||
if LLib = 0 then Exit;
|
||
_PowerRegister := GetProcAddress(LLib, 'PowerRegisterSuspendResumeNotification');
|
||
_PowerUnregister := GetProcAddress(LLib, 'PowerUnregisterSuspendResumeNotification');
|
||
end;
|
||
|
||
// =============================================================================
|
||
// TSecureClipboard
|
||
// =============================================================================
|
||
|
||
constructor TSecureClipboard.Create;
|
||
begin
|
||
inherited;
|
||
FClearTimer := TTimer.Create(nil);
|
||
FClearTimer.Enabled := False;
|
||
FClearTimer.OnTimer := ClearTimerTick;
|
||
end;
|
||
|
||
destructor TSecureClipboard.Destroy;
|
||
begin
|
||
FClearTimer.Free;
|
||
inherited;
|
||
end;
|
||
|
||
procedure TSecureClipboard.ClearTimerTick(Sender: TObject);
|
||
begin
|
||
FClearTimer.Enabled := False;
|
||
ClearIfOurs;
|
||
end;
|
||
|
||
procedure TSecureClipboard.SetText(const AText: string; AClearAfterMs: Integer);
|
||
var
|
||
CFExclude: UINT;
|
||
LMem: THandle; // HGLOBAL — renamed to avoid Pascal's case-insensitive
|
||
// collision with the HGLOBAL type identifier.
|
||
LDest: Pointer;
|
||
LByteCount: NativeUInt;
|
||
begin
|
||
FClearTimer.Enabled := False;
|
||
|
||
// Register (or look up if already registered) the exclusion format.
|
||
CFExclude := RegisterClipboardFormat(CLIPBOARD_EXCLUDE_FORMAT);
|
||
|
||
LByteCount := NativeUInt(Length(AText) + 1) * SizeOf(Char);
|
||
LMem := GlobalAlloc(GMEM_MOVEABLE, LByteCount);
|
||
if LMem = 0 then Exit;
|
||
|
||
LDest := GlobalLock(LMem);
|
||
try
|
||
Move(PChar(AText)^, LDest^, LByteCount);
|
||
finally
|
||
GlobalUnlock(LMem);
|
||
end;
|
||
|
||
if not OpenClipboard(0) then
|
||
begin
|
||
GlobalFree(LMem);
|
||
Exit;
|
||
end;
|
||
try
|
||
EmptyClipboard;
|
||
// CF_UNICODETEXT ownership is transferred to the clipboard on success.
|
||
if SetClipboardData(CF_UNICODETEXT, LMem) = 0 then
|
||
GlobalFree(LMem);
|
||
// Exclusion marker: presence of this format is the signal to Windows;
|
||
// the data handle is nil and ignored by the subsystem.
|
||
SetClipboardData(CFExclude, 0);
|
||
finally
|
||
CloseClipboard;
|
||
end;
|
||
|
||
// Snapshot the sequence AFTER our write — any later copy (ours or another
|
||
// app's) bumps it, which is exactly the "is it still ours?" signal.
|
||
FSetSequence := GetClipboardSequenceNumber;
|
||
|
||
if AClearAfterMs > 0 then
|
||
begin
|
||
FClearTimer.Interval := AClearAfterMs;
|
||
FClearTimer.Enabled := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TSecureClipboard.Clear;
|
||
begin
|
||
if OpenClipboard(0) then
|
||
try
|
||
EmptyClipboard;
|
||
finally
|
||
CloseClipboard;
|
||
end;
|
||
end;
|
||
|
||
procedure TSecureClipboard.ClearIfOurs;
|
||
begin
|
||
if GetClipboardSequenceNumber = FSetSequence then
|
||
Clear;
|
||
end;
|
||
|
||
function TSecureClipboard.ReadText: string;
|
||
var
|
||
H: THandle;
|
||
P: PChar;
|
||
begin
|
||
Result := '';
|
||
if not OpenClipboard(0) then Exit;
|
||
try
|
||
H := GetClipboardData(CF_UNICODETEXT);
|
||
if H = 0 then Exit;
|
||
P := PChar(GlobalLock(H));
|
||
if P <> nil then
|
||
try
|
||
Result := P;
|
||
finally
|
||
GlobalUnlock(H);
|
||
end;
|
||
finally
|
||
CloseClipboard;
|
||
end;
|
||
end;
|
||
|
||
// =============================================================================
|
||
// TPMBridge
|
||
// =============================================================================
|
||
|
||
constructor TPMBridge.Create(AMainForm: TForm);
|
||
begin
|
||
inherited Create;
|
||
FMainForm := AMainForm;
|
||
FSecureClipboard := TSecureClipboard.Create;
|
||
FTrayAdded := False;
|
||
FBalloonShown := False;
|
||
FShowNotifications := True; // default on; JS pushes user pref on load
|
||
// Seed from the pre-window snapshot, then let the hook maintain it: the first
|
||
// close can happen before any other application is activated.
|
||
if (GStartupForeground <> 0) and IsWindow(GStartupForeground) and
|
||
(not IsOwnProcessWindow(GStartupForeground)) and
|
||
(not IsShellWindow(GStartupForeground)) then
|
||
GLastForeground := GStartupForeground;
|
||
CaptureForegroundOwner;
|
||
FForegroundHook := SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
|
||
0, @ForegroundEventProc, 0, 0, WINEVENT_OUTOFCONTEXT or WINEVENT_SKIPOWNPROCESS);
|
||
|
||
// Dedicated message-only window for tray + WTS notifications.
|
||
FMsgWindow := AllocateHWnd(MsgWindowHandler);
|
||
|
||
PrepareNid;
|
||
|
||
// Add the tray icon eagerly so it's visible from app startup, regardless
|
||
// of whether the window is shown or hidden. Without this, the tray icon
|
||
// only appears the first time the user minimizes — meaning fresh-launch
|
||
// users can't lock/quit from the tray and discover the feature only by
|
||
// accident. NIM_DELETE is now only called at shutdown.
|
||
if Shell_NotifyIcon(NIM_ADD, @FNid) then
|
||
FTrayAdded := True;
|
||
|
||
// Session-lock detection (fails silently if wtsapi32.dll is absent).
|
||
LoadWtsApi;
|
||
if Assigned(_WTSRegister) then
|
||
_WTSRegister(FMsgWindow, NOTIFY_FOR_THIS_SESSION);
|
||
|
||
// Sleep/hibernate detection. Forces WM_POWERBROADCAST delivery to our
|
||
// message-only window even if Windows would otherwise skip it. On
|
||
// Windows < 8 this fails silently — only modern systems support this
|
||
// API, but they're also the ones that have aggressive sleep behavior.
|
||
FPowerNotify := 0;
|
||
LoadPowerApi;
|
||
if Assigned(_PowerRegister) then
|
||
_PowerRegister(DEVICE_NOTIFY_WINDOW_HANDLE, FMsgWindow, FPowerNotify);
|
||
|
||
FDebugHotkeyRegistered := RegisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID,
|
||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D'));
|
||
// Default combo; JS overrides it via cmd://autofill/hotkeys (SetNewEntryHotkey)
|
||
// at unlock. Store it so ExecuteAutofill's re-register uses the live combo.
|
||
FNewEntryHotkeyMods := AF_MOD_CONTROL or AF_MOD_SHIFT;
|
||
FNewEntryHotkeyVk := Ord('A');
|
||
FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
|
||
FNewEntryHotkeyMods, FNewEntryHotkeyVk);
|
||
FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow,
|
||
QUICK_SEARCH_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('Q'));
|
||
end;
|
||
|
||
destructor TPMBridge.Destroy;
|
||
begin
|
||
if FForegroundHook <> 0 then
|
||
UnhookWinEvent(FForegroundHook);
|
||
|
||
if FDebugHotkeyRegistered then
|
||
UnregisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID);
|
||
|
||
if FNewEntryHotkeyRegistered then
|
||
UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID);
|
||
|
||
if FQuickSearchHotkeyRegistered then
|
||
UnregisterHotKey(FMsgWindow, QUICK_SEARCH_HOTKEY_ID);
|
||
|
||
if (FPowerNotify <> 0) and Assigned(_PowerUnregister) then
|
||
_PowerUnregister(FPowerNotify);
|
||
|
||
if Assigned(_WTSUnregister) then
|
||
_WTSUnregister(FMsgWindow);
|
||
|
||
if FTrayAdded then
|
||
begin
|
||
Shell_NotifyIcon(NIM_DELETE, @FNid);
|
||
FTrayAdded := False;
|
||
end;
|
||
|
||
if FIconOwned and (FIconHandle <> 0) then
|
||
DestroyIcon(FIconHandle);
|
||
|
||
if FLazyWnd <> 0 then DeallocateHWnd(FLazyWnd);
|
||
DeallocateHWnd(FMsgWindow);
|
||
FSecureClipboard.Free;
|
||
inherited;
|
||
end;
|
||
|
||
procedure TPMBridge.PrepareNid;
|
||
var
|
||
LargeIcon, SmallIcon: HICON;
|
||
begin
|
||
// Attempt to extract the small (16×16) icon from the exe.
|
||
// ExtractIconEx returns the number of icons extracted.
|
||
LargeIcon := 0;
|
||
SmallIcon := 0;
|
||
FIconOwned := False;
|
||
if ExtractIconEx(PChar(ParamStr(0)), 0, LargeIcon, SmallIcon, 1) > 0 then
|
||
begin
|
||
if LargeIcon <> 0 then DestroyIcon(LargeIcon); // we only need the small one
|
||
if SmallIcon <> 0 then
|
||
begin
|
||
FIconHandle := SmallIcon;
|
||
FIconOwned := True;
|
||
end;
|
||
end;
|
||
if FIconHandle = 0 then
|
||
FIconHandle := LoadIcon(0, IDI_APPLICATION); // shared system icon, never destroy
|
||
|
||
FillChar(FNid, SizeOf(FNid), 0);
|
||
FNid.cbSize := SizeOf(FNid);
|
||
FNid.Wnd := FMsgWindow;
|
||
FNid.uID := 1;
|
||
FNid.uFlags := NIF_ICON or NIF_MESSAGE or NIF_TIP;
|
||
FNid.uCallbackMessage := WM_TRAY_ICON;
|
||
FNid.hIcon := FIconHandle;
|
||
// szTip: array[0..127] of WideChar — copy tooltip text safely.
|
||
Move(PChar('Password Manager')^, FNid.szTip[0],
|
||
Min(Length('Password Manager'), High(FNid.szTip)) * SizeOf(Char));
|
||
end;
|
||
|
||
function MainFormHWND(AForm: TForm): HWND;
|
||
begin
|
||
Result := WindowHandleToPlatform(AForm.Handle).Wnd;
|
||
end;
|
||
|
||
function TPMBridge.FindFMXAppWindow: HWND;
|
||
var
|
||
LWnd: HWND;
|
||
LWndPid, LCurrentPid: DWORD;
|
||
begin
|
||
// FMX on Windows creates a hidden per-process window of class "TFMAppClass"
|
||
// that owns the application's taskbar entry — NOT the form's HWND.
|
||
// Hiding the form (via ShowWindow / Visible := False / WS_EX_TOOLWINDOW /
|
||
// ITaskbarList.DeleteTab) is therefore ineffective at removing the taskbar
|
||
// entry: those calls target the wrong window. The correct fix is to find
|
||
// the TFMAppClass window owned by our process and hide IT.
|
||
// Reference: https://stackoverflow.com/q/16768986
|
||
Result := 0;
|
||
LCurrentPid := GetCurrentProcessId;
|
||
LWnd := 0;
|
||
repeat
|
||
LWnd := FindWindowEx(0, LWnd, 'TFMAppClass', nil);
|
||
if LWnd <> 0 then
|
||
begin
|
||
LWndPid := 0;
|
||
GetWindowThreadProcessId(LWnd, LWndPid);
|
||
if LWndPid = LCurrentPid then
|
||
Exit(LWnd);
|
||
end;
|
||
until LWnd = 0;
|
||
end;
|
||
|
||
// Every foreground change in the session lands here (out-of-context, so nothing
|
||
// is injected into other processes). We keep the last one that is neither ours
|
||
// nor a shell window — that is the application to hand focus back to.
|
||
procedure ForegroundEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall;
|
||
begin
|
||
if (idObject <> OBJID_WINDOW) or (AWnd = 0) then Exit;
|
||
if IsOwnProcessWindow(AWnd) or IsShellWindow(AWnd) then Exit;
|
||
GLastForeground := AWnd;
|
||
end;
|
||
|
||
procedure TPMBridge.CaptureForegroundOwner;
|
||
begin
|
||
if (GLastForeground <> 0) and IsWindow(GLastForeground) then
|
||
FPrevForeground := GLastForeground;
|
||
end;
|
||
|
||
procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True;
|
||
AReturnForeground: Boolean = True; ANotify: Boolean = True);
|
||
var
|
||
LFormHwnd, LAppHwnd: HWND;
|
||
begin
|
||
// Tray icon is added at construction time and persists for the app's
|
||
// lifetime — no NIM_ADD here.
|
||
|
||
// Extra safety: clear the clipboard immediately when the user minimizes,
|
||
// rather than waiting for the 30s auto-clear timer to fire. A password
|
||
// the user just copied shouldn't sit in the clipboard while the app is
|
||
// out of sight. SKIPPED when AClearClipboard=False — the quick-search
|
||
// copy-then-hide flow deliberately keeps the password on the clipboard
|
||
// (the 30s auto-clear timer still guards it) so the user can paste it
|
||
// into their target app after we minimise. ClearIfOurs: never wipe
|
||
// content the user copied from another app since.
|
||
if AClearClipboard then
|
||
FSecureClipboard.ClearIfOurs;
|
||
|
||
LFormHwnd := MainFormHWND(FMainForm);
|
||
LAppHwnd := FindFMXAppWindow;
|
||
|
||
// 0. Snapshot the window placement BEFORE hiding so RestoreFromTray can
|
||
// replay the exact same state (maximised / normal + size + position).
|
||
// Without this, ShowWindow(SW_RESTORE) below always returns to the
|
||
// "normal" state — a window that was maximised before hiding comes
|
||
// back un-maximised.
|
||
FillChar(FSavedPlacement, SizeOf(FSavedPlacement), 0);
|
||
FSavedPlacement.length := SizeOf(FSavedPlacement);
|
||
FHasSavedPlacement := GetWindowPlacement(LFormHwnd, @FSavedPlacement);
|
||
|
||
// 0b. Hand the foreground back to the application that had it before we came
|
||
// up, BEFORE hiding: Windows only lets the current foreground owner give
|
||
// focus away, and once our windows are hidden the call is silently
|
||
// refused — which left us foreground while invisible, so the next hotkey
|
||
// captured our own window as its target. Callers that already placed the
|
||
// foreground themselves (post-autofill) pass False.
|
||
CaptureForegroundOwner;
|
||
// Not when it is minimised: ForceForegroundWindow un-minimises its target, and
|
||
// popping another application's window back open is not ours to decide. The
|
||
// user put it away — leave the foreground to Windows.
|
||
if AReturnForeground and (FPrevForeground <> 0) and IsWindow(FPrevForeground) and
|
||
(not IsIconic(FPrevForeground)) then
|
||
ForceForegroundWindow(FPrevForeground);
|
||
|
||
// 1. Hide the visible form via both FMX state and Win32 ShowWindow.
|
||
// Keeps the form invisible to the user.
|
||
FMainForm.Hide;
|
||
ShowWindow(LFormHwnd, SW_HIDE);
|
||
|
||
// 2. Hide the FMX application proxy window (TFMAppClass). THIS is what
|
||
// removes the entry from the taskbar — the form's HWND was never the
|
||
// taskbar-visible one in FMX.
|
||
if LAppHwnd <> 0 then
|
||
ShowWindow(LAppHwnd, SW_HIDE);
|
||
|
||
// 3. First-time only: pop a balloon notification so the user knows the
|
||
// app is still running in the tray (and didn't crash). Skipped when
|
||
// the user opted out via Settings.
|
||
// ANotify=False : absence de quelques centaines de ms (scan du QR), la
|
||
// fenetre revient seule - l'annonce "toujours dans le tray" serait fausse.
|
||
if ANotify and (not FBalloonShown) and FShowNotifications then
|
||
begin
|
||
ShowFirstTimeBalloon;
|
||
FBalloonShown := True;
|
||
end;
|
||
end;
|
||
|
||
procedure TPMBridge.ShowBalloon(const ATitle, AText: string;
|
||
AWarning: Boolean = False);
|
||
var
|
||
LBalloon: TNotifyIconData;
|
||
begin
|
||
// No gating here — each caller applies its own setting (first-time tray
|
||
// balloon → FShowNotifications; autofill-blocked → autofillFailBalloon).
|
||
// Build a separate TNotifyIconData with NIF_INFO set, NIM_MODIFY on the
|
||
// 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(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;
|
||
LWasTrayHidden, LRestoreMax: Boolean;
|
||
LPlace: TWindowPlacement;
|
||
begin
|
||
// Tray icon stays in the tray — we only show the window again.
|
||
LFormHwnd := MainFormHWND(FMainForm);
|
||
LAppHwnd := FindFMXAppWindow;
|
||
|
||
CaptureForegroundOwner;
|
||
|
||
// Already visible and not minimised (quick-search / focus hotkey while the
|
||
// window is open): just bring it to front. Re-applying FSavedPlacement —
|
||
// captured at the LAST MinimizeToTray — would teleport the window to a
|
||
// stale position the user has since moved away from.
|
||
if FMainForm.Visible and (not IsIconic(LFormHwnd)) then
|
||
begin
|
||
SetForegroundWindow(LFormHwnd);
|
||
Exit;
|
||
end;
|
||
|
||
// Captured BEFORE Show flips Visible: tray-hidden (Hide'd by
|
||
// MinimizeToTray) vs merely taskbar-minimised (Visible but iconic).
|
||
LWasTrayHidden := not FMainForm.Visible;
|
||
|
||
// Windows remembers "this minimised window was maximised" in the placement
|
||
// flags, but FMainForm.Show below re-applies FMX's own WindowState (normal)
|
||
// and loses it — hence a maximised window coming back normal after Win+D /
|
||
// Show desktop. Read the flag BEFORE showing so we can re-apply it.
|
||
LRestoreMax := False;
|
||
if not LWasTrayHidden then
|
||
begin
|
||
FillChar(LPlace, SizeOf(LPlace), 0);
|
||
LPlace.length := SizeOf(LPlace);
|
||
LRestoreMax := GetWindowPlacement(LFormHwnd, @LPlace) and
|
||
((LPlace.flags and WPF_RESTORETOMAXIMIZED) <> 0);
|
||
end;
|
||
|
||
// Reverse order: show the app proxy first so the taskbar entry comes back,
|
||
// then show and foreground the form.
|
||
if LAppHwnd <> 0 then
|
||
ShowWindow(LAppHwnd, SW_SHOW);
|
||
|
||
FMainForm.Show;
|
||
|
||
// Restore to the exact pre-tray state (maximised/normal + size + pos).
|
||
// Falls back to SW_SHOW (+ conditional SW_RESTORE) if we never captured
|
||
// a placement (e.g. focus-app / new-entry hotkey on an already-visible
|
||
// window). Unconditional SW_RESTORE would un-maximise a maximised
|
||
// window — surprising for the user who pressed Ctrl+Shift+A / +L /
|
||
// clicked the tray.
|
||
// Replay the snapshot ONLY for a genuine tray return (window was hidden —
|
||
// the exact state MinimizeToTray created). A merely taskbar-minimised
|
||
// window restores via Windows' own placement memory below; replaying our
|
||
// older snapshot would teleport it to wherever it sat at the LAST tray-hide.
|
||
if FHasSavedPlacement and LWasTrayHidden then
|
||
begin
|
||
// showCmd governs whether the window comes back maximised or normal;
|
||
// it's what SW_RESTORE clobbers. We force it ourselves. Captured while
|
||
// MINIMISED (tray-origin fill: ExecuteAutofill minimises us before
|
||
// MinimizeToTray snapshots): never restore as minimised, but honour
|
||
// WPF_RESTORETOMAXIMIZED — a maximised window minimised then trayed
|
||
// must come back maximised, not "normal".
|
||
if FSavedPlacement.showCmd = SW_SHOWMINIMIZED then
|
||
begin
|
||
if (FSavedPlacement.flags and WPF_RESTORETOMAXIMIZED) <> 0 then
|
||
FSavedPlacement.showCmd := SW_SHOWMAXIMIZED
|
||
else
|
||
FSavedPlacement.showCmd := SW_SHOWNORMAL;
|
||
end;
|
||
SetWindowPlacement(LFormHwnd, @FSavedPlacement);
|
||
end
|
||
else if LRestoreMax then
|
||
// Was maximised before Windows minimised it (Win+D, taskbar, Show
|
||
// desktop): come back maximised. SW_RESTORE alone would land on the
|
||
// pre-maximise "normal" geometry.
|
||
ShowWindow(LFormHwnd, SW_SHOWMAXIMIZED)
|
||
else
|
||
begin
|
||
ShowWindow(LFormHwnd, SW_SHOW);
|
||
// Only un-iconify if actually minimised. Win32 SW_RESTORE on a
|
||
// maximised window reverts it to normal — not what we want here.
|
||
if IsIconic(LFormHwnd) then
|
||
ShowWindow(LFormHwnd, SW_RESTORE);
|
||
end;
|
||
SetForegroundWindow(LFormHwnd);
|
||
end;
|
||
|
||
procedure TPMBridge.ShowTrayMenu;
|
||
const
|
||
ID_OPEN = 1;
|
||
ID_LOCK = 2;
|
||
ID_QUIT = 3;
|
||
ID_QUICKSEARCH = 4;
|
||
ID_LAZY = 5;
|
||
var
|
||
LMenu: HMENU;
|
||
LPt: TPoint;
|
||
LCmd: Cardinal;
|
||
begin
|
||
LMenu := CreatePopupMenu;
|
||
if LMenu = 0 then Exit;
|
||
try
|
||
AppendMenu(LMenu, MF_STRING, ID_OPEN, 'Open');
|
||
AppendMenu(LMenu, MF_STRING, ID_QUICKSEARCH, 'Quick search...');
|
||
AppendMenu(LMenu, MF_STRING, ID_LOCK, 'Lock vault');
|
||
AppendMenu(LMenu,
|
||
MF_STRING or IfThen(LazyBarVisible, MF_CHECKED, MF_UNCHECKED),
|
||
// "Fill bar" et non "Lazy mode" : le premier nomme la chose, le second
|
||
// decrivait l'utilisateur. Le signe qui a tranche - l'ancien libelle avait
|
||
// besoin d'une parenthese pour se faire comprendre, et c'etait elle qui
|
||
// portait le sens. Les identifiants internes gardent "lazy" : renommer
|
||
// LAZY_ON_KEY et consorts perdrait les reglages deja poses sur la machine.
|
||
ID_LAZY, 'Fill bar');
|
||
AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
|
||
AppendMenu(LMenu, MF_STRING, ID_QUIT, 'Quit');
|
||
|
||
GetCursorPos(LPt);
|
||
// SetForegroundWindow + WM_NULL post is the canonical Win32 workaround
|
||
// that lets TrackPopupMenu auto-dismiss when the user clicks elsewhere.
|
||
// Without it, the menu can become "sticky" on a hidden window.
|
||
SetForegroundWindow(FMsgWindow);
|
||
// Delphi's TrackPopupMenu is declared as returning BOOL, but with
|
||
// TPM_RETURNCMD it actually returns the selected menu item ID (or 0).
|
||
// Cast through the declared return type to read the real value.
|
||
LCmd := Cardinal(TrackPopupMenu(LMenu,
|
||
TPM_RETURNCMD or TPM_RIGHTBUTTON or TPM_NONOTIFY,
|
||
LPt.X, LPt.Y, 0, FMsgWindow, nil));
|
||
PostMessage(FMsgWindow, WM_NULL, 0, 0);
|
||
|
||
case LCmd of
|
||
ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||
ID_QUICKSEARCH: if Assigned(FOnQuickSearchRequest) then FOnQuickSearchRequest();
|
||
ID_LAZY: SetLazyBar(not LazyBarVisible);
|
||
ID_LOCK: if Assigned(FOnLockRequest) then FOnLockRequest();
|
||
ID_QUIT: if Assigned(FOnQuit) then FOnQuit();
|
||
end;
|
||
finally
|
||
DestroyMenu(LMenu);
|
||
end;
|
||
end;
|
||
|
||
// Hiding to the tray leaves no other window activated, so our own — invisible —
|
||
// window can still be the foreground one. Every hotkey that captures a target
|
||
// from the foreground has to reject that case, or it acts on itself.
|
||
function IsOwnProcessWindow(AHwnd: HWND): Boolean;
|
||
var
|
||
LPid: DWORD;
|
||
begin
|
||
if AHwnd = 0 then Exit(True);
|
||
LPid := 0;
|
||
GetWindowThreadProcessId(AHwnd, LPid);
|
||
Result := LPid = GetCurrentProcessId;
|
||
end;
|
||
|
||
// Desktop and notification-area windows are foreground constantly — clicking
|
||
// the tray icon makes the notification overflow flyout foreground — but they are
|
||
// never "the application the user was working in", so they must not be recorded
|
||
// as the one to give focus back to.
|
||
function IsShellWindow(AHwnd: HWND): Boolean;
|
||
const
|
||
SHELL_CLASSES: array[0..5] of string = (
|
||
'Shell_TrayWnd', 'NotifyIconOverflowWindow', 'TopLevelWindowForOverflowXamlIsland',
|
||
'Progman', 'WorkerW', 'Shell_SecondaryTrayWnd');
|
||
var
|
||
LClass: array[0..255] of Char;
|
||
LName: string;
|
||
I: Integer;
|
||
begin
|
||
if GetClassName(AHwnd, LClass, Length(LClass)) = 0 then Exit(False);
|
||
LName := LClass;
|
||
for I := Low(SHELL_CLASSES) to High(SHELL_CLASSES) do
|
||
if SameText(LName, SHELL_CLASSES[I]) then Exit(True);
|
||
Result := False;
|
||
end;
|
||
|
||
// The target of a fill is whatever window owns the foreground at the moment
|
||
// the user asks for it - a shortcut, or a click on the lazy strip, which takes
|
||
// no foreground precisely so this stays true.
|
||
procedure TPMBridge.FireAutofillRequest(AKind: TAutofillKind;
|
||
AFromBar: Boolean = False);
|
||
var
|
||
LTarget: HWND;
|
||
LTitle: string;
|
||
LLen: Integer;
|
||
begin
|
||
if not Assigned(FOnAutofillRequest) then Exit;
|
||
// QUI A DEMANDE. Les deux gestes partagent tout ce qui suit, donc le journal
|
||
// les confondait sous "hotkey" - et un diagnostic de lenteur qui ne sait pas
|
||
// par ou l'on est entre cherche au mauvais endroit.
|
||
FLastRequestFromBar := AFromBar;
|
||
LTarget := GetForegroundWindow;
|
||
CaptureForegroundOwner;
|
||
// Hiding to the tray does not hand the foreground to anyone else, so ours
|
||
// stays foreground while invisible: from there we would capture our own
|
||
// window and type the password into it. There is no target to fill.
|
||
if IsOwnProcessWindow(LTarget) then
|
||
begin
|
||
ShowBalloon('Nothing to fill',
|
||
'Click the field you want filled first, then ask for the fill.', True);
|
||
Exit;
|
||
end;
|
||
SetLength(LTitle, 512);
|
||
LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
|
||
SetLength(LTitle, LLen);
|
||
FOnAutofillRequest(AKind, LTarget, LTitle);
|
||
end;
|
||
|
||
// Les quatre boutons sont de la meme classe, donc une seule procedure d'origine
|
||
// a retenir.
|
||
var
|
||
GLazyOldBtnProc: Pointer = nil;
|
||
// Bouton de la barrette sous la souris (0 = aucun) : il se peint a l'accent.
|
||
GLazyHotBtn: HWND = 0;
|
||
|
||
/// Un appui sur un bouton de la barrette : clic, ou debut de glissement ?
|
||
///
|
||
/// C'EST DragDetect QUI TRANCHE, ET PERSONNE D'AUTRE. Il bloque jusqu'a ce que
|
||
/// la souris depasse le seuil de glissement du systeme - celui que
|
||
/// l'utilisateur a regle, et qui n'est pas le meme sur un ecran tactile - ou
|
||
/// jusqu'au relachement. Un seuil ecrit a la main serait un troisieme reglage
|
||
/// que personne n'a demande.
|
||
function LazyButtonProc(AWnd: HWND; AMsg: UINT; AWp: WPARAM;
|
||
ALp: LPARAM): LRESULT; stdcall;
|
||
var
|
||
LPt: TPoint;
|
||
begin
|
||
if AMsg = WM_LBUTTONDOWN then
|
||
begin
|
||
GetCursorPos(LPt);
|
||
if DragDetect(AWnd, LPt) then
|
||
begin
|
||
ReleaseCapture; // DragDetect a pris la souris, la boucle de deplacement la veut
|
||
// Exactement le geste "trainer par la barre de titre" : la barrette EST
|
||
// sa propre barre de titre (WM_NCHITTEST -> HTCAPTION).
|
||
SendMessage(GetParent(AWnd), WM_SYSCOMMAND, SC_MOVE or HTCAPTION, 0);
|
||
end
|
||
else
|
||
// Relache sans bouger. DragDetect a AVALE le WM_LBUTTONUP, que le bouton
|
||
// ne verra donc jamais : lui rendre la main le laisserait enfonce pour
|
||
// toujours. On annonce le clic a sa place - c'est le message qu'il aurait
|
||
// envoye lui-meme.
|
||
SendMessage(GetParent(AWnd), WM_COMMAND,
|
||
MAKEWPARAM(GetDlgCtrlID(AWnd), BN_CLICKED), LPARAM(AWnd));
|
||
Exit(0);
|
||
end;
|
||
// LE SURVOL SE VOIT. Un bouton peint a la main n'a aucun etat "survole" :
|
||
// on le suit ici (entree par WM_MOUSEMOVE, sortie par WM_MOUSELEAVE demande a
|
||
// TrackMouseEvent) et on redessine les deux boutons concernes.
|
||
if (AMsg = WM_MOUSEMOVE) and (GLazyHotBtn <> AWnd) then
|
||
begin
|
||
if GLazyHotBtn <> 0 then InvalidateRect(GLazyHotBtn, nil, False);
|
||
GLazyHotBtn := AWnd;
|
||
InvalidateRect(AWnd, nil, False);
|
||
var LTrack: TTrackMouseEvent;
|
||
LTrack.cbSize := SizeOf(LTrack);
|
||
LTrack.dwFlags := TME_LEAVE;
|
||
LTrack.hwndTrack := AWnd;
|
||
LTrack.dwHoverTime := 0;
|
||
TrackMouseEvent(LTrack);
|
||
end
|
||
else if (AMsg = WM_MOUSELEAVE) and (GLazyHotBtn = AWnd) then
|
||
begin
|
||
GLazyHotBtn := 0;
|
||
InvalidateRect(AWnd, nil, False);
|
||
end;
|
||
Result := CallWindowProc(GLazyOldBtnProc, AWnd, AMsg, AWp, ALp);
|
||
end;
|
||
|
||
// LA BARRE DES TACHES EST TOPMOST ELLE AUSSI, et la barrette a le droit de se
|
||
// poser dessus. Redemander HWND_TOPMOST ne suffit pas quand on l'est deja :
|
||
// Windows n'y voit rien a changer et la barrette reste dessous - invisible au
|
||
// demarrage de session, puis des qu'on ouvre le menu Demarrer. Le passage par
|
||
// NOTOPMOST force la remontee en tete de la bande. Fait seulement quand elle
|
||
// est vraiment recouverte (un point de sa surface ne lui appartient plus),
|
||
// pour ne pas remuer l'ordre des fenetres cinq fois par seconde.
|
||
function LazyIsCovered(AWnd: HWND): Boolean;
|
||
var
|
||
LRect: TRect;
|
||
LPt: TPoint;
|
||
LHit: HWND;
|
||
LPid: DWORD;
|
||
begin
|
||
Result := False;
|
||
if not GetWindowRect(AWnd, LRect) then Exit;
|
||
LPt.X := (LRect.Left + LRect.Right) div 2;
|
||
LPt.Y := (LRect.Top + LRect.Bottom) div 2;
|
||
LHit := WindowFromPoint(LPt);
|
||
if LHit = 0 then Exit;
|
||
if (LHit = AWnd) or (GetAncestor(LHit, GA_ROOT) = AWnd) then Exit;
|
||
// NOS PROPRES FENETRES NE RECOUVRENT PAS. L'info-bulle d'un bouton s'affiche
|
||
// PAR-DESSUS la barrette : la compter comme un recouvrement faisait
|
||
// repasser la barrette devant (NOTOPMOST puis TOPMOST - elle disparaissait
|
||
// un instant), ce qui fermait l'info-bulle, qui se rouvrait sous la souris
|
||
// immobile... en boucle.
|
||
GetWindowThreadProcessId(LHit, @LPid);
|
||
Result := LPid <> GetCurrentProcessId;
|
||
end;
|
||
|
||
procedure LazyForceToTop(AWnd: HWND);
|
||
begin
|
||
// UNE SEULE REMONTEE, PAS DE DETOUR PAR LE BAS. Le passage par NOTOPMOST
|
||
// faisait descendre la barrette sous les fenetres ordinaires l'espace d'un
|
||
// instant - un flash visible a chaque battement - et, mesure a l'appui, il
|
||
// ne gagnait rien contre la barre des taches active, seul cas ou elle reste
|
||
// recouverte. Contre une fenetre TOPMOST ordinaire, TOPMOST suffit.
|
||
SetWindowPos(AWnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||
SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE);
|
||
end;
|
||
|
||
procedure TPMBridge.DrawLazyButton(ADraw: PDrawItemStruct);
|
||
var
|
||
LRect: TRect;
|
||
LBrush: HBRUSH;
|
||
LFace, LEdge, LInk: COLORREF;
|
||
LText: array[0..31] of Char;
|
||
LGlyph: Char;
|
||
LFont: HFONT;
|
||
LOldFont: HGDIOBJ;
|
||
begin
|
||
LRect := ADraw.rcItem;
|
||
if FDarkTheme then
|
||
begin
|
||
LFace := LAZY_DARK_FACE; LEdge := LAZY_DARK_EDGE; LInk := LAZY_DARK_INK;
|
||
if (ADraw.itemState and ODS_SELECTED) <> 0 then LFace := LAZY_DARK_DOWN;
|
||
end
|
||
else
|
||
begin
|
||
LFace := LAZY_LITE_FACE; LEdge := LAZY_LITE_EDGE; LInk := LAZY_LITE_INK;
|
||
if (ADraw.itemState and ODS_SELECTED) <> 0 then LFace := LAZY_LITE_DOWN;
|
||
end;
|
||
// Survole : fond releve et encre a l'accent, cadre compris.
|
||
if ADraw.hwndItem = GLazyHotBtn then
|
||
begin
|
||
if FDarkTheme then
|
||
begin
|
||
LFace := LAZY_DARK_DOWN; LInk := LAZY_DARK_ACCENT; LEdge := LAZY_DARK_ACCENT;
|
||
end
|
||
else
|
||
begin
|
||
LFace := LAZY_LITE_DOWN; LInk := LAZY_LITE_ACCENT; LEdge := LAZY_LITE_ACCENT;
|
||
end;
|
||
end;
|
||
|
||
LBrush := CreateSolidBrush(LFace);
|
||
FillRect(ADraw.hDC, LRect, LBrush);
|
||
DeleteObject(LBrush);
|
||
LBrush := CreateSolidBrush(LEdge);
|
||
FrameRect(ADraw.hDC, LRect, LBrush);
|
||
DeleteObject(LBrush);
|
||
|
||
SetBkMode(ADraw.hDC, TRANSPARENT);
|
||
SetTextColor(ADraw.hDC, LInk);
|
||
if FLazyIcons then
|
||
begin
|
||
// Segoe MDL2 Assets, livree avec Windows 10 et 11 : aucune image a
|
||
// embarquer, et le glyphe suit la couleur du texte comme un mot.
|
||
case ADraw.CtlID of
|
||
LAZY_ID_FULL: LGlyph := #$E77B; // Contact
|
||
LAZY_ID_PWD: LGlyph := #$E8D7; // Permissions (cle)
|
||
LAZY_ID_TOTP: LGlyph := #$E823; // Recent (horloge du code)
|
||
LAZY_ID_ADD: LGlyph := #$E710; // Add
|
||
LAZY_ID_SEARCH: LGlyph := #$E721; // Search
|
||
LAZY_ID_OPEN: LGlyph := #$E8A7; // OpenInNewWindow
|
||
LAZY_ID_SETTINGS: LGlyph := #$E713; // Settings (roue)
|
||
else
|
||
LGlyph := #$E711; // Cancel (X)
|
||
end;
|
||
LFont := CreateFont(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
|
||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
||
DEFAULT_PITCH, 'Segoe MDL2 Assets');
|
||
LOldFont := SelectObject(ADraw.hDC, LFont);
|
||
DrawText(ADraw.hDC, PChar(LGlyph), 1, LRect,
|
||
DT_CENTER or DT_VCENTER or DT_SINGLELINE);
|
||
SelectObject(ADraw.hDC, LOldFont);
|
||
DeleteObject(LFont);
|
||
Exit;
|
||
end;
|
||
// Le libelle est celui de la fenetre-bouton : une seule verite, posee a la
|
||
// creation, plutot qu'une table de correspondance a tenir a jour.
|
||
GetWindowText(ADraw.hwndItem, LText, Length(LText));
|
||
DrawText(ADraw.hDC, LText, -1, LRect,
|
||
DT_CENTER or DT_VCENTER or DT_SINGLELINE);
|
||
end;
|
||
|
||
procedure TPMBridge.ApplyLazyAlpha(AHover: Boolean);
|
||
var
|
||
LAlpha: Byte;
|
||
begin
|
||
if FLazyWnd = 0 then Exit;
|
||
// TRANSPARENTE N'EST PAS TRAVERSANTE : la barrette continue d'avaler les
|
||
// clics sur toute sa surface. C'est un allegement du regard, pas un passage.
|
||
// Le pourcentage s'applique toujours aux boutons, fond retire ou non.
|
||
if AHover or (FLazyAlphaPct >= 100) then
|
||
LAlpha := 255
|
||
else
|
||
LAlpha := MulDiv(255, FLazyAlphaPct, 100);
|
||
if FLazyClear then
|
||
SetLayeredWindowAttributes(FLazyWnd, LAZY_KEY_COLOR, LAlpha,
|
||
LWA_ALPHA or LWA_COLORKEY)
|
||
else
|
||
SetLayeredWindowAttributes(FLazyWnd, 0, LAlpha, LWA_ALPHA);
|
||
end;
|
||
|
||
procedure TPMBridge.ShowLazyMenu;
|
||
const
|
||
CHOICES: array[0..3] of Integer = (100, 75, 50, 25);
|
||
var
|
||
LMenu: HMENU;
|
||
LPt: TPoint;
|
||
LCmd: Cardinal;
|
||
I: Integer;
|
||
LIconsLabel: string;
|
||
begin
|
||
LMenu := CreatePopupMenu;
|
||
if LMenu = 0 then Exit;
|
||
try
|
||
for I := Low(CHOICES) to High(CHOICES) do
|
||
AppendMenu(LMenu,
|
||
MF_STRING or IfThen(FLazyAlphaPct = CHOICES[I], MF_CHECKED, MF_UNCHECKED),
|
||
CHOICES[I], PChar(Format('%d%% when the mouse leaves', [CHOICES[I]])));
|
||
if FLazyIcons then LIconsLabel := 'Show text' else LIconsLabel := 'Show icons';
|
||
AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
|
||
AppendMenu(LMenu, MF_STRING, LAZY_MENU_ICONS, PChar(LIconsLabel));
|
||
AppendMenu(LMenu, MF_STRING or IfThen(FLazyClear, MF_CHECKED, MF_UNCHECKED),
|
||
LAZY_MENU_CLEAR, 'Transparent background');
|
||
AppendMenu(LMenu, MF_STRING, LAZY_MENU_SETTINGS, 'Fill bar settings...');
|
||
|
||
// LE BATTEMENT DOIT S'ARRETER PENDANT LE MENU. Il repose la barrette en
|
||
// tete de la bande TOPMOST cinq fois par seconde - or le menu deroulant est
|
||
// lui aussi dans cette bande, et se retrouvait donc recouvert par la
|
||
// barrette qui venait de l'ouvrir. TrackPopupMenu est modal : le minuteur
|
||
// continuerait de battre pendant tout ce temps.
|
||
KillTimer(FLazyWnd, LAZY_TIMER_ID);
|
||
GetCursorPos(LPt);
|
||
// Le proprietaire du menu est la fenetre message-only, JAMAIS la barrette :
|
||
// TrackPopupMenu exige un SetForegroundWindow pour se refermer tout seul, et
|
||
// le faire sur la barrette detruirait le NOACTIVATE qui la fait fonctionner.
|
||
SetForegroundWindow(FMsgWindow);
|
||
LCmd := Cardinal(TrackPopupMenu(LMenu,
|
||
TPM_RETURNCMD or TPM_RIGHTBUTTON or TPM_NONOTIFY,
|
||
LPt.X, LPt.Y, 0, FMsgWindow, nil));
|
||
PostMessage(FMsgWindow, WM_NULL, 0, 0);
|
||
|
||
if LCmd = LAZY_MENU_SETTINGS then
|
||
begin
|
||
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||
if Assigned(FOnFillBarSettings) then FOnFillBarSettings();
|
||
end
|
||
else if LCmd = LAZY_MENU_CLEAR then
|
||
begin
|
||
if FLazyClear then SetPref(LAZY_CLEAR_KEY, '0') else SetPref(LAZY_CLEAR_KEY, '1');
|
||
ReloadLazyBar;
|
||
end
|
||
else if LCmd = LAZY_MENU_ICONS then
|
||
begin
|
||
if FLazyIcons then SetPref(LAZY_ICONS_KEY, '0') else SetPref(LAZY_ICONS_KEY, '1');
|
||
ReloadLazyBar;
|
||
end
|
||
else if LCmd > 0 then
|
||
begin
|
||
FLazyAlphaPct := LCmd;
|
||
SetPref(LAZY_ALPHA_KEY, IntToStr(FLazyAlphaPct));
|
||
ApplyLazyAlpha(FLazyHover);
|
||
if Assigned(FOnFillBarChanged) then FOnFillBarChanged();
|
||
end;
|
||
finally
|
||
DestroyMenu(LMenu);
|
||
if FLazyWnd <> 0 then
|
||
SetTimer(FLazyWnd, LAZY_TIMER_ID, LAZY_POLL_MS, nil);
|
||
end;
|
||
end;
|
||
|
||
function TPMBridge.LazyBarVisible: Boolean;
|
||
begin
|
||
Result := FLazyWnd <> 0;
|
||
end;
|
||
|
||
procedure TPMBridge.LazyWindowHandler(var AMsg: TMessage);
|
||
var
|
||
LRect: TRect;
|
||
LBrush: HBRUSH;
|
||
LPt: TPoint;
|
||
LIn: Boolean;
|
||
begin
|
||
case AMsg.Msg of
|
||
WM_TIMER:
|
||
begin
|
||
GetCursorPos(LPt);
|
||
GetWindowRect(FLazyWnd, LRect);
|
||
LIn := PtInRect(LRect, LPt);
|
||
if LIn <> FLazyHover then
|
||
begin
|
||
FLazyHover := LIn;
|
||
ApplyLazyAlpha(LIn);
|
||
end;
|
||
// TOPMOST N'EST PAS UN RANG, C'EST UNE BANDE. La barre des taches y est
|
||
// elle aussi, et se remet en tete de cette bande au demarrage de la
|
||
// session comme a l'ouverture du menu Demarrer : une barrette posee
|
||
// dessus passe DERRIERE et semble avoir disparu.
|
||
if LazyIsCovered(FLazyWnd) then LazyForceToTop(FLazyWnd);
|
||
Exit;
|
||
end;
|
||
// Fin du glissement (la boucle modale ouverte par HTCAPTION), donc une
|
||
// ecriture par deplacement et non une par pixel parcouru.
|
||
WM_EXITSIZEMOVE:
|
||
begin
|
||
GetWindowRect(FLazyWnd, LRect);
|
||
SetPref(LAZY_POS_KEY, Format('%d,%d', [LRect.Left, LRect.Top]));
|
||
Exit;
|
||
end;
|
||
// Deux portes pour un seul menu : le fond de la barrette est declare
|
||
// non-client (HTCAPTION, pour pouvoir la trainer) et rend donc un clic droit
|
||
// NC, tandis qu'un clic droit sur un bouton remonte en WM_CONTEXTMENU.
|
||
WM_NCRBUTTONUP, WM_CONTEXTMENU:
|
||
begin
|
||
ShowLazyMenu;
|
||
AMsg.Result := 0;
|
||
Exit;
|
||
end;
|
||
// THE STRIP IS ITS OWN TITLE BAR. It has none to grab, and it will end up
|
||
// sitting over the very field it fills - so every pixel that is not a
|
||
// button drags it out of the way.
|
||
WM_NCHITTEST:
|
||
begin
|
||
AMsg.Result := HTCAPTION;
|
||
Exit;
|
||
end;
|
||
// AllocateHWnd's window class registers no background brush, so nothing
|
||
// erases the gaps around the buttons and whatever was behind shows through.
|
||
WM_ERASEBKGND:
|
||
begin
|
||
GetClientRect(FLazyWnd, LRect);
|
||
if FLazyClear then
|
||
begin
|
||
LBrush := CreateSolidBrush(LAZY_KEY_COLOR);
|
||
FillRect(HDC(AMsg.WParam), LRect, LBrush);
|
||
DeleteObject(LBrush);
|
||
AMsg.Result := 1;
|
||
Exit;
|
||
end;
|
||
LBrush := CreateSolidBrush(IfThen(FDarkTheme, LAZY_DARK_BG, LAZY_LITE_BG));
|
||
FillRect(HDC(AMsg.WParam), LRect, LBrush);
|
||
DeleteObject(LBrush);
|
||
// Le cadre est peint ici plutot que par WS_BORDER : celui-ci se sert
|
||
// dans les couleurs systeme, qui ignorent notre theme.
|
||
LBrush := CreateSolidBrush(IfThen(FDarkTheme, LAZY_DARK_EDGE, LAZY_LITE_EDGE));
|
||
FrameRect(HDC(AMsg.WParam), LRect, LBrush);
|
||
DeleteObject(LBrush);
|
||
AMsg.Result := 1;
|
||
Exit;
|
||
end;
|
||
WM_DRAWITEM:
|
||
begin
|
||
DrawLazyButton(PDrawItemStruct(AMsg.LParam));
|
||
AMsg.Result := 1;
|
||
Exit;
|
||
end;
|
||
WM_COMMAND:
|
||
if HiWord(AMsg.WParam) = BN_CLICKED then
|
||
begin
|
||
case LoWord(AMsg.WParam) of
|
||
LAZY_ID_FULL: FireAutofillRequest(akFull, True);
|
||
LAZY_ID_PWD: FireAutofillRequest(akPasswordOnly, True);
|
||
LAZY_ID_TOTP: FireAutofillRequest(akTotp, True);
|
||
LAZY_ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||
LAZY_ID_ADD: FireNewEntryRequest;
|
||
LAZY_ID_SEARCH: FireQuickSearchRequest;
|
||
LAZY_ID_SETTINGS:
|
||
begin
|
||
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||
if Assigned(FOnFillBarSettings) then FOnFillBarSettings();
|
||
end;
|
||
LAZY_ID_CLOSE: PostMessage(FMsgWindow, WM_LAZY_CLOSE, 0, 0);
|
||
end;
|
||
AMsg.Result := 0;
|
||
Exit;
|
||
end;
|
||
end;
|
||
AMsg.Result := DefWindowProc(FLazyWnd, AMsg.Msg, AMsg.WParam, AMsg.LParam);
|
||
end;
|
||
|
||
procedure TPMBridge.FireNewEntryRequest;
|
||
var
|
||
LTarget: HWND;
|
||
LTitle: string;
|
||
LLen: Integer;
|
||
begin
|
||
if not Assigned(FOnNewEntryHotkey) then Exit;
|
||
LTarget := GetForegroundWindow;
|
||
LTitle := '';
|
||
// The window we are about to cover is exactly the one to hand the
|
||
// foreground back to when we return to the tray.
|
||
CaptureForegroundOwner;
|
||
// From the tray the foreground can resolve to OUR own window — don't
|
||
// pre-fill the new entry with "Password Manager". Only capture a real
|
||
// external window's title; otherwise pass '' (blank title).
|
||
if not IsOwnProcessWindow(LTarget) then
|
||
begin
|
||
SetLength(LTitle, 512);
|
||
LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
|
||
SetLength(LTitle, LLen);
|
||
end;
|
||
FOnNewEntryHotkey(LTitle);
|
||
end;
|
||
|
||
procedure TPMBridge.FireQuickSearchRequest;
|
||
var
|
||
LTarget: HWND;
|
||
LTitle: string;
|
||
LLen: Integer;
|
||
begin
|
||
// Quick-search + autofill: capture the foreground HWND BEFORE the JS
|
||
// modal steals focus, hand it to the host so it can stash it in
|
||
// FAutofillTargetHWND (consumed by cmd://autofill/execute later).
|
||
if not Assigned(FOnQuickSearchHotkey) then Exit;
|
||
LTarget := GetForegroundWindow;
|
||
CaptureForegroundOwner;
|
||
// Same trap as the autofill hotkeys: our own hidden window can be the
|
||
// foreground one. Report no target — the host then opens quick search in
|
||
// copy mode instead of offering to type into ourselves.
|
||
if IsOwnProcessWindow(LTarget) then LTarget := 0;
|
||
LTitle := '';
|
||
if LTarget <> 0 then
|
||
begin
|
||
SetLength(LTitle, 512);
|
||
LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
|
||
SetLength(LTitle, LLen);
|
||
end;
|
||
FOnQuickSearchHotkey(akPasswordOnly, LTarget, LTitle);
|
||
end;
|
||
|
||
procedure TPMBridge.ReloadLazyBar;
|
||
begin
|
||
PostMessage(FMsgWindow, WM_LAZY_RELOAD, 0, 0);
|
||
end;
|
||
|
||
procedure TPMBridge.RestoreLazyBar;
|
||
begin
|
||
if GetPref(LAZY_ON_KEY) = '1' then SetLazyBar(True);
|
||
end;
|
||
|
||
procedure TPMBridge.SetLazyBar(AOn: Boolean);
|
||
// Declarees AVANT les routines imbriquees, qui s'en servent : une routine
|
||
// imbriquee ne voit que les variables declarees au-dessus d'elle.
|
||
var
|
||
LWork, LRect: TRect;
|
||
LSaved: TArray<string>;
|
||
LX, LY, LPosX, LPosY: Integer;
|
||
LList: string;
|
||
LTip: HWND;
|
||
|
||
// BS_OWNERDRAW : le systeme garde le suivi de la souris et l'etat enfonce,
|
||
// nous ne reprenons que les couleurs. Un bouton standard resterait gris clair
|
||
// sur fond sombre, comme sur la capture qui a motive ce changement.
|
||
procedure MakeButton(AId: Integer; const ACaption, ATip: string;
|
||
AX, AWidth: Integer);
|
||
var
|
||
LBtn: HWND;
|
||
LTool: TToolInfo;
|
||
begin
|
||
LBtn := CreateWindow('BUTTON', PChar(ACaption),
|
||
WS_CHILD or WS_VISIBLE or BS_OWNERDRAW, AX, 5, AWidth, 28,
|
||
FLazyWnd, HMENU(AId), HInstance, nil);
|
||
if LBtn = 0 then Exit;
|
||
// Without this the button wears the 1995 system font.
|
||
SendMessage(LBtn, WM_SETFONT, WPARAM(GetStockObject(DEFAULT_GUI_FONT)), 1);
|
||
GLazyOldBtnProc := Pointer(SetWindowLongPtr(LBtn, GWLP_WNDPROC,
|
||
LONG_PTR(@LazyButtonProc)));
|
||
// UNE ICONE SANS MOT SE NOMME AU SURVOL. Info-bulle posee dans les deux
|
||
// modes : elle dit aussi ce que fait "Add" ou "Search" en texte.
|
||
if LTip <> 0 then
|
||
begin
|
||
FillChar(LTool, SizeOf(LTool), 0);
|
||
LTool.cbSize := SizeOf(LTool);
|
||
LTool.uFlags := TTF_IDISHWND or TTF_SUBCLASS;
|
||
LTool.hwnd := FLazyWnd;
|
||
LTool.uId := UINT_PTR(LBtn);
|
||
LTool.lpszText := PChar(ATip);
|
||
SendMessage(LTip, TTM_ADDTOOL, 0, LPARAM(@LTool));
|
||
end;
|
||
end;
|
||
|
||
// Un bouton du jeu choisi dans les reglages, pose a la suite des precedents.
|
||
procedure AddButton(const AKey: string; AId: Integer;
|
||
const ACaption, ATip: string; ATextWidth: Integer);
|
||
var
|
||
LW: Integer;
|
||
begin
|
||
if not (',' + LList + ',').Contains(',' + AKey + ',') then Exit;
|
||
LW := IfThen(FLazyIcons, 30, ATextWidth);
|
||
MakeButton(AId, ACaption, ATip, LX, LW);
|
||
Inc(LX, LW + 6);
|
||
end;
|
||
|
||
begin
|
||
if AOn = LazyBarVisible then Exit;
|
||
if AOn then SetPref(LAZY_ON_KEY, '1') else SetPref(LAZY_ON_KEY, '0');
|
||
|
||
if not AOn then
|
||
begin
|
||
DeallocateHWnd(FLazyWnd); // the child buttons go with their parent
|
||
FLazyWnd := 0;
|
||
if Assigned(FOnFillBarChanged) then FOnFillBarChanged();
|
||
Exit;
|
||
end;
|
||
|
||
FLazyWnd := AllocateHWnd(LazyWindowHandler);
|
||
if FLazyWnd = 0 then Exit;
|
||
SetWindowLong(FLazyWnd, GWL_EXSTYLE,
|
||
GetWindowLong(FLazyWnd, GWL_EXSTYLE) or
|
||
WS_EX_NOACTIVATE or WS_EX_TOPMOST or WS_EX_TOOLWINDOW or WS_EX_LAYERED);
|
||
|
||
FLazyAlphaPct := StrToIntDef(GetPref(LAZY_ALPHA_KEY), LAZY_ALPHA_DEFAULT);
|
||
if (FLazyAlphaPct < 25) or (FLazyAlphaPct > 100) then
|
||
FLazyAlphaPct := LAZY_ALPHA_DEFAULT;
|
||
|
||
FLazyIcons := GetPref(LAZY_ICONS_KEY) = '1';
|
||
FLazyClear := GetPref(LAZY_CLEAR_KEY) = '1';
|
||
LList := GetPref(LAZY_BTNS_KEY);
|
||
if LList = '' then LList := LAZY_BTNS_DEFAULT;
|
||
InitCommonControls;
|
||
LTip := CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, nil,
|
||
WS_POPUP or TTS_ALWAYSTIP or TTS_NOPREFIX,
|
||
Integer(CW_USEDEFAULT), Integer(CW_USEDEFAULT),
|
||
Integer(CW_USEDEFAULT), Integer(CW_USEDEFAULT),
|
||
FLazyWnd, 0, HInstance, nil);
|
||
|
||
LX := 5;
|
||
AddButton('full', LAZY_ID_FULL, 'Full', 'Fill username and password', 56);
|
||
AddButton('pwd', LAZY_ID_PWD, 'Password', 'Fill the password only', 76);
|
||
AddButton('totp', LAZY_ID_TOTP, '2FA', 'Fill the 2FA code', 52);
|
||
AddButton('add', LAZY_ID_ADD, 'Add', 'New entry from the current window', 48);
|
||
AddButton('search', LAZY_ID_SEARCH, 'Search', 'Quick search', 60);
|
||
// Meme raison que le X : quand la barrette est allumee, elle est souvent la
|
||
// seule chose de l'application a l'ecran. Revenir au coffre ne doit pas
|
||
// demander de retrouver l'icone du tray.
|
||
AddButton('open', LAZY_ID_OPEN, 'Open', 'Open the vault', 56);
|
||
AddButton('settings', LAZY_ID_SETTINGS, 'Settings', 'Fill bar settings', 64);
|
||
// Fermer la barrette sans passer par le menu du tray : elle flotte au-dessus
|
||
// de tout, donc s'en debarrasser doit se faire la ou elle est.
|
||
MakeButton(LAZY_ID_CLOSE, 'X', 'Close the fill bar', LX, 24);
|
||
FLazyW := LX + 24 + 5;
|
||
|
||
// Bottom right of the WORK area, so it does not start life under the
|
||
// taskbar. From there the user drags it wherever the login form is.
|
||
if not SystemParametersInfo(SPI_GETWORKAREA, 0, @LWork, 0) then
|
||
LWork := Rect(0, 0, GetSystemMetrics(SM_CXSCREEN),
|
||
GetSystemMetrics(SM_CYSCREEN));
|
||
LX := LWork.Right - FLazyW - 24;
|
||
LY := LWork.Bottom - LAZY_H - 24;
|
||
|
||
// LA POSITION RETENUE, SI ELLE TOMBE ENCORE SUR UN ECRAN. Un ecran debranche
|
||
// depuis, ou une resolution changee, laisserait sinon la barrette hors du
|
||
// bureau - et elle est sans entree dans la barre des taches, donc perdue pour
|
||
// de bon : ni a cliquer, ni a ramener. MonitorFromRect le dit d'un mot.
|
||
LSaved := GetPref(LAZY_POS_KEY).Split([',']);
|
||
if (Length(LSaved) = 2) and TryStrToInt(LSaved[0], LPosX)
|
||
and TryStrToInt(LSaved[1], LPosY) then
|
||
begin
|
||
LRect := Rect(LPosX, LPosY, LPosX + FLazyW, LPosY + LAZY_H);
|
||
if MonitorFromRect(@LRect, MONITOR_DEFAULTTONULL) <> 0 then
|
||
begin
|
||
LX := LPosX;
|
||
LY := LPosY;
|
||
end;
|
||
end;
|
||
|
||
SetWindowPos(FLazyWnd, HWND_TOPMOST, LX, LY, FLazyW, LAZY_H,
|
||
SWP_NOACTIVATE or SWP_SHOWWINDOW);
|
||
|
||
// La souris est ailleurs a la seconde ou la barrette parait : elle nait donc
|
||
// au repos, et non pleine puis fondue une fraction de seconde plus tard.
|
||
FLazyHover := False;
|
||
ApplyLazyAlpha(False);
|
||
SetTimer(FLazyWnd, LAZY_TIMER_ID, LAZY_POLL_MS, nil);
|
||
if Assigned(FOnFillBarChanged) then FOnFillBarChanged();
|
||
end;
|
||
|
||
procedure TPMBridge.MsgWindowHandler(var AMsg: TMessage);
|
||
var
|
||
LMouseEvent: Word;
|
||
begin
|
||
// AllocateHWnd creates the window on the thread that called it (here: the
|
||
// main thread, since TPMBridge.Create runs from FormCreate). Windows
|
||
// dispatches messages on the owning thread, so this handler is already
|
||
// on the main thread — no need to marshal via TThread.Queue/ForceQueue.
|
||
if AMsg.Msg = WM_LAZY_CLOSE then
|
||
begin
|
||
SetLazyBar(False);
|
||
Exit;
|
||
end
|
||
|
||
else if AMsg.Msg = WM_LAZY_RELOAD then
|
||
begin
|
||
if LazyBarVisible then
|
||
begin
|
||
SetLazyBar(False);
|
||
SetLazyBar(True);
|
||
end;
|
||
Exit;
|
||
end
|
||
|
||
else if AMsg.Msg = WM_TRAY_ICON then
|
||
begin
|
||
// For Shell_NotifyIcon callback messages, the mouse event is in the
|
||
// low word of LParam (regardless of NOTIFYICON_VERSION). Extracting
|
||
// it via LOWORD is more portable than comparing the full LPARAM.
|
||
LMouseEvent := Word(AMsg.LParam and $FFFF);
|
||
if (LMouseEvent = WM_LBUTTONUP) or (LMouseEvent = WM_LBUTTONDBLCLK) then
|
||
begin
|
||
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||
end
|
||
else if (LMouseEvent = WM_RBUTTONUP) or (LMouseEvent = WM_CONTEXTMENU) then
|
||
begin
|
||
ShowTrayMenu;
|
||
end;
|
||
end
|
||
else if AMsg.Msg = WM_WTSSESSION_CHANGE then
|
||
begin
|
||
if AMsg.WParam = WTS_SESSION_LOCK then
|
||
if Assigned(FOnSystemLock) then FOnSystemLock();
|
||
end
|
||
|
||
else if AMsg.Msg = WM_POWERBROADCAST then
|
||
begin
|
||
// Sleep/hibernate fires PBT_APMSUSPEND. Treat it identically to a
|
||
// session lock: the user is leaving the machine unattended, so the
|
||
// vault must be locked. Without this, closing a laptop lid (which
|
||
// doesn't always trigger WTS_SESSION_LOCK if the system goes straight
|
||
// to sleep) would leave the decrypted state in memory until resume.
|
||
// PBT_APMSUSPEND is delivered SYNCHRONOUSLY before the system
|
||
// suspends — fast handler required (no UI prompts, no network).
|
||
if AMsg.WParam = PBT_APMSUSPEND then
|
||
if Assigned(FOnSystemLock) then FOnSystemLock();
|
||
end
|
||
|
||
else if (AMsg.Msg = WM_QUERYENDSESSION) or (AMsg.Msg = WM_ENDSESSION) then
|
||
begin
|
||
// Windows is logging off / shutting down / restarting. Flip the flag
|
||
// so FormCloseQuery lets the form actually close instead of
|
||
// minimizing to tray — otherwise Windows force-kills us after the
|
||
// shutdown timeout and SQLite's WAL/SHM never get checkpointed.
|
||
// Return TRUE (do not block shutdown). DefWindowProc returns TRUE
|
||
// by default for WM_QUERYENDSESSION, so we just don't assign Result.
|
||
FShutdownPending := True;
|
||
AMsg.Result := 1;
|
||
end
|
||
|
||
else if (AMsg.Msg <> 0) and (AMsg.Msg = WM_PMShowMessage) then
|
||
begin
|
||
// A second instance was launched and PostMessage'd HWND_BROADCAST.
|
||
// Bring our window back to the front instead of letting that second
|
||
// process spawn its own UI.
|
||
if Assigned(FOnTrayRestore) then FOnTrayRestore();
|
||
end
|
||
|
||
else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = DEBUG_HOTKEY_ID) then
|
||
begin
|
||
if Assigned(FOnDebugHotkey) then FOnDebugHotkey();
|
||
end
|
||
|
||
else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = NEW_ENTRY_HOTKEY_ID) then
|
||
FireNewEntryRequest
|
||
|
||
else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = QUICK_SEARCH_HOTKEY_ID) then
|
||
FireQuickSearchRequest
|
||
|
||
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
|
||
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
|
||
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY) or
|
||
(AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP)) then
|
||
begin
|
||
// Capture the foreground window BEFORE any focus change, then fire the
|
||
// callback so the host can match the title against vault entries.
|
||
var LKind: TAutofillKind;
|
||
if AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY then
|
||
LKind := akPasswordOnly
|
||
else if AMsg.WParam = AUTOFILL_HOTKEY_ID_TOTP then
|
||
LKind := akTotp
|
||
else
|
||
LKind := akFull;
|
||
FireAutofillRequest(LKind);
|
||
end;
|
||
|
||
AMsg.Result := DefWindowProc(FMsgWindow, AMsg.Msg, AMsg.WParam, AMsg.LParam);
|
||
end;
|
||
|
||
// =============================================================================
|
||
// TPMBridge — Autofill hotkey + SendInput
|
||
// =============================================================================
|
||
|
||
function TPMBridge.SetAutofillHotkeys(AFullMods, AFullVk,
|
||
APwdMods, APwdVk, ATotpMods, ATotpVk: Word): Boolean;
|
||
begin
|
||
// Tear down whatever is currently registered before installing the new
|
||
// combos. RegisterHotKey would fail if the same ID is already taken.
|
||
if FAutofillFullActive then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_FULL);
|
||
FAutofillFullActive := False;
|
||
end;
|
||
if FAutofillPwdActive then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
||
FAutofillPwdActive := False;
|
||
end;
|
||
if FAutofillTotpActive then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
|
||
FAutofillTotpActive := False;
|
||
end;
|
||
|
||
// Best-effort registration. A failure (typically MOD_x clash with another
|
||
// app's global hotkey) is silent: the other slot can still be live.
|
||
if (AFullVk <> 0) and (AFullMods <> 0) then
|
||
FAutofillFullActive := RegisterHotKey(FMsgWindow,
|
||
AUTOFILL_HOTKEY_ID_FULL, AFullMods, AFullVk);
|
||
if (APwdVk <> 0) and (APwdMods <> 0) then
|
||
FAutofillPwdActive := RegisterHotKey(FMsgWindow,
|
||
AUTOFILL_HOTKEY_ID_PWDONLY, APwdMods, APwdVk);
|
||
if (ATotpVk <> 0) and (ATotpMods <> 0) then
|
||
FAutofillTotpActive := RegisterHotKey(FMsgWindow,
|
||
AUTOFILL_HOTKEY_ID_TOTP, ATotpMods, ATotpVk);
|
||
|
||
FAutofillRegistered := FAutofillFullActive or FAutofillPwdActive
|
||
or FAutofillTotpActive;
|
||
// An unconfigured 2FA combo (vk=0) must not report a registration failure.
|
||
Result := FAutofillFullActive and FAutofillPwdActive
|
||
and (FAutofillTotpActive or (ATotpVk = 0));
|
||
end;
|
||
|
||
function TPMBridge.SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
||
begin
|
||
if FQuickSearchHotkeyRegistered then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, QUICK_SEARCH_HOTKEY_ID);
|
||
FQuickSearchHotkeyRegistered := False;
|
||
end;
|
||
if (AVk <> 0) and (AMods <> 0) then
|
||
FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow,
|
||
QUICK_SEARCH_HOTKEY_ID, AMods, AVk);
|
||
Result := FQuickSearchHotkeyRegistered;
|
||
end;
|
||
|
||
function TPMBridge.SetNewEntryHotkey(AMods, AVk: Word): Boolean;
|
||
begin
|
||
if FNewEntryHotkeyRegistered then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID);
|
||
FNewEntryHotkeyRegistered := False;
|
||
end;
|
||
if (AVk <> 0) and (AMods <> 0) then
|
||
begin
|
||
FNewEntryHotkeyMods := AMods;
|
||
FNewEntryHotkeyVk := AVk;
|
||
FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow,
|
||
NEW_ENTRY_HOTKEY_ID, AMods, AVk);
|
||
end;
|
||
Result := FNewEntryHotkeyRegistered;
|
||
end;
|
||
|
||
procedure TPMBridge.ApplyTitleBarTheme(ADark: Boolean);
|
||
const
|
||
DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||
// uxtheme.dll private API, stable since Win10 1809. File Explorer, Edge
|
||
// and Office use this to opt their UI (including popup menus, scrollbars,
|
||
// tooltips) into dark mode. Signature changed in 1903 to take an enum:
|
||
// 0=Default 1=AllowDark 2=ForceDark 3=ForceLight 4=Max
|
||
// We use ForceDark / ForceLight for unambiguous behaviour.
|
||
APPMODE_DEFAULT = 0;
|
||
APPMODE_FORCE_DARK = 2;
|
||
APPMODE_FORCE_LIGHT = 3;
|
||
type
|
||
TDwmSetWindowAttribute = function(hwnd: HWND; dwAttribute: DWORD;
|
||
pvAttribute: Pointer; cbAttribute: DWORD): HRESULT; stdcall;
|
||
TSetPreferredAppMode = function(AppMode: Integer): Integer; stdcall;
|
||
TFlushMenuThemes = procedure; stdcall;
|
||
var
|
||
DwmLib, UxLib: HMODULE;
|
||
DwmSetWindowAttribute: TDwmSetWindowAttribute;
|
||
SetPreferredAppMode: TSetPreferredAppMode;
|
||
FlushMenuThemes: TFlushMenuThemes;
|
||
DarkFlag: BOOL;
|
||
FormHwnd: HWND;
|
||
begin
|
||
FDarkTheme := ADark;
|
||
// La barrette ne suit aucun theme systeme : c'est nous qui la peignons, donc
|
||
// c'est nous qui la redessinons quand la page bascule.
|
||
if FLazyWnd <> 0 then
|
||
RedrawWindow(FLazyWnd, nil, 0,
|
||
RDW_INVALIDATE or RDW_ERASE or RDW_ALLCHILDREN or RDW_UPDATENOW);
|
||
|
||
if FMainForm = nil then Exit;
|
||
FormHwnd := MainFormHWND(FMainForm);
|
||
if FormHwnd = 0 then Exit;
|
||
|
||
// 1. Title bar (DWM immersive dark mode).
|
||
DwmLib := LoadLibrary('dwmapi.dll');
|
||
if DwmLib <> 0 then
|
||
try
|
||
@DwmSetWindowAttribute := GetProcAddress(DwmLib, 'DwmSetWindowAttribute');
|
||
if Assigned(DwmSetWindowAttribute) then
|
||
begin
|
||
DarkFlag := ADark;
|
||
DwmSetWindowAttribute(FormHwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||
@DarkFlag, SizeOf(DarkFlag));
|
||
end;
|
||
finally
|
||
FreeLibrary(DwmLib);
|
||
end;
|
||
|
||
// 2. App-wide preferred mode (themes popup menus, scrollbars, tooltips).
|
||
// Loaded by ordinal because the functions are not exported by name.
|
||
UxLib := LoadLibrary('uxtheme.dll');
|
||
if UxLib <> 0 then
|
||
try
|
||
@SetPreferredAppMode := GetProcAddress(UxLib, MAKEINTRESOURCE(135));
|
||
@FlushMenuThemes := GetProcAddress(UxLib, MAKEINTRESOURCE(136));
|
||
if Assigned(SetPreferredAppMode) then
|
||
begin
|
||
if ADark then SetPreferredAppMode(APPMODE_FORCE_DARK)
|
||
else SetPreferredAppMode(APPMODE_FORCE_LIGHT);
|
||
if Assigned(FlushMenuThemes) then FlushMenuThemes;
|
||
end;
|
||
finally
|
||
FreeLibrary(UxLib);
|
||
end;
|
||
end;
|
||
|
||
procedure TPMBridge.RegisterAutofillHotkey;
|
||
begin
|
||
// Convenience defaults — Ctrl+Shift+L (full), Ctrl+Shift+P (password),
|
||
// Ctrl+Shift+T (2FA code).
|
||
// Idempotent: calling twice with the same combos is harmless.
|
||
SetAutofillHotkeys(AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('L'),
|
||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('P'),
|
||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('T'));
|
||
end;
|
||
|
||
procedure TPMBridge.UnregisterAutofillHotkey;
|
||
begin
|
||
if FAutofillFullActive then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_FULL);
|
||
FAutofillFullActive := False;
|
||
end;
|
||
if FAutofillPwdActive then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_PWDONLY);
|
||
FAutofillPwdActive := False;
|
||
end;
|
||
if FAutofillTotpActive then
|
||
begin
|
||
UnregisterHotKey(FMsgWindow, AUTOFILL_HOTKEY_ID_TOTP);
|
||
FAutofillTotpActive := False;
|
||
end;
|
||
FAutofillRegistered := False;
|
||
end;
|
||
|
||
// Block until the user releases Ctrl, Shift, Alt, and Win, or until ATimeoutMs
|
||
// elapses. Without this, an autofill triggered by Ctrl+Shift+L injects
|
||
// keystrokes WHILE Ctrl+Shift are physically held — turning our Tab into
|
||
// Ctrl+Tab (next tab in Chrome), our 's' into Ctrl+S, etc. 1000 ms is a
|
||
// generous bound; typical release happens within 50-150 ms.
|
||
procedure WaitForModifierRelease(ATimeoutMs: Cardinal);
|
||
var
|
||
LStart: Cardinal;
|
||
begin
|
||
LStart := GetTickCount;
|
||
while ((GetAsyncKeyState(VK_CONTROL) and $8000) <> 0)
|
||
or ((GetAsyncKeyState(VK_SHIFT) and $8000) <> 0)
|
||
or ((GetAsyncKeyState(VK_MENU) and $8000) <> 0) // Alt
|
||
or ((GetAsyncKeyState(VK_LWIN) and $8000) <> 0)
|
||
or ((GetAsyncKeyState(VK_RWIN) and $8000) <> 0) do
|
||
begin
|
||
Sleep(15);
|
||
if GetTickCount - LStart > ATimeoutMs then Break;
|
||
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);
|
||
var
|
||
LInputs: TArray<TInput>;
|
||
LCount, I: Integer;
|
||
begin
|
||
if AText = '' then Exit;
|
||
SetLength(LInputs, Length(AText) * 2);
|
||
LCount := 0;
|
||
for I := 1 to Length(AText) do
|
||
begin
|
||
FillChar(LInputs[LCount], SizeOf(TInput), 0);
|
||
FillChar(LInputs[LCount + 1], SizeOf(TInput), 0);
|
||
LInputs[LCount].Itype := INPUT_KEYBOARD;
|
||
LInputs[LCount].ki.wScan := Ord(AText[I]);
|
||
LInputs[LCount].ki.dwFlags := KEYEVENTF_UNICODE;
|
||
LInputs[LCount + 1] := LInputs[LCount];
|
||
LInputs[LCount + 1].ki.dwFlags := KEYEVENTF_UNICODE or KEYEVENTF_KEYUP;
|
||
Inc(LCount, 2);
|
||
end;
|
||
SendInput(LCount, @LInputs[0], SizeOf(TInput));
|
||
end;
|
||
|
||
// Send one virtual-key press (down+up).
|
||
procedure SendVKey(AVk: Word);
|
||
var
|
||
LInputs: array[0..1] of TInput;
|
||
begin
|
||
FillChar(LInputs, SizeOf(LInputs), 0);
|
||
LInputs[0].Itype := INPUT_KEYBOARD;
|
||
LInputs[0].ki.wVk := AVk;
|
||
LInputs[1] := LInputs[0];
|
||
LInputs[1].ki.dwFlags := KEYEVENTF_KEYUP;
|
||
SendInput(2, @LInputs[0], SizeOf(TInput));
|
||
end;
|
||
|
||
procedure SendSelectAllAndDelete;
|
||
var
|
||
LInputs: array[0..5] of TInput;
|
||
begin
|
||
// This is the ONLY real-VK chord we inject (password chars go out as
|
||
// KEYEVENTF_UNICODE / VK_PACKET, which can't match a RegisterHotKey).
|
||
// If the user re-presses Ctrl+Shift mid-sequence, our VK_A below becomes
|
||
// a physical Ctrl+Shift+A = our own new-entry hotkey firing mid-fill.
|
||
// Force-release right here, at the risky instant — not just once up front.
|
||
ForceReleaseModifiers;
|
||
FillChar(LInputs, SizeOf(LInputs), 0);
|
||
LInputs[0].Itype := INPUT_KEYBOARD;
|
||
LInputs[0].ki.wVk := VK_CONTROL;
|
||
LInputs[1].Itype := INPUT_KEYBOARD;
|
||
LInputs[1].ki.wVk := Ord('A');
|
||
LInputs[2].Itype := INPUT_KEYBOARD;
|
||
LInputs[2].ki.wVk := Ord('A');
|
||
LInputs[2].ki.dwFlags := KEYEVENTF_KEYUP;
|
||
LInputs[3].Itype := INPUT_KEYBOARD;
|
||
LInputs[3].ki.wVk := VK_CONTROL;
|
||
LInputs[3].ki.dwFlags := KEYEVENTF_KEYUP;
|
||
LInputs[4].Itype := INPUT_KEYBOARD;
|
||
LInputs[4].ki.wVk := VK_DELETE;
|
||
LInputs[5] := LInputs[4];
|
||
LInputs[5].ki.dwFlags := KEYEVENTF_KEYUP;
|
||
SendInput(6, @LInputs[0], SizeOf(TInput));
|
||
end;
|
||
|
||
// Always-attach foreground switch. The early SetForegroundWindow shortcut
|
||
// was unreliable after the picker click — Win10/11 still refused the focus
|
||
// hand-off even when our process was foreground. Always doing the attach
|
||
// dance is slightly slower but actually works.
|
||
function ForceForegroundWindow(ATargetHwnd: HWND): Boolean;
|
||
const
|
||
ForegroundPollIntervalMs = 20;
|
||
ForegroundPollTimeoutMs = 600;
|
||
var
|
||
CallerThread, TargetThread, TargetPid: DWORD;
|
||
ThreadsAttached: Boolean;
|
||
WaitStart: Cardinal;
|
||
begin
|
||
Result := False;
|
||
if (ATargetHwnd = 0) or not IsWindow(ATargetHwnd) then Exit;
|
||
|
||
TargetPid := 0;
|
||
TargetThread := GetWindowThreadProcessId(ATargetHwnd, TargetPid);
|
||
CallerThread := GetCurrentThreadId;
|
||
if TargetThread = 0 then Exit;
|
||
|
||
ThreadsAttached := (TargetThread <> CallerThread) and
|
||
AttachThreadInput(CallerThread, TargetThread, True);
|
||
try
|
||
if IsIconic(ATargetHwnd) then
|
||
ShowWindow(ATargetHwnd, SW_RESTORE);
|
||
BringWindowToTop(ATargetHwnd);
|
||
SetWindowPos(ATargetHwnd, HWND_TOP, 0, 0, 0, 0,
|
||
SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE);
|
||
SetForegroundWindow(ATargetHwnd);
|
||
|
||
WaitStart := GetTickCount;
|
||
while GetForegroundWindow <> ATargetHwnd do
|
||
begin
|
||
if GetTickCount - WaitStart > ForegroundPollTimeoutMs then Break;
|
||
Sleep(ForegroundPollIntervalMs);
|
||
SetForegroundWindow(ATargetHwnd);
|
||
end;
|
||
Result := GetForegroundWindow = ATargetHwnd;
|
||
finally
|
||
if ThreadsAttached then
|
||
AttachThreadInput(CallerThread, TargetThread, False);
|
||
end;
|
||
end;
|
||
|
||
// True when the process owning AHwnd runs elevated (admin). UIPI silently
|
||
// DISCARDS SendInput from a non-elevated process into an elevated one —
|
||
// SendInput even reports success — so detecting elevation up front is the
|
||
// only way to tell the user the fill can't work instead of lying "sent".
|
||
// Can't-tell (OpenProcess denied, which protected/elevated processes do)
|
||
// counts as elevated: better an honest "blocked" than a silent no-op.
|
||
function IsWindowProcessElevated(AHwnd: HWND): Boolean;
|
||
const
|
||
// Missing from Winapi.Windows — Win32 constant (WinNT.h, Vista+).
|
||
PROCESS_QUERY_LIMITED_INFORMATION = $1000;
|
||
var
|
||
LPid: DWORD;
|
||
LProc, LToken: THandle;
|
||
LElev: TOKEN_ELEVATION;
|
||
LLen: DWORD;
|
||
begin
|
||
Result := False;
|
||
LPid := 0;
|
||
GetWindowThreadProcessId(AHwnd, LPid);
|
||
if LPid = 0 then Exit;
|
||
LProc := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, LPid);
|
||
if LProc = 0 then Exit(True);
|
||
try
|
||
if not OpenProcessToken(LProc, TOKEN_QUERY, LToken) then Exit(True);
|
||
try
|
||
LLen := 0;
|
||
if GetTokenInformation(LToken, TokenElevation, @LElev, SizeOf(LElev), LLen) then
|
||
Result := LElev.TokenIsElevated <> 0;
|
||
finally
|
||
CloseHandle(LToken);
|
||
end;
|
||
finally
|
||
CloseHandle(LProc);
|
||
end;
|
||
end;
|
||
|
||
procedure ClickTargetCenterToGrabFocus(ATargetHwnd: HWND);
|
||
const
|
||
PostClickSettleMs = 40;
|
||
var
|
||
WindowRect: TRect;
|
||
CenterAbsX, CenterAbsY, ScreenW, ScreenH: Integer;
|
||
SavedCursor: TPoint;
|
||
MouseInputs: array[0..2] of TInput;
|
||
begin
|
||
if (ATargetHwnd = 0) or not IsWindow(ATargetHwnd) then Exit;
|
||
if not GetWindowRect(ATargetHwnd, WindowRect) then Exit;
|
||
|
||
CenterAbsX := (WindowRect.Left + WindowRect.Right) div 2;
|
||
CenterAbsY := (WindowRect.Top + WindowRect.Bottom) div 2;
|
||
ScreenW := GetSystemMetrics(SM_CXSCREEN);
|
||
ScreenH := GetSystemMetrics(SM_CYSCREEN);
|
||
if (ScreenW <= 0) or (ScreenH <= 0) then Exit;
|
||
|
||
GetCursorPos(SavedCursor);
|
||
|
||
FillChar(MouseInputs, SizeOf(MouseInputs), 0);
|
||
MouseInputs[0].Itype := INPUT_MOUSE;
|
||
MouseInputs[0].mi.dx := (CenterAbsX * 65535) div ScreenW;
|
||
MouseInputs[0].mi.dy := (CenterAbsY * 65535) div ScreenH;
|
||
MouseInputs[0].mi.dwFlags:= MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE or MOUSEEVENTF_LEFTDOWN;
|
||
MouseInputs[1] := MouseInputs[0];
|
||
MouseInputs[1].mi.dwFlags:= MOUSEEVENTF_LEFTUP;
|
||
MouseInputs[2].Itype := INPUT_MOUSE;
|
||
MouseInputs[2].mi.dx := (SavedCursor.X * 65535) div ScreenW;
|
||
MouseInputs[2].mi.dy := (SavedCursor.Y * 65535) div ScreenH;
|
||
MouseInputs[2].mi.dwFlags:= MOUSEEVENTF_ABSOLUTE or MOUSEEVENTF_MOVE;
|
||
SendInput(3, @MouseInputs[0], SizeOf(TInput));
|
||
|
||
Sleep(PostClickSettleMs);
|
||
end;
|
||
|
||
function TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
|
||
const AUsername, APassword: string; AUsernameOnly: Boolean = False;
|
||
ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean;
|
||
const
|
||
MinimizeSettleMs = 80;
|
||
FocusSettleDelayMs = 120;
|
||
|
||
// Ctrl+A + Del to empty the target field — skipped when the user turned
|
||
// "Clear the field before typing" off (Ctrl+A isn't select-all everywhere:
|
||
// terminals, some remote desktops).
|
||
procedure ClearFieldIfWanted;
|
||
begin
|
||
if not AClearFirst then Exit;
|
||
SendSelectAllAndDelete;
|
||
Sleep(60);
|
||
end;
|
||
|
||
var
|
||
OwnFormHwnd: HWND;
|
||
begin
|
||
Result := False;
|
||
OwnFormHwnd := MainFormHWND(FMainForm);
|
||
|
||
// UIPI: keystrokes into an elevated target are silently dropped by Windows
|
||
// (SendInput even claims success). Detect it up front and report failure so
|
||
// the UI can say "blocked" instead of a false "password sent".
|
||
if (ATargetHWND <> 0) and IsWindowProcessElevated(ATargetHWND) then Exit;
|
||
|
||
// ARestoreAfter (window was open before the hotkey): DON'T minimize at all.
|
||
// Being the foreground process is precisely what lets us hand the focus to
|
||
// the target via ForceForegroundWindow — the window just stays where it is,
|
||
// beside the target. (The old minimize-then-restore flickered and sometimes
|
||
// lost the restore race.) Without ARestoreAfter (tray-origin flow), the
|
||
// restored window is a temporary overlay: shove it out of the way as before
|
||
// — the caller trays it after the fill anyway.
|
||
if (GetForegroundWindow = OwnFormHwnd) and (not ARestoreAfter) then
|
||
begin
|
||
ShowWindow(OwnFormHwnd, SW_MINIMIZE);
|
||
Sleep(MinimizeSettleMs);
|
||
end;
|
||
|
||
// LES DEUX SEULES ATTENTES QUI PEUVENT ENFLER, mesurees separement.
|
||
//
|
||
// Le total d'un remplissage est d'environ 660 ms par construction - des
|
||
// Sleep fixes. Quand il depasse plusieurs secondes, c'est forcement l'une de
|
||
// ces deux-la : la prise du premier plan, qui sonde jusqu'a 600 ms, ou
|
||
// l'attente de la relache des modificateurs, jusqu'a 1000 ms. Les confondre
|
||
// ferait chercher du cote du moteur web, qui n'y est pour rien.
|
||
var LSw := TStopwatch.StartNew;
|
||
if ATargetHWND <> 0 then
|
||
ForceForegroundWindow(ATargetHWND);
|
||
var LFgMs := LSw.ElapsedMilliseconds;
|
||
|
||
LSw := TStopwatch.StartNew;
|
||
WaitForModifierRelease(1000);
|
||
ForceReleaseModifiers; // timeout hit with keys still down → clean state
|
||
FLastFillWaits := Format('foreground=%d ms, modifiers=%d ms',
|
||
[LFgMs, LSw.ElapsedMilliseconds]);
|
||
Sleep(FocusSettleDelayMs);
|
||
|
||
// Never type into our own window: if the target refused the foreground,
|
||
// the keystrokes would land in the vault UI itself — a password typed
|
||
// into a visible search box. Bail instead.
|
||
if GetForegroundWindow = OwnFormHwnd then Exit;
|
||
|
||
// Past every bail-out — the keystrokes below are the fill itself.
|
||
Result := True;
|
||
|
||
// Our own new-entry hotkey is Ctrl+Shift+A. The clear-field step injects a
|
||
// real Ctrl+A; if the user still physically holds Ctrl+Shift (they just
|
||
// pressed the autofill hotkey, and auto-repeat re-presses the modifiers
|
||
// faster than ForceReleaseModifiers can clear them), the system sees
|
||
// Ctrl+Shift+A and fires new-entry mid-fill. Unregister it for the duration
|
||
// of the SendInput so no WM_HOTKEY is generated; re-register in finally.
|
||
UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID);
|
||
try
|
||
// Username-only: type just the username into the focused field, no Tab,
|
||
// no password. Used by the quick-search right-click / Shift+Enter path.
|
||
if AUsernameOnly then
|
||
begin
|
||
ClearFieldIfWanted;
|
||
SendUnicodeString(AUsername);
|
||
Exit;
|
||
end;
|
||
|
||
if AUsername = '' then
|
||
begin
|
||
ClearFieldIfWanted;
|
||
SendUnicodeString(APassword);
|
||
Exit;
|
||
end;
|
||
|
||
ClearFieldIfWanted;
|
||
SendUnicodeString(AUsername);
|
||
Sleep(200);
|
||
// Real-VK send: a physically held Shift here turns Tab into Shift+Tab —
|
||
// focus moves BACKWARD and the password lands in the username field.
|
||
ForceReleaseModifiers;
|
||
SendVKey(VK_TAB);
|
||
Sleep(200);
|
||
// No clear here: tabbing INTO an input already selects its whole content,
|
||
// so typing replaces it. Sending Ctrl+A as well is not just redundant — when
|
||
// Tab lands somewhere that is not an editable field (a link, a reveal
|
||
// button, the document itself, as on the Reddit login form) it selects the
|
||
// ENTIRE PAGE, and the password is then typed into nothing.
|
||
SendUnicodeString(APassword);
|
||
finally
|
||
// Re-register with the LIVE combo (user may have rebound it), not the
|
||
// hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A.
|
||
if FNewEntryHotkeyRegistered then
|
||
RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
|
||
FNewEntryHotkeyMods, FNewEntryHotkeyVk);
|
||
end;
|
||
end;
|
||
|
||
initialization
|
||
// Runs before Application.Initialize, so no window of ours exists yet and the
|
||
// answer is genuinely "the application the user launched us from".
|
||
GStartupForeground := GetForegroundWindow;
|
||
|
||
end.
|