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 @@ + + + +