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:
@@ -1,11 +1,27 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
RequestedThemeVariant="Light">
|
||||
<!-- the colours behind these keys are filled in from the theme in use, in
|
||||
App.ApplyTheme -->
|
||||
<Application.Resources>
|
||||
<SolidColorBrush x:Key="FormBackground">#CDDAEB</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="FormForeground">#000000</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="ButtonBackground">#FFFAF0</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="FieldBackground">#FFFFFF</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="FieldForeground">#000000</SolidColorBrush>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="Background" Value="{DynamicResource FormBackground}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource FormForeground}" />
|
||||
</Style>
|
||||
<Style Selector="Menu">
|
||||
<Setter Property="Background" Value="{DynamicResource FormBackground}" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
</Application>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
27
src/Nonemm.App/Dialogs/ThemeDialog.axaml
Normal file
27
src/Nonemm.App/Dialogs/ThemeDialog.axaml
Normal file
@@ -0,0 +1,27 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.Dialogs.ThemeDialog"
|
||||
Title="Colors" Width="360" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner" CanResize="False">
|
||||
<DockPanel Margin="14">
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center"
|
||||
Spacing="30" Margin="0,14,0,0">
|
||||
<Button Content="Ok" Width="85" HorizontalContentAlignment="Center"
|
||||
Click="OnOk" IsDefault="True" />
|
||||
<Button Content="Cancel" Width="85" HorizontalContentAlignment="Center"
|
||||
Click="OnCancel" IsCancel="True" />
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Colors" FontSize="11" />
|
||||
<ComboBox Name="ThemeBox" HorizontalAlignment="Stretch" SelectionChanged="OnPicked" />
|
||||
<TextBlock Name="AboutText" FontSize="11" TextWrapping="Wrap" />
|
||||
<ItemsControl Name="Swatches" Margin="0,4,0,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
65
src/Nonemm.App/Dialogs/ThemeDialog.axaml.cs
Normal file
65
src/Nonemm.App/Dialogs/ThemeDialog.axaml.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Nonemm.App.Theming;
|
||||
|
||||
namespace Nonemm.App.Dialogs;
|
||||
|
||||
/// Which colours the program paints with. N1MM keeps skins, colours and fonts
|
||||
/// in one place; this picks between the themes that ship with the program and
|
||||
/// shows what each one paints a station.
|
||||
public sealed partial class ThemeDialog : Window
|
||||
{
|
||||
public ThemeDialog(string current)
|
||||
{
|
||||
InitializeComponent();
|
||||
ThemeBox.ItemsSource = Themes.All.Select(Label).ToList();
|
||||
ThemeBox.SelectedIndex = Math.Max(0, Themes.All.ToList().FindIndex(t => t.Name == Themes.Find(current).Name));
|
||||
}
|
||||
|
||||
private Theme Picked => Themes.All[Math.Max(0, ThemeBox.SelectedIndex)];
|
||||
|
||||
private static string Label(Theme theme) => theme.Name switch
|
||||
{
|
||||
"light" => "Light — N1MM's own colours",
|
||||
"dark" => "Dark — for operating at night",
|
||||
_ => theme.Name,
|
||||
};
|
||||
|
||||
private void OnPicked(object? sender, SelectionChangedEventArgs e) => ShowSwatches();
|
||||
|
||||
private void ShowSwatches()
|
||||
{
|
||||
Theme theme = Picked;
|
||||
AboutText.Text = "What a station is painted in the callsign box, left to right: worth " +
|
||||
"working, one new multiplier, more than one, a dupe.";
|
||||
Swatches.ItemsSource = new[]
|
||||
{
|
||||
Swatch(theme.Worth, theme.FieldBackground),
|
||||
Swatch(theme.Multiplier, theme.FieldBackground),
|
||||
Swatch(theme.Multipliers, theme.FieldBackground),
|
||||
Swatch(theme.Dupe, theme.FieldBackground),
|
||||
};
|
||||
}
|
||||
|
||||
private static Border Swatch(Color text, Color background) => new()
|
||||
{
|
||||
Background = new SolidColorBrush(background),
|
||||
Width = 66,
|
||||
Height = 26,
|
||||
Margin = new Avalonia.Thickness(0, 0, 6, 6),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "OM3KFF",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(text),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
|
||||
private void OnOk(object? sender, RoutedEventArgs e) => Close(Picked.Name);
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
|
||||
}
|
||||
132
src/Nonemm.App/Theming/Theme.cs
Normal file
132
src/Nonemm.App/Theming/Theme.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
|
||||
namespace Nonemm.App.Theming;
|
||||
|
||||
/// The colours one theme paints with. The names follow N1MM's
|
||||
/// `ApplicationStyles`, because the light theme is N1MM's own palette and the
|
||||
/// windows are laid out the same way.
|
||||
public sealed record Theme
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// Which set of control colours the theme sits on, so a text box or a
|
||||
/// scroll bar looks right under the colours above.
|
||||
public required ThemeVariant Variant { get; init; }
|
||||
|
||||
/// Behind a window, and the text on it.
|
||||
public required Color FormBackground { get; init; }
|
||||
|
||||
public required Color FormForeground { get; init; }
|
||||
|
||||
public required Color ButtonBackground { get; init; }
|
||||
|
||||
/// Inside an entry box or a dialog box, and the text in it.
|
||||
public required Color FieldBackground { get; init; }
|
||||
|
||||
public required Color FieldForeground { get; init; }
|
||||
|
||||
/// A log row, and every second one.
|
||||
public required Color GridBackground { get; init; }
|
||||
|
||||
public required Color GridAlternateBackground { get; init; }
|
||||
|
||||
/// Worth a contact, no new multiplier.
|
||||
public required Color Worth { get; init; }
|
||||
|
||||
public required Color WorthBackground { get; init; }
|
||||
|
||||
/// New in one multiplier.
|
||||
public required Color Multiplier { get; init; }
|
||||
|
||||
public required Color MultiplierBackground { get; init; }
|
||||
|
||||
/// New in more than one.
|
||||
public required Color Multipliers { get; init; }
|
||||
|
||||
public required Color MultipliersBackground { get; init; }
|
||||
|
||||
public required Color Dupe { get; init; }
|
||||
|
||||
public required Color DupeBackground { get; init; }
|
||||
|
||||
/// The frequency we are calling CQ on.
|
||||
public required Color Cq { get; init; }
|
||||
|
||||
/// Behind a line that is in the log, one that is not filled in yet, and one
|
||||
/// that cannot be read.
|
||||
public required Color GoodBackground { get; init; }
|
||||
|
||||
public required Color EmptyBackground { get; init; }
|
||||
|
||||
public required Color BadBackground { get; init; }
|
||||
|
||||
/// The boxes while quick editing a logged contact.
|
||||
public required Color QuickEdit { get; init; }
|
||||
|
||||
/// The light that says a radio is answering, and the one that says
|
||||
/// something is going out on the air.
|
||||
public required Color RadioLight { get; init; }
|
||||
|
||||
public required Color TransmitLight { get; init; }
|
||||
|
||||
/// N1MM's own defaults, from `ApplicationStyles`: the program as it looks
|
||||
/// on a fresh install.
|
||||
public static readonly Theme Light = new()
|
||||
{
|
||||
Name = "light",
|
||||
Variant = ThemeVariant.Light,
|
||||
FormBackground = Color.FromRgb(0xCD, 0xDA, 0xEB),
|
||||
FormForeground = Color.FromRgb(0x00, 0x00, 0x00),
|
||||
ButtonBackground = Color.FromRgb(0xFF, 0xFA, 0xF0),
|
||||
FieldBackground = Color.FromRgb(0xFF, 0xFF, 0xFF),
|
||||
FieldForeground = Color.FromRgb(0x00, 0x00, 0x00),
|
||||
GridBackground = Color.FromRgb(0xF2, 0xFC, 0xFF),
|
||||
GridAlternateBackground = Color.FromRgb(0xAB, 0xC0, 0xDC),
|
||||
Worth = Color.FromRgb(0x00, 0x00, 0xFF),
|
||||
WorthBackground = Color.FromRgb(0x51, 0xA8, 0xFF),
|
||||
Multiplier = Color.FromRgb(0xE8, 0x00, 0x00),
|
||||
MultiplierBackground = Color.FromRgb(0xFF, 0x37, 0x37),
|
||||
Multipliers = Color.FromRgb(0x0D, 0x9D, 0x0D),
|
||||
MultipliersBackground = Color.FromRgb(0x5F, 0xF1, 0x5F),
|
||||
Dupe = Color.FromRgb(0x80, 0x80, 0x80),
|
||||
DupeBackground = Color.FromRgb(0xD4, 0xD4, 0xD4),
|
||||
Cq = Color.FromRgb(0xFF, 0x00, 0x00),
|
||||
GoodBackground = Color.FromRgb(0xD4, 0xF5, 0xD4),
|
||||
EmptyBackground = Color.FromRgb(0xFF, 0xE4, 0xE1),
|
||||
BadBackground = Color.FromRgb(0xFF, 0xF8, 0xC4),
|
||||
QuickEdit = Color.FromRgb(0xFF, 0xFF, 0xC0),
|
||||
RadioLight = Color.FromRgb(0x0D, 0x9D, 0x0D),
|
||||
TransmitLight = Color.FromRgb(0xFF, 0x00, 0x00),
|
||||
};
|
||||
|
||||
/// For operating at night: the same states, in colours that carry on a dark
|
||||
/// background.
|
||||
public static readonly Theme Dark = new()
|
||||
{
|
||||
Name = "dark",
|
||||
Variant = ThemeVariant.Dark,
|
||||
FormBackground = Color.FromRgb(0x20, 0x22, 0x25),
|
||||
FormForeground = Color.FromRgb(0xE8, 0xE8, 0xE8),
|
||||
ButtonBackground = Color.FromRgb(0x2E, 0x31, 0x35),
|
||||
FieldBackground = Color.FromRgb(0x17, 0x19, 0x1B),
|
||||
FieldForeground = Color.FromRgb(0xE8, 0xE8, 0xE8),
|
||||
GridBackground = Color.FromRgb(0x1B, 0x1D, 0x20),
|
||||
GridAlternateBackground = Color.FromRgb(0x23, 0x26, 0x2A),
|
||||
Worth = Color.FromRgb(0x29, 0x80, 0xB9),
|
||||
WorthBackground = Color.FromRgb(0x1F, 0x4E, 0x6E),
|
||||
Multiplier = Color.FromRgb(0xC0, 0x39, 0x2B),
|
||||
MultiplierBackground = Color.FromRgb(0x6E, 0x24, 0x1C),
|
||||
Multipliers = Color.FromRgb(0x27, 0xAE, 0x60),
|
||||
MultipliersBackground = Color.FromRgb(0x1B, 0x5E, 0x36),
|
||||
Dupe = Color.FromRgb(0x7F, 0x8C, 0x8D),
|
||||
DupeBackground = Color.FromRgb(0x3A, 0x40, 0x40),
|
||||
Cq = Color.FromRgb(0xE7, 0x4C, 0x3C),
|
||||
GoodBackground = Color.FromRgb(0x1E, 0x38, 0x24),
|
||||
EmptyBackground = Color.FromRgb(0x3A, 0x24, 0x24),
|
||||
BadBackground = Color.FromRgb(0x3C, 0x36, 0x1C),
|
||||
QuickEdit = Color.FromRgb(0x4A, 0x44, 0x1E),
|
||||
RadioLight = Color.FromRgb(0x27, 0xAE, 0x60),
|
||||
TransmitLight = Color.FromRgb(0xC0, 0x39, 0x2B),
|
||||
};
|
||||
}
|
||||
46
src/Nonemm.App/Theming/Themes.cs
Normal file
46
src/Nonemm.App/Theming/Themes.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Nonemm.App.Theming;
|
||||
|
||||
/// Which theme the program is painting with. Everything that draws a colour
|
||||
/// asks here rather than holding a brush of its own, so changing the theme
|
||||
/// changes every window.
|
||||
public static class Themes
|
||||
{
|
||||
public static readonly IReadOnlyList<Theme> All = [Theme.Light, Theme.Dark];
|
||||
|
||||
private static readonly Dictionary<Color, IBrush> brushes = [];
|
||||
|
||||
public static Theme Current { get; private set; } = Theme.Light;
|
||||
|
||||
/// Raised after the theme changes, for the windows that paint what they
|
||||
/// hold rather than rebuilding it.
|
||||
public static event EventHandler? Changed;
|
||||
|
||||
public static Theme Find(string name) =>
|
||||
All.FirstOrDefault(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase))
|
||||
?? Theme.Light;
|
||||
|
||||
public static void Use(string name)
|
||||
{
|
||||
Theme wanted = Find(name);
|
||||
if (wanted == Current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Current = wanted;
|
||||
Changed?.Invoke(null, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// One brush per colour, because a brush is created for every row of every
|
||||
/// window that is painted.
|
||||
public static IBrush Brush(Color colour)
|
||||
{
|
||||
if (!brushes.TryGetValue(colour, out IBrush? brush))
|
||||
{
|
||||
brush = new SolidColorBrush(colour);
|
||||
brushes[colour] = brush;
|
||||
}
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
using Avalonia.Media;
|
||||
using Nonemm.App.Theming;
|
||||
using Nonemm.Contests;
|
||||
|
||||
namespace Nonemm.App;
|
||||
|
||||
/// 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.
|
||||
/// disagree. The colours come from the theme in use.
|
||||
public static class Verdicts
|
||||
{
|
||||
/// Worth a contact but no new multiplier.
|
||||
public static readonly IBrush Worth = new SolidColorBrush(Color.FromRgb(0x00, 0x00, 0xFF));
|
||||
public static IBrush Worth => Themes.Brush(Themes.Current.Worth);
|
||||
|
||||
/// New in one multiplier.
|
||||
public static readonly IBrush NewMultiplier = new SolidColorBrush(Color.FromRgb(0xE8, 0x00, 0x00));
|
||||
public static IBrush NewMultiplier => Themes.Brush(Themes.Current.Multiplier);
|
||||
|
||||
/// 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 IBrush NewMultipliers => Themes.Brush(Themes.Current.Multipliers);
|
||||
|
||||
public static readonly IBrush Dupe = new SolidColorBrush(Color.FromRgb(0x80, 0x80, 0x80));
|
||||
public static IBrush Dupe => Themes.Brush(Themes.Current.Dupe);
|
||||
|
||||
public static readonly IBrush Nothing = Dupe;
|
||||
public static IBrush Nothing => Dupe;
|
||||
|
||||
/// The frequency we are calling CQ on.
|
||||
public static readonly IBrush Cq = new SolidColorBrush(Color.FromRgb(0xFF, 0x00, 0x00));
|
||||
public static IBrush Cq => Themes.Brush(Themes.Current.Cq);
|
||||
|
||||
/// Nothing typed yet is the same blue as a station worth working: that is
|
||||
/// Nothing typed yet is the same colour as a station worth working: that is
|
||||
/// what N1MM leaves the callsign box.
|
||||
public static IBrush Colour(Verdict? verdict) =>
|
||||
verdict is null ? Worth
|
||||
@@ -35,18 +32,13 @@ public static class Verdicts
|
||||
: Worth;
|
||||
|
||||
/// The same four states as backgrounds, which is what N1MM paints behind a
|
||||
/// spot: QBackColor, MultBackColor and DupeBackColor.
|
||||
/// spot.
|
||||
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));
|
||||
verdict is null ? Themes.Brush(Themes.Current.WorthBackground)
|
||||
: verdict.IsDupe ? Themes.Brush(Themes.Current.DupeBackground)
|
||||
: verdict.NewMultipliers.Count > 1 ? Themes.Brush(Themes.Current.MultipliersBackground)
|
||||
: verdict.NewMultipliers.Count == 1 ? Themes.Brush(Themes.Current.MultiplierBackground)
|
||||
: Themes.Brush(Themes.Current.WorthBackground);
|
||||
|
||||
public static string Describe(Verdict? verdict)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Nonemm.App.Configuration;
|
||||
using Nonemm.App.Dialogs;
|
||||
using Nonemm.App.Theming;
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Country;
|
||||
@@ -469,6 +470,18 @@ public sealed partial class EntryWindow
|
||||
|
||||
private void OnEditPhoneMessages(object? sender, RoutedEventArgs e) => _ = EditMessages(ModeCategory.Phone);
|
||||
|
||||
private async void OnThemeSettings(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ThemeDialog dialog = new(session.Settings.Theme);
|
||||
if (await dialog.ShowDialog<string?>(this) is not { } picked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.Save(session.Settings with { Theme = picked });
|
||||
Themes.Use(picked);
|
||||
Status($"{picked} colours");
|
||||
}
|
||||
|
||||
private async void OnQtcSetup(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
QtcSetupDialog dialog = new(session.Settings);
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
<MenuItem Header="Change _SSB Function Key Definitions" Click="OnEditPhoneMessages" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="Change Band Plan…" Click="OnSubBandSettings" />
|
||||
<MenuItem Header="Manage Skins, Colors and Fonts…" Click="OnThemeSettings" />
|
||||
<MenuItem Header="Edit Networked-Computer Names…" Click="OnNetworkSettings" />
|
||||
<MenuItem Header="WAE">
|
||||
<MenuItem Header="Open QTC Window setup area" Click="OnQtcSetup" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Media;
|
||||
using Nonemm.App.Configuration;
|
||||
using Nonemm.App.Dialogs;
|
||||
using Nonemm.Contests.Rules;
|
||||
using Nonemm.App.Theming;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Session;
|
||||
|
||||
@@ -20,9 +21,11 @@ namespace Nonemm.App.Windows;
|
||||
/// SSB by voice, on RTTY from the digital window there is not yet.
|
||||
public sealed partial class QtcWindow : Window
|
||||
{
|
||||
private static readonly IBrush Saved = new SolidColorBrush(Color.FromRgb(0xD4, 0xF5, 0xD4));
|
||||
private static readonly IBrush Unsaved = new SolidColorBrush(Color.FromRgb(0xFF, 0xE4, 0xE1));
|
||||
private static readonly IBrush Malformed = new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0xC4));
|
||||
private static IBrush Saved => Themes.Brush(Themes.Current.GoodBackground);
|
||||
|
||||
private static IBrush Unsaved => Themes.Brush(Themes.Current.EmptyBackground);
|
||||
|
||||
private static IBrush Malformed => Themes.Brush(Themes.Current.BadBackground);
|
||||
|
||||
private readonly AppSession session;
|
||||
private readonly OperatingPosition position;
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using Nonemm.App.Configuration;
|
||||
using Nonemm.App.Theming;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Session;
|
||||
using Nonemm.Spotting;
|
||||
@@ -18,9 +19,11 @@ public sealed partial class TelnetWindow : RefreshableWindow
|
||||
{
|
||||
private const int LinesKept = 500;
|
||||
|
||||
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 static IBrush SpotColour => Themes.Brush(Themes.Current.Multipliers);
|
||||
|
||||
private static IBrush SentColour => Themes.Brush(Themes.Current.Worth);
|
||||
|
||||
private static IBrush NoticeColour => Themes.Brush(Themes.Current.Dupe);
|
||||
|
||||
private readonly AppSession session;
|
||||
private readonly Action<Frequency, string> tune;
|
||||
|
||||
Reference in New Issue
Block a user