fix(hotkeys): name the combo another app already owns
Two gaps when Windows refuses a RegisterHotKey: - the toast said "one or both hotkeys" — wrong since there are three autofill combos, and it never said which one lost. - Quick search and New entry failures were only written to the debug panel, which is off in production: the hotkey silently did nothing with no clue why. Delphi now exposes the per-slot registration outcome and the host accumulates the failed combo names across all five hotkeys, emitting one notification for the batch. A combo the user deliberately cleared (vk=0) is never listed. JS names them: "Already used by another app: 2FA code — pick another combo in Settings". Registration stays best-effort per slot: one clash doesn't disarm the others. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -308,6 +308,12 @@ l'éditeur slideover : créer + éditer une entrée, ajouter/retirer des tags,
|
|||||||
- **ACTION** : verrouiller → `Ctrl+Shift+T`.
|
- **ACTION** : verrouiller → `Ctrl+Shift+T`.
|
||||||
- **ATTENDU** : l'app revient au premier plan avec le focus sur le master password (même comportement que les autres hotkeys), pas de no-op silencieux.
|
- **ATTENDU** : l'app revient au premier plan avec le focus sur le master password (même comportement que les autres hotkeys), pas de no-op silencieux.
|
||||||
|
|
||||||
|
### H7 · combo déjà pris par une autre application
|
||||||
|
- **ACTION** : assigner à un hotkey un combo qu'une autre app tient déjà en global (ex. `Ctrl+Shift+Q` si Quick Assist / un lanceur l'occupe ; sinon, tester en assignant le même combo dans deux apps).
|
||||||
|
- **ATTENDU** : toast **nommant le combo refusé** — « Already used by another app: 2FA code — pick another combo in Settings ». Les **autres** hotkeys restent fonctionnels (enregistrement best-effort, pas de tout-ou-rien).
|
||||||
|
- **ACTION 2** : fermer l'app concurrente → *Reset to defaults* (ou re-capturer le combo) → le hotkey redevient actif.
|
||||||
|
- **Historique** : le message disait « one or both hotkeys » (faux depuis qu'il y en a 3) sans dire lequel ; et un échec sur **Quick search** ou **New entry** n'était écrit que dans le panel debug — invisible en prod, le hotkey semblait juste cassé.
|
||||||
|
|
||||||
### H6 · code en fin de période
|
### H6 · code en fin de période
|
||||||
- **ACTION** : presser le hotkey juste avant la fin du compte à rebours de 30 s.
|
- **ACTION** : presser le hotkey juste avant la fin du compte à rebours de 30 s.
|
||||||
- **ATTENDU** : le code tapé est celui de la période **courante** au moment de la frappe (généré à la demande, pas mis en cache).
|
- **ATTENDU** : le code tapé est celui de la période **courante** au moment de la frappe (généré à la demande, pas mis en cache).
|
||||||
|
|||||||
@@ -202,6 +202,12 @@ type
|
|||||||
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
property SecureClipboard: TSecureClipboard read FSecureClipboard;
|
||||||
property TrayAdded: Boolean read FTrayAdded;
|
property TrayAdded: Boolean read FTrayAdded;
|
||||||
property AutofillRegistered: Boolean read FAutofillRegistered;
|
property AutofillRegistered: Boolean read FAutofillRegistered;
|
||||||
|
// Per-slot outcome of the last SetAutofillHotkeys — lets the host name
|
||||||
|
// exactly which combo lost the race with another app's global hotkey
|
||||||
|
// instead of a vague "one of them failed".
|
||||||
|
property AutofillFullActive: Boolean read FAutofillFullActive;
|
||||||
|
property AutofillPwdActive: Boolean read FAutofillPwdActive;
|
||||||
|
property AutofillTotpActive: Boolean read FAutofillTotpActive;
|
||||||
// Fired on main thread when Windows locks the session (WTS_SESSION_LOCK).
|
// Fired on main thread when Windows locks the session (WTS_SESSION_LOCK).
|
||||||
property OnSystemLock: TProc read FOnSystemLock write FOnSystemLock;
|
property OnSystemLock: TProc read FOnSystemLock write FOnSystemLock;
|
||||||
// True once WM_QUERYENDSESSION (or WM_ENDSESSION) has been received.
|
// True once WM_QUERYENDSESSION (or WM_ENDSESSION) has been received.
|
||||||
|
|||||||
@@ -863,6 +863,11 @@ begin
|
|||||||
// we register both with the supplied combos (replacing any prior).
|
// we register both with the supplied combos (replacing any prior).
|
||||||
else if ACmd = 'autofill/hotkeys' then
|
else if ACmd = 'autofill/hotkeys' then
|
||||||
begin
|
begin
|
||||||
|
// Names of the combos Windows refused (already held by another app), so
|
||||||
|
// the toast can say WHICH one is dead instead of leaving the user to
|
||||||
|
// wonder why a hotkey silently does nothing. A combo the user cleared
|
||||||
|
// (vk=0) is skipped on purpose and never listed here.
|
||||||
|
var LFailed := '';
|
||||||
// Quick-search combo is set unconditionally (independent of the
|
// Quick-search combo is set unconditionally (independent of the
|
||||||
// autofill enabled flag) so the picker stays armed even when the
|
// autofill enabled flag) so the picker stays armed even when the
|
||||||
// autofill hotkeys are turned off.
|
// autofill hotkeys are turned off.
|
||||||
@@ -871,6 +876,7 @@ begin
|
|||||||
var LQsMods := Word(StrToIntDef(GetParam('qs_mods'), 6));
|
var LQsMods := Word(StrToIntDef(GetParam('qs_mods'), 6));
|
||||||
var LQsVk := Word(StrToIntDef(GetParam('qs_vk'), Ord('Q')));
|
var LQsVk := Word(StrToIntDef(GetParam('qs_vk'), Ord('Q')));
|
||||||
var LQsOk := FBridge.SetQuickSearchHotkey(LQsMods, LQsVk);
|
var LQsOk := FBridge.SetQuickSearchHotkey(LQsMods, LQsVk);
|
||||||
|
if (not LQsOk) and (LQsVk <> 0) then LFailed := LFailed + 'Quick search, ';
|
||||||
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;
|
||||||
@@ -881,6 +887,7 @@ begin
|
|||||||
var LNeMods := Word(StrToIntDef(GetParam('ne_mods'), 6));
|
var LNeMods := Word(StrToIntDef(GetParam('ne_mods'), 6));
|
||||||
var LNeVk := Word(StrToIntDef(GetParam('ne_vk'), Ord('A')));
|
var LNeVk := Word(StrToIntDef(GetParam('ne_vk'), Ord('A')));
|
||||||
var LNeOk := FBridge.SetNewEntryHotkey(LNeMods, LNeVk);
|
var LNeOk := FBridge.SetNewEntryHotkey(LNeMods, LNeVk);
|
||||||
|
if (not LNeOk) and (LNeVk <> 0) then LFailed := LFailed + 'New entry, ';
|
||||||
LogLine(Format('New-entry hotkey set — mods:%d vk:%d (ok=%s)',
|
LogLine(Format('New-entry hotkey set — mods:%d vk:%d (ok=%s)',
|
||||||
[LNeMods, LNeVk, BoolToStr(LNeOk, True)]));
|
[LNeMods, LNeVk, BoolToStr(LNeOk, True)]));
|
||||||
end;
|
end;
|
||||||
@@ -903,12 +910,19 @@ begin
|
|||||||
LTotpMods, LTotpVk);
|
LTotpMods, LTotpVk);
|
||||||
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d totp=mods:%d vk:%d (all_ok=%s)',
|
LogLine(Format('Autofill hotkeys set — full=mods:%d vk:%d pwd=mods:%d vk:%d totp=mods:%d vk:%d (all_ok=%s)',
|
||||||
[LFullMods, LFullVk, LPwdMods, LPwdVk, LTotpMods, LTotpVk, BoolToStr(LAllOk, True)]));
|
[LFullMods, LFullVk, LPwdMods, LPwdVk, LTotpMods, LTotpVk, BoolToStr(LAllOk, True)]));
|
||||||
// Notify JS of the result so the UI can flag a failed-to-register combo
|
if (not FBridge.AutofillFullActive) and (LFullVk <> 0) then
|
||||||
// (typically a clash with another app's global hotkey).
|
LFailed := LFailed + 'Autofill (full), ';
|
||||||
|
if (not FBridge.AutofillPwdActive) and (LPwdVk <> 0) then
|
||||||
|
LFailed := LFailed + 'Password only, ';
|
||||||
|
if (not FBridge.AutofillTotpActive) and (LTotpVk <> 0) then
|
||||||
|
LFailed := LFailed + '2FA code, ';
|
||||||
|
end;
|
||||||
|
// One notification for the whole batch, naming the combos that failed —
|
||||||
|
// empty string = everything registered.
|
||||||
|
LFailed := LFailed.TrimRight([',', ' ']);
|
||||||
WebBrowser.ExecuteJavaScript(
|
WebBrowser.ExecuteJavaScript(
|
||||||
'if(window.Bridge&&Bridge.onAutofillHotkeysResult)' +
|
'if(window.Bridge&&Bridge.onAutofillHotkeysResult)' +
|
||||||
'Bridge.onAutofillHotkeysResult(' + BoolToStr(LAllOk, True).ToLower + ')');
|
'Bridge.onAutofillHotkeysResult("' + LFailed.Replace('"', '') + '")');
|
||||||
end;
|
|
||||||
end
|
end
|
||||||
|
|
||||||
// execute: JS has matched an entry, decrypted the password, and is
|
// execute: JS has matched an entry, decrypted the password, and is
|
||||||
|
|||||||
@@ -218,12 +218,15 @@ const Bridge = (() => {
|
|||||||
// Called by Delphi after a setAutofillHotkeys request, with true if
|
// Called by Delphi after a setAutofillHotkeys request, with true if
|
||||||
// BOTH combos registered successfully, false otherwise (clash with
|
// BOTH combos registered successfully, false otherwise (clash with
|
||||||
// another app holding a global hotkey). Surfaces a toast.
|
// another app holding a global hotkey). Surfaces a toast.
|
||||||
onAutofillHotkeysResult(allOk) {
|
// `failed` = comma-separated names of the combos Windows refused
|
||||||
if (allOk) {
|
// (another app already owns them); empty string = all registered.
|
||||||
toast('Autofill hotkeys updated');
|
// Naming them matters: a clashing hotkey is otherwise indistinguishable
|
||||||
} else {
|
// from a broken feature — it just silently does nothing.
|
||||||
toast('Autofill: one or both hotkeys are already used by another app', 'warning');
|
onAutofillHotkeysResult(failed) {
|
||||||
}
|
const list = String(failed || '').trim();
|
||||||
|
if (!list) { toast('Hotkeys updated'); return; }
|
||||||
|
toast('Already used by another app: ' + list +
|
||||||
|
' — pick another combo in Settings', 'warning');
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Device-bound prefs (DPAPI-backed) ----------------------------
|
// ---- Device-bound prefs (DPAPI-backed) ----------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user