diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 25cc95ec..151af14b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -96,7 +96,11 @@ A failed mutation or failed write does not replace committed settings and does n | Target availability and frame positioning/source updates | availability strategies and `MagnifierFrameRenderer` | | Native window/control lifetime | `MagnifierOverlay` | | Notification-area presentation | `TrayPresenter` | -| Application-table presentation and edit mechanics | `ApplicationAssignmentsGrid` | +| Application-table selection, binding, row updates and typed publication | `ApplicationAssignmentsGrid` | +| Assignment-grid columns and selector options | `ApplicationAssignmentsGridColumns` | +| Assignment-grid cell-to-command mapping | `ApplicationAssignmentCellChangeMapper` | +| Assignment enabled-state painting | `ApplicationAssignmentEnabledCellRenderer` | +| Assignment selector data-error classification | `ApplicationAssignmentSelectorErrorPolicy` | | Configuration use cases and dialogs | `ConfigurationForm` | | Selector editing contract | `ModernSelectorEditingControl` | @@ -179,7 +183,7 @@ Native menu detection, menu-overlay creation, and cross-filter refresh are subor ## Configuration grid boundary -`ApplicationAssignmentsGrid` owns columns, rows, selectors, status painting, selection, empty state, stable executable-path keys, separate typed change events, row updates, and failed-cell restoration. It does not know about persistence or dialogs. +`ApplicationAssignmentsGrid` remains the UserControl boundary and owns selection, binding, row updates, empty-state visibility, stable executable-path keys and typed change publication. Stable column/selector construction lives in `ApplicationAssignmentsGridColumns`; pure cell-to-command decoding lives in `ApplicationAssignmentCellChangeMapper`; enabled-state indicator geometry/painting lives in `ApplicationAssignmentEnabledCellRenderer`; selector data-error classification and diagnostics live in `ApplicationAssignmentSelectorErrorPolicy`. The grid does not know about persistence or dialogs. `ConfigurationForm` resolves current committed assignments and translates typed grid events into domain-service mutations wrapped by `SettingsCoordinator.Commit`. It suppresses only its own synchronous full refresh during a grid-originated commit. diff --git a/src/SightAdapt/ApplicationAssignmentsGrid.cs b/src/SightAdapt/ApplicationAssignmentsGrid.cs index 14998f4c..c9b57e6f 100644 --- a/src/SightAdapt/ApplicationAssignmentsGrid.cs +++ b/src/SightAdapt/ApplicationAssignmentsGrid.cs @@ -1,24 +1,13 @@ -using System.Drawing.Drawing2D; - namespace SightAdapt; internal sealed class ApplicationAssignmentsGrid : UserControl { - private const string EnabledColumnName = "Enabled"; - private const string ApplicationColumnName = "Application"; - internal const string VisualProfileColumnName = "VisualProfile"; + internal const string VisualProfileColumnName = + ApplicationAssignmentsGridColumns.VisualProfileColumnName; internal const string MenuVisualProfileColumnName = - "MenuVisualProfile"; - internal const string OverlayScopeColumnName = "OverlayScope"; - - private const DataGridViewDataErrorContexts - RecoverableSelectorContexts = - DataGridViewDataErrorContexts.Formatting | - DataGridViewDataErrorContexts.Display | - DataGridViewDataErrorContexts.PreferredSize | - DataGridViewDataErrorContexts.InitialValueRestoration; - private const string ExecutableColumnName = "Executable"; - private const string PathColumnName = "Path"; + ApplicationAssignmentsGridColumns.MenuVisualProfileColumnName; + internal const string OverlayScopeColumnName = + ApplicationAssignmentsGridColumns.OverlayScopeColumnName; private readonly DataGridView _grid; private readonly Label _emptyStateLabel; @@ -62,9 +51,10 @@ public void Bind( _binding = true; try { - SetVisualProfiles(visualProfiles); - SetMenuVisualProfiles(visualProfiles); - SetOverlayScopes(); + ApplicationAssignmentsGridColumns + .SetSelectorOptions( + _grid, + visualProfiles); _grid.Rows.Clear(); foreach (var assignment in assignments) @@ -78,10 +68,13 @@ public void Bind( } UpdateVisibility(assignments.Count); - SelectedApplicationChanged?.Invoke(this, EventArgs.Empty); + SelectedApplicationChanged?.Invoke( + this, + EventArgs.Empty); } - public void UpdateAssignment(ApplicationAssignmentRow assignment) + public void UpdateAssignment( + ApplicationAssignmentRow assignment) { ArgumentNullException.ThrowIfNull(assignment); @@ -102,7 +95,6 @@ public void UpdateAssignment(ApplicationAssignmentRow assignment) } } - private DataGridView CreateGrid() { var grid = new DataGridView @@ -115,79 +107,23 @@ private DataGridView CreateGrid() EditMode = DataGridViewEditMode.EditOnEnter, MultiSelect = false, ReadOnly = false, - SelectionMode = DataGridViewSelectionMode.FullRowSelect, + SelectionMode = + DataGridViewSelectionMode.FullRowSelect, }; AppTheme.StyleGrid(grid); - - var enabled = new DataGridViewCheckBoxColumn - { - Name = EnabledColumnName, - HeaderText = "ACTIVE", - Width = 92, - MinimumWidth = 92, - Resizable = DataGridViewTriState.False, - FlatStyle = FlatStyle.Flat, - SortMode = DataGridViewColumnSortMode.NotSortable, - }; - enabled.HeaderCell.Style.Alignment = - DataGridViewContentAlignment.MiddleCenter; - enabled.DefaultCellStyle.Alignment = - DataGridViewContentAlignment.MiddleCenter; - enabled.DefaultCellStyle.Padding = Padding.Empty; - - grid.Columns.Add(enabled); - grid.Columns.Add(FormPresentation.CreateReadOnlyTextColumn( - ApplicationColumnName, - "APPLICATION", - 205)); - grid.Columns.Add(new StableModernSelectorComboBoxColumn - { - Name = VisualProfileColumnName, - HeaderText = "VISUAL PROFILE", - DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox, - FlatStyle = FlatStyle.Flat, - Width = 185, - MinimumWidth = 160, - SortMode = DataGridViewColumnSortMode.NotSortable, - }); - grid.Columns.Add(new StableModernSelectorComboBoxColumn - { - Name = MenuVisualProfileColumnName, - HeaderText = "MENU PROFILE", - DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox, - FlatStyle = FlatStyle.Flat, - Width = 185, - MinimumWidth = 160, - SortMode = DataGridViewColumnSortMode.NotSortable, - }); - grid.Columns.Add(new StableModernSelectorComboBoxColumn - { - Name = OverlayScopeColumnName, - HeaderText = "OVERLAY SCOPE", - DisplayStyle = DataGridViewComboBoxDisplayStyle.ComboBox, - FlatStyle = FlatStyle.Flat, - Width = 170, - MinimumWidth = 150, - SortMode = DataGridViewColumnSortMode.NotSortable, - }); - grid.Columns.Add(FormPresentation.CreateReadOnlyTextColumn( - ExecutableColumnName, - "EXECUTABLE", - 155)); - grid.Columns.Add(FormPresentation.CreateReadOnlyTextColumn( - PathColumnName, - "FULL PATH", - 220, - fill: true)); + ApplicationAssignmentsGridColumns.AddTo(grid); grid.CellPainting += GridCellPainting; grid.CellValueChanged += GridCellValueChanged; - grid.CurrentCellDirtyStateChanged += GridCurrentCellDirtyStateChanged; + grid.CurrentCellDirtyStateChanged += + GridCurrentCellDirtyStateChanged; grid.SelectionChanged += (_, _) => { if (!_binding) { - SelectedApplicationChanged?.Invoke(this, EventArgs.Empty); + SelectedApplicationChanged?.Invoke( + this, + EventArgs.Empty); } }; grid.DataError += GridDataError; @@ -211,69 +147,46 @@ private void AddRow( } row.Selected = true; - _grid.CurrentCell = row.Cells[ApplicationColumnName]; + _grid.CurrentCell = + row.Cells[ + ApplicationAssignmentsGridColumns + .ApplicationColumnName]; } private static void WriteRow( DataGridViewRow row, ApplicationAssignmentRow assignment) { - row.Cells[EnabledColumnName].Value = assignment.Enabled; - row.Cells[ApplicationColumnName].Value = assignment.DisplayName; - row.Cells[VisualProfileColumnName].Value = assignment.VisualProfileId; - row.Cells[MenuVisualProfileColumnName].Value = + row.Cells[ + ApplicationAssignmentsGridColumns + .EnabledColumnName].Value = + assignment.Enabled; + row.Cells[ + ApplicationAssignmentsGridColumns + .ApplicationColumnName].Value = + assignment.DisplayName; + row.Cells[ + VisualProfileColumnName].Value = + assignment.VisualProfileId; + row.Cells[ + MenuVisualProfileColumnName].Value = assignment.MenuVisualProfileSelectorId; - row.Cells[OverlayScopeColumnName].Value = assignment.OverlayScopeId; - row.Cells[ExecutableColumnName].Value = assignment.ExecutableName; - row.Cells[PathColumnName].Value = assignment.ExecutablePath; + row.Cells[ + OverlayScopeColumnName].Value = + assignment.OverlayScopeId; + row.Cells[ + ApplicationAssignmentsGridColumns + .ExecutableColumnName].Value = + assignment.ExecutableName; + row.Cells[ + ApplicationAssignmentsGridColumns + .PathColumnName].Value = + assignment.ExecutablePath; row.Tag = assignment.ExecutablePath; } - private void SetVisualProfiles(IReadOnlyList profiles) - { - if (_grid.Columns[VisualProfileColumnName] is - StableModernSelectorComboBoxColumn column) - { - column.SetProfiles(profiles); - } - } - - private void SetMenuVisualProfiles( - IReadOnlyList profiles) - { - if (_grid.Columns[MenuVisualProfileColumnName] is not - StableModernSelectorComboBoxColumn column) - { - return; - } - - column.SetOptions( - new[] - { - new ModernSelectorOption( - ApplicationMenuProfilePolicy.InheritSelectorId, - ApplicationMenuProfilePolicy.InheritDisplayName), - }.Concat(profiles.Select(profile => - new ModernSelectorOption( - profile.Id, - profile.Name)))); - } - - private void SetOverlayScopes() - { - if (_grid.Columns[OverlayScopeColumnName] is not - StableModernSelectorComboBoxColumn column) - { - return; - } - - column.SetOptions(OverlayScopePolicy.All.Select(scope => - new ModernSelectorOption( - OverlayScopePolicy.ToId(scope), - OverlayScopePolicy.GetDisplayName(scope)))); - } - - private DataGridViewRow? FindRow(string executablePath) + private DataGridViewRow? FindRow( + string executablePath) { return _grid.Rows .Cast() @@ -297,7 +210,8 @@ private void GridCurrentCellDirtyStateChanged( { if (_grid.IsCurrentCellDirty) { - _grid.CommitEdit(DataGridViewDataErrorContexts.Commit); + _grid.CommitEdit( + DataGridViewDataErrorContexts.Commit); } } @@ -305,7 +219,9 @@ private void GridCellValueChanged( object? sender, DataGridViewCellEventArgs eventArgs) { - if (_binding || eventArgs.RowIndex < 0 || eventArgs.ColumnIndex < 0) + if (_binding || + eventArgs.RowIndex < 0 || + eventArgs.ColumnIndex < 0) { return; } @@ -316,39 +232,16 @@ private void GridCellValueChanged( return; } - var columnName = _grid.Columns[eventArgs.ColumnIndex].Name; - if (columnName == EnabledColumnName && - row.Cells[eventArgs.ColumnIndex].Value is bool enabled) - { - AssignmentChanged?.Invoke( - new ApplicationAssignmentChange.Enabled( - executablePath, - enabled)); - } - else if (columnName == VisualProfileColumnName && - row.Cells[eventArgs.ColumnIndex].Value is string profileId) - { - AssignmentChanged?.Invoke( - new ApplicationAssignmentChange.VisualProfile( - executablePath, - profileId)); - } - else if (columnName == MenuVisualProfileColumnName && - row.Cells[eventArgs.ColumnIndex].Value is string menuProfileId) - { - AssignmentChanged?.Invoke( - new ApplicationAssignmentChange.MenuVisualProfile( - executablePath, - ApplicationMenuProfilePolicy.FromSelectorId( - menuProfileId))); - } - else if (columnName == OverlayScopeColumnName && - row.Cells[eventArgs.ColumnIndex].Value is string scopeId) + var columnName = + _grid.Columns[eventArgs.ColumnIndex].Name; + var change = + ApplicationAssignmentCellChangeMapper.Map( + executablePath, + columnName, + row.Cells[eventArgs.ColumnIndex].Value); + if (change is not null) { - AssignmentChanged?.Invoke( - new ApplicationAssignmentChange.OverlayScope( - executablePath, - OverlayScopePolicy.ParseRequired(scopeId))); + AssignmentChanged?.Invoke(change); } } @@ -363,10 +256,12 @@ private static void GridDataError( var executablePath = GetExecutablePath( grid, eventArgs.RowIndex); - var recovered = IsExpectedSelectorDataError( - eventArgs.Exception, - eventArgs.Context, - columnName); + var recovered = + ApplicationAssignmentSelectorErrorPolicy + .IsRecoverable( + eventArgs.Exception, + eventArgs.Context, + columnName); Diagnostics.Report( nameof(ApplicationAssignmentsGrid), @@ -377,72 +272,19 @@ private static void GridDataError( recovered ? DiagnosticFailurePolicy.Recovered : DiagnosticFailurePolicy.None, - CreateDataErrorDiagnostic( - eventArgs.Exception, - eventArgs.Context, - eventArgs.RowIndex, - eventArgs.ColumnIndex, - columnName, - executablePath, - recovered), + ApplicationAssignmentSelectorErrorPolicy + .CreateDiagnostic( + eventArgs.Exception, + eventArgs.Context, + eventArgs.RowIndex, + eventArgs.ColumnIndex, + columnName, + executablePath, + recovered), eventArgs.Exception); eventArgs.ThrowException = !recovered; } - internal static bool IsExpectedSelectorDataError( - Exception? exception, - DataGridViewDataErrorContexts context, - string? columnName) - { - if (exception is not ArgumentException || - !IsSelectorColumn(columnName)) - { - return false; - } - - var recoverableContext = - context & RecoverableSelectorContexts; - var unexpectedContext = - context & ~RecoverableSelectorContexts; - return recoverableContext != 0 && - unexpectedContext == 0; - } - - internal static string CreateDataErrorDiagnostic( - Exception? exception, - DataGridViewDataErrorContexts context, - int rowIndex, - int columnIndex, - string? columnName, - string? executablePath, - bool recovered) - { - return - $"SightAdapt grid data error; recovered={recovered}; " + - $"row={rowIndex}; column={columnIndex}; " + - $"columnName={columnName ?? ""}; " + - $"executablePath={executablePath ?? ""}; " + - $"context={context}; " + - $"exception={exception?.ToString() ?? ""}"; - } - - private static bool IsSelectorColumn( - string? columnName) - { - return string.Equals( - columnName, - VisualProfileColumnName, - StringComparison.Ordinal) || - string.Equals( - columnName, - MenuVisualProfileColumnName, - StringComparison.Ordinal) || - string.Equals( - columnName, - OverlayScopeColumnName, - StringComparison.Ordinal); - } - private static string? GetColumnName( DataGridView? grid, int columnIndex) @@ -475,7 +317,8 @@ private static void GridCellPainting( eventArgs.ColumnIndex < 0 || !string.Equals( grid.Columns[eventArgs.ColumnIndex].Name, - EnabledColumnName, + ApplicationAssignmentsGridColumns + .EnabledColumnName, StringComparison.Ordinal)) { return; @@ -487,40 +330,23 @@ private static void GridCellPainting( return; } + var selected = + (eventArgs.State & + DataGridViewElementStates.Selected) != 0; eventArgs.PaintBackground( eventArgs.CellBounds, - (eventArgs.State & DataGridViewElementStates.Selected) != 0); - - var enabled = eventArgs.FormattedValue is true; - const int diameter = 15; - var bounds = new Rectangle( - eventArgs.CellBounds.Left + - (eventArgs.CellBounds.Width - diameter) / 2, - eventArgs.CellBounds.Top + - (eventArgs.CellBounds.Height - diameter) / 2, - diameter, - diameter); - - graphics.SmoothingMode = SmoothingMode.AntiAlias; - using var fill = new SolidBrush( - enabled ? AppTheme.Success : AppTheme.Surface); - using var border = new Pen( - enabled ? AppTheme.Success : AppTheme.TextMuted, - enabled ? 1.5f : 1.2f); - graphics.FillEllipse(fill, bounds); - graphics.DrawEllipse(border, bounds); - - if ((eventArgs.State & DataGridViewElementStates.Selected) != 0 && - grid.CurrentCellAddress.X == eventArgs.ColumnIndex && - grid.CurrentCellAddress.Y == eventArgs.RowIndex) - { - var focusBounds = Rectangle.Inflate(bounds, 5, 5); - ControlPaint.DrawFocusRectangle( - graphics, - focusBounds, - AppTheme.TextPrimary, - AppTheme.Selection); - } + selected); + + ApplicationAssignmentEnabledCellRenderer.Paint( + graphics, + eventArgs.CellBounds, + new EnabledCellRenderState( + eventArgs.FormattedValue is true, + selected, + grid.CurrentCellAddress.X == + eventArgs.ColumnIndex && + grid.CurrentCellAddress.Y == + eventArgs.RowIndex)); eventArgs.Handled = true; } @@ -540,6 +366,4 @@ private static Label CreateEmptyStateLabel() Visible = false, }; } - - } diff --git a/src/SightAdapt/ApplicationAssignmentsGridPolicies.cs b/src/SightAdapt/ApplicationAssignmentsGridPolicies.cs new file mode 100644 index 00000000..0eab6437 --- /dev/null +++ b/src/SightAdapt/ApplicationAssignmentsGridPolicies.cs @@ -0,0 +1,309 @@ +using System.Drawing.Drawing2D; + +namespace SightAdapt; + +internal static class ApplicationAssignmentsGridColumns +{ + public const string EnabledColumnName = "Enabled"; + public const string ApplicationColumnName = "Application"; + public const string VisualProfileColumnName = "VisualProfile"; + public const string MenuVisualProfileColumnName = + "MenuVisualProfile"; + public const string OverlayScopeColumnName = "OverlayScope"; + public const string ExecutableColumnName = "Executable"; + public const string PathColumnName = "Path"; + + public static void AddTo(DataGridView grid) + { + ArgumentNullException.ThrowIfNull(grid); + + var enabled = new DataGridViewCheckBoxColumn + { + Name = EnabledColumnName, + HeaderText = "ACTIVE", + Width = 92, + MinimumWidth = 92, + Resizable = DataGridViewTriState.False, + FlatStyle = FlatStyle.Flat, + SortMode = DataGridViewColumnSortMode.NotSortable, + }; + enabled.HeaderCell.Style.Alignment = + DataGridViewContentAlignment.MiddleCenter; + enabled.DefaultCellStyle.Alignment = + DataGridViewContentAlignment.MiddleCenter; + enabled.DefaultCellStyle.Padding = Padding.Empty; + + grid.Columns.Add(enabled); + grid.Columns.Add(FormPresentation.CreateReadOnlyTextColumn( + ApplicationColumnName, + "APPLICATION", + 205)); + grid.Columns.Add(CreateSelector( + VisualProfileColumnName, + "VISUAL PROFILE", + 185, + 160)); + grid.Columns.Add(CreateSelector( + MenuVisualProfileColumnName, + "MENU PROFILE", + 185, + 160)); + grid.Columns.Add(CreateSelector( + OverlayScopeColumnName, + "OVERLAY SCOPE", + 170, + 150)); + grid.Columns.Add(FormPresentation.CreateReadOnlyTextColumn( + ExecutableColumnName, + "EXECUTABLE", + 155)); + grid.Columns.Add(FormPresentation.CreateReadOnlyTextColumn( + PathColumnName, + "FULL PATH", + 220, + fill: true)); + } + + public static void SetSelectorOptions( + DataGridView grid, + IReadOnlyList profiles) + { + ArgumentNullException.ThrowIfNull(grid); + ArgumentNullException.ThrowIfNull(profiles); + + Selector(grid, VisualProfileColumnName) + ?.SetProfiles(profiles); + + Selector(grid, MenuVisualProfileColumnName) + ?.SetOptions( + new[] + { + new ModernSelectorOption( + ApplicationMenuProfilePolicy + .InheritSelectorId, + ApplicationMenuProfilePolicy + .InheritDisplayName), + }.Concat(profiles.Select(profile => + new ModernSelectorOption( + profile.Id, + profile.Name)))); + + Selector(grid, OverlayScopeColumnName) + ?.SetOptions( + OverlayScopePolicy.All.Select(scope => + new ModernSelectorOption( + OverlayScopePolicy.ToId(scope), + OverlayScopePolicy.GetDisplayName( + scope)))); + } + + public static bool IsSelectorColumn( + string? columnName) + { + return string.Equals( + columnName, + VisualProfileColumnName, + StringComparison.Ordinal) || + string.Equals( + columnName, + MenuVisualProfileColumnName, + StringComparison.Ordinal) || + string.Equals( + columnName, + OverlayScopeColumnName, + StringComparison.Ordinal); + } + + private static StableModernSelectorComboBoxColumn + CreateSelector( + string name, + string header, + int width, + int minimumWidth) + { + return new StableModernSelectorComboBoxColumn + { + Name = name, + HeaderText = header, + DisplayStyle = + DataGridViewComboBoxDisplayStyle.ComboBox, + FlatStyle = FlatStyle.Flat, + Width = width, + MinimumWidth = minimumWidth, + SortMode = + DataGridViewColumnSortMode.NotSortable, + }; + } + + private static StableModernSelectorComboBoxColumn? + Selector( + DataGridView grid, + string name) + { + return grid.Columns[name] as + StableModernSelectorComboBoxColumn; + } +} + +internal static class ApplicationAssignmentCellChangeMapper +{ + public static ApplicationAssignmentChange? Map( + string executablePath, + string columnName, + object? value) + { + ArgumentException.ThrowIfNullOrWhiteSpace( + executablePath); + ArgumentException.ThrowIfNullOrWhiteSpace( + columnName); + + if (string.Equals( + columnName, + ApplicationAssignmentsGridColumns.EnabledColumnName, + StringComparison.Ordinal) && + value is bool enabled) + { + return new ApplicationAssignmentChange.Enabled( + executablePath, + enabled); + } + + if (string.Equals( + columnName, + ApplicationAssignmentsGridColumns.VisualProfileColumnName, + StringComparison.Ordinal) && + value is string profileId) + { + return new ApplicationAssignmentChange.VisualProfile( + executablePath, + profileId); + } + + if (string.Equals( + columnName, + ApplicationAssignmentsGridColumns.MenuVisualProfileColumnName, + StringComparison.Ordinal) && + value is string menuProfileId) + { + return new ApplicationAssignmentChange.MenuVisualProfile( + executablePath, + ApplicationMenuProfilePolicy.FromSelectorId( + menuProfileId)); + } + + if (string.Equals( + columnName, + ApplicationAssignmentsGridColumns.OverlayScopeColumnName, + StringComparison.Ordinal) && + value is string scopeId) + { + return new ApplicationAssignmentChange.OverlayScope( + executablePath, + OverlayScopePolicy.ParseRequired(scopeId)); + } + + return null; + } +} + +internal static class ApplicationAssignmentSelectorErrorPolicy +{ + private const DataGridViewDataErrorContexts + RecoverableContexts = + DataGridViewDataErrorContexts.Formatting | + DataGridViewDataErrorContexts.Display | + DataGridViewDataErrorContexts.PreferredSize | + DataGridViewDataErrorContexts + .InitialValueRestoration; + + public static bool IsRecoverable( + Exception? exception, + DataGridViewDataErrorContexts context, + string? columnName) + { + if (exception is not ArgumentException || + !ApplicationAssignmentsGridColumns + .IsSelectorColumn(columnName)) + { + return false; + } + + var recoverable = + context & RecoverableContexts; + var unexpected = + context & ~RecoverableContexts; + return recoverable != 0 && + unexpected == 0; + } + + public static string CreateDiagnostic( + Exception? exception, + DataGridViewDataErrorContexts context, + int rowIndex, + int columnIndex, + string? columnName, + string? executablePath, + bool recovered) + { + return + $"SightAdapt grid data error; recovered={recovered}; " + + $"row={rowIndex}; column={columnIndex}; " + + $"columnName={columnName ?? ""}; " + + $"executablePath={executablePath ?? ""}; " + + $"context={context}; " + + $"exception={exception?.ToString() ?? ""}"; + } +} + +internal readonly record struct EnabledCellRenderState( + bool Enabled, + bool Selected, + bool Focused); + +internal static class ApplicationAssignmentEnabledCellRenderer +{ + private const int IndicatorDiameter = 15; + + public static Rectangle GetIndicatorBounds( + Rectangle cellBounds) + { + return new Rectangle( + cellBounds.Left + + (cellBounds.Width - IndicatorDiameter) / 2, + cellBounds.Top + + (cellBounds.Height - IndicatorDiameter) / 2, + IndicatorDiameter, + IndicatorDiameter); + } + + public static void Paint( + Graphics graphics, + Rectangle cellBounds, + EnabledCellRenderState state) + { + ArgumentNullException.ThrowIfNull(graphics); + + var bounds = GetIndicatorBounds(cellBounds); + graphics.SmoothingMode = SmoothingMode.AntiAlias; + using var fill = new SolidBrush( + state.Enabled + ? AppTheme.Success + : AppTheme.Surface); + using var border = new Pen( + state.Enabled + ? AppTheme.Success + : AppTheme.TextMuted, + state.Enabled ? 1.5f : 1.2f); + graphics.FillEllipse(fill, bounds); + graphics.DrawEllipse(border, bounds); + + if (state.Selected && state.Focused) + { + ControlPaint.DrawFocusRectangle( + graphics, + Rectangle.Inflate(bounds, 5, 5), + AppTheme.TextPrimary, + AppTheme.Selection); + } + } +} diff --git a/tests/SightAdapt.Tests/ApplicationAssignmentsGridDataErrorTests.cs b/tests/SightAdapt.Tests/ApplicationAssignmentsGridDataErrorTests.cs index 58735e98..3b0e82ef 100644 --- a/tests/SightAdapt.Tests/ApplicationAssignmentsGridDataErrorTests.cs +++ b/tests/SightAdapt.Tests/ApplicationAssignmentsGridDataErrorTests.cs @@ -15,7 +15,7 @@ public void SelectorPresentationArgumentErrorIsRecoverable( string columnName) { Assert.IsTrue( - ApplicationAssignmentsGrid.IsExpectedSelectorDataError( + ApplicationAssignmentSelectorErrorPolicy.IsRecoverable( new ArgumentException("Transient selector value"), DataGridViewDataErrorContexts.Formatting | DataGridViewDataErrorContexts.Display, @@ -26,7 +26,7 @@ public void SelectorPresentationArgumentErrorIsRecoverable( public void CommitErrorIsNotClassifiedAsPresentationRace() { Assert.IsFalse( - ApplicationAssignmentsGrid.IsExpectedSelectorDataError( + ApplicationAssignmentSelectorErrorPolicy.IsRecoverable( new ArgumentException("Invalid committed value"), DataGridViewDataErrorContexts.Commit, ApplicationAssignmentsGrid.VisualProfileColumnName)); @@ -36,7 +36,7 @@ public void CommitErrorIsNotClassifiedAsPresentationRace() public void InvalidOperationIsNeverBlanketSuppressed() { Assert.IsFalse( - ApplicationAssignmentsGrid.IsExpectedSelectorDataError( + ApplicationAssignmentSelectorErrorPolicy.IsRecoverable( new InvalidOperationException("Broken selector"), DataGridViewDataErrorContexts.Formatting, ApplicationAssignmentsGrid.VisualProfileColumnName)); @@ -46,7 +46,7 @@ public void InvalidOperationIsNeverBlanketSuppressed() public void NonSelectorArgumentErrorIsNotSuppressed() { Assert.IsFalse( - ApplicationAssignmentsGrid.IsExpectedSelectorDataError( + ApplicationAssignmentSelectorErrorPolicy.IsRecoverable( new ArgumentException("Invalid enabled value"), DataGridViewDataErrorContexts.Formatting, "Enabled")); @@ -56,7 +56,7 @@ public void NonSelectorArgumentErrorIsNotSuppressed() public void DiagnosticContainsRowColumnPathAndContext() { var diagnostic = - ApplicationAssignmentsGrid.CreateDataErrorDiagnostic( + ApplicationAssignmentSelectorErrorPolicy.CreateDiagnostic( new ArgumentException("Transient selector value"), DataGridViewDataErrorContexts.Display, rowIndex: 4, diff --git a/tests/SightAdapt.Tests/ApplicationAssignmentsGridPolicyTests.cs b/tests/SightAdapt.Tests/ApplicationAssignmentsGridPolicyTests.cs new file mode 100644 index 00000000..fb3fc93d --- /dev/null +++ b/tests/SightAdapt.Tests/ApplicationAssignmentsGridPolicyTests.cs @@ -0,0 +1,125 @@ +using System.Drawing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SightAdapt.Tests; + +[TestClass] +public sealed class ApplicationAssignmentsGridPolicyTests +{ + private const string PathValue = + @"C:\Apps\reader.exe"; + + [TestMethod] + public void CellChangeMapperCreatesTypedChangesWithoutWinFormsEvents() + { + var enabled = + ApplicationAssignmentCellChangeMapper.Map( + PathValue, + ApplicationAssignmentsGridColumns + .EnabledColumnName, + false); + var visual = + ApplicationAssignmentCellChangeMapper.Map( + PathValue, + ApplicationAssignmentsGridColumns + .VisualProfileColumnName, + "profile-id"); + var menu = + ApplicationAssignmentCellChangeMapper.Map( + PathValue, + ApplicationAssignmentsGridColumns + .MenuVisualProfileColumnName, + ApplicationMenuProfilePolicy + .InheritSelectorId); + var scope = + ApplicationAssignmentCellChangeMapper.Map( + PathValue, + ApplicationAssignmentsGridColumns + .OverlayScopeColumnName, + "screen"); + + Assert.AreEqual( + new ApplicationAssignmentChange.Enabled( + PathValue, + false), + enabled); + Assert.AreEqual( + new ApplicationAssignmentChange.VisualProfile( + PathValue, + "profile-id"), + visual); + Assert.AreEqual( + new ApplicationAssignmentChange.MenuVisualProfile( + PathValue, + null), + menu); + Assert.AreEqual( + new ApplicationAssignmentChange.OverlayScope( + PathValue, + OverlayScope.Screen), + scope); + } + + [TestMethod] + public void CellChangeMapperIgnoresReadOnlyColumn() + { + Assert.IsNull( + ApplicationAssignmentCellChangeMapper.Map( + PathValue, + ApplicationAssignmentsGridColumns + .ApplicationColumnName, + "Reader")); + } + + [TestMethod] + public void EnabledIndicatorGeometryIsCenteredAndStable() + { + var cell = new Rectangle( + x: 10, + y: 20, + width: 92, + height: 40); + + var indicator = + ApplicationAssignmentEnabledCellRenderer + .GetIndicatorBounds(cell); + + Assert.AreEqual(15, indicator.Width); + Assert.AreEqual(15, indicator.Height); + Assert.AreEqual( + cell.Left + + (cell.Width - indicator.Width) / 2, + indicator.Left); + Assert.AreEqual( + cell.Top + + (cell.Height - indicator.Height) / 2, + indicator.Top); + } + + [TestMethod] + public void ColumnPolicyCreatesExpectedSelectorColumns() + { + StaTest.Run(() => + { + using var grid = new DataGridView(); + ApplicationAssignmentsGridColumns.AddTo(grid); + + Assert.AreEqual(7, grid.Columns.Count); + Assert.IsInstanceOfType< + StableModernSelectorComboBoxColumn>( + grid.Columns[ + ApplicationAssignmentsGridColumns + .VisualProfileColumnName]); + Assert.IsInstanceOfType< + StableModernSelectorComboBoxColumn>( + grid.Columns[ + ApplicationAssignmentsGridColumns + .MenuVisualProfileColumnName]); + Assert.IsInstanceOfType< + StableModernSelectorComboBoxColumn>( + grid.Columns[ + ApplicationAssignmentsGridColumns + .OverlayScopeColumnName]); + }); + } +}