feat: Ctrl+Shift+Q quick-search-fill + Edge browser directive

Ctrl+Shift+Q quick-search + autofill
- New global hotkey: capture the foreground HWND, restore the window
  if hidden, pop the quick-search modal in "fill mode". On pick, the
  password is SendInput'd into the saved HWND — no clipboard touch.
- hide_after flag added to cmd://autofill/execute: when set (tray-mode
  hotkey), Delphi MinimizeToTray's *after* SendInput completes. Hiding
  before SendInput would trip Win10/11 anti-focus-stealing rules and
  block focus handoff to the target.
- Quick-search modal hint text adapts to fill vs copy mode.
- Esc / close in fill mode sends cmd://autofill/cancel so a stale
  HWND doesn't get reused by an unrelated Ctrl+Shift+L later.

Compile-time browser engine switch
- {.$DEFINE USE_EDGE_BROWSER} in UMainForm.pas selects between
  TTMSFNCWebBrowser (default, cross-platform abstraction) and
  TTMSFNCEdgeWebBrowser (Windows-only WebView2 wrapper). Both
  inherit from TTMSFNCCustomWebBrowser so the bridge cmd:// glue is
  unchanged; the field type is a conditional alias TWebBrowserClass.
- WebBrowser is created dynamically in FormCreate so neither variant
  needs a second .fmx. Events are wired BEFORE Parent assignment so
  OnInitialized doesn't race the WebView2 async init on fast/pre-warmed
  Edge installs (was silently missing the disable-context-menu /
  disable-accelerator-keys calls).
- Native context menu disabled by assigning an empty PopupMenu1 (works
  for both backends, unlike OnGetContextMenu which is publish-gated
  via {$IFNDEF FNCLIB} on TTMSFNCWebBrowser).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:31:12 +01:00
parent f047fba9a3
commit 39406d712e
5 changed files with 252 additions and 22 deletions
+33
View File
@@ -109,6 +109,12 @@ type
FOnDebugHotkey: TProc; FOnDebugHotkey: TProc;
FNewEntryHotkeyRegistered: Boolean; FNewEntryHotkeyRegistered: Boolean;
FOnNewEntryHotkey: TNewEntryHotkeyEvent; 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;
procedure MsgWindowHandler(var AMsg: TMessage); procedure MsgWindowHandler(var AMsg: TMessage);
procedure PrepareNid; procedure PrepareNid;
procedure ShowTrayMenu; procedure ShowTrayMenu;
@@ -183,6 +189,11 @@ type
// browser suffix by the JS layer before pre-fill). // browser suffix by the JS layer before pre-fill).
property OnNewEntryHotkey: TNewEntryHotkeyEvent property OnNewEntryHotkey: TNewEntryHotkeyEvent
read FOnNewEntryHotkey write FOnNewEntryHotkey; 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; end;
implementation implementation
@@ -227,6 +238,7 @@ const
AUTOFILL_HOTKEY_ID_PWDONLY = 43; // Ctrl+Shift+P → password only AUTOFILL_HOTKEY_ID_PWDONLY = 43; // Ctrl+Shift+P → password only
DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel DEBUG_HOTKEY_ID = 44; // Ctrl+Shift+D → toggle debug panel
NEW_ENTRY_HOTKEY_ID = 45; // Ctrl+Shift+A → quick-add from window title 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
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
@@ -422,6 +434,8 @@ begin
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D')); AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D'));
FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID, FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A')); AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A'));
FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow,
QUICK_SEARCH_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('Q'));
end; end;
destructor TPMBridge.Destroy; destructor TPMBridge.Destroy;
@@ -432,6 +446,9 @@ begin
if FNewEntryHotkeyRegistered then if FNewEntryHotkeyRegistered then
UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID); UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID);
if FQuickSearchHotkeyRegistered then
UnregisterHotKey(FMsgWindow, QUICK_SEARCH_HOTKEY_ID);
if (FPowerNotify <> 0) and Assigned(_PowerUnregister) then if (FPowerNotify <> 0) and Assigned(_PowerUnregister) then
_PowerUnregister(FPowerNotify); _PowerUnregister(FPowerNotify);
@@ -739,6 +756,22 @@ begin
end; end;
end end
else if (AMsg.Msg = WM_HOTKEY) and (AMsg.WParam = QUICK_SEARCH_HOTKEY_ID) then
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 Assigned(FOnQuickSearchHotkey) then
begin
var LTarget := GetForegroundWindow;
var LTitle: string;
SetLength(LTitle, 512);
var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
SetLength(LTitle, LLen);
FOnQuickSearchHotkey(akPasswordOnly, LTarget, LTitle);
end;
end
else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and else if (AMsg.Msg = WM_HOTKEY) and Assigned(FOnAutofillRequest) and
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or ((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then (AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then
+3 -7
View File
@@ -123,12 +123,8 @@ object MainForm: TMainForm
Size.Height = 6.000000000000000000 Size.Height = 6.000000000000000000
Size.PlatformDefault = False Size.PlatformDefault = False
end end
object WebBrowser: TTMSFNCWebBrowser object PopupMenu1: TPopupMenu
Align = Client Left = 424
Size.Width = 1100.000000000000000000 Top = 240
Size.Height = 502.000000000000000000
Size.PlatformDefault = False
TabOrder = 3
DesigntimeEnabled = False
end end
end end
+160 -8
View File
@@ -1,19 +1,48 @@
unit UMainForm; unit UMainForm;
// ----------------------------------------------------------------------
// WebBrowser engine switch — compile-time directive.
//
// Uncomment USE_EDGE_BROWSER to build against TTMSFNCEdgeWebBrowser
// (Windows-only, direct WebView2 wrapper). Default = TTMSFNCWebBrowser
// (cross-platform abstraction also on WebView2 under Windows). Both
// expose the same TTMSFNCCustomWebBrowser API for OnBeforeNavigate /
// ExecuteJavaScript / Navigate, so the bridge cmd:// glue is unchanged.
// ----------------------------------------------------------------------
{$DEFINE USE_EDGE_BROWSER}
interface interface
uses uses
System.SysUtils, System.Classes, System.UITypes, System.NetEncoding, System.SysUtils, System.Classes, System.UITypes, System.NetEncoding,
System.StrUtils, System.StrUtils, System.Generics.Collections,
Winapi.Windows, Winapi.Windows,
FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls,
FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types, FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types,
FMX.Dialogs, FMX.DialogService, FMX.Dialogs, FMX.DialogService,
FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes, FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes,
FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser, FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser,
// ICoreWebView2 / ICoreWebView2Settings3 COM interface declarations —
// used to disable browser accelerator keys (Ctrl+P, Ctrl+J, F12, etc.)
// via the AreBrowserAcceleratorKeysEnabled property that Settings.* doesn't
// expose at the TMS wrapper level.
FMX.TMSFNCWebBrowser.Win,
{$IFDEF USE_EDGE_BROWSER}
FMX.TMSFNCEdgeWebBrowser,
{$ENDIF}
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart, PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart,
PM.Favicon, PM.Favicon,
FMX.Platform.Win; // WindowHandleToPlatform → HWND for visibility check FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
type
// Concrete class chosen at compile time. Both inherit from
// TTMSFNCCustomWebBrowser so we use that as the field type — events
// and ExecuteJavaScript live on the base class.
{$IFDEF USE_EDGE_BROWSER}
TWebBrowserClass = TTMSFNCEdgeWebBrowser;
{$ELSE}
TWebBrowserClass = TTMSFNCWebBrowser;
{$ENDIF}
type type
TMainForm = class(TForm) TMainForm = class(TForm)
@@ -28,7 +57,7 @@ type
PanelLog: TPanel; PanelLog: TPanel;
Memo: TMemo; Memo: TMemo;
Splitter: TSplitter; Splitter: TSplitter;
WebBrowser: TTMSFNCWebBrowser; PopupMenu1: TPopupMenu;
procedure FormCreate(Sender: TObject); procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject); procedure FormDestroy(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean); procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
@@ -51,6 +80,17 @@ type
FAutofillPendingHWND: HWND; FAutofillPendingHWND: HWND;
FAutofillPendingUser: string; FAutofillPendingUser: string;
FAutofillPendingPass: string; FAutofillPendingPass: string;
// True when the JS layer asked us to hide back to the tray AFTER the
// autofill SendInput completes. Used by the Ctrl+Shift+Q hotkey path
// when invoked while the app was in the tray — we can't hide before
// SendInput because Win10/11 anti-focus-stealing rules then refuse to
// hand focus to the target window.
FAutofillPendingHide: Boolean;
// Created dynamically in FormCreate so the directive can pick either
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
// needing two .fmx variants. Aligned to Client to fill the remaining
// space between PanelTop (top) and PanelLog/Splitter (bottom).
WebBrowser: TWebBrowserClass;
procedure AutofillTimerTick(Sender: TObject); procedure AutofillTimerTick(Sender: TObject);
procedure LogLine(const AMsg: string); procedure LogLine(const AMsg: string);
procedure UpdateButtons; procedure UpdateButtons;
@@ -59,6 +99,9 @@ type
// JS↔Delphi bridge // JS↔Delphi bridge
procedure WebBrowserBeforeNavigate(Sender: TObject; procedure WebBrowserBeforeNavigate(Sender: TObject;
var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams); var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams);
procedure WebBrowserGetContextMenu(Sender: TObject;
ATarget: TTMSFNCWebBrowserTargetItem;
AContextMenu: TObjectList<TTMSFNCWebBrowserContextMenuItem>);
procedure HandleBridgeCommand(const ACmd, AParams: string); procedure HandleBridgeCommand(const ACmd, AParams: string);
procedure BridgeSystemLock; procedure BridgeSystemLock;
procedure BridgeTrayRestore; procedure BridgeTrayRestore;
@@ -69,6 +112,8 @@ type
procedure BridgeDebugHotkey; procedure BridgeDebugHotkey;
procedure BridgeNewEntryHotkey(const AWindowTitle: string); procedure BridgeNewEntryHotkey(const AWindowTitle: string);
procedure BridgeQuickSearchRequest; procedure BridgeQuickSearchRequest;
procedure BridgeQuickSearchHotkey(AKind: TAutofillKind;
ATargetHWND: HWND; const ATitle: string);
procedure WebBrowserInitialized(Sender: TObject); procedure WebBrowserInitialized(Sender: TObject);
end; end;
@@ -85,6 +130,30 @@ function MaskAccessToken(const AUrl: string): string; forward;
procedure TMainForm.FormCreate(Sender: TObject); procedure TMainForm.FormCreate(Sender: TObject);
begin begin
// Create the WebBrowser control programmatically so the {$IFDEF
// USE_EDGE_BROWSER} directive can swap the concrete class without a
// second .fmx variant. Align Client to fill the area below PanelTop
// and above the (collapsible) PanelLog + Splitter.
WebBrowser := TWebBrowserClass.Create(Self);
// Wire events BEFORE Parent assignment: setting Parent triggers the
// TMS browser's async WebView2 init, which fires OnInitialized when
// Edge Chromium is ready. If we assigned the handlers afterwards we'd
// miss the event on fast / pre-warmed Edge installs (and the
// Settings.EnableContextMenu / SetAcceleratorKeys calls inside would
// never run, leaving the native context menu and Ctrl+P/J live).
WebBrowser.OnBeforeNavigate := WebBrowserBeforeNavigate;
WebBrowser.OnInitialized := WebBrowserInitialized;
{$IFDEF USE_EDGE_BROWSER}
// On TTMSFNCEdgeWebBrowser the event is published.
// On TTMSFNCWebBrowser the same property is only re-published inside a
// conditional ($IFNDEF FNCLIB) block — depending on the FNC build, it
// may not be accessible. Restricting the assignment to the Edge build
// keeps the unconditional path TTMSFNCWebBrowser-safe.
WebBrowser.OnGetContextMenu := WebBrowserGetContextMenu;
{$ENDIF}
WebBrowser.Parent := Self;
WebBrowser.Align := TAlignLayout.Client;
FServer := TPMHTTPServer.Create; FServer := TPMHTTPServer.Create;
FServer.OnLog := LogLine; FServer.OnLog := LogLine;
@@ -97,13 +166,10 @@ begin
FBridge.OnDebugHotkey := BridgeDebugHotkey; FBridge.OnDebugHotkey := BridgeDebugHotkey;
FBridge.OnNewEntryHotkey := BridgeNewEntryHotkey; FBridge.OnNewEntryHotkey := BridgeNewEntryHotkey;
FBridge.OnQuickSearchRequest := BridgeQuickSearchRequest; FBridge.OnQuickSearchRequest := BridgeQuickSearchRequest;
FBridge.OnQuickSearchHotkey := BridgeQuickSearchHotkey;
FBridge.RegisterAutofillHotkey; // Ctrl+Shift+L active from startup FBridge.RegisterAutofillHotkey; // Ctrl+Shift+L active from startup
FBridge.ApplyTitleBarTheme(True); // dark by default, JS may toggle later FBridge.ApplyTitleBarTheme(True); // dark by default, JS may toggle later
FAutofillTargetHWND := 0; FAutofillTargetHWND := 0;
WebBrowser.OnBeforeNavigate := WebBrowserBeforeNavigate;
WebBrowser.OnInitialized := WebBrowserInitialized;
// Delayed-Navigate timer: TTMSFNCWebBrowser (WebView2 backend) ignores // Delayed-Navigate timer: TTMSFNCWebBrowser (WebView2 backend) ignores
// Navigate() calls until Edge Chromium finishes its async init (~1-2s). // Navigate() calls until Edge Chromium finishes its async init (~1-2s).
// We wait 1.5 s after Start, then issue a SINGLE Navigate — no retry loop // We wait 1.5 s after Start, then issue a SINGLE Navigate — no retry loop
@@ -115,7 +181,7 @@ begin
FNavTimer.OnTimer := NavTimerTick; FNavTimer.OnTimer := NavTimerTick;
UpdateButtons; UpdateButtons;
LogLine('Password Manager - Delphi backend ready.'); LogLine('Password Manager - d backend ready.');
LogLine('Click Start to launch server + embedded web vault.'); LogLine('Click Start to launch server + embedded web vault.');
PanelTop.Visible := False; PanelTop.Visible := False;
FRequireAccessToken := True; FRequireAccessToken := True;
@@ -162,9 +228,47 @@ begin
end; end;
procedure TMainForm.WebBrowserInitialized(Sender: TObject); procedure TMainForm.WebBrowserInitialized(Sender: TObject);
var
LUnk: IUnknown;
LCtrl: ICoreWebView2Controller;
LWv2: ICoreWebView2;
LSettings: ICoreWebView2Settings;
LSettings3: ICoreWebView2Settings3;
LPtr: Pointer;
begin begin
// Disable native Edge context menu + DevTools via the TMS-wrapped settings.
// Disable browser-level accelerator keys (Ctrl+P / Ctrl+J / Ctrl+H /
// Ctrl+S / F12 / etc.). TMS's NativeBrowser returns the
// ICoreWebView2Controller, not the ICoreWebView2 — so we walk the chain:
// Controller → get_CoreWebView2 → get_Settings → QI(Settings3) → set_.
// Pointer→Interface cast must go through an explicit AddRef to keep the
// refcount balanced (otherwise the auto-Release at scope exit frees a
// reference TMS is still holding → AV at next use).
LPtr := WebBrowser.NativeBrowser;
if LPtr <> nil then
begin
Pointer(LUnk) := LPtr;
LUnk._AddRef;
if Supports(LUnk, ICoreWebView2Controller, LCtrl) then
begin
LCtrl.get_CoreWebView2(LWv2);
if Assigned(LWv2) then
begin
LWv2.get_Settings(LSettings);
if Assigned(LSettings) and
Supports(LSettings, ICoreWebView2Settings3, LSettings3) then
LSettings3.set_AreBrowserAcceleratorKeysEnabled(False);
end;
end;
end;
{$IFDEF USE_EDGE_BROWSER}
WebBrowser.Settings.EnableContextMenu := False;
WebBrowser.Settings.EnableShowDebugConsole := False;
{$ELSE}
WebBrowser.EnableContextMenu := False; WebBrowser.EnableContextMenu := False;
WebBrowser.EnableShowDebugConsole := False; WebBrowser.EnableShowDebugConsole := False;
WebBrowser.PopupMenu := PopupMenu1;
{$ENDIF}
// Race-safe navigation fallback: the 1.5 s timer in NavigateToVault // Race-safe navigation fallback: the 1.5 s timer in NavigateToVault
// assumes WebView2 finishes its async init within that window. On slow // assumes WebView2 finishes its async init within that window. On slow
@@ -183,6 +287,29 @@ begin
end; end;
end; end;
procedure TMainForm.BridgeQuickSearchHotkey(AKind: TAutofillKind;
ATargetHWND: HWND; const ATitle: string);
var
LWasHidden: Boolean;
LWasHiddenJs: string;
begin
// Ctrl+Shift+Q anywhere — save the foreground HWND so the JS layer's
// cmd://autofill/execute (fired after the user picks an entry) sends
// the password into it. Then pop the modal in fill mode.
if not FServer.Active then Exit;
FAutofillTargetHWND := ATargetHWND;
LWasHidden := (not Self.Visible) or
IsIconic(WindowHandleToPlatform(Self.Handle).Wnd);
FBridge.RestoreFromTray;
LWasHiddenJs := BoolToStr(LWasHidden, True).ToLower;
// Second arg forFill=true tells JS to SendInput on pick instead of copy.
WebBrowser.ExecuteJavaScript(
'if(window.Bridge&&typeof Bridge.openQuickSearch==="function")' +
'Bridge.openQuickSearch(' + LWasHiddenJs + ',true)');
LogLine(Format('Quick-search hotkey — target=%s, title="%s"',
[IntToHex(ATargetHWND, 8), ATitle]));
end;
procedure TMainForm.BridgeQuickSearchRequest; procedure TMainForm.BridgeQuickSearchRequest;
var var
LWasHidden: Boolean; LWasHidden: Boolean;
@@ -303,6 +430,8 @@ procedure TMainForm.btnStartClick(Sender: TObject);
var var
LPort: Integer; LPort: Integer;
begin begin
// WebBrowser.Navigate('about:blank');
// exit;
LPort := StrToIntDef(edtPort.Text, 8765); LPort := StrToIntDef(edtPort.Text, 8765);
try try
FServer.Start(LPort, True, FRequireAccessToken, FRequireProcessCheck); FServer.Start(LPort, True, FRequireAccessToken, FRequireProcessCheck);
@@ -347,6 +476,16 @@ end;
// JS↔Delphi bridge // JS↔Delphi bridge
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
procedure TMainForm.WebBrowserGetContextMenu(Sender: TObject;
ATarget: TTMSFNCWebBrowserTargetItem;
AContextMenu: TObjectList<TTMSFNCWebBrowserContextMenuItem>);
begin
// Clearing the items leaves WebView2 with nothing to show → the native
// right-click menu is suppressed entirely. The JS layer renders a
// custom Cut/Copy/Paste menu on text inputs via installCustomContextMenu().
AContextMenu.Clear;
end;
procedure TMainForm.WebBrowserBeforeNavigate(Sender: TObject; procedure TMainForm.WebBrowserBeforeNavigate(Sender: TObject;
var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams); var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams);
var var
@@ -544,6 +683,7 @@ begin
FAutofillPendingUser := GetParam('username'); FAutofillPendingUser := GetParam('username');
FAutofillPendingPass := GetParam('password'); FAutofillPendingPass := GetParam('password');
FAutofillPendingHWND := FAutofillTargetHWND; FAutofillPendingHWND := FAutofillTargetHWND;
FAutofillPendingHide := GetParam('hide_after') = '1';
FAutofillTargetHWND := 0; FAutofillTargetHWND := 0;
// Small timer so SetForegroundWindow has time to take effect before // Small timer so SetForegroundWindow has time to take effect before
@@ -756,14 +896,17 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
var var
TargetHwnd: HWND; TargetHwnd: HWND;
PendingUser, PendingPass: string; PendingUser, PendingPass: string;
HideAfter: Boolean;
ForegroundAfter: HWND; ForegroundAfter: HWND;
begin begin
TargetHwnd := FAutofillPendingHWND; TargetHwnd := FAutofillPendingHWND;
PendingUser := FAutofillPendingUser; PendingUser := FAutofillPendingUser;
PendingPass := FAutofillPendingPass; PendingPass := FAutofillPendingPass;
HideAfter := FAutofillPendingHide;
FAutofillPendingHWND := 0; FAutofillPendingHWND := 0;
FAutofillPendingUser := ''; FAutofillPendingUser := '';
FAutofillPendingPass := ''; FAutofillPendingPass := '';
FAutofillPendingHide := False;
TTimer(Sender).Enabled := False; TTimer(Sender).Enabled := False;
TTimer(Sender).Free; TTimer(Sender).Free;
@@ -773,6 +916,15 @@ begin
LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s', LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s',
[IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8), [IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8),
BoolToStr(ForegroundAfter = TargetHwnd, True)])); BoolToStr(ForegroundAfter = TargetHwnd, True)]));
// Hide-after (Ctrl+Shift+Q from tray): SendInput is done, the target
// already has focus — now we can safely tray ourselves without
// confusing the Win10/11 foreground-stealing watchdog.
if HideAfter then
begin
FBridge.MinimizeToTray;
LogLine('Hidden back to tray (post-autofill).');
end;
end; end;
procedure TMainForm.BridgeAutofillRequest(AKind: TAutofillKind; procedure TMainForm.BridgeAutofillRequest(AKind: TAutofillKind;
Binary file not shown.
+56 -7
View File
@@ -88,11 +88,15 @@ const Bridge = (() => {
}, },
// Tell Delphi to simulate keystrokes. Empty username = password only // Tell Delphi to simulate keystrokes. Empty username = password only
// (no Tab is sent). // (no Tab is sent). hideAfter=true asks Delphi to hide our window
executeAutofill(username, password) { // back to the tray AFTER SendInput completes — necessary for the
// Ctrl+Shift+Q-from-tray flow (we cannot hide before SendInput or
// Win10/11 anti-focus-stealing rules block the target).
executeAutofill(username, password, hideAfter) {
if (!active) return; if (!active) return;
cmd('cmd://autofill/execute?username=' + encodeURIComponent(username) + cmd('cmd://autofill/execute?username=' + encodeURIComponent(username) +
'&password=' + encodeURIComponent(password)); '&password=' + encodeURIComponent(password) +
(hideAfter ? '&hide_after=1' : ''));
}, },
// Ask Delphi to bring the main window to front (used when the // Ask Delphi to bring the main window to front (used when the
@@ -247,7 +251,7 @@ const Bridge = (() => {
// hide the window again so the paste workflow is one keystroke // hide the window again so the paste workflow is one keystroke
// (Ctrl+V in the target app). // (Ctrl+V in the target app).
// Locked vault → fall through to the master-password screen. // Locked vault → fall through to the master-password screen.
openQuickSearch(wasHidden) { openQuickSearch(wasHidden, forFill) {
if (state.locked || !state.cryptoKey || !state.token) { if (state.locked || !state.cryptoKey || !state.token) {
const pwd = document.getElementById('loginPassword'); const pwd = document.getElementById('loginPassword');
if (pwd && !document.getElementById('authScreen').classList.contains('is-hidden')) { if (pwd && !document.getElementById('authScreen').classList.contains('is-hidden')) {
@@ -255,10 +259,13 @@ const Bridge = (() => {
} }
if (typeof toast === 'function') if (typeof toast === 'function')
toast('Vault is locked — unlock to search', 'warning'); toast('Vault is locked — unlock to search', 'warning');
// Tell Delphi we cancelled so the captured HWND doesn't
// linger waiting for a never-coming /execute.
if (forFill && Bridge.cancelAutofill) Bridge.cancelAutofill();
return; return;
} }
if (typeof openQuickSearchModal === 'function') if (typeof openQuickSearchModal === 'function')
openQuickSearchModal(!!wasHidden); openQuickSearchModal(!!wasHidden, !!forFill);
}, },
// Hide the window back to the tray icon. Used by Quick search to // Hide the window back to the tray icon. Used by Quick search to
@@ -688,6 +695,10 @@ let quickSearchSelected = 0;
// picks an entry — so the previously-foreground app comes back and // picks an entry — so the previously-foreground app comes back and
// Ctrl+V drops the password in. // Ctrl+V drops the password in.
let quickSearchHideAfter = false; let quickSearchHideAfter = false;
// When opened by Ctrl+Shift+Q hotkey, Delphi has saved the foreground
// HWND and is waiting for cmd://autofill/execute. On pick we SendInput
// the password instead of copying to the clipboard.
let quickSearchFillMode = false;
function quickSearchScoreEntry(e, q) { function quickSearchScoreEntry(e, q) {
if (!q) return 1; // empty query → all entries pass, ordering preserved if (!q) return 1; // empty query → all entries pass, ordering preserved
@@ -755,6 +766,28 @@ function quickSearchRender() {
} }
async function quickSearchPickEntry(entry, copyUsername) { async function quickSearchPickEntry(entry, copyUsername) {
// Fill mode (Ctrl+Shift+Q hotkey): SendInput the password directly into
// the HWND Delphi saved when the hotkey fired. No clipboard touch.
if (quickSearchFillMode && !copyUsername) {
const pwd = await decryptPwd(entry.encrypted_password, entry.iv);
if (pwd === '[ERROR]') {
toast('Decryption error', 'error');
if (Bridge.active) Bridge.cancelAutofill();
return;
}
// Single command — Delphi defers the SendInput by 60 ms then,
// if hide_after=1, MinimizeToTray's AFTER the keystrokes land.
// Hiding before SendInput would tip the Win10/11 anti-focus-stealing
// rules into refusing to hand focus to the target window.
if (Bridge.active) Bridge.executeAutofill('', pwd, quickSearchHideAfter);
toast(entryDisplayName(entry) + ' · password sent');
// Both flags consumed — closeQuickSearchModal must not re-trigger.
quickSearchFillMode = false;
quickSearchHideAfter = false;
closeQuickSearchModal();
return;
}
if (copyUsername) { if (copyUsername) {
const u = entry.username || ''; const u = entry.username || '';
if (!u) { if (!u) {
@@ -777,19 +810,35 @@ async function quickSearchPickEntry(entry, copyUsername) {
closeQuickSearchModal(); closeQuickSearchModal();
} }
function openQuickSearchModal(hideAfter) { function openQuickSearchModal(hideAfter, forFill) {
const modal = document.getElementById('quickSearchModal'); const modal = document.getElementById('quickSearchModal');
const input = document.getElementById('quickSearchInput'); const input = document.getElementById('quickSearchInput');
modal.classList.remove('is-hidden'); modal.classList.remove('is-hidden');
input.value = ''; input.value = '';
quickSearchSelected = 0; quickSearchSelected = 0;
quickSearchHideAfter = !!hideAfter; quickSearchHideAfter = !!hideAfter;
quickSearchFillMode = !!forFill;
// Subtle hint to the user about what Enter will do.
const hintEl = modal.querySelector('.quick-search-hint');
if (hintEl) {
hintEl.textContent = forFill
? 'Enter = type password into the active window · Esc = cancel'
: 'Enter = copy password · Shift+Enter = copy username · Esc = close';
}
quickSearchRender(); quickSearchRender();
setTimeout(() => input.focus(), 50); setTimeout(() => input.focus(), 50);
} }
function closeQuickSearchModal() { function closeQuickSearchModal() {
document.getElementById('quickSearchModal').classList.add('is-hidden'); document.getElementById('quickSearchModal').classList.add('is-hidden');
// Fill-mode cancel: tell Delphi to drop the saved HWND so the next
// /execute (e.g. an unrelated Ctrl+Shift+L) doesn't accidentally
// target the stale window.
if (quickSearchFillMode) {
if (Bridge.active && typeof Bridge.cancelAutofill === 'function')
Bridge.cancelAutofill();
quickSearchFillMode = false;
}
// If the modal was opened from the tray (window was hidden), restore // If the modal was opened from the tray (window was hidden), restore
// the previous "in tray" state so the user can paste straight into // the previous "in tray" state so the user can paste straight into
// the target app. Cancel (Esc / close X) also triggers this — they // the target app. Cancel (Esc / close X) also triggers this — they