Put N1MM's status bar along the bottom of the entry window

Three parts, as N1MM has them: what just happened on the left, the contacts
and multipliers in the middle, the score on the right.

The left says where the call being typed is from, in N1MM's own shape:
OM: EU -> Slovak Republic, Zn 15. The zone is the one the contest counts in, so
Contest.UsesItuZones answers that the way N1MM's ZoneType does, and IARU
overrides it. A message from something the operator just did stands there until
the next keystroke.

The middle is N1MM's breakdown: the contacts and then one number per multiplier
the contest counts, separated by slashes, and No Score before the first
contact. Points are not in it, because they are not in N1MM's; the score window
has them.

Those numbers were in the line at the top of the window, which now carries the
contest name alone, and the country was in the bar under the callsign box,
which now says only what working the call would bring. Neither is said twice.

Looked at under Xvfb: the country line for OM3KFF and then G4AB, 10/7/8 and
300 beside them, and a Mark message standing in the left until the next
keystroke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
2026-08-30 22:46:13 +00:00
parent 98d51ab7f5
commit ff46cb00ed
5 changed files with 106 additions and 19 deletions

View File

@@ -314,6 +314,25 @@ in colours for a dark screen. Config ▸ Manage Skins, Colors and Fonts picks on
and the choice is kept in `settings.json` as `Theme`. Fonts and skins are not and the choice is kept in `settings.json` as `Theme`. Fonts and skins are not
part of it: the name is N1MM's, the dialog only changes colours. part of it: the name is N1MM's, the dialog only changes colours.
### The status bar
Along the bottom of the entry window is N1MM's status bar, in three parts:
- On the left, where the call being typed is from, in N1MM's own shape:
`OM: EU -> Slovak Republic, Zn 15`. The zone is the one the contest counts in,
so IARU shows the ITU zone and CQ WW the CQ zone. When something has just
happened — a contact logged, a call stacked, a download finished — that stands
there instead until the next keystroke.
- In the middle, the contacts and multipliers: `10/7/8` is ten contacts, seven
zones and eight countries, one number per multiplier the contest counts. It
reads `No Score` before the first contact, as N1MM's does.
- On the right, the score.
The numbers were in the line at the top of the window before. They are here now,
where N1MM keeps them, so the top line is the contest name alone. The bar under
the callsign box says what working the call would bring — the points and any new
multiplier — and no longer repeats the country.
### 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

View File

@@ -123,6 +123,20 @@
</MenuItem> </MenuItem>
</Menu> </Menu>
<!-- N1MM's status strip: what just happened on the left, then the contacts
and multipliers, then the score -->
<Border DockPanel.Dock="Bottom" Padding="6,3" BorderThickness="0,1,0,0"
BorderBrush="#33808080">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Name="StatusText" FontSize="11" Opacity="0.8" Text=""
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Name="BreakdownText" FontSize="11" Opacity="0.8" Text=""
Margin="12,0" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Name="ScoreText" FontSize="11" FontWeight="Bold" Text=""
VerticalAlignment="Center" />
</Grid>
</Border>
<Border DockPanel.Dock="Left" Padding="6,6,4,6" BorderThickness="0,0,1,0" <Border DockPanel.Dock="Left" Padding="6,6,4,6" BorderThickness="0,0,1,0"
BorderBrush="#33808080"> BorderBrush="#33808080">
<Grid Name="BandButtons" ColumnDefinitions="Auto,Auto" /> <Grid Name="BandButtons" ColumnDefinitions="Auto,Auto" />
@@ -183,7 +197,6 @@
<TextBlock Name="UserTextText" FontSize="11" Margin="2,2,0,0" Text="" <TextBlock Name="UserTextText" FontSize="11" Margin="2,2,0,0" Text=""
Foreground="#2980B9" TextTrimming="CharacterEllipsis" /> Foreground="#2980B9" TextTrimming="CharacterEllipsis" />
<TextBlock Name="StatusText" FontSize="11" Opacity="0.7" Margin="2,4,0,0" Text="" />
</StackPanel> </StackPanel>
</DockPanel> </DockPanel>
</Window> </Window>

View File

@@ -37,6 +37,11 @@ public sealed partial class EntryWindow : Window
private readonly Dictionary<Type, Window> openWindows = []; private readonly Dictionary<Type, Window> openWindows = [];
private bool updating; private bool updating;
/// What the last thing the operator did had to say. It stands in the status
/// bar until the next keystroke, and then the country of the call being
/// typed takes the place of it.
private string message = "";
public EntryWindow(AppSession session, int radioNumber = 1) public EntryWindow(AppSession session, int radioNumber = 1)
{ {
this.session = session; this.session = session;
@@ -315,6 +320,8 @@ public sealed partial class EntryWindow : Window
{ {
ResetEsm(); ResetEsm();
} }
// typing answers whatever the status bar was reporting
message = "";
Refresh(); Refresh();
} }
@@ -737,6 +744,9 @@ public sealed partial class EntryWindow : Window
UserTextText.Text = Logging.Session.History.Find(Logging.Entry.Call)?.UserText ?? ""; UserTextText.Text = Logging.Session.History.Find(Logging.Entry.Call)?.UserText ?? "";
ShowLights(); ShowLights();
ShowScore();
StatusText.Text = message.Length > 0 ? message : CountryLine();
Verdict? verdict = Logging.Verdict(); Verdict? verdict = Logging.Verdict();
ShowCallColour(verdict); ShowCallColour(verdict);
VerdictBorder.Background = Verdicts.Background(verdict); VerdictBorder.Background = Verdicts.Background(verdict);
@@ -783,19 +793,57 @@ public sealed partial class EntryWindow : Window
&& !call.All(char.IsAsciiDigit); && !call.All(char.IsAsciiDigit);
} }
/// The contacts and multipliers so far, and the score. N1MM keeps them in
/// the status bar in this shape: the contacts, then one number per
/// multiplier the contest counts, separated by slashes.
private void ShowScore()
{
if (Logging is null)
{
BreakdownText.Text = "";
ScoreText.Text = "";
return;
}
if (Logging.Log.Qsos.Count == 0)
{
BreakdownText.Text = "No Score";
ScoreText.Text = "";
return;
}
BreakdownText.Text = string.Join(
"/",
new[] { Logging.Log.Tally.Qsos }.Concat(
Logging.Contest.MultiplierNames.Select(
(_, at) => Logging.Log.Tally.MultiplierCount(at + 1))));
ScoreText.Text = $"{Logging.Log.TotalScore:N0}";
}
private string ContestLine() private string ContestLine()
{ {
if (Logging is null) if (Logging is null)
{ {
return ""; return "";
} }
string mults = string.Join(
" ",
Logging.Contest.MultiplierNames.Select(
(name, at) => $"{name} {Logging.Log.Tally.MultiplierCount(at + 1)}"));
string radio = session.Radios.Count > 1 ? $" · radio {session.ActiveRadioNumber}" : ""; string radio = session.Radios.Count > 1 ? $" · radio {session.ActiveRadioNumber}" : "";
return $"{Logging.Contest.DisplayName} · {Logging.Log.Tally.Qsos} Q · " + return $"{Logging.Contest.DisplayName}{radio}";
$"{Logging.Log.Tally.Points} pts · {mults} · {Logging.Log.TotalScore:N0}{radio}"; }
/// N1MM's status bar says where the call being typed is from, in this
/// shape: `OM: EU -> Slovak Republic, Zn 15`. The zone is the one the
/// contest counts in. A message from something the operator just did takes
/// the place of it until the next keystroke.
private string CountryLine()
{
if (Logging is null || Logging.Entry.Call.Trim().Length == 0)
{
return "";
}
if (Logging.Country() is not { } found)
{
return "unknown country";
}
int zone = Logging.Contest.UsesItuZones ? found.ItuZone : found.CqZone;
return $"{found.Entity.PrimaryPrefix}: {found.Continent} -> {found.Entity.Name}, Zn {zone}";
} }
/// The two lights N1MM has beside the run switch: the left one says a radio /// The two lights N1MM has beside the run switch: the left one says a radio
@@ -857,21 +905,21 @@ public sealed partial class EntryWindow : Window
: "no bearing — the country file does not place this callsign"; : "no bearing — the country file does not place this callsign";
} }
private string VerdictLine(Verdict? verdict) /// What working the call being typed would bring. Where it is from is in
{ /// the status bar rather than here, so neither says it twice.
if (Logging is null || Logging.Entry.Call.Trim().Length == 0) private string VerdictLine(Verdict? verdict) =>
{ Logging is null || Logging.Entry.Call.Trim().Length == 0
return $"sending {Logging?.SentNumber}"; ? $"sending {Logging?.SentNumber}"
} : Verdicts.Describe(verdict);
string country = Logging.Country() is { } found
? $"{found.Entity.Name} · {found.Continent} · CQ {found.CqZone} · ITU {found.ItuZone}"
: "unknown country";
return $"{country} — {Verdicts.Describe(verdict)}";
}
private void UpdateClock() => ClockText.Text = DateTime.UtcNow.ToString("HH:mm:ss") + "Z"; private void UpdateClock() => ClockText.Text = DateTime.UtcNow.ToString("HH:mm:ss") + "Z";
private void Status(string text) => StatusText.Text = text; /// What just happened, in the left of the status bar.
private void Status(string text)
{
message = text;
StatusText.Text = text;
}
/// The labels come from the message file, so what the buttons say is what /// The labels come from the message file, so what the buttons say is what
/// the operator wrote. Right-clicking one opens the editor, as it does on /// the operator wrote. Right-clicking one opens the editor, as it does on

View File

@@ -41,6 +41,11 @@ public interface Contest
/// WARC bands, so only general logging says yes, as in N1MM. /// WARC bands, so only general logging says yes, as in N1MM.
bool ShowsWarcBands => false; bool ShowsWarcBands => false;
/// True for a contest counted in ITU zones rather than CQ zones, which
/// decides which zone the status bar shows for a call. N1MM asks its
/// contests the same question.
bool UsesItuZones => false;
string SentExchangeFor(StationInfo me); string SentExchangeFor(StationInfo me);
int PointsFor(QsoContext qso); int PointsFor(QsoContext qso);

View File

@@ -20,6 +20,8 @@ public sealed class IaruHf : Contest
public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"]; public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"];
public bool UsesItuZones => true;
public DupeScope DupeScope => DupeScope.PerBandAndMode; public DupeScope DupeScope => DupeScope.PerBandAndMode;
public bool HasSerialNumbers => false; public bool HasSerialNumbers => false;