fix(spectrum): Pan Lock stands down during slice drag, recenters on release#3786
Conversation
…elease (aethersdr#3785) With Pan Lock on, dragging a slice made the pan lurch in ~0.33 MHz steps / jump, and afterwards Pan Lock stopped centering the slice until toggled. Two features both drove the pan center: Pan Lock (aethersdr#3408) recenters on every slice-0 frequencyChanged; edge auto-pan (aethersdr#3581) advances the center and parks the slice at the edge. The edge-pan "avoid pan-follow" bypass was incomplete -- it still calls setFrequency(), which emits frequencyChanged, which Pan Lock is wired to, so both fired each tick with opposite intents (slice-at-center vs slice-at-edge). SpectrumWidget emits sliceDragActiveChanged(bool) on slice-drag start/end. MainWindow suppresses the Pan Lock recenter for the drag's whole duration and recenters once on release (recenterPanFollowOnSlice0(), extracted + self-guarded on the toggle state). Mirrors the existing WFM precedence. Measured (aether.perf, Pan Lock on): in-window panCenterCmdRate 34 -> 0-2/s, center stable instead of snapping to slice; edge drag smooth instead of ~0.33 MHz lurches; exactly one recenter on release so Pan Lock re-asserts. Principle XI -- Fixes Are Demonstrated (before/after measured on real hardware). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for this @svabi79 — clear root-cause writeup, and the fix is well-architected. I traced the diff against the surrounding code on main; it holds up.
What works well
- Single source of truth for the recenter. Extracting
recenterPanFollowOnSlice0()and self-guarding it onm_panFollowActive(andm_sliceDragInProgress, and the existing WFM precedence) means all three call sites — enable, slice-0frequencyChanged, and drag-end — go through one guarded path. That's cleaner than the capturedcenterPanlambda it replaces, and refetchingslice(0)fresh each call rather than capturingsby value is the right move. - Drag-end signalling is sound.
m_draggingVfois settrueat both drag-start sites (in-window passband + slice-flag hit) and is only ever cleared inmouseReleaseEvent(line 6361). The earlier early-returns in that handler are all mutually-exclusive drag states (TNF / divider / dBm / time-scale / bandwidth), so the VFO release always reaches the newemit sliceDragActiveChanged(false). No double-emit, no missed end. - Mirrors the documented WFM precedence, and the UX call (slice follows cursor during drag, snaps to center on release) is reasonable and flagged for maintainer sign-off — consistent with the existing convention.
- CI is green across build / check-windows / check-macos / analyze(cpp) / CodeQL.
One edge case worth a note (non-blocking)
The defensive m_sliceDragInProgress = false in setPanFollow() covers drag-abandoned-then-Pan-Lock-toggled. But if a VFO drag is ever torn down without a mouseReleaseEvent and without a subsequent Pan Lock toggle — e.g. the pane is removed mid-drag, or the mouse grab is lost — m_sliceDragInProgress sticks true and Pan Lock stays silently suppressed until the next toggle. In practice Qt delivers the release to the grabbing widget so this is narrow, and there's no leaveEvent/hideEvent path that resets m_draggingVfo today, so it's unlikely to trigger. Just flagging it; if you wanted belt-and-suspenders, clearing the flag wherever a drag could be cancelled would close it, but I don't think it blocks merge.
Minor observation (not a bug): sliceDragActiveChanged fires from every panadapter's SpectrumWidget into the single shared m_sliceDragInProgress, so dragging a slice on a non-Slice-A pane also stands Pan Lock down for that drag. It's a harmless no-op — recenter only triggers on slice-0 frequencyChanged, which a different slice's drag won't emit, and the release-time recenter is qFuzzyCompare-guarded.
Nicely measured and scoped. 👍
🤖 aethersdr-agent · cost: $3.9199 · model: claude-opus-4-8
rfoust
left a comment
There was a problem hiding this comment.
I reviewed this against the current merge ref and I do not see a merge blocker.
The change is nicely scoped: SpectrumWidget now reports the full VFO drag lifetime, and MainWindow centralizes Pan Lock recentering through recenterPanFollowOnSlice0(), guarded by Pan Lock state, drag state, and the existing WFM precedence. That fixes the root conflict cleanly: during drag, the drag controller owns pan movement; on release, Pan Lock gets one chance to reassert.
Validation:
- Built the PR merge ref successfully with
cmake --build build --target AetherSDR --parallel 8 - Ran the automation bridge smoke checks:
ping,dumpTree, Pan Lock invoke, andtools/automation_validate.py - Confirmed automation stayed non-transmitting
- Ran
git diff --check - Ran
python3 tools/check_a11y.py; only existing repo-wide warnings, none in the touched files - Manually tested the slice-drag/Pan Lock behavior and it worked correctly
The remaining abandoned-drag edge case already noted looks narrow and non-blocking. From my review and manual test, this looks ready to merge once maintainer approval is satisfied.
ten9876
left a comment
There was a problem hiding this comment.
Approved. Reviewed the diff and tested live on a FLEX-8600: with Pan Lock on, dragging Slice A now stands down the per-tick recenter for the whole drag (no more ~0.33 MHz pan lurching/jumping) and recenters once on release so Pan Lock re-asserts.
- Clean refactor: the inline
centerPanlambda becomesrecenterPanFollowOnSlice0(), called on enable, on slice-0 frequency change, and once on drag-end. - Drag state is driven by a new
SpectrumWidget::sliceDragActiveChanged(bool)emitted on VFO-drag start (marker + filter-passband paths) and release;m_sliceDragInProgresssuppresses recentering during the drag. setPanFollow()resetsm_sliceDragInProgressdefensively (covers a drag abandoned without a mouse-release).- WFM precedence preserved (WFM-held pan still wins; Pan Follow stands down).
Not a keying path (Principle VI N/A). CI green on all 6. Thanks @svabi79 — nice fix for a real annoyance.
Summary
Fixes #3785.
With Pan Lock on, dragging a slice made the panadapter jump in large lurching steps instead of scrolling smoothly, and afterwards Pan Lock stopped keeping the slice centered (button lit) until toggled off/on. Two independently-developed features both drive the pan center and fought each other:
frequencyChanged(slice at center).The edge-pan path tried to "avoid pan-follow" but the bypass was incomplete — it still calls
setFrequency(), which emitsfrequencyChanged, and Pan Lock is wired straight to that signal, so both fired each tick with opposite intents. The in-window drag had the matching symptom (slice snapping back to center instead of following the cursor).Fix: Pan Lock stands down for the whole duration of a slice drag (in-window tune and edge auto-pan), then recenters once on release so Pan Lock re-asserts.
SpectrumWidgetemitssliceDragActiveChanged(bool)on slice-drag start/end;MainWindowsuppresses the recenter while the flag is set and recenters on release. The recenter logic is extracted intoMainWindow::recenterPanFollowOnSlice0()(called on enable, on slice-0frequencyChanged, and once on drag-end), self-guarded on the Pan Lock toggle state. This mirrors the existing WFM precedence (WFM already makes Pan Lock stand down).This is a UX precedence choice (during a drag the slice follows the cursor / the band scrolls; on release the slice snaps back to center). Flagged for maintainer confirmation, consistent with the WFM-precedence note already in the code.
Measurements
i9-13900K / RTX 4090, GPU path, dummy load, Pan Lock on; gated
aether.perfdrag=1windows + theSliceDragcenter trace.panCenterCmdRatepanCenterCmds=0→ slice left off-centreVerified on real hardware: no jumping during in-window or edge drag, and Pan Lock stays active after release (no toggle needed).
Constitution principle honored
Principle XI — Fixes Are Demonstrated (before/after measured via gated
aether.perf, table above). Principle VIII — Evidence Over Assertion (root cause and fix confirmed by instrumented measurement on real hardware).Test plan
Checklist
AppSettingscalls (N/A)MeterSmoother(N/A — no meter changes)