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:
@@ -109,6 +109,12 @@ type
|
||||
FOnDebugHotkey: TProc;
|
||||
FNewEntryHotkeyRegistered: Boolean;
|
||||
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 PrepareNid;
|
||||
procedure ShowTrayMenu;
|
||||
@@ -183,6 +189,11 @@ type
|
||||
// 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
|
||||
@@ -227,6 +238,7 @@ const
|
||||
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
|
||||
AF_MOD_CONTROL = $0002; // same value as MOD_CONTROL
|
||||
AF_MOD_SHIFT = $0004; // same value as MOD_SHIFT
|
||||
|
||||
@@ -422,6 +434,8 @@ begin
|
||||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D'));
|
||||
FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
|
||||
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;
|
||||
|
||||
destructor TPMBridge.Destroy;
|
||||
@@ -432,6 +446,9 @@ begin
|
||||
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);
|
||||
|
||||
@@ -739,6 +756,22 @@ begin
|
||||
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
|
||||
((AMsg.WParam = AUTOFILL_HOTKEY_ID_FULL) or
|
||||
(AMsg.WParam = AUTOFILL_HOTKEY_ID_PWDONLY)) then
|
||||
|
||||
@@ -123,12 +123,8 @@ object MainForm: TMainForm
|
||||
Size.Height = 6.000000000000000000
|
||||
Size.PlatformDefault = False
|
||||
end
|
||||
object WebBrowser: TTMSFNCWebBrowser
|
||||
Align = Client
|
||||
Size.Width = 1100.000000000000000000
|
||||
Size.Height = 502.000000000000000000
|
||||
Size.PlatformDefault = False
|
||||
TabOrder = 3
|
||||
DesigntimeEnabled = False
|
||||
object PopupMenu1: TPopupMenu
|
||||
Left = 424
|
||||
Top = 240
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
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
|
||||
|
||||
uses
|
||||
System.SysUtils, System.Classes, System.UITypes, System.NetEncoding,
|
||||
System.StrUtils,
|
||||
System.StrUtils, System.Generics.Collections,
|
||||
Winapi.Windows,
|
||||
FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls,
|
||||
FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types,
|
||||
FMX.Dialogs, FMX.DialogService,
|
||||
FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes,
|
||||
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.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
|
||||
TMainForm = class(TForm)
|
||||
@@ -28,7 +57,7 @@ type
|
||||
PanelLog: TPanel;
|
||||
Memo: TMemo;
|
||||
Splitter: TSplitter;
|
||||
WebBrowser: TTMSFNCWebBrowser;
|
||||
PopupMenu1: TPopupMenu;
|
||||
procedure FormCreate(Sender: TObject);
|
||||
procedure FormDestroy(Sender: TObject);
|
||||
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
|
||||
@@ -51,6 +80,17 @@ type
|
||||
FAutofillPendingHWND: HWND;
|
||||
FAutofillPendingUser: 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 LogLine(const AMsg: string);
|
||||
procedure UpdateButtons;
|
||||
@@ -59,6 +99,9 @@ type
|
||||
// JS↔Delphi bridge
|
||||
procedure WebBrowserBeforeNavigate(Sender: TObject;
|
||||
var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams);
|
||||
procedure WebBrowserGetContextMenu(Sender: TObject;
|
||||
ATarget: TTMSFNCWebBrowserTargetItem;
|
||||
AContextMenu: TObjectList<TTMSFNCWebBrowserContextMenuItem>);
|
||||
procedure HandleBridgeCommand(const ACmd, AParams: string);
|
||||
procedure BridgeSystemLock;
|
||||
procedure BridgeTrayRestore;
|
||||
@@ -69,6 +112,8 @@ type
|
||||
procedure BridgeDebugHotkey;
|
||||
procedure BridgeNewEntryHotkey(const AWindowTitle: string);
|
||||
procedure BridgeQuickSearchRequest;
|
||||
procedure BridgeQuickSearchHotkey(AKind: TAutofillKind;
|
||||
ATargetHWND: HWND; const ATitle: string);
|
||||
procedure WebBrowserInitialized(Sender: TObject);
|
||||
end;
|
||||
|
||||
@@ -85,6 +130,30 @@ function MaskAccessToken(const AUrl: string): string; forward;
|
||||
|
||||
procedure TMainForm.FormCreate(Sender: TObject);
|
||||
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.OnLog := LogLine;
|
||||
|
||||
@@ -97,13 +166,10 @@ begin
|
||||
FBridge.OnDebugHotkey := BridgeDebugHotkey;
|
||||
FBridge.OnNewEntryHotkey := BridgeNewEntryHotkey;
|
||||
FBridge.OnQuickSearchRequest := BridgeQuickSearchRequest;
|
||||
FBridge.OnQuickSearchHotkey := BridgeQuickSearchHotkey;
|
||||
FBridge.RegisterAutofillHotkey; // Ctrl+Shift+L active from startup
|
||||
FBridge.ApplyTitleBarTheme(True); // dark by default, JS may toggle later
|
||||
FAutofillTargetHWND := 0;
|
||||
|
||||
WebBrowser.OnBeforeNavigate := WebBrowserBeforeNavigate;
|
||||
WebBrowser.OnInitialized := WebBrowserInitialized;
|
||||
|
||||
// Delayed-Navigate timer: TTMSFNCWebBrowser (WebView2 backend) ignores
|
||||
// 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
|
||||
@@ -115,7 +181,7 @@ begin
|
||||
FNavTimer.OnTimer := NavTimerTick;
|
||||
|
||||
UpdateButtons;
|
||||
LogLine('Password Manager - Delphi backend ready.');
|
||||
LogLine('Password Manager - d backend ready.');
|
||||
LogLine('Click Start to launch server + embedded web vault.');
|
||||
PanelTop.Visible := False;
|
||||
FRequireAccessToken := True;
|
||||
@@ -162,9 +228,47 @@ begin
|
||||
end;
|
||||
|
||||
procedure TMainForm.WebBrowserInitialized(Sender: TObject);
|
||||
var
|
||||
LUnk: IUnknown;
|
||||
LCtrl: ICoreWebView2Controller;
|
||||
LWv2: ICoreWebView2;
|
||||
LSettings: ICoreWebView2Settings;
|
||||
LSettings3: ICoreWebView2Settings3;
|
||||
LPtr: Pointer;
|
||||
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.EnableShowDebugConsole := False;
|
||||
WebBrowser.PopupMenu := PopupMenu1;
|
||||
{$ENDIF}
|
||||
|
||||
// Race-safe navigation fallback: the 1.5 s timer in NavigateToVault
|
||||
// assumes WebView2 finishes its async init within that window. On slow
|
||||
@@ -183,6 +287,29 @@ begin
|
||||
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;
|
||||
var
|
||||
LWasHidden: Boolean;
|
||||
@@ -303,6 +430,8 @@ procedure TMainForm.btnStartClick(Sender: TObject);
|
||||
var
|
||||
LPort: Integer;
|
||||
begin
|
||||
// WebBrowser.Navigate('about:blank');
|
||||
// exit;
|
||||
LPort := StrToIntDef(edtPort.Text, 8765);
|
||||
try
|
||||
FServer.Start(LPort, True, FRequireAccessToken, FRequireProcessCheck);
|
||||
@@ -347,6 +476,16 @@ end;
|
||||
// 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;
|
||||
var Params: TTMSFNCCustomWebBrowserBeforeNavigateParams);
|
||||
var
|
||||
@@ -544,6 +683,7 @@ begin
|
||||
FAutofillPendingUser := GetParam('username');
|
||||
FAutofillPendingPass := GetParam('password');
|
||||
FAutofillPendingHWND := FAutofillTargetHWND;
|
||||
FAutofillPendingHide := GetParam('hide_after') = '1';
|
||||
FAutofillTargetHWND := 0;
|
||||
|
||||
// Small timer so SetForegroundWindow has time to take effect before
|
||||
@@ -756,14 +896,17 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
|
||||
var
|
||||
TargetHwnd: HWND;
|
||||
PendingUser, PendingPass: string;
|
||||
HideAfter: Boolean;
|
||||
ForegroundAfter: HWND;
|
||||
begin
|
||||
TargetHwnd := FAutofillPendingHWND;
|
||||
PendingUser := FAutofillPendingUser;
|
||||
PendingPass := FAutofillPendingPass;
|
||||
HideAfter := FAutofillPendingHide;
|
||||
FAutofillPendingHWND := 0;
|
||||
FAutofillPendingUser := '';
|
||||
FAutofillPendingPass := '';
|
||||
FAutofillPendingHide := False;
|
||||
|
||||
TTimer(Sender).Enabled := False;
|
||||
TTimer(Sender).Free;
|
||||
@@ -773,6 +916,15 @@ begin
|
||||
LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s',
|
||||
[IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8),
|
||||
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;
|
||||
|
||||
procedure TMainForm.BridgeAutofillRequest(AKind: TAutofillKind;
|
||||
|
||||
Binary file not shown.
@@ -88,11 +88,15 @@ const Bridge = (() => {
|
||||
},
|
||||
|
||||
// Tell Delphi to simulate keystrokes. Empty username = password only
|
||||
// (no Tab is sent).
|
||||
executeAutofill(username, password) {
|
||||
// (no Tab is sent). hideAfter=true asks Delphi to hide our window
|
||||
// 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;
|
||||
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
|
||||
@@ -247,7 +251,7 @@ const Bridge = (() => {
|
||||
// hide the window again so the paste workflow is one keystroke
|
||||
// (Ctrl+V in the target app).
|
||||
// Locked vault → fall through to the master-password screen.
|
||||
openQuickSearch(wasHidden) {
|
||||
openQuickSearch(wasHidden, forFill) {
|
||||
if (state.locked || !state.cryptoKey || !state.token) {
|
||||
const pwd = document.getElementById('loginPassword');
|
||||
if (pwd && !document.getElementById('authScreen').classList.contains('is-hidden')) {
|
||||
@@ -255,10 +259,13 @@ const Bridge = (() => {
|
||||
}
|
||||
if (typeof toast === 'function')
|
||||
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;
|
||||
}
|
||||
if (typeof openQuickSearchModal === 'function')
|
||||
openQuickSearchModal(!!wasHidden);
|
||||
openQuickSearchModal(!!wasHidden, !!forFill);
|
||||
},
|
||||
|
||||
// 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
|
||||
// Ctrl+V drops the password in.
|
||||
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) {
|
||||
if (!q) return 1; // empty query → all entries pass, ordering preserved
|
||||
@@ -755,6 +766,28 @@ function quickSearchRender() {
|
||||
}
|
||||
|
||||
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) {
|
||||
const u = entry.username || '';
|
||||
if (!u) {
|
||||
@@ -777,19 +810,35 @@ async function quickSearchPickEntry(entry, copyUsername) {
|
||||
closeQuickSearchModal();
|
||||
}
|
||||
|
||||
function openQuickSearchModal(hideAfter) {
|
||||
function openQuickSearchModal(hideAfter, forFill) {
|
||||
const modal = document.getElementById('quickSearchModal');
|
||||
const input = document.getElementById('quickSearchInput');
|
||||
modal.classList.remove('is-hidden');
|
||||
input.value = '';
|
||||
quickSearchSelected = 0;
|
||||
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();
|
||||
setTimeout(() => input.focus(), 50);
|
||||
}
|
||||
|
||||
function closeQuickSearchModal() {
|
||||
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
|
||||
// the previous "in tray" state so the user can paste straight into
|
||||
// the target app. Cancel (Esc / close X) also triggers this — they
|
||||
|
||||
Reference in New Issue
Block a user