diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index f7aff3f..2638f36 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -121,6 +121,10 @@ type FDebugHotkeyRegistered: Boolean; FOnDebugHotkey: TProc; FNewEntryHotkeyRegistered: Boolean; + // Current new-entry combo (user-configurable, default Ctrl+Shift+A). Kept + // so ExecuteAutofill can re-register it after unregistering during a fill. + FNewEntryHotkeyMods: Word; + FNewEntryHotkeyVk: Word; 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 @@ -163,6 +167,9 @@ type // Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a // user-chosen combo. True on success. function SetQuickSearchHotkey(AMods, AVk: Word): Boolean; + // Re-register the new-entry hotkey with a user-chosen combo (default + // Ctrl+Shift+A). Stored so ExecuteAutofill can restore it after the fill. + function SetNewEntryHotkey(AMods, AVk: Word): Boolean; // Convenience wrapper: register the historical defaults (Ctrl+Shift+L // and Ctrl+Shift+P). Used by the host on first start; runtime changes // go through SetAutofillHotkeys. @@ -483,8 +490,12 @@ begin FDebugHotkeyRegistered := RegisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D')); + // Default combo; JS overrides it via cmd://autofill/hotkeys (SetNewEntryHotkey) + // at unlock. Store it so ExecuteAutofill's re-register uses the live combo. + FNewEntryHotkeyMods := AF_MOD_CONTROL or AF_MOD_SHIFT; + FNewEntryHotkeyVk := Ord('A'); FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID, - AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A')); + FNewEntryHotkeyMods, FNewEntryHotkeyVk); FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow, QUICK_SEARCH_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('Q')); end; @@ -850,10 +861,18 @@ begin if Assigned(FOnNewEntryHotkey) then begin var LTarget := GetForegroundWindow; - var LTitle: string; - SetLength(LTitle, 512); - var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512); - SetLength(LTitle, LLen); + var LTitle: string := ''; + var LPid: DWORD := 0; + GetWindowThreadProcessId(LTarget, LPid); + // From the tray the foreground can resolve to OUR own window — don't + // pre-fill the new entry with "Password Manager". Only capture a real + // external window's title; otherwise pass '' (blank title). + if (LTarget <> 0) and (LPid <> GetCurrentProcessId) then + begin + SetLength(LTitle, 512); + var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512); + SetLength(LTitle, LLen); + end; FOnNewEntryHotkey(LTitle); end; end @@ -942,6 +961,23 @@ begin Result := FQuickSearchHotkeyRegistered; end; +function TPMBridge.SetNewEntryHotkey(AMods, AVk: Word): Boolean; +begin + if FNewEntryHotkeyRegistered then + begin + UnregisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID); + FNewEntryHotkeyRegistered := False; + end; + if (AVk <> 0) and (AMods <> 0) then + begin + FNewEntryHotkeyMods := AMods; + FNewEntryHotkeyVk := AVk; + FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, + NEW_ENTRY_HOTKEY_ID, AMods, AVk); + end; + Result := FNewEntryHotkeyRegistered; +end; + procedure TPMBridge.ApplyTitleBarTheme(ADark: Boolean); const DWMWA_USE_IMMERSIVE_DARK_MODE = 20; @@ -1350,9 +1386,11 @@ begin ClearFieldIfWanted; SendUnicodeString(APassword); finally + // Re-register with the LIVE combo (user may have rebound it), not the + // hardcoded default — otherwise a fill would reset new-entry to Ctrl+Shift+A. if FNewEntryHotkeyRegistered then RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID, - AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A')); + FNewEntryHotkeyMods, FNewEntryHotkeyVk); end; end; diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 891cf31..c612ccf 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -874,6 +874,16 @@ begin LogLine(Format('Quick-search hotkey set — mods:%d vk:%d (ok=%s)', [LQsMods, LQsVk, BoolToStr(LQsOk, True)])); end; + // New-entry combo — also independent of the autofill enabled flag + // (Ctrl+Shift+A works even with autofill turned off). + if GetParam('ne_vk') <> '' then + begin + var LNeMods := Word(StrToIntDef(GetParam('ne_mods'), 6)); + var LNeVk := Word(StrToIntDef(GetParam('ne_vk'), Ord('A'))); + var LNeOk := FBridge.SetNewEntryHotkey(LNeMods, LNeVk); + LogLine(Format('New-entry hotkey set — mods:%d vk:%d (ok=%s)', + [LNeMods, LNeVk, BoolToStr(LNeOk, True)])); + end; if GetParam('enabled') <> '1' then begin FBridge.UnregisterAutofillHotkey; diff --git a/index.html b/index.html index 054bad8..9923a6a 100644 --- a/index.html +++ b/index.html @@ -687,6 +687,12 @@ +
Click a button, then press your new combo. Needs Ctrl, Alt or Win + a letter / digit / F-key.
diff --git a/js/app.autofill.js b/js/app.autofill.js index 8a04c60..5d43d8f 100644 --- a/js/app.autofill.js +++ b/js/app.autofill.js @@ -82,11 +82,13 @@ function autofillPushHotkeys() { localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull)); localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd)); localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey)); + localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey)); if (Bridge.active) { Bridge.setAutofillHotkeys(state.autofillEnabled, { full: state.autofillHotkeyFull, password: state.autofillHotkeyPwd, quickSearch: state.quickSearchHotkey, + newEntry: state.newEntryHotkey, }); } } diff --git a/js/app.js b/js/app.js index 1e8e944..a31fbd5 100644 --- a/js/app.js +++ b/js/app.js @@ -200,9 +200,14 @@ const Bridge = (() => { const q = autofillComboToWin32(combos.quickSearch); qs = '&qs_mods=' + q.mods + '&qs_vk=' + q.vk; } + let ne = ''; + if (combos.newEntry) { + const n = autofillComboToWin32(combos.newEntry); + ne = '&ne_mods=' + n.mods + '&ne_vk=' + n.vk; + } cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') + '&full_mods=' + f.mods + '&full_vk=' + f.vk + - '&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs); + '&pwd_mods=' + p.mods + '&pwd_vk=' + p.vk + qs + ne); }, // Called by Delphi after a setAutofillHotkeys request, with true if @@ -667,6 +672,8 @@ const state = { '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'), quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') || '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'), + newEntryHotkey: JSON.parse(localStorage.getItem('newEntryHotkey') || + '{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"A"}'), sidebarCollapsed: JSON.parse(localStorage.getItem('sidebarCollapsed') || '{"folders":false,"tags":false,"tools":false}'), // Fetch website favicons via the Delphi DuckDuckGo proxy. OFF by @@ -6282,6 +6289,7 @@ function openSettings() { $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); $('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey); + $('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey); $('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none'; // Start-with-Windows toggle: only meaningful inside the Delphi host // (registry access). Hide for the PHP frontend. @@ -6668,7 +6676,7 @@ const SYNCED_SETTING_KEYS = [ // Hotkey combos are user preferences — values are portable. The // registration itself is Windows-only, so non-Windows clients just // ignore them. - 'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey', + 'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey', 'newEntryHotkey', // Sidebar section collapsed state. Object of { folders, tags, tools } // booleans. Synced so the user gets the same fold state across devices. 'sidebarCollapsed', @@ -6742,6 +6750,7 @@ async function loadServerSettings() { case 'autofillHotkeyFull': case 'autofillHotkeyPwd': case 'quickSearchHotkey': + case 'newEntryHotkey': case 'sidebarCollapsed': // Object; persist as JSON so the next cold start picks it up. localStorage.setItem(k, JSON.stringify(v)); @@ -7706,6 +7715,7 @@ async function init() { kind !== 'full' ? state.autofillHotkeyFull : null, kind !== 'password' ? state.autofillHotkeyPwd : null, kind !== 'quickSearch' ? state.quickSearchHotkey : null, + kind !== 'newEntry' ? state.newEntryHotkey : null, ].filter(Boolean); const capJson = JSON.stringify(captured); if (others.some(o => JSON.stringify(o) === capJson)) { @@ -7716,7 +7726,8 @@ async function init() { // Commit. if (kind === 'full') state.autofillHotkeyFull = captured; else if (kind === 'password') state.autofillHotkeyPwd = captured; - else /* quickSearch */ state.quickSearchHotkey = captured; + else if (kind === 'quickSearch') state.quickSearchHotkey = captured; + else /* newEntry */ state.newEntryHotkey = captured; btn.textContent = autofillComboLabel(captured); finish(false); autofillPushHotkeys(); // re-register in Delphi @@ -7728,14 +7739,17 @@ async function init() { bindHotkeyCapture('#settingAutofillFullCombo', 'full'); bindHotkeyCapture('#settingAutofillPwdCombo', 'password'); bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch'); + bindHotkeyCapture('#settingNewEntryCombo', 'newEntry'); $('#settingAutofillResetHotkeys').addEventListener('click', () => { state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' }; state.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' }; state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' }; + state.newEntryHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'A' }; $('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull); $('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd); $('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey); + $('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey); autofillPushHotkeys(); saveServerSettings(); toast('Hotkeys reset to defaults');