Skip to content

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

Description

@ten9876

Context

PR #2744 (merged at 22:12 UTC) fixed the split-mode flag-overlap symptom from #2663 by locking split-pair VFO flags to their outward-facing sides (LockLeft / LockRight — no edge-clip flip). That fixes the overlap but leaves the outward-facing flag clipping the pan edge when the slice marker is near an edge. The PR's inline comment is honest about the trade-off: "the outward-facing panel may clip the pan edge — the user pans toward center to read it."

This issue tracks the real fix: instead of accepting the clip, auto-pan the panadapter so the outer edge of the VFO flag stays inside the visible spectrum.

What the existing code already does

There is already a fully-working Pan Follow VFO system, gated by the View → Pan Follows VFO menu item (AppSettings["PanFollowVfo"], default True). The whole machinery lives in:

The trigger logic:

const double triggerDistanceFromCenter = halfBw - bandwidthMhz * triggerEdgeMarginFrac;
// ...
if (mhz > center + triggerDistanceFromCenter) {
    const double settleCenterTarget = mhz - settleDistanceFromCenter;
    // ... slide pan center toward settleCenterTarget ...
}

Key concept: when the slice frequency (mhz) crosses an inner trigger boundary, the pan center slides until the slice sits comfortably back inside the visible window. Trigger margin and settle margin are separate (kIncrementalTriggerEdgeMarginFrac, kIncrementalSettleEdgeMarginFrac) so the slice doesn't dead-center — it just glides into a comfortable visible position.

This is the foundation we're keeping.

What needs to change

Today: trigger condition is based on slice frequency.
Needed: trigger condition should be based on outer edge of the VFO flag.

The VFO flag (VfoWidget) is a fixed-width panel that hangs off the slice marker on either the left or right side. Its outer edge — the side facing away from the marker — is what actually clips the pan when the slice gets close to the edge. For a left-side flag the outer edge is mhz - flagWidthMhz; for a right-side flag it's mhz + flagWidthMhz, where flagWidthMhz is the flag panel width converted from pixels to MHz at the current zoom level.

So the change is mechanical: where revealFrequencyIfNeeded today compares mhz against the trigger boundaries, it should instead compare mhz - flagWidthMhz (left flag) and mhz + flagWidthMhz (right flag) — i.e., the flag's outer edge. For an unflagged slice the value collapses back to mhz and the existing behavior is preserved.

Critical: in split mode, both LockLeft and LockRight flags are attached to the same marker, so the auto-pan trigger needs to consider whichever outer edge is closer to a pan edge. When the marker is on the left side of the pan, the LockLeft flag's outer edge (mhz - flagWidthMhz) drives the pan; when on the right, the LockRight flag's outer edge (mhz + flagWidthMhz) drives it. Single-flag (non-split) slices use whichever side the flag is currently rendered on.

Acceptance criteria

  • Tuning a slice toward a pan edge in non-split mode pans the spectrum once the outer edge of the VFO flag crosses the trigger boundary — before the flag clips.
  • In split mode with the slice marker near a pan edge, the spectrum pans automatically so the outward-facing flag does not clip. Both LockLeft and LockRight flags stay fully visible at all times.
  • Behavior for unflagged or compact-mode slices is unchanged — same trigger margins, same settle behavior, same animation duration as today.
  • View → Pan Follows VFO toggle still controls the whole behavior; disabling it disables both the existing slice-frequency follow and the new flag-edge follow.
  • The existing kIncrementalTriggerEdgeMarginFrac / kIncrementalSettleEdgeMarginFrac margins continue to apply — only the comparison input changes (slice freq → flag outer edge).
  • At the absolute pan-bandwidth edge of the band, the pan stops at the limit (existing behavior, don't try to pan beyond what the radio supports).
  • No regression for TuneIntent::CommandedTargetCenter, TuneIntent::AbsoluteJump, TuneIntent::RevealOffscreen — those code paths in revealFrequencyIfNeeded are independent of incremental tuning and should not see flag-width adjustments.

Implementation sketch

The cleanest signature is to thread the flag width into revealFrequencyIfNeeded (or panFollowVfo) as an optional offset in MHz, defaulted to 0 for backward compatibility:

TuneCenteringResult MainWindow::revealFrequencyIfNeeded(
    SliceModel* slice, double mhz, TuneIntent intent, const char* source,
    double leftFlagEdgeOffsetMhz = 0.0,
    double rightFlagEdgeOffsetMhz = 0.0);

Then inside the trigger comparison:

const double effectiveLeftMhz  = mhz - leftFlagEdgeOffsetMhz;
const double effectiveRightMhz = mhz + rightFlagEdgeOffsetMhz;

if (effectiveRightMhz > center + triggerDistanceFromCenter) {
    const double settleCenterTarget = effectiveRightMhz - settleDistanceFromCenter;
    // ... existing logic, just using the flag-edge value ...
} else if (effectiveLeftMhz < center - triggerDistanceFromCenter) {
    const double settleCenterTarget = effectiveLeftMhz + settleDistanceFromCenter;
    // ... existing logic ...
}

The wrapper panFollowVfo reads the flag width from the slice's VfoWidget (via the existing wireVfoWidget callsite plumbing — or query VfoWidget::width() directly from the rendered widget) and passes the per-side offset. For non-split slices, only one of the two offsets is non-zero. For split LockLeft+LockRight, both are non-zero and the comparison picks the side that crosses the boundary first.

Risks / edge cases

  1. Compact-mode flags (VfoWidget has a compact-mode toggle) have a different width. Use the current rendered width, not a constant.
  2. Animation feedback loop: the pan-center status echo from the radio mustn't re-trigger the follow logic mid-animation. The existing code already guards against this with m_panCenterTarget comparison (SpectrumWidget.cpp:1875) — should be preserved.
  3. Very narrow panadapters where flagWidthMhz > halfBw * triggerEdgeMarginFrac: the trigger boundary moves inside the settle boundary and the pan oscillates. Cap leftFlagEdgeOffsetMhz / rightFlagEdgeOffsetMhz to a max fraction of halfBw so this stays stable.
  4. Sliding mode: the user can manually pan the spectrum while a slice is tuned. Manual pan should override auto-pan for some grace period (existing kPanFollowAnimationDurationMs already gives a smooth animation; I don't think extra logic is needed here, but worth testing).

Out of scope

  • Adding new user-facing knobs. The existing Pan Follows VFO toggle is sufficient; this issue just makes that toggle's behavior smarter about flag width.
  • Auto-pan based on filter passband edges (separate concern; can be filed as a follow-up if anyone wants it).
  • Animation tuning. Reuse kPanFollowAnimationDurationMs as-is.

Pickup

AetherClaude-eligible — the trigger-comparison change is mechanical and well-scoped, with clear callsites and existing tests for the surrounding logic. Risk is mostly around correctly threading the flag width from VfoWidget to panFollowVfo at the per-slice level.

73, Jeremy KK7GWY & Claude (AI dev partner)

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfaceenhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is takenmulti-panMulti-panadapter layout and slicingspectrumPanadapter and waterfall

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions