fix(core): drive auto-lock idle from real input, not widget focus

The auto-lock idle clock was reset whenever any ImGui item was active,
so a text field left focused (a half-filled send form, say) counted as
continuous activity and kept the wallet unlocked indefinitely, well past
the configured idle timeout.

- The idle clock is now fed by real input events (key, text, mouse,
  touch, pen, gamepad button) as they are ingested on the main thread,
  plus a still-held mouse button so a drag or scrollbar grab that emits
  no new event still counts. A focused but untouched widget no longer
  does.
- A new header-only IdleTracker owns the clock and never moves backwards,
  so an event stamped before the previous one cannot shorten the idle
  time.

Behaviour change: a form left focused and idle past the timeout now
auto-locks. The form's buffers are app state and survive the lock and
unlock, so nothing typed is lost.

Tests: a new IdleTracker case covers idle time growing with no events
(enough to trip a 900s lock), activity resetting it, the clock never
moving backwards, and a query before the last activity reading zero
rather than a negative value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 21:42:23 -05:00
co-authored by Claude Opus 4.8
parent 1ea584311e
commit 0aaf33ecf0
6 changed files with 66 additions and 12 deletions
+7 -4
View File
@@ -868,14 +868,17 @@ void App::update()
// If a wallet switch's daemon failed to start, revert to the previous wallet (main-thread-safe).
processWalletSwitchRevert();
// Track user interaction for auto-lock. Not in the background: a minimized window runs update() without
// new frames, so the IO state is the last frame's, and a stale mouse delta kept auto-lock from ever firing.
// Real input arrives through App::noteUserInput() (main.cpp event ingest). What stays here covers input
// that produces no new SDL event this frame: a held mouse button (a drag or scrollbar grab without
// motion). NOT in the background: a minimized window runs update() with the last frame's IO, and a stale
// mouse delta once kept auto-lock from ever firing. Deliberately NOT IsAnyItemActive(): a focused text
// field stays active indefinitely (C21).
if (!window_background_ &&
(io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
io.MouseClicked[0] || io.MouseClicked[1] ||
io.InputQueueCharacters.Size > 0 ||
ImGui::IsAnyItemActive())) {
last_interaction_ = std::chrono::steady_clock::now();
ImGui::IsAnyMouseDown())) {
idle_.noteActivity();
}
// Advance the refresh timers by the real time since the last update BEFORE the RPC results are