Files
Password-Manager/delphi-backend/Source/PM.Bridge.pas
T
r-zakarya 1ae1d482f0 fix(fillbar): remonte la barrette quand elle est recouverte, au lieu de la deplacer
Elle a le droit de se poser sur la barre des taches. Redemander HWND_TOPMOST
quand on l'est deja ne change rien : le shell restait devant au demarrage de
session et au menu Demarrer. Le passage NOTOPMOST -> TOPMOST force la remontee,
fait seulement quand le centre de la barrette appartient a une autre fenetre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:38:05 +01:00

2339 lines
93 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
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;
Result := (LHit <> AWnd) and (GetAncestor(LHit, GA_ROOT) <> AWnd);
end;
procedure LazyForceToTop(AWnd: HWND);
begin
SetWindowPos(AWnd, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE);
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.