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
+56 -7
View File
@@ -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;
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