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;
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user