Skip to content

feat(spectrum): pan-follow-VFO triggers off flag outer edge (#2761)#2784

Merged
ten9876 merged 1 commit into
mainfrom
auto/fix-2761
May 17, 2026
Merged

feat(spectrum): pan-follow-VFO triggers off flag outer edge (#2761)#2784
ten9876 merged 1 commit into
mainfrom
auto/fix-2761

Conversation

@ten9876

@ten9876 ten9876 commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the plan from #2761. Replaces incomplete agent attempt #2778 (which committed the API surface without the implementation wiring).

When a slice tunes near a pan edge, the existing Pan Follows VFO machinery slides the pan center so the slice stays visible. But the VFO flag panel hanging off the slice marker is fixed-width — the slice can be well inside the trigger boundary while the flag panel is already clipping the pan edge. Split pairs (LockLeft + LockRight on the same marker, #2663 / #2744) are particularly affected: PR #2744 intentionally accepted that the outward-facing flag clips and documented "the user pans toward center to read it" as the workaround.

This change removes that workaround by treating the flag's outer edge as the trigger reference, not the slice frequency.

What changes

  • `revealFrequencyIfNeeded` gains `leftFlagEdgeOffsetMhz` / `rightFlagEdgeOffsetMhz` defaulted to `0.0`. Only consumed on the `IncrementalTune` path — `CommandedTargetCenter` / `RevealOffscreen` ignore them.
  • Offsets are clamped to 95% of `triggerDistanceFromCenter` so tiny panadapters where flag-width > trigger-margin don't oscillate the pan around the slice.
  • `panFollowVfo` now computes flag-width-in-MHz from the bound `VfoWidget` (`width() * (bandwidthMhz / specPixelW)`) and threads it through:
    • Split pair (`SpectrumWidget::sliceHasSplitPartner` returns true): both offsets non-zero — extends the trigger on both sides since LockLeft + LockRight flags render on both sides simultaneously.
    • Single-flag slice: only the side where the flag currently renders (per `VfoWidget::onLeft()`).
    • Collapsed / compact-mode flag: both offsets stay 0.0 — compact mode doesn't render a wide flag.

Files

File Change
`src/gui/MainWindow.h` `revealFrequencyIfNeeded` signature + comment
`src/gui/MainWindow.cpp` Implementation: clamp + apply offsets in the trigger comparison; `panFollowVfo` computes offsets from VfoWidget state
`src/gui/SpectrumWidget.h` `sliceHasSplitPartner(int)` accessor
`src/gui/SpectrumWidget.cpp` `sliceHasSplitPartner` implementation (scans `m_sliceOverlays` for the slice + checks `splitPartnerId`)
`src/gui/VfoWidget.h` `onLeft()` accessor exposing the existing `m_lastOnLeft` state set by `updatePosition`

Net: 85 insertions / 9 deletions across 5 files.

Local verification

  • Build clean (`cmake --build build --target AetherSDR`) — 401/401 link, only pre-existing warnings
  • Test-target link audit: none of the touched headers are linked by isolated test targets — the new `onLeft()` accessor and `sliceHasSplitPartner()` accessor are GUI-only and only consumed from MainWindow.cpp. No CMakeLists changes needed.

Test plan (manual, post-merge)

  • Tune a slice toward a pan edge with a wide VFO flag visible — pan should slide before the flag clips
  • In split mode (LockLeft + LockRight on the same marker) — pan should slide before either flag clips
  • Non-flag callers (Memory recall, click-to-tune absolute jump) — no behavior change (CommandedTargetCenter ignores the offsets)
  • Compact-mode (collapsed) flag — falls back to original slice-frequency comparison
  • Toggle `View → Pan Follows VFO` off — pan does not slide at all (existing override preserved)

Closes

🤖 Generated with Claude Code

When a slice tunes near a pan edge, the existing Pan Follows VFO
machinery slides the pan center so the slice stays visible.  But the
VFO flag panel (callsign / freq / mode badge hanging off the slice
marker) is fixed-width and renders OUTSIDE the slice marker — so the
slice can be well within the trigger boundary while the flag panel
is already clipping the pan edge.  Split pairs (LockLeft + LockRight
on the same marker, #2663) are particularly affected: PR #2744
intentionally accepted that the outward-facing flag clips and
documented "the user pans toward center to read it" as the workaround.
This change removes that workaround by treating the flag's outer edge
as the trigger reference.

revealFrequencyIfNeeded gains two defaulted offset parameters,
leftFlagEdgeOffsetMhz and rightFlagEdgeOffsetMhz.  When non-zero
(IncrementalTune only — other intents ignore them), the trigger
compares (mhz - leftOffset) against the left pan boundary and
(mhz + rightOffset) against the right pan boundary, instead of
mhz on both sides.  Default 0.0 preserves the original slice-
frequency comparison for non-flag callers (CommandedTargetCenter,
RevealOffscreen).  Offsets are clamped to 95% of triggerDistance-
FromCenter so a tiny panadapter where flag-width > trigger-margin
doesn't oscillate the pan around the slice.

panFollowVfo computes the offsets by:
  1. Reading the spectrum's bandwidthMhz and pixel width to derive
     mhzPerPixel.
  2. Multiplying VfoWidget::width() by mhzPerPixel for flag-width-
     in-MHz.
  3. For split pairs (SpectrumWidget::sliceHasSplitPartner returns
     true — flags on both sides via LockLeft/LockRight), passing
     the same offset for both sides.  For non-split slices, passing
     the offset only on the side where the flag currently renders
     (VfoWidget::onLeft()).  For collapsed/compact-mode flags,
     both offsets stay 0.0 (compact mode doesn't render a wide flag).

Existing PR #2744 still locks split-pair flags to their outward
sides; this change ensures the pan slides early enough that the
lock doesn't cause clipping in the first place.

Files:
  src/gui/MainWindow.h        — revealFrequencyIfNeeded signature +
                                explanatory comment block
  src/gui/MainWindow.cpp      — implementation: clamp + apply
                                offsets in the trigger comparison;
                                panFollowVfo: compute offsets from
                                VfoWidget width + onLeft + split
                                state
  src/gui/SpectrumWidget.h    — sliceHasSplitPartner(int) accessor
  src/gui/SpectrumWidget.cpp  — sliceHasSplitPartner implementation
                                (scans m_sliceOverlays for the
                                slice + checks splitPartnerId)
  src/gui/VfoWidget.h         — onLeft() accessor exposing the
                                existing m_lastOnLeft state set by
                                updatePosition

Local build clean (401/401 link).  Manual verification deferred to
post-merge smoke test: tune toward a pan edge with a wide VFO flag
visible and confirm the pan slides before the flag clips, both for
single-flag and split-pair scenarios.

Closes #2761.  Closes-as-replacement for #2778 (incomplete recovery).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 requested a review from jensenpat as a code owner May 17, 2026 01:37
@ten9876 ten9876 enabled auto-merge (squash) May 17, 2026 01:37

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ten9876 — clean recovery of #2778 with the wiring actually plumbed through this time. The implementation reads well; defaulted offsets preserve every existing caller, and the incremental ? … : mhz collapse means only the IncrementalTune path consumes them, exactly as the PR description claims.

Things I checked and like

  • Null safety: both spectrumForSlice(s) and sw->vfoWidget(s->sliceId()) are guarded with if (auto*), and specW > 0 / bw > 0.0 cover the early-construction window where width hasn't been laid out yet.
  • Compact-mode handling: !vfo->isCollapsed() keeps both offsets at 0.0, which is the right call — the collapsed flag isn't wide enough to need the extension.
  • Split-pair detection: sliceHasSplitPartner() correctly scans m_sliceOverlays for splitPartnerId >= 0, which is the same source-of-truth the renderer uses for LockLeft/LockRight layout.
  • Anti-oscillation clamp: triggerDistanceFromCenter * 0.95 is a sensible safety margin so flag-wider-than-trigger doesn't ping-pong the pan center around the slice. Worth noting that on a very narrow pan this means the flag will clip a bit rather than oscillate — that's the right trade.
  • onLeft() accessor: just exposes the existing m_lastOnLeft that updatePosition() already maintains, so the read is always consistent with whatever the flag last rendered as.

Minor observations (non-blocking)

  • clampedRightOffset = std::min(rightFlagEdgeOffsetMhz, safeOffsetCap) only clamps the upper bound; a hypothetical negative offset would pass through and shrink the trigger zone. Callers in this PR always pass >= 0.0, so it's fine — but if you ever add a caller that computes the offset by subtraction, a std::clamp(x, 0.0, safeOffsetCap) would be more defensive. Not worth changing for this PR.
  • vfo->width() is the laid-out widget width. If the flag is mid-animation or the layout hasn't settled, this could briefly be slightly off, but the 95% clamp absorbs it and the trigger re-evaluates on the next tune step. Reasonable.

No conventions issues (no settings touched, no resource lifecycle, no thread-boundary concerns — all main-thread GUI state). The 5 touched files are all in scope. LGTM as a COMMENT review — leaving the merge decision to @jensenpat.

@ten9876 ten9876 merged commit d159066 into main May 17, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/fix-2761 branch May 17, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VFO flag outer-edge should trigger Pan Follow VFO (so flags don't clip in split mode near pan edge)

1 participant