From da04764756b283e57f8b0968cf8b2dfbf021c9b8 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 28 Aug 2026 13:06:32 +0000 Subject: [PATCH] Two themes, and one place that decides what colour a station is Colours came from static brushes scattered over the windows. They now come from a Theme, and everything that paints a station, a spot or a log row asks Themes.Current as it paints, so changing the theme changes every window without reopening it. light is N1MM's own palette, read out of ApplicationStyles: the pale blue behind a window, ivory on a button, white boxes, and the four colours a station is painted in. dark is the same states in colours for a dark screen, which is roughly what the program looked like before. Config > Manage Skins, Colors and Fonts picks one, under N1MM's name for the same dialog, and shows what each theme paints a callsign. The choice is kept in settings.json as Theme, and light is the default. The window chrome follows through resources the Fluent controls read, so a text box and a button take the theme's colours in every state. Fonts and skins are not part of it. Looked at under Xvfb in both themes: the entry window, the theme dialog, and the log window with its rows still coloured by what each contact was worth. Co-Authored-By: Claude Opus 5 --- README.md | 18 +++ src/Nonemm.App/App.axaml | 22 ++- src/Nonemm.App/App.axaml.cs | 56 +++++++- src/Nonemm.App/Configuration/Settings.cs | 4 + .../Dialogs/MessagesDialog.axaml.cs | 9 +- src/Nonemm.App/Dialogs/ThemeDialog.axaml | 27 ++++ src/Nonemm.App/Dialogs/ThemeDialog.axaml.cs | 65 +++++++++ src/Nonemm.App/Theming/Theme.cs | 132 ++++++++++++++++++ src/Nonemm.App/Theming/Themes.cs | 46 ++++++ src/Nonemm.App/Verdicts.cs | 38 ++--- src/Nonemm.App/Windows/EntryWindow.Menu.cs | 13 ++ src/Nonemm.App/Windows/EntryWindow.axaml | 1 + src/Nonemm.App/Windows/EntryWindow.axaml.cs | 22 ++- src/Nonemm.App/Windows/QtcWindow.axaml.cs | 9 +- src/Nonemm.App/Windows/TelnetWindow.axaml.cs | 9 +- tests/Nonemm.App.Tests/ThemesTests.cs | 30 ++++ 16 files changed, 452 insertions(+), 49 deletions(-) create mode 100644 src/Nonemm.App/Dialogs/ThemeDialog.axaml create mode 100644 src/Nonemm.App/Dialogs/ThemeDialog.axaml.cs create mode 100644 src/Nonemm.App/Theming/Theme.cs create mode 100644 src/Nonemm.App/Theming/Themes.cs create mode 100644 tests/Nonemm.App.Tests/ThemesTests.cs diff --git a/README.md b/README.md index d41433d..64e30c0 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,24 @@ sitting on. How close the radio has to be to a spot is one number per mode in `DigitalTuningToleranceHertz` — 300 Hz each out of the box, which is how N1MM keeps them and what it defaults them to. +### Colours + +The callsign is coloured as it is typed, in N1MM's own colours: blue for a +station worth working, red when it is new in one multiplier, green when it is +new in more than one, grey for a dupe. A call under three characters stays blue. +The same four colours paint a row in the log, a spot on the bandmap and a line +in the check window, so the three windows cannot disagree. + +At the right-hand end of the callsign box is N1MM's mark: a tick when the call +is in `MASTER.SCP`, a question mark while it is not. As in N1MM it only appears +while the check window is open, for three characters or more. + +Two themes ship with the program. `light` is N1MM's own palette, down to the +pale blue behind a window and the ivory on a button; `dark` is the same states +in colours for a dark screen. Config ▸ Manage Skins, Colors and Fonts picks one, +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. + ### Where the other station is Under the entry window is the line N1MM writes there: the beam heading, the diff --git a/src/Nonemm.App/App.axaml b/src/Nonemm.App/App.axaml index 76dc11b..4eb7fe3 100644 --- a/src/Nonemm.App/App.axaml +++ b/src/Nonemm.App/App.axaml @@ -1,11 +1,27 @@ - + RequestedThemeVariant="Light"> + + + #CDDAEB + #000000 + #FFFAF0 + #FFFFFF + #000000 + + + + - \ No newline at end of file + diff --git a/src/Nonemm.App/App.axaml.cs b/src/Nonemm.App/App.axaml.cs index 45db542..d1c5a40 100644 --- a/src/Nonemm.App/App.axaml.cs +++ b/src/Nonemm.App/App.axaml.cs @@ -1,7 +1,9 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Nonemm.App.Configuration; +using Nonemm.App.Theming; using Nonemm.App.Windows; namespace Nonemm.App; @@ -16,10 +18,62 @@ public partial class App : Application { UserPaths paths = UserPaths.Default(); paths.CreateFolders(); - AppSession session = new(paths, Settings.Load(paths.SettingsFile)); + Settings settings = Settings.Load(paths.SettingsFile); + Themes.Use(settings.Theme); + Themes.Changed += (_, _) => ApplyTheme(); + ApplyTheme(); + AppSession session = new(paths, settings); desktop.MainWindow = new EntryWindow(session); desktop.ShutdownRequested += (_, _) => session.Dispose(); } base.OnFrameworkInitializationCompleted(); } + + /// The window chrome follows the theme through these resources; everything + /// that draws a station or a spot asks `Themes.Current` as it paints. + private void ApplyTheme() + { + Theme theme = Themes.Current; + RequestedThemeVariant = theme.Variant; + Set("FormBackground", theme.FormBackground); + Set("FormForeground", theme.FormForeground); + Set("FieldBackground", theme.FieldBackground); + Set("FieldForeground", theme.FieldForeground); + // the keys the Fluent controls read, so a text box and a button take + // the theme's colours in every state rather than only where a style of + // ours reaches them + foreach (string key in new[] + { + "TextControlBackground", "TextControlBackgroundPointerOver", + "TextControlBackgroundFocused", "TextControlBackgroundDisabled", + "ComboBoxBackground", "ComboBoxBackgroundPointerOver", "ComboBoxBackgroundPressed", + "DataGridRowBackgroundBrush", + }) + { + Set(key, theme.FieldBackground); + } + foreach (string key in new[] + { + "TextControlForeground", "TextControlForegroundPointerOver", + "TextControlForegroundFocused", "ComboBoxForeground", "ButtonForeground", + "ButtonForegroundPointerOver", "ButtonForegroundPressed", + }) + { + Set(key, theme.FieldForeground); + } + Set("ButtonBackground", theme.ButtonBackground); + Set("ButtonBackgroundPointerOver", Lighter(theme.ButtonBackground)); + Set("ButtonBackgroundPressed", Darker(theme.ButtonBackground)); + } + + private void Set(string key, Color colour) => Resources[key] = new SolidColorBrush(colour); + + private static Color Lighter(Color colour) => Shifted(colour, 12); + + private static Color Darker(Color colour) => Shifted(colour, -18); + + private static Color Shifted(Color colour, int by) => Color.FromRgb( + (byte)Math.Clamp(colour.R + by, 0, 255), + (byte)Math.Clamp(colour.G + by, 0, 255), + (byte)Math.Clamp(colour.B + by, 0, 255)); } diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index 0072257..dfbd63d 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -9,6 +9,10 @@ public sealed record Settings { public string DatabasePath { get; init; } = ""; + /// `light` is N1MM's own palette, `dark` the same states in colours for a + /// dark screen. Config > Manage Skins, Colors and Fonts picks one. + public string Theme { get; init; } = "light"; + public int ContestNumber { get; init; } public StoredStation Station { get; init; } = new(); diff --git a/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs b/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs index fde043c..a5ecc59 100644 --- a/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs +++ b/src/Nonemm.App/Dialogs/MessagesDialog.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Platform.Storage; +using Nonemm.App.Theming; using Nonemm.Core; using Nonemm.Session; @@ -22,13 +23,13 @@ public sealed partial class MessagesDialog : Window /// 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 IBrush Blue => Themes.Brush(Themes.Current.Worth); - private static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60)); + private static IBrush Green => Themes.Brush(Themes.Current.Multipliers); - private static readonly IBrush Red = new SolidColorBrush(Color.FromRgb(0xC0, 0x39, 0x2B)); + private static IBrush Red => Themes.Brush(Themes.Current.Multiplier); - private static readonly IBrush Grey = new SolidColorBrush(Color.FromRgb(0x7F, 0x8C, 0x8D)); + private static IBrush Grey => Themes.Brush(Themes.Current.Dupe); private readonly ModeCategory mode; diff --git a/src/Nonemm.App/Dialogs/ThemeDialog.axaml b/src/Nonemm.App/Dialogs/ThemeDialog.axaml new file mode 100644 index 0000000..fa0ec62 --- /dev/null +++ b/src/Nonemm.App/Dialogs/ThemeDialog.axaml @@ -0,0 +1,27 @@ + + + +