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