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
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
- Compact-mode flags (
VfoWidget has a compact-mode toggle) have a different width. Use the current rendered width, not a constant.
- 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.
- 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.
- 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)
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 VFOmenu item (AppSettings["PanFollowVfo"], default True). The whole machinery lives in:MainWindow::panFollowVfo(slice, mhz, source)— thin wrapper that calls...MainWindow::revealFrequencyIfNeeded(slice, mhz, TuneIntent::IncrementalTune, source)— the real implementationThe trigger logic:
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 ismhz - flagWidthMhz; for a right-side flag it'smhz + flagWidthMhz, whereflagWidthMhzis the flag panel width converted from pixels to MHz at the current zoom level.So the change is mechanical: where
revealFrequencyIfNeededtoday comparesmhzagainst the trigger boundaries, it should instead comparemhz - flagWidthMhz(left flag) andmhz + flagWidthMhz(right flag) — i.e., the flag's outer edge. For an unflagged slice the value collapses back tomhzand 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
View → Pan Follows VFOtoggle still controls the whole behavior; disabling it disables both the existing slice-frequency follow and the new flag-edge follow.kIncrementalTriggerEdgeMarginFrac/kIncrementalSettleEdgeMarginFracmargins continue to apply — only the comparison input changes (slice freq → flag outer edge).TuneIntent::CommandedTargetCenter,TuneIntent::AbsoluteJump,TuneIntent::RevealOffscreen— those code paths inrevealFrequencyIfNeededare independent of incremental tuning and should not see flag-width adjustments.Implementation sketch
The cleanest signature is to thread the flag width into
revealFrequencyIfNeeded(orpanFollowVfo) as an optional offset in MHz, defaulted to 0 for backward compatibility:Then inside the trigger comparison:
The wrapper
panFollowVforeads the flag width from the slice'sVfoWidget(via the existingwireVfoWidgetcallsite plumbing — or queryVfoWidget::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
VfoWidgethas a compact-mode toggle) have a different width. Use the current rendered width, not a constant.m_panCenterTargetcomparison (SpectrumWidget.cpp:1875) — should be preserved.flagWidthMhz > halfBw * triggerEdgeMarginFrac: the trigger boundary moves inside the settle boundary and the pan oscillates. CapleftFlagEdgeOffsetMhz/rightFlagEdgeOffsetMhzto a max fraction ofhalfBwso this stays stable.kPanFollowAnimationDurationMsalready gives a smooth animation; I don't think extra logic is needed here, but worth testing).Out of scope
kPanFollowAnimationDurationMsas-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
VfoWidgettopanFollowVfoat the per-slice level.73, Jeremy KK7GWY & Claude (AI dev partner)