diff --git a/src/SightAdapt/ConfigurationForm.cs b/src/SightAdapt/ConfigurationForm.cs index cf76062f..0fb33471 100644 --- a/src/SightAdapt/ConfigurationForm.cs +++ b/src/SightAdapt/ConfigurationForm.cs @@ -7,6 +7,8 @@ internal sealed class ConfigurationForm : Form { private readonly SettingsCoordinator _settingsCoordinator; private readonly Func _getCurrentApplication; + private readonly Action + _showVisualProfileManager; private readonly ToggleSwitch _automaticModeSwitch; private readonly Label _automaticModeStateLabel; private readonly Label _profileCountLabel; @@ -17,12 +19,16 @@ internal sealed class ConfigurationForm : Form public ConfigurationForm( SettingsCoordinator settingsCoordinator, - Func getCurrentApplication) + Func getCurrentApplication, + Action? + showVisualProfileManager = null) { _settingsCoordinator = settingsCoordinator ?? throw new ArgumentNullException(nameof(settingsCoordinator)); _getCurrentApplication = getCurrentApplication ?? throw new ArgumentNullException(nameof(getCurrentApplication)); + _showVisualProfileManager = showVisualProfileManager ?? + VisualProfileManagerForm.ShowManager; Text = ProductInfo.WindowTitle; StartPosition = FormStartPosition.CenterScreen; @@ -62,6 +68,8 @@ public ConfigurationForm( private SightAdaptSettings Settings => _settingsCoordinator.Current; + internal int RefreshGeneration { get; private set; } + public void RefreshProfiles() { if (IsDisposed) @@ -88,6 +96,8 @@ public void RefreshProfiles() { _refreshing = false; } + + RefreshGeneration++; } private Control CreateRootLayout() @@ -628,10 +638,9 @@ private void EditSelectedVisualProfile() } } - private void ManageVisualProfiles() + internal void ManageVisualProfiles() { - VisualProfileManagerForm.ShowManager(this, _settingsCoordinator); - RefreshProfiles(); + _showVisualProfileManager(this, _settingsCoordinator); } private ApplicationProfile? GetSelectedApplicationProfile() diff --git a/src/SightAdapt/ModernSelectorComboBoxColumn.cs b/src/SightAdapt/ModernSelectorComboBoxColumn.cs index 98d6c234..c2b61b3c 100644 --- a/src/SightAdapt/ModernSelectorComboBoxColumn.cs +++ b/src/SightAdapt/ModernSelectorComboBoxColumn.cs @@ -155,7 +155,9 @@ protected override void Paint( graphics, Rectangle.Inflate(cellBounds, -7, -6), formattedValue?.ToString() ?? string.Empty, - cellStyle.Font ?? AppTheme.CreateUiFont(9.5f), + ResolvePaintFont( + cellStyle.Font, + DataGridView?.Font), foreground, selected, focused: @@ -163,12 +165,22 @@ protected override void Paint( DataGridView?.CurrentCellAddress.X == ColumnIndex && DataGridView.CurrentCellAddress.Y == rowIndex); } + + internal static Font ResolvePaintFont( + Font? cellStyleFont, + Font? gridFont) + { + return cellStyleFont ?? + gridFont ?? + Control.DefaultFont; + } } internal sealed class ModernSelectorEditingControl : Control, IDataGridViewEditingControl { + private readonly Font _defaultFont; private readonly ListBox _list; private readonly ToolStripDropDown _dropDown; private ModernSelectorOption[] _options = []; @@ -177,10 +189,12 @@ internal sealed class ModernSelectorEditingControl : public ModernSelectorEditingControl() { + _defaultFont = AppTheme.CreateUiFont(9.5f); + AccessibleRole = AccessibleRole.ComboBox; BackColor = AppTheme.SurfaceRaised; Cursor = Cursors.Hand; - Font = AppTheme.CreateUiFont(9.5f); + Font = _defaultFont; ForeColor = AppTheme.TextPrimary; TabStop = true; SetStyle( @@ -196,7 +210,7 @@ public ModernSelectorEditingControl() BackColor = AppTheme.SurfaceRaised, BorderStyle = BorderStyle.None, DrawMode = DrawMode.OwnerDrawFixed, - Font = AppTheme.CreateUiFont(9.5f), + Font = _defaultFont, ForeColor = AppTheme.TextPrimary, IntegralHeight = false, ItemHeight = 34, @@ -248,7 +262,7 @@ public void Configure( { _options = options.ToArray(); EditingControlValueChanged = false; - Font = style.Font ?? AppTheme.CreateUiFont(9.5f); + Font = style.Font ?? _defaultFont; ForeColor = AppTheme.TextPrimary; BackColor = AppTheme.SurfaceRaised; SelectByValue(selectedId); @@ -261,7 +275,7 @@ public void Configure( public void ApplyCellStyleToEditingControl( DataGridViewCellStyle dataGridViewCellStyle) { - Font = dataGridViewCellStyle.Font ?? AppTheme.CreateUiFont(9.5f); + Font = dataGridViewCellStyle.Font ?? _defaultFont; ForeColor = AppTheme.TextPrimary; BackColor = AppTheme.SurfaceRaised; } @@ -397,6 +411,7 @@ protected override void Dispose(bool disposing) { _dropDown.Dispose(); _list.Dispose(); + _defaultFont.Dispose(); } base.Dispose(disposing); diff --git a/src/SightAdapt/ModernTheme.cs b/src/SightAdapt/ModernTheme.cs index 8efd4276..a9de410f 100644 --- a/src/SightAdapt/ModernTheme.cs +++ b/src/SightAdapt/ModernTheme.cs @@ -14,6 +14,7 @@ internal static class AppTheme public static readonly Color WindowBackground = Color.FromArgb(20, 23, 31); public static readonly Color HeaderBackground = Color.FromArgb(24, 28, 38); public static readonly Color Surface = Color.FromArgb(29, 34, 45); + public static readonly Color SurfaceAlternate = Color.FromArgb(32, 38, 50); public static readonly Color SurfaceRaised = Color.FromArgb(36, 42, 55); public static readonly Color SurfaceHover = Color.FromArgb(45, 53, 69); public static readonly Color Border = Color.FromArgb(54, 63, 81); @@ -28,6 +29,9 @@ internal static class AppTheme public static readonly Color SuccessSoft = Color.FromArgb(30, 76, 67); public static readonly Color Danger = Color.FromArgb(255, 111, 128); public static readonly Color DangerSoft = Color.FromArgb(84, 43, 54); + public static readonly Color DangerHover = Color.FromArgb(96, 47, 59); + public static readonly Color DangerPressed = Color.FromArgb(105, 47, 60); + public static readonly Color DangerBorder = Color.FromArgb(120, 58, 70); public static readonly Color Selection = Color.FromArgb(52, 67, 105); public static Font CreateUiFont(float size = 9.5f, FontStyle style = FontStyle.Regular) @@ -109,7 +113,7 @@ public static void StyleGrid(DataGridView grid) grid.AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle { - BackColor = Color.FromArgb(32, 38, 50), + BackColor = SurfaceAlternate, ForeColor = TextPrimary, SelectionBackColor = Selection, SelectionForeColor = TextPrimary, @@ -243,28 +247,71 @@ protected override void OnPaint(PaintEventArgs eventArgs) private (Color Background, Color Border, Color Foreground) ResolveColors() { - if (!Enabled) + return ResolveColors( + VisualStyle, + Enabled, + _hovered, + _pressed); + } + + internal static ( + Color Background, + Color Border, + Color Foreground) ResolveColors( + ModernButtonStyle visualStyle, + bool enabled, + bool hovered, + bool pressed) + { + if (!enabled) { - return (AppTheme.Surface, AppTheme.Border, AppTheme.TextMuted); + return ( + AppTheme.Surface, + AppTheme.Border, + AppTheme.TextMuted); } - return VisualStyle switch + return visualStyle switch { ModernButtonStyle.Primary => ( - _pressed ? AppTheme.AccentPressed : _hovered ? AppTheme.AccentHover : AppTheme.Accent, - _pressed ? AppTheme.AccentPressed : AppTheme.AccentHover, + pressed + ? AppTheme.AccentPressed + : hovered + ? AppTheme.AccentHover + : AppTheme.Accent, + pressed + ? AppTheme.AccentPressed + : AppTheme.AccentHover, Color.White), ModernButtonStyle.Danger => ( - _pressed ? Color.FromArgb(105, 47, 60) : _hovered ? Color.FromArgb(96, 47, 59) : AppTheme.DangerSoft, - _hovered ? AppTheme.Danger : Color.FromArgb(120, 58, 70), - _hovered ? Color.White : AppTheme.Danger), + pressed + ? AppTheme.DangerPressed + : hovered + ? AppTheme.DangerHover + : AppTheme.DangerSoft, + hovered + ? AppTheme.Danger + : AppTheme.DangerBorder, + hovered + ? Color.White + : AppTheme.Danger), ModernButtonStyle.Ghost => ( - _pressed ? AppTheme.SurfaceRaised : _hovered ? AppTheme.SurfaceHover : AppTheme.WindowBackground, - _hovered ? AppTheme.Border : AppTheme.WindowBackground, + pressed + ? AppTheme.SurfaceRaised + : hovered + ? AppTheme.SurfaceHover + : AppTheme.WindowBackground, + hovered + ? AppTheme.Border + : AppTheme.WindowBackground, AppTheme.TextSecondary), _ => ( - _pressed ? AppTheme.SurfaceHover : _hovered ? AppTheme.SurfaceHover : AppTheme.SurfaceRaised, - _hovered ? AppTheme.Accent : AppTheme.Border, + pressed || hovered + ? AppTheme.SurfaceHover + : AppTheme.SurfaceRaised, + hovered + ? AppTheme.Accent + : AppTheme.Border, AppTheme.TextPrimary), }; } diff --git a/src/SightAdapt/SightAdapt.csproj b/src/SightAdapt/SightAdapt.csproj index 61c91af9..8e63bdf3 100644 --- a/src/SightAdapt/SightAdapt.csproj +++ b/src/SightAdapt/SightAdapt.csproj @@ -16,10 +16,10 @@ Copyright © $(Company) MIT https://github.com/KeyffMS/SightAdapt - 0.5.0.33 + 0.5.0.34 0.5.0.0 - 0.5.0.33 - 0.5.0.33 + 0.5.0.34 + 0.5.0.34 x64 x64 10.0.19041.0 diff --git a/tests/SightAdapt.Tests/ConfigurationProfileManagerRefreshTests.cs b/tests/SightAdapt.Tests/ConfigurationProfileManagerRefreshTests.cs new file mode 100644 index 00000000..20822686 --- /dev/null +++ b/tests/SightAdapt.Tests/ConfigurationProfileManagerRefreshTests.cs @@ -0,0 +1,133 @@ +using System.Windows.Forms; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SightAdapt.Tests; + +[TestClass] +public sealed class ConfigurationProfileManagerRefreshTests +{ + [TestMethod] + public void ClosingManagerWithoutChangesDoesNotRefreshConfiguration() + { + RunOnSta(() => + { + using var temporaryDirectory = + new TemporaryDirectory(); + var coordinator = CreateCoordinator( + temporaryDirectory.Path); + var managerCalls = 0; + using var form = new ConfigurationForm( + coordinator, + () => null, + (_, receivedCoordinator) => + { + Assert.AreSame( + coordinator, + receivedCoordinator); + managerCalls++; + }); + var generation = form.RefreshGeneration; + + form.ManageVisualProfiles(); + + Assert.AreEqual(1, managerCalls); + Assert.AreEqual( + generation, + form.RefreshGeneration); + }); + } + + [TestMethod] + public void ManagerMutationRefreshesConfigurationExactlyOnce() + { + RunOnSta(() => + { + using var temporaryDirectory = + new TemporaryDirectory(); + var coordinator = CreateCoordinator( + temporaryDirectory.Path); + using var form = new ConfigurationForm( + coordinator, + () => null, + (_, receivedCoordinator) => + { + var result = + receivedCoordinator.Commit(settings => + VisualProfileManagementService.Create( + settings, + "Reader").Id); + Assert.IsTrue(result.Succeeded); + }); + var generation = form.RefreshGeneration; + + form.ManageVisualProfiles(); + + Assert.AreEqual( + generation + 1, + form.RefreshGeneration); + Assert.IsTrue( + coordinator.Current.VisualProfiles.Any( + profile => profile.Name == "Reader")); + }); + } + + private static SettingsCoordinator CreateCoordinator( + string directory) + { + return new SettingsCoordinator( + new SettingsStore( + Path.Combine( + directory, + "settings.json"))); + } + + private static void RunOnSta(Action action) + { + Exception? failure = null; + var thread = new Thread(() => + { + try + { + action(); + } + catch (Exception exception) + { + failure = exception; + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + Assert.IsTrue( + thread.Join(TimeSpan.FromSeconds(10)), + "The configuration refresh test did not finish in time."); + if (failure is not null) + { + Assert.Fail(failure.ToString()); + } + } + + private sealed class TemporaryDirectory : IDisposable + { + public TemporaryDirectory() + { + Path = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + "SightAdapt.Tests", + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete( + Path, + recursive: true); + } + } + } +} diff --git a/tests/SightAdapt.Tests/ThemeTokenTests.cs b/tests/SightAdapt.Tests/ThemeTokenTests.cs new file mode 100644 index 00000000..6a7ab20a --- /dev/null +++ b/tests/SightAdapt.Tests/ThemeTokenTests.cs @@ -0,0 +1,109 @@ +using System.Windows.Forms; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SightAdapt.Tests; + +[TestClass] +public sealed class ThemeTokenTests +{ + [TestMethod] + public void DangerButtonStatesUseSemanticThemeTokens() + { + var resting = ModernButton.ResolveColors( + ModernButtonStyle.Danger, + enabled: true, + hovered: false, + pressed: false); + var hovered = ModernButton.ResolveColors( + ModernButtonStyle.Danger, + enabled: true, + hovered: true, + pressed: false); + var pressed = ModernButton.ResolveColors( + ModernButtonStyle.Danger, + enabled: true, + hovered: false, + pressed: true); + + Assert.AreEqual( + AppTheme.DangerSoft, + resting.Background); + Assert.AreEqual( + AppTheme.DangerBorder, + resting.Border); + Assert.AreEqual( + AppTheme.DangerHover, + hovered.Background); + Assert.AreEqual( + AppTheme.Danger, + hovered.Border); + Assert.AreEqual( + AppTheme.DangerPressed, + pressed.Background); + } + + [TestMethod] + public void GridUsesAlternateSurfaceThemeToken() + { + RunOnSta(() => + { + using var grid = new DataGridView(); + AppTheme.StyleGrid(grid); + + Assert.AreEqual( + AppTheme.SurfaceAlternate, + grid.AlternatingRowsDefaultCellStyle.BackColor); + }); + } + + [TestMethod] + public void SelectorPaintFontResolutionReusesExistingFonts() + { + using var cellStyleFont = + AppTheme.CreateUiFont(9.5f); + using var gridFont = + AppTheme.CreateUiFont(10f); + + Assert.AreSame( + cellStyleFont, + ModernSelectorComboBoxCell.ResolvePaintFont( + cellStyleFont, + gridFont)); + Assert.AreSame( + gridFont, + ModernSelectorComboBoxCell.ResolvePaintFont( + null, + gridFont)); + Assert.AreSame( + Control.DefaultFont, + ModernSelectorComboBoxCell.ResolvePaintFont( + null, + null)); + } + + private static void RunOnSta(Action action) + { + Exception? failure = null; + var thread = new Thread(() => + { + try + { + action(); + } + catch (Exception exception) + { + failure = exception; + } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + Assert.IsTrue( + thread.Join(TimeSpan.FromSeconds(10)), + "The theme test did not finish in time."); + if (failure is not null) + { + Assert.Fail(failure.ToString()); + } + } +}