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 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 13:06:32 +00:00
parent e4b3dc9572
commit da04764756
16 changed files with 452 additions and 49 deletions

View File

@@ -7,6 +7,7 @@ using Avalonia.Media;
using Avalonia.Threading;
using Nonemm.Contests;
using Nonemm.Core;
using Nonemm.App.Theming;
using Nonemm.Session;
namespace Nonemm.App.Windows;
@@ -24,13 +25,6 @@ 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;
@@ -52,6 +46,8 @@ public sealed partial class EntryWindow : Window
BuildEntryBoxes();
ShowSecondRadio();
});
Themes.Changed += OnThemeChanged;
Closed += (_, _) => Themes.Changed -= OnThemeChanged;
clock.Tick += (_, _) => UpdateClock();
clock.Start();
session.ActiveRadioChanged += (_, number) =>
@@ -67,6 +63,8 @@ public sealed partial class EntryWindow : Window
}
private void OnThemeChanged(object? sender, EventArgs e) => Dispatcher.UIThread.Post(Refresh);
/// Resolved every time rather than held, because opening a contest builds
/// new positions.
private OperatingPosition? Logging =>
@@ -697,8 +695,8 @@ public sealed partial class EntryWindow : Window
}
else
{
box.Background = QuickEditBackground;
box.Foreground = Brushes.Black;
box.Background = Themes.Brush(Themes.Current.QuickEdit);
box.Foreground = Themes.Brush(Themes.Current.FormForeground);
}
}
FrequencyText.Text = Logging.TransmitFrequency.Hertz > 0
@@ -719,7 +717,7 @@ public sealed partial class EntryWindow : Window
ShowCallColour(verdict);
VerdictBorder.Background = Verdicts.Background(verdict);
VerdictText.Text = VerdictLine(verdict);
VerdictText.Foreground = Brushes.Black;
VerdictText.Foreground = Themes.Brush(Themes.Current.FormForeground);
foreach (Window window in openWindows.Values)
{
(window as RefreshableWindow)?.Refresh();
@@ -781,8 +779,8 @@ public sealed partial class EntryWindow : Window
private void ShowLights()
{
bool radio = session.Radios.Any(r => r.Number == radioNumber && r.IsConnected);
RadioLight.Fill = radio ? GreenLight : Dark;
TransmitLight.Fill = sending ? RedLight : Dark;
RadioLight.Fill = radio ? Themes.Brush(Themes.Current.RadioLight) : Dark;
TransmitLight.Fill = sending ? Themes.Brush(Themes.Current.TransmitLight) : Dark;
}
/// What N1MM writes in its title bar: the frequency, the mode, whether the