Make the run switch a switch, and split the tuning tolerance by mode

Three things you could not see or could not set.

Run and S&P were a label saying which one you were in. They are two buttons
now, the one you are in marked, either one clickable, which is what N1MM has.
Ctrl+? and F1 still work.

The tuning tolerance is one number per mode, as N1MM keeps it: CW, phone and
digital, 300 Hz each, which are its defaults. A phone signal is wide and a CW
one is not, so one number for both was wrong.

The bearing line went blank whenever it could not be worked out, which looks
exactly like a missing feature. It now says which end could not be placed and
what to do — no country file, no position for your own station, or a callsign
the country file does not place. It also gives miles beside the kilometres, as
N1MM does.

While clicking it: a marked button lost its mark under the pointer, because the
theme paints its own background there. The band buttons had the same fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 10:39:32 +00:00
parent 7e7ce41477
commit 682932c35d
5 changed files with 90 additions and 17 deletions

View File

@@ -198,8 +198,11 @@ name and the comment — and round again; shift and tab go the other way. N1MM
does not touch the tab key at all, so tab there walks its boxes in the order does not touch the tab key at all, so tab there walks its boxes in the order
they are laid out, which is what this does. they are laid out, which is what this does.
Two lights sit beside the run switch: the left one is green while a radio is Run and S&P are two buttons beside the lights; the one you are in is marked.
answering, the right one red while something is going out on the air. Ctrl+? switches between them, and F1 puts you into run mode, as in N1MM.
Two lights sit beside them: the left one is green while a radio is answering,
the right one red while something is going out on the air.
### The buttons and the title bar ### The buttons and the title bar
@@ -226,14 +229,18 @@ when this is the frequency you last called CQ on. While searching, space with an
empty callsign box takes the station out of the frame and into the box, so a empty callsign box takes the station out of the frame and into the box, so a
spotted station is worked without typing its call. While running it does not, spotted station is worked without typing its call. While running it does not,
because a running station is answering callers rather than chasing the one it is because a running station is answering callers rather than chasing the one it is
sitting on. How close the radio has to be to a spot is `TuningToleranceHertz` in sitting on. How close the radio has to be to a spot is one number per mode in
`settings.json`, 300 Hz out of the box, which is N1MM's default. `settings.json``CwTuningToleranceHertz`, `PhoneTuningToleranceHertz` and
`DigitalTuningToleranceHertz` — 300 Hz each out of the box, which is how N1MM
keeps them and what it defaults them to.
### Where the other station is ### Where the other station is
Under the entry window is the line N1MM writes there: the beam heading, the Under the entry window is the line N1MM writes there: the beam heading, the
heading the long way round, the distance in kilometres, and the sunrise and heading the long way round, the distance in kilometres, and the sunrise and
sunset times at the other station. sunset times at the other station, in kilometres and miles as N1MM shows them.
When it cannot be worked out the line says which end could not be placed, rather
than going blank.
The position comes from the grid square when the contest exchanges one and it The position comes from the grid square when the contest exchanges one and it
has been copied, or from the call history file, and from the country file has been copied, or from the call history file, and from the country file

View File

@@ -67,9 +67,19 @@ public sealed record Settings
public bool EsmSendsCorrectedCall { get; init; } = true; public bool EsmSendsCorrectedCall { get; init; } = true;
/// How close to a spot the radio has to be for the entry window to say the /// How close to a spot the radio has to be for the entry window to say the
/// station is there. N1MM asks for the same number, one per mode; this is /// station is there. N1MM keeps one per mode and defaults each to 300 Hz.
/// one number for all of them. public int CwTuningToleranceHertz { get; init; } = 300;
public int TuningToleranceHertz { get; init; } = 300;
public int PhoneTuningToleranceHertz { get; init; } = 300;
public int DigitalTuningToleranceHertz { get; init; } = 300;
public int TuningToleranceFor(ModeCategory mode) => mode switch
{
ModeCategory.Phone => PhoneTuningToleranceHertz,
ModeCategory.Digital => DigitalTuningToleranceHertz,
_ => CwTuningToleranceHertz,
};
/// How far `{FREQUP}` and `{FREQDN}` move the radio. N1MM asks for the same /// How far `{FREQUP}` and `{FREQDN}` move the radio. N1MM asks for the same
/// number in its Configurer. /// number in its Configurer.

View File

@@ -20,8 +20,12 @@ public sealed partial class EntryWindow
/// one, and neither is nothing. /// one, and neither is nothing.
private string framedCall = ""; private string framedCall = "";
/// How close counts as sitting on a spot, which is a different number for
/// each mode: a phone signal is wide and a CW one is not.
private Frequency TuningTolerance => private Frequency TuningTolerance =>
Frequency.FromHertz(Math.Max(0, session.Settings.TuningToleranceHertz)); Frequency.FromHertz(Math.Max(
0,
session.Settings.TuningToleranceFor(Logging?.Mode.Category ?? ModeCategory.Cw)));
private void ShowCallFrame() private void ShowCallFrame()
{ {

View File

@@ -17,6 +17,21 @@
<Setter Property="Padding" Value="6,2" /> <Setter Property="Padding" Value="6,2" />
<Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" />
</Style> </Style>
<Style Selector="Button.mode">
<Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="8,1" />
<Setter Property="MinWidth" Value="44" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.mode.here">
<Setter Property="Background" Value="#2980B9" />
<Setter Property="Foreground" Value="White" />
</Style>
<!-- the theme paints its own background under the pointer, which would
hide the mark on the button that is on -->
<Style Selector="Button.mode.here:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#3498DB" />
</Style>
<Style Selector="Button.band"> <Style Selector="Button.band">
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
<Setter Property="Padding" Value="4,1" /> <Setter Property="Padding" Value="4,1" />
@@ -28,6 +43,9 @@
<Setter Property="Background" Value="#2980B9" /> <Setter Property="Background" Value="#2980B9" />
<Setter Property="Foreground" Value="White" /> <Setter Property="Foreground" Value="White" />
</Style> </Style>
<Style Selector="Button.band.here:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#3498DB" />
</Style>
<Style Selector="Button.fkey"> <Style Selector="Button.fkey">
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
<Setter Property="Padding" Value="6,3" /> <Setter Property="Padding" Value="6,3" />
@@ -119,10 +137,8 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<Ellipse Name="TransmitLight" Width="9" Height="9" Fill="#33808080" <Ellipse Name="TransmitLight" Width="9" Height="9" Fill="#33808080"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<Border Name="RunBorder" Padding="6,1" CornerRadius="3" Background="#22808080" <Button Name="RunButton" Classes="mode" Content="Run" Click="OnRun" Margin="4,0,0,0" />
Margin="4,0,0,0"> <Button Name="SearchButton" Classes="mode" Content="S&amp;P" Click="OnSearch" />
<TextBlock Name="RunText" FontSize="12" Text="S&amp;P" />
</Border>
</StackPanel> </StackPanel>
<TextBlock Grid.Column="3" Name="ContestText" FontSize="12" Opacity="0.75" <TextBlock Grid.Column="3" Name="ContestText" FontSize="12" Opacity="0.75"
VerticalAlignment="Center" Margin="12,0,12,0" Text="no contest" VerticalAlignment="Center" Margin="12,0,12,0" Text="no contest"

View File

@@ -20,6 +20,8 @@ public sealed partial class EntryWindow : Window
private EntryWindow? secondRadio; private EntryWindow? secondRadio;
private readonly List<TextBox> boxes = []; private readonly List<TextBox> boxes = [];
private readonly List<Button> functionButtons = []; private readonly List<Button> functionButtons = [];
private const double MilesPerKilometre = 0.621371;
private static readonly IBrush Dark = new SolidColorBrush(Color.FromArgb(0x33, 0x80, 0x80, 0x80)); private static readonly IBrush Dark = new SolidColorBrush(Color.FromArgb(0x33, 0x80, 0x80, 0x80));
private bool sending; private bool sending;
@@ -527,6 +529,21 @@ public sealed partial class EntryWindow : Window
} }
} }
private void OnRun(object? sender, RoutedEventArgs e) => SetRunning(true);
private void OnSearch(object? sender, RoutedEventArgs e) => SetRunning(false);
private void SetRunning(bool running)
{
if (Logging is null)
{
return;
}
Logging.IsRunning = running;
boxes[Logging.Entry.Focus].Focus();
Refresh();
}
private void ToggleRun() private void ToggleRun()
{ {
if (Logging is null) if (Logging is null)
@@ -572,8 +589,8 @@ public sealed partial class EntryWindow : Window
? $"{Logging.Frequency.Kilohertz:0.00} ▸ {Logging.TransmitFrequency.Kilohertz:0.0}" ? $"{Logging.Frequency.Kilohertz:0.00} ▸ {Logging.TransmitFrequency.Kilohertz:0.0}"
: Logging.Frequency.Kilohertz.ToString("0.00"); : Logging.Frequency.Kilohertz.ToString("0.00");
ModeText.Text = Logging.Mode.Name; ModeText.Text = Logging.Mode.Name;
RunText.Text = Logging.IsRunning ? "RUN" : "S&P"; RunButton.Classes.Set("here", Logging.IsRunning);
RunBorder.Background = Logging.IsRunning ? Verdicts.Worth : new SolidColorBrush(Color.FromArgb(0x22, 0x80, 0x80, 0x80)); SearchButton.Classes.Set("here", !Logging.IsRunning);
ContestText.Text = ContestLine(); ContestText.Text = ContestLine();
Title = TitleLine(); Title = TitleLine();
@@ -637,14 +654,33 @@ public sealed partial class EntryWindow : Window
/// line under its entry window. /// line under its entry window.
private string PathLine() private string PathLine()
{ {
if (Logging is null || StationPath.For(Logging, DateTime.UtcNow) is not { } path) if (Logging is null || Logging.Entry.Call.Trim().Length == 0)
{ {
return ""; return "";
} }
if (StationPath.For(Logging, DateTime.UtcNow) is not { } path)
{
return WhyNoPath();
}
string sun = path.Sunrise is { } rise && path.Sunset is { } set string sun = path.Sunrise is { } rise && path.Sunset is { } set
? $" · SR {rise:HH:mm}Z SS {set:HH:mm}Z" ? $" · SR {rise:HH:mm}Z SS {set:HH:mm}Z"
: " · no sunrise or sunset today"; : " · no sunrise or sunset today";
return $"Hdg {path.Heading:0}° · LP {path.LongPath:0}° · {path.DistanceKm:N0} km{sun}"; return $"Hdg {path.Heading:0}° · LP {path.LongPath:0}° · "
+ $"{path.DistanceKm:N0} km · {path.DistanceKm * MilesPerKilometre:N0} mi{sun}";
}
/// A blank line where a bearing should be is a puzzle. This says which of
/// the two ends could not be placed and what to do about it.
private string WhyNoPath()
{
if (session.Countries is null)
{
return "no bearing — download the country file, in Config ▸ Download Country File";
}
return Logging?.Me.GridSquare.Trim().Length == 0
&& session.Countries.Find(Logging.Me.Callsign) is null
? "no bearing — your own station has no position, so set your grid in Config ▸ Station"
: "no bearing — the country file does not place this callsign";
} }
private string VerdictLine(Verdict? verdict) private string VerdictLine(Verdict? verdict)