Colour the callsign the way N1MM colours it, and mark the call

The colours are N1MM's own defaults, read out of ApplicationStyles: blue
for a station worth working, red for one new multiplier, green for more
than one, grey for a dupe, and red on the CQ frequency line. A call under
three characters stays blue, as N1MM leaves it.

The mark at the right-hand end of the callsign box is N1MM's OKCall: a
tick when the call is in MASTER.SCP, a question mark while it is not, in
the colour of the call. Like N1MM it only shows while the check window is
open, for three characters or more, and only while the call is short
enough to leave room for it.

The band under the boxes takes N1MM's background colours instead of white
on the text colours. The two lights, the telnet window and the message
editor were borrowing the verdict brushes for things that are not
verdicts; they have their own colours now.

The callsign and the exchange boxes uppercase what is typed. Name and
comment do not, which is what N1MM does.

Scored contest 63 of om5m.s3db (CQ WW SSB, 3775 QSOs) against the points
and multiplier flags N1MM stored: points identical, multipliers 601
against 602. The difference is B1Z on 10m, which N1MM logged with an
empty country prefix and counted as a country of its own though BY was
already worked on that band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 12:50:56 +00:00
parent 1c118f9ce4
commit e4b3dc9572
5 changed files with 139 additions and 23 deletions

View File

@@ -20,6 +20,16 @@ public sealed partial class MessagesDialog : Window
Patterns = ["*.mc"], Patterns = ["*.mc"],
}; };
/// The three banks of function keys, coloured so the editor shows which
/// line is which key.
private static readonly IBrush Blue = new SolidColorBrush(Color.FromRgb(0x29, 0x80, 0xB9));
private static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60));
private static readonly IBrush Red = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B));
private static readonly IBrush Grey = new SolidColorBrush(Color.FromRgb(0x7F, 0x8C, 0x8D));
private readonly ModeCategory mode; private readonly ModeCategory mode;
public MessagesDialog(ModeCategory mode, string text, string what = "Function key messages") public MessagesDialog(ModeCategory mode, string text, string what = "Function key messages")
@@ -62,12 +72,12 @@ public sealed partial class MessagesDialog : Window
int key = 0; int key = 0;
for (int at = 0; at < lines.Length; at++) for (int at = 0; at < lines.Length; at++)
{ {
IBrush colour = Verdicts.Nothing; IBrush colour = Grey;
if (!lines[at].StartsWith('#')) if (!lines[at].StartsWith('#'))
{ {
colour = key < MessageFile.KeyCount ? Verdicts.Worth colour = key < MessageFile.KeyCount ? Blue
: key < 2 * MessageFile.KeyCount ? Verdicts.NewMultiplier : key < 2 * MessageFile.KeyCount ? Green
: Verdicts.Dupe; : Red;
key++; key++;
} }
TextBlock line = (TextBlock)Highlight.Children[at]; TextBlock line = (TextBlock)Highlight.Children[at];

View File

@@ -3,21 +3,50 @@ using Nonemm.Contests;
namespace Nonemm.App; namespace Nonemm.App;
/// One place decides what colour a station is, so the frame round the callsign /// One place decides what colour a station is, so the callsign box, the frame
/// box, a row in the check window and a spot on the bandmap cannot disagree. /// above it, a row in the check window and a spot on the bandmap cannot
/// disagree. The colours are N1MM's own defaults, from ApplicationStyles:
/// QColor, MultColor, MultiMultColor and DupeColor.
public static class Verdicts public static class Verdicts
{ {
public static readonly IBrush Dupe = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B)); /// Worth a contact but no new multiplier.
public static readonly IBrush NewMultiplier = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60)); public static readonly IBrush Worth = new SolidColorBrush(Color.FromRgb(0x00, 0x00, 0xFF));
public static readonly IBrush Worth = new SolidColorBrush(Color.FromRgb(0x29, 0x80, 0xB9));
public static readonly IBrush Nothing = new SolidColorBrush(Color.FromRgb(0x7F, 0x8C, 0x8D));
/// New in one multiplier.
public static readonly IBrush NewMultiplier = new SolidColorBrush(Color.FromRgb(0xE8, 0x00, 0x00));
/// New in more than one, which is worth turning the radio for.
public static readonly IBrush NewMultipliers = new SolidColorBrush(Color.FromRgb(0x0D, 0x9D, 0x0D));
public static readonly IBrush Dupe = new SolidColorBrush(Color.FromRgb(0x80, 0x80, 0x80));
public static readonly IBrush Nothing = Dupe;
/// The frequency we are calling CQ on.
public static readonly IBrush Cq = new SolidColorBrush(Color.FromRgb(0xFF, 0x00, 0x00));
/// Nothing typed yet is the same blue as a station worth working: that is
/// what N1MM leaves the callsign box.
public static IBrush Colour(Verdict? verdict) => public static IBrush Colour(Verdict? verdict) =>
verdict is null ? Nothing verdict is null ? Worth
: verdict.IsDupe ? Dupe : verdict.IsDupe ? Dupe
: verdict.IsNewMultiplier ? NewMultiplier : verdict.NewMultipliers.Count > 1 ? NewMultipliers
: verdict.Points > 0 ? Worth : verdict.NewMultipliers.Count == 1 ? NewMultiplier
: Nothing; : Worth;
/// The same four states as backgrounds, which is what N1MM paints behind a
/// spot: QBackColor, MultBackColor and DupeBackColor.
public static IBrush Background(Verdict? verdict) =>
verdict is null ? WorthBack
: verdict.IsDupe ? DupeBack
: verdict.NewMultipliers.Count > 0 ? MultiplierBack
: WorthBack;
private static readonly IBrush WorthBack = new SolidColorBrush(Color.FromRgb(0x51, 0xA8, 0xFF));
private static readonly IBrush MultiplierBack = new SolidColorBrush(Color.FromRgb(0xFF, 0x37, 0x37));
private static readonly IBrush DupeBack = new SolidColorBrush(Color.FromRgb(0xD4, 0xD4, 0xD4));
public static string Describe(Verdict? verdict) public static string Describe(Verdict? verdict)
{ {

View File

@@ -39,7 +39,7 @@ public sealed partial class EntryWindow
&& Math.Abs(Logging.Frequency.Hertz - cqFrequency.Hertz) <= TuningTolerance.Hertz) && Math.Abs(Logging.Frequency.Hertz - cqFrequency.Hertz) <= TuningTolerance.Hertz)
{ {
CallFrameText.Text = "CQ-Frequency"; CallFrameText.Text = "CQ-Frequency";
CallFrameText.Foreground = Verdicts.Worth; CallFrameText.Foreground = Verdicts.Cq;
return; return;
} }
if (session.Bandmap.Near(Logging.Frequency, TuningTolerance) is not { } spot) if (session.Bandmap.Near(Logging.Frequency, TuningTolerance) is not { } spot)

View File

@@ -24,10 +24,15 @@ public sealed partial class EntryWindow : Window
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 static readonly IBrush GreenLight = new SolidColorBrush(Color.FromRgb(0x0D, 0x9D, 0x0D));
private static readonly IBrush RedLight = new SolidColorBrush(Color.FromRgb(0xFF, 0x00, 0x00));
/// The pale yellow N1MM paints the entry boxes while quick editing. /// The pale yellow N1MM paints the entry boxes while quick editing.
private static readonly IBrush QuickEditBackground = new SolidColorBrush(Color.FromRgb(0xFF, 0xFF, 0xC0)); private static readonly IBrush QuickEditBackground = new SolidColorBrush(Color.FromRgb(0xFF, 0xFF, 0xC0));
private bool sending; private bool sending;
private TextBlock? callMark;
private TextBox? nameBox; private TextBox? nameBox;
private TextBox? commentBox; private TextBox? commentBox;
private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) }; private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) };
@@ -176,6 +181,7 @@ public sealed partial class EntryWindow : Window
EntryGrid.Children.Clear(); EntryGrid.Children.Clear();
EntryGrid.ColumnDefinitions.Clear(); EntryGrid.ColumnDefinitions.Clear();
boxes.Clear(); boxes.Clear();
callMark = null;
if (Logging is null) if (Logging is null)
{ {
ContestText.Text = "no contest — File ▸ New Contest"; ContestText.Text = "no contest — File ▸ New Contest";
@@ -260,6 +266,29 @@ public sealed partial class EntryWindow : Window
Grid.SetRow(box, 1); Grid.SetRow(box, 1);
EntryGrid.Children.Add(box); EntryGrid.Children.Add(box);
boxes.Add(box); boxes.Add(box);
if (index == 0)
{
AddCallMark(index);
}
}
/// The mark N1MM puts at the right-hand end of the callsign box: a tick
/// when the call is in the check partial file, a question mark while it is
/// not. It sits over the box, in the same colour as the call.
private void AddCallMark(int index)
{
callMark = new TextBlock
{
FontSize = 21,
FontWeight = FontWeight.Bold,
Margin = new Avalonia.Thickness(0, 0, 12, 0),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
IsHitTestVisible = false,
};
Grid.SetColumn(callMark, index);
Grid.SetRow(callMark, 1);
EntryGrid.Children.Add(callMark);
} }
private void OnBoxChanged(int index, TextBox box) private void OnBoxChanged(int index, TextBox box)
@@ -268,7 +297,19 @@ public sealed partial class EntryWindow : Window
{ {
return; return;
} }
Logging.Entry[index] = box.Text ?? ""; // the callsign and the exchange go out in upper case, and N1MM puts
// them in the box that way as they are typed
string typed = box.Text ?? "";
string upper = typed.ToUpperInvariant();
if (upper != typed)
{
int caret = box.CaretIndex;
updating = true;
box.Text = upper;
box.CaretIndex = caret;
updating = false;
}
Logging.Entry[index] = upper;
if (index == 0 && Logging.Entry.Call.Trim().Length == 0) if (index == 0 && Logging.Entry.Call.Trim().Length == 0)
{ {
ResetEsm(); ResetEsm();
@@ -675,15 +716,51 @@ public sealed partial class EntryWindow : Window
ShowLights(); ShowLights();
Verdict? verdict = Logging.Verdict(); Verdict? verdict = Logging.Verdict();
VerdictBorder.Background = Verdicts.Colour(verdict); ShowCallColour(verdict);
VerdictBorder.Background = Verdicts.Background(verdict);
VerdictText.Text = VerdictLine(verdict); VerdictText.Text = VerdictLine(verdict);
VerdictText.Foreground = Brushes.White; VerdictText.Foreground = Brushes.Black;
foreach (Window window in openWindows.Values) foreach (Window window in openWindows.Values)
{ {
(window as RefreshableWindow)?.Refresh(); (window as RefreshableWindow)?.Refresh();
} }
} }
/// N1MM colours the callsign itself rather than the box: blue for a station
/// worth working, red for one multiplier, green for more than one, grey for
/// a dupe. A call too short to judge stays blue.
private void ShowCallColour(Verdict? verdict)
{
if (Logging is null || boxes.Count == 0)
{
return;
}
string call = Logging.Entry.Call.Trim();
IBrush colour = call.Length < 3 || call.All(char.IsAsciiDigit)
? Verdicts.Worth
: Verdicts.Colour(verdict);
boxes[0].Foreground = colour;
ShowCallMark(call, colour);
}
/// The tick and the question mark, which N1MM only shows while the check
/// window is open, and only while the call is short enough to leave room
/// for them.
private void ShowCallMark(string call, IBrush colour)
{
if (callMark is null)
{
return;
}
bool known = call.Length > 0 && session.Calls.Holds(call);
callMark.Text = known ? "✓" : "?";
callMark.Foreground = colour;
callMark.IsVisible = Logging?.Editing is null
&& openWindows.ContainsKey(typeof(CheckWindow))
&& call.Length is >= 3 and <= 9
&& !call.All(char.IsAsciiDigit);
}
private string ContestLine() private string ContestLine()
{ {
if (Logging is null) if (Logging is null)
@@ -704,8 +781,8 @@ public sealed partial class EntryWindow : Window
private void ShowLights() private void ShowLights()
{ {
bool radio = session.Radios.Any(r => r.Number == radioNumber && r.IsConnected); bool radio = session.Radios.Any(r => r.Number == radioNumber && r.IsConnected);
RadioLight.Fill = radio ? Verdicts.NewMultiplier : Dark; RadioLight.Fill = radio ? GreenLight : Dark;
TransmitLight.Fill = sending ? Verdicts.Dupe : Dark; TransmitLight.Fill = sending ? RedLight : Dark;
} }
/// What N1MM writes in its title bar: the frequency, the mode, whether the /// What N1MM writes in its title bar: the frequency, the mode, whether the

View File

@@ -18,9 +18,9 @@ public sealed partial class TelnetWindow : RefreshableWindow
{ {
private const int LinesKept = 500; private const int LinesKept = 500;
private static readonly IBrush SpotColour = Verdicts.NewMultiplier; private static readonly IBrush SpotColour = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60));
private static readonly IBrush SentColour = Verdicts.Worth; private static readonly IBrush SentColour = new SolidColorBrush(Color.FromRgb(0x29, 0x80, 0xB9));
private static readonly IBrush NoticeColour = Verdicts.Dupe; private static readonly IBrush NoticeColour = new SolidColorBrush(Color.FromRgb(0x7F, 0x8C, 0x8D));
private readonly AppSession session; private readonly AppSession session;
private readonly Action<Frequency, string> tune; private readonly Action<Frequency, string> tune;