feat(hotkey): make the new-entry hotkey configurable + fix tray title capture
Part A — Ctrl+Shift+A (new entry from window title) is now editable like the other 3 global hotkeys, following the same pattern end to end: state.newEntryHotkey (synced), a capture button in Settings > Autofill, dup cross-check, reset-to- defaults, autofillPushHotkeys, and Bridge.setAutofillHotkeys carrying ne_mods/ ne_vk. Delphi gets SetNewEntryHotkey + stores the live combo (FNewEntryHotkeyMods/ Vk) so ExecuteAutofill's post-fill re-register uses the user's combo instead of resetting to Ctrl+Shift+A. UMainForm parses ne_* unconditionally (works with autofill off). Part B — new-entry from the tray pre-filled the title with "Password Manager": GetForegroundWindow resolved to our own hidden window. Only capture the title when the foreground window belongs to another process; otherwise pass '' (blank). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,10 @@ type
|
|||||||
FDebugHotkeyRegistered: Boolean;
|
FDebugHotkeyRegistered: Boolean;
|
||||||
FOnDebugHotkey: TProc;
|
FOnDebugHotkey: TProc;
|
||||||
FNewEntryHotkeyRegistered: Boolean;
|
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;
|
FOnNewEntryHotkey: TNewEntryHotkeyEvent;
|
||||||
// Ctrl+Shift+Q — "Quick search + autofill": open the JS quick-search
|
// Ctrl+Shift+Q — "Quick search + autofill": open the JS quick-search
|
||||||
// modal, the user picks an entry, the password is SendInput'd into the
|
// 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
|
// Re-register the Ctrl+Shift+Q (default) quick-search hotkey with a
|
||||||
// user-chosen combo. True on success.
|
// user-chosen combo. True on success.
|
||||||
function SetQuickSearchHotkey(AMods, AVk: Word): Boolean;
|
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
|
// Convenience wrapper: register the historical defaults (Ctrl+Shift+L
|
||||||
// and Ctrl+Shift+P). Used by the host on first start; runtime changes
|
// and Ctrl+Shift+P). Used by the host on first start; runtime changes
|
||||||
// go through SetAutofillHotkeys.
|
// go through SetAutofillHotkeys.
|
||||||
@@ -483,8 +490,12 @@ begin
|
|||||||
|
|
||||||
FDebugHotkeyRegistered := RegisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID,
|
FDebugHotkeyRegistered := RegisterHotKey(FMsgWindow, DEBUG_HOTKEY_ID,
|
||||||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('D'));
|
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,
|
FNewEntryHotkeyRegistered := RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
|
||||||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A'));
|
FNewEntryHotkeyMods, FNewEntryHotkeyVk);
|
||||||
FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow,
|
FQuickSearchHotkeyRegistered := RegisterHotKey(FMsgWindow,
|
||||||
QUICK_SEARCH_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('Q'));
|
QUICK_SEARCH_HOTKEY_ID, AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('Q'));
|
||||||
end;
|
end;
|
||||||
@@ -850,10 +861,18 @@ begin
|
|||||||
if Assigned(FOnNewEntryHotkey) then
|
if Assigned(FOnNewEntryHotkey) then
|
||||||
begin
|
begin
|
||||||
var LTarget := GetForegroundWindow;
|
var LTarget := GetForegroundWindow;
|
||||||
var LTitle: string;
|
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);
|
SetLength(LTitle, 512);
|
||||||
var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
|
var LLen := GetWindowTextW(LTarget, PChar(LTitle), 512);
|
||||||
SetLength(LTitle, LLen);
|
SetLength(LTitle, LLen);
|
||||||
|
end;
|
||||||
FOnNewEntryHotkey(LTitle);
|
FOnNewEntryHotkey(LTitle);
|
||||||
end;
|
end;
|
||||||
end
|
end
|
||||||
@@ -942,6 +961,23 @@ begin
|
|||||||
Result := FQuickSearchHotkeyRegistered;
|
Result := FQuickSearchHotkeyRegistered;
|
||||||
end;
|
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);
|
procedure TPMBridge.ApplyTitleBarTheme(ADark: Boolean);
|
||||||
const
|
const
|
||||||
DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||||||
@@ -1350,9 +1386,11 @@ begin
|
|||||||
ClearFieldIfWanted;
|
ClearFieldIfWanted;
|
||||||
SendUnicodeString(APassword);
|
SendUnicodeString(APassword);
|
||||||
finally
|
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
|
if FNewEntryHotkeyRegistered then
|
||||||
RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
|
RegisterHotKey(FMsgWindow, NEW_ENTRY_HOTKEY_ID,
|
||||||
AF_MOD_CONTROL or AF_MOD_SHIFT, Ord('A'));
|
FNewEntryHotkeyMods, FNewEntryHotkeyVk);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
@@ -874,6 +874,16 @@ begin
|
|||||||
LogLine(Format('Quick-search hotkey set — mods:%d vk:%d (ok=%s)',
|
LogLine(Format('Quick-search hotkey set — mods:%d vk:%d (ok=%s)',
|
||||||
[LQsMods, LQsVk, BoolToStr(LQsOk, True)]));
|
[LQsMods, LQsVk, BoolToStr(LQsOk, True)]));
|
||||||
end;
|
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
|
if GetParam('enabled') <> '1' then
|
||||||
begin
|
begin
|
||||||
FBridge.UnregisterAutofillHotkey;
|
FBridge.UnregisterAutofillHotkey;
|
||||||
|
|||||||
@@ -687,6 +687,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingQuickSearchCombo">Ctrl+Shift+Q</button>
|
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingQuickSearchCombo">Ctrl+Shift+Q</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span style="font-size:12px;color:var(--text-dim)">
|
||||||
|
New entry from the foreground window title
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-ghost btn-sm hotkey-capture-btn" id="settingNewEntryCombo">Ctrl+Shift+A</button>
|
||||||
|
</div>
|
||||||
<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0;line-height:1.4">
|
<p style="font-size:11px;color:var(--text-faint);margin:6px 0 0;line-height:1.4">
|
||||||
Click a button, then press your new combo. Needs Ctrl, Alt or Win + a letter / digit / F-key.
|
Click a button, then press your new combo. Needs Ctrl, Alt or Win + a letter / digit / F-key.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -82,11 +82,13 @@ function autofillPushHotkeys() {
|
|||||||
localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull));
|
localStorage.setItem('autofillHotkeyFull', JSON.stringify(state.autofillHotkeyFull));
|
||||||
localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd));
|
localStorage.setItem('autofillHotkeyPwd', JSON.stringify(state.autofillHotkeyPwd));
|
||||||
localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey));
|
localStorage.setItem('quickSearchHotkey', JSON.stringify(state.quickSearchHotkey));
|
||||||
|
localStorage.setItem('newEntryHotkey', JSON.stringify(state.newEntryHotkey));
|
||||||
if (Bridge.active) {
|
if (Bridge.active) {
|
||||||
Bridge.setAutofillHotkeys(state.autofillEnabled, {
|
Bridge.setAutofillHotkeys(state.autofillEnabled, {
|
||||||
full: state.autofillHotkeyFull,
|
full: state.autofillHotkeyFull,
|
||||||
password: state.autofillHotkeyPwd,
|
password: state.autofillHotkeyPwd,
|
||||||
quickSearch: state.quickSearchHotkey,
|
quickSearch: state.quickSearchHotkey,
|
||||||
|
newEntry: state.newEntryHotkey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,9 +200,14 @@ const Bridge = (() => {
|
|||||||
const q = autofillComboToWin32(combos.quickSearch);
|
const q = autofillComboToWin32(combos.quickSearch);
|
||||||
qs = '&qs_mods=' + q.mods + '&qs_vk=' + q.vk;
|
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') +
|
cmd('cmd://autofill/hotkeys?enabled=' + (enabled ? '1' : '0') +
|
||||||
'&full_mods=' + f.mods + '&full_vk=' + f.vk +
|
'&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
|
// 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"}'),
|
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"P"}'),
|
||||||
quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') ||
|
quickSearchHotkey: JSON.parse(localStorage.getItem('quickSearchHotkey') ||
|
||||||
'{"ctrl":true,"shift":true,"alt":false,"win":false,"key":"Q"}'),
|
'{"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') ||
|
sidebarCollapsed: JSON.parse(localStorage.getItem('sidebarCollapsed') ||
|
||||||
'{"folders":false,"tags":false,"tools":false}'),
|
'{"folders":false,"tags":false,"tools":false}'),
|
||||||
// Fetch website favicons via the Delphi DuckDuckGo proxy. OFF by
|
// Fetch website favicons via the Delphi DuckDuckGo proxy. OFF by
|
||||||
@@ -6282,6 +6289,7 @@ function openSettings() {
|
|||||||
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||||
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
||||||
|
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
||||||
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
|
$('#settingAutofillHotkeysRow').style.display = Bridge.active ? '' : 'none';
|
||||||
// Start-with-Windows toggle: only meaningful inside the Delphi host
|
// Start-with-Windows toggle: only meaningful inside the Delphi host
|
||||||
// (registry access). Hide for the PHP frontend.
|
// (registry access). Hide for the PHP frontend.
|
||||||
@@ -6668,7 +6676,7 @@ const SYNCED_SETTING_KEYS = [
|
|||||||
// Hotkey combos are user preferences — values are portable. The
|
// Hotkey combos are user preferences — values are portable. The
|
||||||
// registration itself is Windows-only, so non-Windows clients just
|
// registration itself is Windows-only, so non-Windows clients just
|
||||||
// ignore them.
|
// ignore them.
|
||||||
'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey',
|
'autofillHotkeyFull', 'autofillHotkeyPwd', 'quickSearchHotkey', 'newEntryHotkey',
|
||||||
// Sidebar section collapsed state. Object of { folders, tags, tools }
|
// Sidebar section collapsed state. Object of { folders, tags, tools }
|
||||||
// booleans. Synced so the user gets the same fold state across devices.
|
// booleans. Synced so the user gets the same fold state across devices.
|
||||||
'sidebarCollapsed',
|
'sidebarCollapsed',
|
||||||
@@ -6742,6 +6750,7 @@ async function loadServerSettings() {
|
|||||||
case 'autofillHotkeyFull':
|
case 'autofillHotkeyFull':
|
||||||
case 'autofillHotkeyPwd':
|
case 'autofillHotkeyPwd':
|
||||||
case 'quickSearchHotkey':
|
case 'quickSearchHotkey':
|
||||||
|
case 'newEntryHotkey':
|
||||||
case 'sidebarCollapsed':
|
case 'sidebarCollapsed':
|
||||||
// Object; persist as JSON so the next cold start picks it up.
|
// Object; persist as JSON so the next cold start picks it up.
|
||||||
localStorage.setItem(k, JSON.stringify(v));
|
localStorage.setItem(k, JSON.stringify(v));
|
||||||
@@ -7706,6 +7715,7 @@ async function init() {
|
|||||||
kind !== 'full' ? state.autofillHotkeyFull : null,
|
kind !== 'full' ? state.autofillHotkeyFull : null,
|
||||||
kind !== 'password' ? state.autofillHotkeyPwd : null,
|
kind !== 'password' ? state.autofillHotkeyPwd : null,
|
||||||
kind !== 'quickSearch' ? state.quickSearchHotkey : null,
|
kind !== 'quickSearch' ? state.quickSearchHotkey : null,
|
||||||
|
kind !== 'newEntry' ? state.newEntryHotkey : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const capJson = JSON.stringify(captured);
|
const capJson = JSON.stringify(captured);
|
||||||
if (others.some(o => JSON.stringify(o) === capJson)) {
|
if (others.some(o => JSON.stringify(o) === capJson)) {
|
||||||
@@ -7716,7 +7726,8 @@ async function init() {
|
|||||||
// Commit.
|
// Commit.
|
||||||
if (kind === 'full') state.autofillHotkeyFull = captured;
|
if (kind === 'full') state.autofillHotkeyFull = captured;
|
||||||
else if (kind === 'password') state.autofillHotkeyPwd = 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);
|
btn.textContent = autofillComboLabel(captured);
|
||||||
finish(false);
|
finish(false);
|
||||||
autofillPushHotkeys(); // re-register in Delphi
|
autofillPushHotkeys(); // re-register in Delphi
|
||||||
@@ -7728,14 +7739,17 @@ async function init() {
|
|||||||
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
bindHotkeyCapture('#settingAutofillFullCombo', 'full');
|
||||||
bindHotkeyCapture('#settingAutofillPwdCombo', 'password');
|
bindHotkeyCapture('#settingAutofillPwdCombo', 'password');
|
||||||
bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch');
|
bindHotkeyCapture('#settingQuickSearchCombo', 'quickSearch');
|
||||||
|
bindHotkeyCapture('#settingNewEntryCombo', 'newEntry');
|
||||||
|
|
||||||
$('#settingAutofillResetHotkeys').addEventListener('click', () => {
|
$('#settingAutofillResetHotkeys').addEventListener('click', () => {
|
||||||
state.autofillHotkeyFull = { ctrl: true, shift: true, alt: false, win: false, key: 'L' };
|
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.autofillHotkeyPwd = { ctrl: true, shift: true, alt: false, win: false, key: 'P' };
|
||||||
state.quickSearchHotkey = { ctrl: true, shift: true, alt: false, win: false, key: 'Q' };
|
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);
|
$('#settingAutofillFullCombo').textContent = autofillComboLabel(state.autofillHotkeyFull);
|
||||||
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
$('#settingAutofillPwdCombo').textContent = autofillComboLabel(state.autofillHotkeyPwd);
|
||||||
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
$('#settingQuickSearchCombo').textContent = autofillComboLabel(state.quickSearchHotkey);
|
||||||
|
$('#settingNewEntryCombo').textContent = autofillComboLabel(state.newEntryHotkey);
|
||||||
autofillPushHotkeys();
|
autofillPushHotkeys();
|
||||||
saveServerSettings();
|
saveServerSettings();
|
||||||
toast('Hotkeys reset to defaults');
|
toast('Hotkeys reset to defaults');
|
||||||
|
|||||||
Reference in New Issue
Block a user