diff --git a/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs b/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs index 2faea9f..fde043c 100644 --- a/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs +++ b/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs @@ -20,6 +20,16 @@ public sealed partial class MessagesDialog : Window 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; public MessagesDialog(ModeCategory mode, string text, string what = "Function key messages") @@ -62,12 +72,12 @@ public sealed partial class MessagesDialog : Window int key = 0; for (int at = 0; at < lines.Length; at++) { - IBrush colour = Verdicts.Nothing; + IBrush colour = Grey; if (!lines[at].StartsWith('#')) { - colour = key < MessageFile.KeyCount ? Verdicts.Worth - : key < 2 * MessageFile.KeyCount ? Verdicts.NewMultiplier - : Verdicts.Dupe; + colour = key < MessageFile.KeyCount ? Blue + : key < 2 * MessageFile.KeyCount ? Green + : Red; key++; } TextBlock line = (TextBlock)Highlight.Children[at]; diff --git a/src/Nonemm.App/Verdicts.cs b/src/Nonemm.App/Verdicts.cs index 0a03e61..d6b062e 100644 --- a/src/Nonemm.App/Verdicts.cs +++ b/src/Nonemm.App/Verdicts.cs @@ -3,21 +3,50 @@ using Nonemm.Contests; namespace Nonemm.App; -/// One place decides what colour a station is, so the frame round the callsign -/// box, a row in the check window and a spot on the bandmap cannot disagree. +/// One place decides what colour a station is, so the callsign box, the frame +/// 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 readonly IBrush Dupe = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B)); - public static readonly IBrush NewMultiplier = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60)); - public static readonly IBrush Worth = new SolidColorBrush(Color.FromRgb(0x29, 0x80, 0xB9)); - public static readonly IBrush Nothing = new SolidColorBrush(Color.FromRgb(0x7F, 0x8C, 0x8D)); + /// Worth a contact but no new multiplier. + public static readonly IBrush Worth = new SolidColorBrush(Color.FromRgb(0x00, 0x00, 0xFF)); + /// 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) => - verdict is null ? Nothing + verdict is null ? Worth : verdict.IsDupe ? Dupe - : verdict.IsNewMultiplier ? NewMultiplier - : verdict.Points > 0 ? Worth - : Nothing; + : verdict.NewMultipliers.Count > 1 ? NewMultipliers + : verdict.NewMultipliers.Count == 1 ? NewMultiplier + : 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) { diff --git a/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs b/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs index 203e58b..4e0e7e5 100644 --- a/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs +++ b/src/Nonemm.App/Windows/EntryWindow.CallFrame.cs @@ -39,7 +39,7 @@ public sealed partial class EntryWindow && Math.Abs(Logging.Frequency.Hertz - cqFrequency.Hertz) <= TuningTolerance.Hertz) { CallFrameText.Text = "CQ-Frequency"; - CallFrameText.Foreground = Verdicts.Worth; + CallFrameText.Foreground = Verdicts.Cq; return; } if (session.Bandmap.Near(Logging.Frequency, TuningTolerance) is not { } spot) diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml.cs b/src/Nonemm.App/Windows/EntryWindow.axaml.cs index a495206..e1b7dd5 100644 --- a/src/Nonemm.App/Windows/EntryWindow.axaml.cs +++ b/src/Nonemm.App/Windows/EntryWindow.axaml.cs @@ -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 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. private static readonly IBrush QuickEditBackground = new SolidColorBrush(Color.FromRgb(0xFF, 0xFF, 0xC0)); private bool sending; + private TextBlock? callMark; private TextBox? nameBox; private TextBox? commentBox; private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) }; @@ -176,6 +181,7 @@ public sealed partial class EntryWindow : Window EntryGrid.Children.Clear(); EntryGrid.ColumnDefinitions.Clear(); boxes.Clear(); + callMark = null; if (Logging is null) { ContestText.Text = "no contest — File ▸ New Contest"; @@ -260,6 +266,29 @@ public sealed partial class EntryWindow : Window Grid.SetRow(box, 1); EntryGrid.Children.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) @@ -268,7 +297,19 @@ public sealed partial class EntryWindow : Window { 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) { ResetEsm(); @@ -675,15 +716,51 @@ public sealed partial class EntryWindow : Window ShowLights(); Verdict? verdict = Logging.Verdict(); - VerdictBorder.Background = Verdicts.Colour(verdict); + ShowCallColour(verdict); + VerdictBorder.Background = Verdicts.Background(verdict); VerdictText.Text = VerdictLine(verdict); - VerdictText.Foreground = Brushes.White; + VerdictText.Foreground = Brushes.Black; foreach (Window window in openWindows.Values) { (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() { if (Logging is null) @@ -704,8 +781,8 @@ public sealed partial class EntryWindow : Window private void ShowLights() { bool radio = session.Radios.Any(r => r.Number == radioNumber && r.IsConnected); - RadioLight.Fill = radio ? Verdicts.NewMultiplier : Dark; - TransmitLight.Fill = sending ? Verdicts.Dupe : Dark; + RadioLight.Fill = radio ? GreenLight : Dark; + TransmitLight.Fill = sending ? RedLight : Dark; } /// What N1MM writes in its title bar: the frequency, the mode, whether the diff --git a/src/Nonemm.App/Windows/TelnetWindow.axaml.cs b/src/Nonemm.App/Windows/TelnetWindow.axaml.cs index e5780e6..25b7769 100644 --- a/src/Nonemm.App/Windows/TelnetWindow.axaml.cs +++ b/src/Nonemm.App/Windows/TelnetWindow.axaml.cs @@ -18,9 +18,9 @@ public sealed partial class TelnetWindow : RefreshableWindow { private const int LinesKept = 500; - private static readonly IBrush SpotColour = Verdicts.NewMultiplier; - private static readonly IBrush SentColour = Verdicts.Worth; - private static readonly IBrush NoticeColour = Verdicts.Dupe; + private static readonly IBrush SpotColour = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60)); + private static readonly IBrush SentColour = new SolidColorBrush(Color.FromRgb(0x29, 0x80, 0xB9)); + private static readonly IBrush NoticeColour = new SolidColorBrush(Color.FromRgb(0x7F, 0x8C, 0x8D)); private readonly AppSession session; private readonly Action tune;