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:
@@ -160,6 +160,32 @@ Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`.
|
||||
Combos autofill configurables depuis Settings (synced en DB
|
||||
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
|
||||
|
||||
Fichier `config.txt` à côté de l'exe (tous champs optionnels) :
|
||||
|
||||
@@ -142,6 +142,21 @@ type
|
||||
// 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;
|
||||
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 PrepareNid;
|
||||
procedure ShowTrayMenu;
|
||||
@@ -217,6 +232,9 @@ type
|
||||
ARestoreAfter: Boolean = False; AClearFirst: Boolean = True): Boolean;
|
||||
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
||||
property TrayAdded: Boolean read FTrayAdded;
|
||||
// Show or hide the lazy strip. Idempotent.
|
||||
procedure SetLazyBar(AOn: Boolean);
|
||||
function LazyBarVisible: Boolean;
|
||||
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
|
||||
@@ -353,6 +371,15 @@ const
|
||||
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_W = 148;
|
||||
LAZY_H = 40;
|
||||
|
||||
// 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.
|
||||
@@ -601,6 +628,7 @@ begin
|
||||
if FIconOwned and (FIconHandle <> 0) then
|
||||
DestroyIcon(FIconHandle);
|
||||
|
||||
if FLazyWnd <> 0 then DeallocateHWnd(FLazyWnd);
|
||||
DeallocateHWnd(FMsgWindow);
|
||||
FSecureClipboard.Free;
|
||||
inherited;
|
||||
@@ -879,6 +907,7 @@ const
|
||||
ID_LOCK = 2;
|
||||
ID_QUIT = 3;
|
||||
ID_QUICKSEARCH = 4;
|
||||
ID_LAZY = 5;
|
||||
var
|
||||
LMenu: HMENU;
|
||||
LPt: TPoint;
|
||||
@@ -890,6 +919,9 @@ begin
|
||||
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),
|
||||
ID_LAZY, 'Lazy mode (fill with the mouse)');
|
||||
AppendMenu(LMenu, MF_SEPARATOR, 0, nil);
|
||||
AppendMenu(LMenu, MF_STRING, ID_QUIT, 'Quit');
|
||||
|
||||
@@ -909,6 +941,7 @@ begin
|
||||
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;
|
||||
@@ -951,6 +984,121 @@ begin
|
||||
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);
|
||||
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);
|
||||
var
|
||||
LMouseEvent: Word;
|
||||
@@ -1078,23 +1226,7 @@ begin
|
||||
LKind := akTotp
|
||||
else
|
||||
LKind := akFull;
|
||||
var LTarget := GetForegroundWindow;
|
||||
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);
|
||||
FireAutofillRequest(LKind);
|
||||
end;
|
||||
|
||||
AMsg.Result := DefWindowProc(FMsgWindow, AMsg.Msg, AMsg.WParam, AMsg.LParam);
|
||||
|
||||
Reference in New Issue
Block a user