fix: three more bugs of the same classes (Esc fall-through, chord, placement)

Hunted the classes behind the recent quick-search fixes across the codebase:

- Esc priority: the command palette is .cmd-palette (not .modal), so the
  slideover's capture-phase Esc handler didn't see it and popped the
  discard-confirm UNDER the open palette. Capture handler now also yields
  to the palette.
- Esc fall-through: the fallback Esc branch closed palette + slideover +
  entry modal + generator ALL on one keystroke. Now closes exactly one
  surface per keystroke, topmost first.
- Modifier chord: the real-VK Tab between username and password becomes
  Shift+Tab if the user holds Shift mid-fill -> focus moves backward and
  the password lands in the username field. ForceReleaseModifiers before
  SendVKey(VK_TAB).
- Stale placement: RestoreFromTray replayed the MinimizeToTray snapshot for
  a merely taskbar-minimised window, teleporting it to the last tray-hide
  position. Snapshot now replays only on a genuine tray return (captured
  before Show flips Visible); iconic windows use Windows' own placement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-12 22:46:42 +01:00
parent 3d217e82d4
commit bf606493bd
2 changed files with 38 additions and 8 deletions
+13 -1
View File
@@ -669,6 +669,7 @@ end;
procedure TPMBridge.RestoreFromTray; procedure TPMBridge.RestoreFromTray;
var var
LFormHwnd, LAppHwnd: HWND; LFormHwnd, LAppHwnd: HWND;
LWasTrayHidden: Boolean;
begin begin
// Tray icon stays in the tray — we only show the window again. // Tray icon stays in the tray — we only show the window again.
LFormHwnd := MainFormHWND(FMainForm); LFormHwnd := MainFormHWND(FMainForm);
@@ -684,6 +685,10 @@ begin
Exit; Exit;
end; end;
// Captured BEFORE Show flips Visible: tray-hidden (Hide'd by
// MinimizeToTray) vs merely taskbar-minimised (Visible but iconic).
LWasTrayHidden := not FMainForm.Visible;
// Reverse order: show the app proxy first so the taskbar entry comes back, // Reverse order: show the app proxy first so the taskbar entry comes back,
// then show and foreground the form. // then show and foreground the form.
if LAppHwnd <> 0 then if LAppHwnd <> 0 then
@@ -697,7 +702,11 @@ begin
// window). Unconditional SW_RESTORE would un-maximise a maximised // window). Unconditional SW_RESTORE would un-maximise a maximised
// window — surprising for the user who pressed Ctrl+Shift+A / +L / // window — surprising for the user who pressed Ctrl+Shift+A / +L /
// clicked the tray. // clicked the tray.
if FHasSavedPlacement then // Replay the snapshot ONLY for a genuine tray return (window was hidden —
// the exact state MinimizeToTray created). A merely taskbar-minimised
// window restores via Windows' own placement memory below; replaying our
// older snapshot would teleport it to wherever it sat at the LAST tray-hide.
if FHasSavedPlacement and LWasTrayHidden then
begin begin
// showCmd governs whether the window comes back maximised or normal; // showCmd governs whether the window comes back maximised or normal;
// it's what SW_RESTORE clobbers. We force it ourselves. Captured while // it's what SW_RESTORE clobbers. We force it ourselves. Captured while
@@ -1325,6 +1334,9 @@ begin
ClearFieldIfWanted; ClearFieldIfWanted;
SendUnicodeString(AUsername); SendUnicodeString(AUsername);
Sleep(200); Sleep(200);
// Real-VK send: a physically held Shift here turns Tab into Shift+Tab —
// focus moves BACKWARD and the password lands in the username field.
ForceReleaseModifiers;
SendVKey(VK_TAB); SendVKey(VK_TAB);
Sleep(200); Sleep(200);
ClearFieldIfWanted; ClearFieldIfWanted;
+25 -7
View File
@@ -7222,6 +7222,10 @@ async function init() {
if (e.key !== 'Escape') return; if (e.key !== 'Escape') return;
if (!$('#slideover').classList.contains('is-open')) return; if (!$('#slideover').classList.contains('is-open')) return;
if (document.querySelector('.modal:not(.is-hidden)')) return; if (document.querySelector('.modal:not(.is-hidden)')) return;
// Command palette is .cmd-palette, not .modal — it sits on top of
// the editor, so its Esc must close IT (bubble handler), not pop
// the editor's discard-confirm underneath.
if (!$('#cmdPalette').classList.contains('is-hidden')) return;
// Settings panel takes priority — when both Settings AND the // Settings panel takes priority — when both Settings AND the
// editor are open, the first Esc should close Settings (the // editor are open, the first Esc should close Settings (the
// thing the user just opened on top), the second Esc handles // thing the user just opened on top), the second Esc handles
@@ -7912,13 +7916,27 @@ async function init() {
closeHistoryModal(); closeHistoryModal();
return; return;
} }
closePalette(); // ONE surface per keystroke, topmost first — closing several at
requestCloseSlideOver(); // once meant "Esc closes the palette AND pops the editor's
closeEntryModal(); // discard prompt" (same class as the quick-search Esc bug).
closeGen(); if (!$('#cmdPalette').classList.contains('is-hidden')) {
// If nothing else needed dismissing and there's an active closePalette();
// selection, clear it. Replaces the "click empty space to return;
// deselect" path that the marquee provided. }
if (!$('#entryModal').classList.contains('is-hidden')) {
closeEntryModal();
return;
}
if (!$('#genModal').classList.contains('is-hidden')) {
closeGen();
return;
}
if ($('#slideover').classList.contains('is-open')) {
requestCloseSlideOver();
return;
}
// Nothing else needed dismissing — clear an active selection.
// Replaces the "click empty space to deselect" marquee path.
if (state.checked.size > 0) { if (state.checked.size > 0) {
state.checked.clear(); state.checked.clear();
renderGrid(); renderGrid();