diff --git a/src/main.cpp b/src/main.cpp index fdeb0cf..69ea376 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -318,6 +318,9 @@ struct TickState { double panelX = 0, panelY = 0; // its desktop position (sub-pixel) RECT panelSavedClip{}; // the clip to give back when the panel closes double viewVx = 0, viewVy = 0; // tracking spring velocity (trackGlideMode=1) + // trackLog diagnostics (#326): the last logged caret seq and the last tracking-enable reason. + unsigned diagSeq = 0; + int diagEnableBits = -1; HCURSOR bodyCursor = nullptr; // edge mode: the cursor cursorBody was measured from wind::CursorBody cursorBody; // its visible body around the hotspot (desktop px) unsigned long long lastButtonMs = 0; // last tick a mouse button was down (click quiet period) @@ -1776,6 +1779,16 @@ static void RunTick(TickState& t) { const bool trackEnabled = lvl > 1.001 && !panel && !inspect && !t.detector.locked() && !fsCover && (t.cfg.trackCaret != 0 || t.cfg.trackFocus != 0); g_track.setActive(trackEnabled, t.cfg.trackCaret != 0, t.cfg.trackFocus != 0, t.cfg.trackLog != 0); + if (t.cfg.trackLog) { // #326: why tracking is on or off, logged on every change + const int bits = (lvl > 1.001 ? 1 : 0) | (panel ? 2 : 0) | (inspect ? 4 : 0) | + (t.detector.locked() ? 8 : 0) | (fsCover ? 16 : 0); + if (bits != t.diagEnableBits) { + t.diagEnableBits = bits; + wind::Log(wind::LogLevel::Info, "track", "diag enabled=%d zoomed=%d panel=%d inspect=%d locked=%d fsCover=%d lvl=%.2f", + trackEnabled ? 1 : 0, bits & 1 ? 1 : 0, bits & 2 ? 1 : 0, bits & 4 ? 1 : 0, + bits & 8 ? 1 : 0, bits & 16 ? 1 : 0, lvl); + } + } // Keyboard panning (#287): only presses the hook swallowed count (a key that went to the app // at 1x never pans after a zoom-in mid-press); without the hook, the polled combo. const bool panEnabled = panArmed && !panel; @@ -1808,10 +1821,27 @@ static void RunTick(TickState& t) { // hook no longer switches the gate off. No stamp at all and no hook: no information, no gate. const unsigned long long lastKey = g_input.lastAnyKeyDownMs(); vi.msSinceKey = lastKey ? double(nowMs - lastKey) : (g_input.kbHookActive() ? 1e9 : 0.0); + // #328: a key after the last mouse button means typing, so the click quiet period ends early. + vi.keyAfterButton = lastKey && t.lastButtonMs && lastKey > t.lastButtonMs; vi.dtMs = dt * 1000.0; vi.snap = g_track.snapshot(); const wind::ViewOwner was = t.viewOwner.owner; + const unsigned diagTargetSeqBefore = t.viewOwner.target.seq; const wind::ViewOwner owner = wind::StepViewOwner(t.viewOwner, vi); + if (t.cfg.trackLog && vi.snap.seq != t.diagSeq) { // #326: every caret/focus event, with the gate inputs + t.diagSeq = vi.snap.seq; + const bool latched = t.viewOwner.target.seq == vi.snap.seq && t.viewOwner.target.seq != diagTargetSeqBefore; + double ax = -1, ay = -1; + const wind::TrackRect drc{ vi.snap.l - t.mon.x, vi.snap.t - t.mon.y, vi.snap.r - t.mon.x, vi.snap.b - t.mon.y }; + wind::TrackTargetCenter(drc, t.viewCx, t.viewCy, lvl, t.mon.w, t.mon.h, t.cfg.trackAlign, t.cfg.trackMarginPct, ax, ay); + static const char* kOwn[] = { "mouse", "caret", "focus", "keys" }; + wind::Log(wind::LogLevel::Info, "track", + "diag evt seq=%u kind=%d rect=%.0f,%.0f-%.0f,%.0f owner=%s latched=%d msKey=%.0f msBtn=%.0f en=%d lvl=%.2f view=%.0f,%.0f (%s) aim=%.0f,%.0f viewW=%.0f", + vi.snap.seq, (int)vi.snap.kind, drc.l, drc.t, drc.r, drc.b, kOwn[(int)owner], latched ? 1 : 0, + vi.msSinceKey, vi.msSinceButton, vi.enabled ? 1 : 0, lvl, + t.viewDetached ? t.viewCx : r.centerX, t.viewDetached ? t.viewCy : r.centerY, + t.viewDetached ? "detached" : "mouse", ax, ay, t.mon.w / lvl); + } if (owner != was && t.cfg.trackLog) { static const char* kName[] = { "mouse", "caret", "focus", "keys" }; wind::Log(wind::LogLevel::Info, "track", "view %s -> %s%s", kName[(int)was], kName[(int)owner], diff --git a/src/version.h b/src/version.h index c79c886..83e4068 100644 --- a/src/version.h +++ b/src/version.h @@ -4,7 +4,7 @@ #define WIND_VER_MAJOR 0 #define WIND_VER_MINOR 21 -#define WIND_VER_PATCH 2 +#define WIND_VER_PATCH 3 // String form for logs/snapshot/UI. Keep in sync with the numeric parts above. -#define WIND_VERSION_STR "0.21.2" +#define WIND_VERSION_STR "0.21.3" diff --git a/src/view_target.h b/src/view_target.h index 709d144..a708823 100644 --- a/src/view_target.h +++ b/src/view_target.h @@ -10,7 +10,9 @@ // would drag. (The old "glide the view back" chased a moving pointer and never arrived - the // field-reported wobble.) // - Caret/focus changes within kClickQuietMs of a mouse button are the click's own doing -// (opening a page, clicking into a field): they are consumed without taking the view. +// (opening a page, clicking into a field): they are consumed without taking the view. A key +// pressed AFTER the click ends that early (#328: clicking into Notepad and typing at once lost the +// first ~8 characters while the caret was off screen). // - And they need a KEY: a caret/focus change takes the view only if a key went down or up within // kKeyDrivenMs (issue #289; key-ups count so Alt+Tab released after a long look still counts). Scrolling a page moves a focused control's caret on screen with // no key at all (field: the Settings page dragged the view while scrolling). @@ -45,6 +47,7 @@ struct ViewOwnerInputs { bool buttonDown = false; double msSinceButton = 1e9; // time since a mouse button was last down double msSinceKey = 0; // time since any key went down (0 when unknown: no gate) + bool keyAfterButton = false; // a key went down AFTER the last mouse button: typing, so the click quiet period is over (#328) double dtMs = 0; TrackSnapshot snap; bool panning = false; // a pan key is held or its motion is still gliding (#287) @@ -94,7 +97,7 @@ inline ViewOwner StepViewOwner(ViewOwnerState& s, const ViewOwnerInputs& in) { // A new tracking event, unless a recent click caused it. if (in.trackActive && in.snap.seq != s.lastSeq) { // tracking off: nothing takes the view s.lastSeq = in.snap.seq; - if (in.msSinceButton >= kClickQuietMs && in.msSinceKey <= kKeyDrivenMs) { + if ((in.msSinceButton >= kClickQuietMs || in.keyAfterButton) && in.msSinceKey <= kKeyDrivenMs) { if (in.snap.kind == TrackKind::Caret && in.trackCaret) { s.owner = ViewOwner::Caret; s.target = in.snap; } else if (in.snap.kind == TrackKind::Focus && in.trackFocus) { s.owner = ViewOwner::Focus; s.target = in.snap; } } diff --git a/tests/test_view_target.cpp b/tests/test_view_target.cpp index 640bd8d..9493617 100644 --- a/tests/test_view_target.cpp +++ b/tests/test_view_target.cpp @@ -63,6 +63,18 @@ TEST_CASE("caret or focus changes right after a click are consumed, never follow in.snap = Snap(TrackKind::Caret, 2); // a NEW change after the quiet period does CHECK(StepViewOwner(s, in) == ViewOwner::Caret); } +TEST_CASE("typing right after a click is followed: a key AFTER the click ends the quiet period (#328)") { + // Field 2026-10-02 (Notepad): click into the text, type at once; the first 8 characters fell inside + // the 1 s click quiet period and the view sat on the pointer, the caret off screen. + ViewOwnerState s; auto in = Base(); + in.msSinceButton = 300; in.msSinceKey = 20; in.keyAfterButton = true; in.snap = Snap(TrackKind::Caret, 1); + CHECK(StepViewOwner(s, in) == ViewOwner::Caret); +} +TEST_CASE("the click's own caret move is still consumed when no key followed the click (#328)") { + ViewOwnerState s; auto in = Base(); + in.msSinceButton = 300; in.msSinceKey = 700; in.keyAfterButton = false; in.snap = Snap(TrackKind::Caret, 1); + CHECK(StepViewOwner(s, in) == ViewOwner::Mouse); +} TEST_CASE("tracking turned off mid-caret goes straight back to the mouse, no warp") { ViewOwnerState s; auto in = Base(); in.snap = Snap(TrackKind::Caret, 1); StepViewOwner(s, in); in.enabled = false; diff --git a/ui/src/controls/Preview.svelte b/ui/src/controls/Preview.svelte index 4e7b824..850a357 100644 --- a/ui/src/controls/Preview.svelte +++ b/ui/src/controls/Preview.svelte @@ -23,7 +23,7 @@ const visible = (r) => !r.showIf || String(values[r.showIf.key]) === String(r.showIf.eq); const set = (key, val) => { values[key] = val; calls.push([key, val]); }; const extra = { - runningModel: 'hybrid', mpoNeedsRestart: false, version: '0.21.2', + runningModel: 'hybrid', mpoNeedsRestart: false, version: '0.21.3', onRestart: () => calls.push('restart'), onAction: (a) => calls.push(['action', a]), onRepo: () => calls.push('repo'), pick: async () => 'RDR2.exe',