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