feat(autofill): lazy mode - une barrette de deux boutons pour remplir a la souris

Menu du tray -> "Lazy mode". Deux boutons Full / Password qui flottent
au-dessus de tout et font ce que font Ctrl+Shift+L / Ctrl+Shift+P.

WS_EX_NOACTIVATE est toute l'astuce : une fenetre ordinaire prendrait le
premier plan au clic, le champ que l'utilisateur vient de cliquer perdrait
son curseur, et GetForegroundWindow nous designerait nous-memes comme cible.
Le clic ne deplace donc jamais le premier plan.

Le corps du handler WM_HOTKEY est extrait dans FireAutofillRequest : hotkey
et barrette partagent la capture de la cible, le matching de titre et le
picker. Un seul chemin, deux gestes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-26 15:42:49 +01:00
parent ba4ab18d0f
commit 09bbd3fe9e
2 changed files with 175 additions and 17 deletions
+26
View File
@@ -160,6 +160,32 @@ Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`.
Combos autofill configurables depuis Settings (synced en DB Combos autofill configurables depuis Settings (synced en DB
via `settings_json`). via `settings_json`).
## Lazy mode (remplir à la souris)
Menu du tray → **Lazy mode (fill with the mouse)** : une barrette de deux
boutons *Full* / *Password* qui flotte au-dessus de tout et fait exactement ce
que font `Ctrl+Shift+L` / `Ctrl+Shift+P`.
**`WS_EX_NOACTIVATE` est toute l'astuce.** Une fenêtre ordinaire prendrait le
premier plan au clic — le champ que l'utilisateur vient de cliquer perdrait son
curseur, et `GetForegroundWindow` nous désignerait nous-mêmes comme cible.
NOACTIVATE veut dire que le clic ne déplace jamais le premier plan : le
navigateur reste actif, son champ reste focalisé, et la capture voit exactement
ce que voit le chemin hotkey.
Détails :
- Fenêtre créée par un **second** `AllocateHWnd` (`FLazyWnd`), séparée de la
fenêtre message-only : `WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE`,
deux enfants `BUTTON` (IDs 101/102) → `WM_COMMAND` → `FireAutofillRequest`.
- `FireAutofillRequest(AKind)` est le corps **extrait** du handler `WM_HOTKEY` :
hotkey et barrette partagent capture de la cible, matching de titre et picker.
Un seul chemin, deux gestes.
- `WM_NCHITTEST` → `HTCAPTION` : toute la barrette est sa propre barre de titre
(elle finit par se poser sur le champ qu'elle remplit, il faut pouvoir la
pousser). `WM_ERASEBKGND` peint le fond — la classe `TPUtilWindow`
d'`AllocateHWnd` n'a **pas** de brosse de fond.
- Position : coin bas-droit de la *work area* au démarrage, **pas persistée**.
## Debug mode ## Debug mode
Fichier `config.txt` à côté de l'exe (tous champs optionnels) : Fichier `config.txt` à côté de l'exe (tous champs optionnels) :
+149 -17
View File
@@ -142,6 +142,21 @@ type
// by the host). // by the host).
FQuickSearchHotkeyRegistered: Boolean; FQuickSearchHotkeyRegistered: Boolean;
FOnQuickSearchHotkey: TAutofillRequestEvent; 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;
procedure LazyWindowHandler(var AMsg: TMessage);
// 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);
procedure MsgWindowHandler(var AMsg: TMessage); procedure MsgWindowHandler(var AMsg: TMessage);
procedure PrepareNid; procedure PrepareNid;
procedure ShowTrayMenu; procedure ShowTrayMenu;
@@ -217,6 +232,9 @@ type
ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean; ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean;
property SecureClipboard: TSecureClipboard read FSecureClipboard; property SecureClipboard: TSecureClipboard read FSecureClipboard;
property TrayAdded: Boolean read FTrayAdded; property TrayAdded: Boolean read FTrayAdded;
// Show or hide the lazy strip. Idempotent.
procedure SetLazyBar(AOn: Boolean);
function LazyBarVisible: Boolean;
property AutofillRegistered: Boolean read FAutofillRegistered; property AutofillRegistered: Boolean read FAutofillRegistered;
// Per-slot outcome of the last SetAutofillHotkeys — lets the host name // Per-slot outcome of the last SetAutofillHotkeys — lets the host name
// exactly which combo lost the race with another app's global hotkey // exactly which combo lost the race with another app's global hotkey
@@ -353,6 +371,15 @@ const
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT 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_W = 148;
LAZY_H = 40;
// Dynamic WTS function pointers — wtsapi32.dll is not guaranteed on all // Dynamic WTS function pointers — wtsapi32.dll is not guaranteed on all
// Windows SKUs (e.g. minimal Server Core without Session Services), so // Windows SKUs (e.g. minimal Server Core without Session Services), so
// we load it at runtime and tolerate absence gracefully. // we load it at runtime and tolerate absence gracefully.
@@ -601,6 +628,7 @@ begin
if FIconOwned and (FIconHandle <> 0) then if FIconOwned and (FIconHandle <> 0) then
DestroyIcon(FIconHandle); DestroyIcon(FIconHandle);
if FLazyWnd <> 0 then DeallocateHWnd(FLazyWnd);
DeallocateHWnd(FMsgWindow); DeallocateHWnd(FMsgWindow);
FSecureClipboard.Free; FSecureClipboard.Free;
inherited; inherited;
@@ -879,6 +907,7 @@ const
ID_LOCK = 2; ID_LOCK = 2;
ID_QUIT = 3; ID_QUIT = 3;
ID_QUICKSEARCH = 4; ID_QUICKSEARCH = 4;
ID_LAZY = 5;
var var
LMenu: HMENU; LMenu: HMENU;
LPt: TPoint; LPt: TPoint;
@@ -890,6 +919,9 @@ begin
AppendMenu(LMenu, MF_STRING, ID_OPEN, 'Open'); AppendMenu(LMenu, MF_STRING, ID_OPEN, 'Open');
AppendMenu(LMenu, MF_STRING, ID_QUICKSEARCH, 'Quick search…'); AppendMenu(LMenu, MF_STRING, ID_QUICKSEARCH, 'Quick search…');
AppendMenu(LMenu, MF_STRING, ID_LOCK, 'Lock vault'); AppendMenu(LMenu, MF_STRING, ID_LOCK, 'Lock vault');
AppendMenu(LMenu,
MF_STRING or IfThen(LazyBarVisible, MF_CHECKED, MF_UNCHECKED),
ID_LAZY, 'Lazy mode (fill with the mouse)');
AppendMenu(LMenu, MF_SEPARATOR, 0, nil); AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
AppendMenu(LMenu, MF_STRING, ID_QUIT, 'Quit'); AppendMenu(LMenu, MF_STRING, ID_QUIT, 'Quit');
@@ -909,6 +941,7 @@ begin
case LCmd of case LCmd of
ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore(); ID_OPEN: if Assigned(FOnTrayRestore) then FOnTrayRestore();
ID_QUICKSEARCH: if Assigned(FOnQuickSearchRequest) then FOnQuickSearchRequest(); ID_QUICKSEARCH: if Assigned(FOnQuickSearchRequest) then FOnQuickSearchRequest();
ID_LAZY: SetLazyBar(not LazyBarVisible);
ID_LOCK: if Assigned(FOnLockRequest) then FOnLockRequest(); ID_LOCK: if Assigned(FOnLockRequest) then FOnLockRequest();
ID_QUIT: if Assigned(FOnQuit) then FOnQuit(); ID_QUIT: if Assigned(FOnQuit) then FOnQuit();
end; end;
@@ -951,6 +984,121 @@ begin
Result := False; Result := False;
end; 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);
var
LTarget: HWND;
LTitle: string;
LLen: Integer;
begin
if not Assigned(FOnAutofillRequest) then Exit;
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;
function TPMBridge.LazyBarVisible: Boolean;
begin
Result := FLazyWnd <> 0;
end;
procedure TPMBridge.LazyWindowHandler(var AMsg: TMessage);
var
LRect: TRect;
begin
case AMsg.Msg of
// 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);
FillRect(HDC(AMsg.WParam), LRect, HBRUSH(COLOR_BTNFACE + 1));
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);
LAZY_ID_PWD: FireAutofillRequest(akPasswordOnly);
end;
AMsg.Result := 0;
Exit;
end;
end;
AMsg.Result := DefWindowProc(FLazyWnd, AMsg.Msg, AMsg.WParam, AMsg.LParam);
end;
procedure TPMBridge.SetLazyBar(AOn: Boolean);
procedure MakeButton(AId: Integer; const ACaption: string; AX: Integer);
var
LBtn: HWND;
begin
LBtn := CreateWindow('BUTTON', PChar(ACaption),
WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON, AX, 5, 66, 28,
FLazyWnd, HMENU(AId), HInstance, nil);
// Without this the button wears the 1995 system font.
if LBtn <> 0 then
SendMessage(LBtn, WM_SETFONT, WPARAM(GetStockObject(DEFAULT_GUI_FONT)), 1);
end;
var
LWork: TRect;
begin
if AOn = LazyBarVisible then Exit;
if not AOn then
begin
DeallocateHWnd(FLazyWnd); // the child buttons go with their parent
FLazyWnd := 0;
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);
SetWindowLong(FLazyWnd, GWL_STYLE,
GetWindowLong(FLazyWnd, GWL_STYLE) or WS_BORDER);
MakeButton(LAZY_ID_FULL, 'Full', 5);
MakeButton(LAZY_ID_PWD, 'Password', 77);
// 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));
SetWindowPos(FLazyWnd, HWND_TOPMOST,
LWork.Right - LAZY_W - 24, LWork.Bottom - LAZY_H - 24, LAZY_W, LAZY_H,
SWP_NOACTIVATE or SWP_SHOWWINDOW);
end;
procedure TPMBridge.MsgWindowHandler(var AMsg: TMessage); procedure TPMBridge.MsgWindowHandler(var AMsg: TMessage);
var var
LMouseEvent: Word; LMouseEvent: Word;
@@ -1078,23 +1226,7 @@ begin
LKind := akTotp LKind := akTotp
else else
LKind := akFull; LKind := akFull;
var LTarget := GetForegroundWindow; FireAutofillRequest(LKind);
CaptureForegroundOwner;
// Hiding to the tray does not hand the foreground to anyone else, so ours
// stays foreground while invisible: from there the hotkey 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, then press the shortcut.', True);
AMsg.Result := DefWindowProc(FMsgWindow, AMsg.Msg, AMsg.WParam, AMsg.LParam);
Exit;
end;
var LTitle: string;
SetLength(LTitle, 512);
var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
SetLength(LTitle, LLen);
FOnAutofillRequest(LKind, LTarget, LTitle);
end; end;
AMsg.Result := DefWindowProc(FMsgWindow, AMsg.Msg, AMsg.WParam, AMsg.LParam); AMsg.Result := DefWindowProc(FMsgWindow, AMsg.Msg, AMsg.WParam, AMsg.LParam);