Zoom the bandmap with the mouse wheel and add a Centre on VFO toggle

The wheel steps along the span list and keeps the frequency under the
pointer in place. With Centre on VFO off, the slice stays put until the
receiver nears an edge. The span and the toggle are saved in the settings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 00:52:06 +00:00
co-authored by Claude Sonnet 5.5
parent 476844a9f0
commit 047a3612cb
7 changed files with 200 additions and 14 deletions
+7
View File
@@ -53,6 +53,13 @@ public sealed record Settings
/// How long a spot stays on the bandmap. N1MM's own default is 60.
public int SpotTimeoutMinutes { get; init; } = 60;
/// The width of the slice the bandmap shows, in hertz. Zero is the whole band.
public int BandmapSpanHertz { get; init; } = 40_000;
/// The bandmap keeps the receiver in the middle instead of moving only when
/// the receiver nears an edge.
public bool BandmapCentred { get; init; }
/// Moves each incoming CW spot by a few tens of hertz, so the operator has
/// to find the station by ear. N1MM calls it randomising.
public bool RandomizeSpots { get; init; }
+15
View File
@@ -11,6 +11,8 @@ namespace Nonemm.App.Windows;
/// Draws the band: a frequency scale down the left, the receiver and
/// transmitter as bars on it, and the stations as callsigns on the right with a
/// leader line back to where each one really is.
public sealed record BandmapZoom(bool In, Frequency At);
public sealed class BandmapView : Control
{
private const double ScaleX = 52;
@@ -64,6 +66,9 @@ public sealed class BandmapView : Control
/// Clicking where no station is asks for the radio, not a callsign.
public event EventHandler<Frequency>? FrequencyPicked;
/// The wheel was turned: forward zooms in on the frequency under the pointer.
public event EventHandler<BandmapZoom>? Zoomed;
public override void Render(DrawingContext context)
{
scale = new BandmapScale(Low, High, Math.Max(1, Bounds.Height - (2 * Inset)));
@@ -152,6 +157,16 @@ public sealed class BandmapView : Control
context.DrawText(Text(label.Spot.Call.Text, colour), new Point(LabelX, label.LabelY));
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if (e.Delta.Y != 0)
{
Zoomed?.Invoke(this, new BandmapZoom(e.Delta.Y > 0, scale.At(e.GetPosition(this).Y - Inset)));
e.Handled = true;
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
@@ -7,6 +7,7 @@
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="6,6,6,4" Spacing="8">
<TextBlock Name="BandText" FontSize="12" VerticalAlignment="Center" />
<ComboBox Name="SpanBox" FontSize="11" Width="96" />
<CheckBox Name="CentreBox" Content="Centre on VFO" FontSize="11" />
</StackPanel>
<TextBlock DockPanel.Dock="Bottom" Name="StatusText" Margin="6,2,6,6"
FontSize="11" Opacity="0.7" />
+47 -14
View File
@@ -15,6 +15,7 @@ public sealed partial class BandmapWindow : RefreshableWindow
{
private static readonly (string Label, Frequency Span)[] Spans =
[
("5 kHz", Frequency.FromKilohertz(5)),
("10 kHz", Frequency.FromKilohertz(10)),
("20 kHz", Frequency.FromKilohertz(20)),
("40 kHz", Frequency.FromKilohertz(40)),
@@ -25,14 +26,23 @@ public sealed partial class BandmapWindow : RefreshableWindow
private readonly AppSession session;
private readonly Action<Frequency, string> tune;
/// Where the slice was last drawn from, so it stays put until the receiver
/// nears an edge.
private Frequency? sliceLow;
public BandmapWindow(AppSession session, Action<Frequency, string> tune)
{
this.session = session;
this.tune = tune;
InitializeComponent();
SpanBox.ItemsSource = Spans.Select(s => s.Label).ToList();
SpanBox.SelectedIndex = 2;
SpanBox.SelectionChanged += (_, _) => Refresh();
SpanBox.SelectedIndex = Math.Max(
0,
Array.FindIndex(Spans, s => s.Span.Hertz == session.Settings.BandmapSpanHertz));
CentreBox.IsChecked = session.Settings.BandmapCentred;
SpanBox.SelectionChanged += (_, _) => SaveView();
CentreBox.IsCheckedChanged += (_, _) => SaveView();
View.Zoomed += (_, zoom) => Zoom(zoom);
View.ColourOf = Colour;
View.SpotPicked += (_, spot) => tune(spot.Frequency, spot.Call.Text);
View.FrequencyPicked += (_, frequency) => tune(frequency, "");
@@ -40,6 +50,16 @@ public sealed partial class BandmapWindow : RefreshableWindow
Refresh();
}
private void SaveView()
{
session.Save(session.Settings with
{
BandmapSpanHertz = (int)Spans[Math.Max(0, SpanBox.SelectedIndex)].Span.Hertz,
BandmapCentred = CentreBox.IsChecked == true,
});
Refresh();
}
public override void Refresh()
{
session.Bandmap.DropOlderThan(DateTime.UtcNow);
@@ -104,21 +124,34 @@ public sealed partial class BandmapWindow : RefreshableWindow
return markers;
}
/// The slice to show, centred on the receiver. Contest operating goes
/// outside the band edges, so the slice stretches to wherever the receiver
/// is rather than leaving it off the top of the scale.
private (Frequency Low, Frequency High) Window(Band band, Frequency centre)
private (Frequency Low, Frequency High) Window(Band band, Frequency receiver)
{
long lowEdge = Math.Min(band.Low.Hertz, centre.Hertz);
long highEdge = Math.Max(band.High.Hertz, centre.Hertz);
Frequency span = Spans[Math.Max(0, SpanBox.SelectedIndex)].Span;
if (span.Hertz == 0 || span.Hertz >= highEdge - lowEdge)
(Frequency low, Frequency high) = BandmapSlice.Choose(
band,
receiver,
Spans[Math.Max(0, SpanBox.SelectedIndex)].Span.Hertz,
CentreBox.IsChecked == true,
sliceLow);
sliceLow = low;
return (low, high);
}
/// One wheel notch moves one step along the span list. Unless the slice
/// follows the receiver, the frequency under the pointer stays where it is.
private void Zoom(BandmapZoom zoom)
{
int next = Math.Clamp(SpanBox.SelectedIndex + (zoom.In ? -1 : 1), 0, Spans.Length - 1);
if (next == SpanBox.SelectedIndex)
{
return (Frequency.FromHertz(lowEdge), Frequency.FromHertz(highEdge));
return;
}
long half = span.Hertz / 2;
long low = Math.Clamp(centre.Hertz - half, lowEdge, highEdge - span.Hertz);
return (Frequency.FromHertz(low), Frequency.FromHertz(low + span.Hertz));
long shown = View.High.Hertz - View.Low.Hertz;
long span = Spans[next].Span.Hertz;
if (span > 0)
{
sliceLow = BandmapSlice.Zoom(View.Low, shown, span, zoom.At);
}
SpanBox.SelectedIndex = next;
}
private static Frequency StepFor(long span) => span switch