diff --git a/App.xaml.cs b/App.xaml.cs index 19e1aaac3..a0a3fa053 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -37,9 +37,9 @@ protected override void OnStartup(StartupEventArgs e) return; } - // P2 installs one calm industrial visual system before StartupUri materializes. + // P0 installs one cool-blue/white workstation system before StartupUri materializes. // Existing XAML keeps its semantic resource keys while the overlay replaces - // glare-heavy white/blue surfaces with Blue Steel + Light Greige equivalents. + // warm legacy surfaces with shared blue-white semantic resources. InstallP2BlueSteelGreigeTheme(); // Keep every IED card on one reusable template key while replacing the @@ -92,6 +92,8 @@ private void AppWindow_Loaded(object sender, RoutedEventArgs e) if (_styledWindows.TryGetValue(window, out _)) return; + // Initial placement is applied ONCE before first paint; never on activation or resize. + WorkstationWindowStartupBounds.Apply(window); P2BlueSteelGreigeUx.ApplyToOpenWindows(this); _styledWindows.Add(window, new object()); diff --git a/IoListTestingWindow.xaml b/IoListTestingWindow.xaml index 02464b849..f1d6ab1b1 100644 --- a/IoListTestingWindow.xaml +++ b/IoListTestingWindow.xaml @@ -4,8 +4,8 @@ xmlns:io="clr-namespace:ArIED61850Tester.Models.IoTesting" xmlns:local="clr-namespace:ArIED61850Tester" Title="ARSAS - IO List Testing" - Height="900" Width="1500" MinHeight="760" MinWidth="1180" - WindowStartupLocation="CenterOwner" WindowState="Maximized" + Height="820" Width="1440" MinHeight="640" MinWidth="1100" + WindowStartupLocation="CenterOwner" WindowState="Normal" Background="{StaticResource AppBackgroundGradient}" FontFamily="{StaticResource AppFontFamily}" UseLayoutRounding="True" SnapsToDevicePixels="True" diff --git a/MainWindow.xaml b/MainWindow.xaml index 2c8e00b73..368bae1e1 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -3,8 +3,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:ArIED61850Tester.Views" Title="ARSAS - Smart IEC 61850 Communication Tester" - Height="900" Width="1480" MinHeight="760" MinWidth="1180" - WindowStartupLocation="CenterScreen" WindowState="Maximized" Background="{StaticResource AppBackgroundGradient}" + Height="840" Width="1440" MinHeight="640" MinWidth="1100" + WindowStartupLocation="CenterScreen" WindowState="Normal" Background="{StaticResource AppBackgroundGradient}" FontFamily="{StaticResource AppFontFamily}" UseLayoutRounding="True" SnapsToDevicePixels="True" TextOptions.TextFormattingMode="Ideal" diff --git a/P2BlueSteelGreigeUx.cs b/P2BlueSteelGreigeUx.cs index 87a504ef1..732aa1a1d 100644 --- a/P2BlueSteelGreigeUx.cs +++ b/P2BlueSteelGreigeUx.cs @@ -7,11 +7,10 @@ namespace ArIED61850Tester; /// -/// Small visual adapter for the P2 Blue Steel + Light Greige theme. +/// Visual adapter for the P0 Blue Steel + Cool Blue workstation system. /// -/// Most ARSAS surfaces already consume application resource keys. A few legacy -/// windows still own local white grid/card styles or hard-coded navigation chrome; -/// this adapter replaces only those visual contracts at runtime. It deliberately +/// Most ARSAS surfaces consume application resource keys. Handle legacy chrome +/// without overwriting scope-specific grid colors, SMV visuals or user window bounds. It deliberately /// does not alter IEC 61850 workflows, bindings, command handlers, or session state. /// internal static class P2BlueSteelGreigeUx @@ -56,10 +55,6 @@ private static void Apply(Window window) private static void ApplyMainWindow(MainWindow window) { - // ARSAS is an engineering workstation: use the available display by default. - if (window.WindowState == WindowState.Normal) - window.WindowState = WindowState.Maximized; - if (window.FindName("WorkflowNavShell") is Border navShell) { navShell.Background = Brush(window, "BlueSteelNavSurface"); @@ -78,15 +73,10 @@ private static void ApplyMainWindow(MainWindow window) if (deviceList != null && Resource(window, "P2MainIedListItemStyle") is Style itemStyle) deviceList.ItemContainerStyle = itemStyle; - foreach (var grid in Descendants(window)) - ApplyEngineeringGrid(window, grid); } private static void ApplyIoFatWindow(IoListTestingWindow window) { - if (window.WindowState != WindowState.Maximized) - window.WindowState = WindowState.Maximized; - var fatIedList = Descendants(window) .FirstOrDefault(list => ReferenceEquals(list.ItemsSource, window.Project.Ieds)); if (fatIedList != null && Resource(window, "P2FatIedListItemStyle") is Style itemStyle) @@ -98,13 +88,13 @@ private static void ApplyIoFatWindow(IoListTestingWindow window) foreach (var grid in Descendants(window)) { - grid.Background = Brush(window, "GreigeSurface"); - grid.BorderBrush = Brush(window, "SteelDividerStrong"); + grid.Background = Brush(window, "PanelSurface"); + grid.BorderBrush = Brush(window, "CoolDivider"); grid.BorderThickness = new Thickness(1); - grid.HorizontalGridLinesBrush = new SolidColorBrush(Color.FromRgb(0xD4, 0xD0, 0xC9)); + grid.HorizontalGridLinesBrush = Brush(window, "CoolDivider"); grid.GridLinesVisibility = DataGridGridLinesVisibility.Horizontal; - grid.RowBackground = Brush(window, "GreigeRowA"); - grid.AlternatingRowBackground = Brush(window, "GreigeRowB"); + grid.RowBackground = Brush(window, "GridRowPrimary"); + grid.AlternatingRowBackground = Brush(window, "GridRowAlternate"); grid.AlternationCount = 2; if (rowStyle != null) @@ -116,19 +106,6 @@ private static void ApplyIoFatWindow(IoListTestingWindow window) } } - private static void ApplyEngineeringGrid(FrameworkElement owner, DataGrid grid) - { - // Preserve explicit command-grid styles. The regular explorer/live tables - // already use ModernDataGrid; setting these semantic surfaces locally keeps - // zebra rows stable even when a legacy window has an old local brush. - if (grid.Style == Resource(owner, "CommandDataGrid")) - return; - - grid.RowBackground = Brush(owner, "GreigeRowA"); - grid.AlternatingRowBackground = Brush(owner, "GreigeRowB"); - grid.AlternationCount = 2; - } - private static object? Resource(FrameworkElement owner, object key) => owner.TryFindResource(key) ?? Application.Current?.TryFindResource(key); diff --git a/P2IndustrialWorkstationUx.cs b/P2IndustrialWorkstationUx.cs index fa558bda2..bdfd8a457 100644 --- a/P2IndustrialWorkstationUx.cs +++ b/P2IndustrialWorkstationUx.cs @@ -408,6 +408,10 @@ private static void ApplyIndustrialGridHeaders(FrameworkElement owner, bool skip { if (skipCommandGrid && grid.Style == commandStyle) continue; + // A tab-specific instrument (e.g. SMV Measurements) owns its explicit + // header style. The global adapter must not override that contract. + if (grid.ReadLocalValue(DataGrid.ColumnHeaderStyleProperty) != DependencyProperty.UnsetValue) + continue; grid.ColumnHeaderStyle = header; } } diff --git a/Resources/P2BlueSteelGreige.xaml b/Resources/P2BlueSteelGreige.xaml index b24959046..42564a713 100644 --- a/Resources/P2BlueSteelGreige.xaml +++ b/Resources/P2BlueSteelGreige.xaml @@ -3,11 +3,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + + #F5F8FD + #F8FBFF + #FFFFFF + #EDF5FF + #E8F2FF + #D7E5F6 + #E4F0FF + #DCEAFF + #28343D #65727B @@ -23,11 +33,11 @@ #185FB8 #DDE8F2 #263F52 - #C8CDD0 - #EEEAE3 - #F3F0EA - #C6CCCF - #9DA9B0 + #D7E5F6 + #F8FBFF + #FFFFFF + #D7E5F6 + #B5CAE5 #3E8E5B #C84C4C #C28A2C @@ -46,26 +56,37 @@ - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -75,18 +96,18 @@ - - + + - - + + - - + + @@ -95,39 +116,39 @@ - - + + - - + + - - + + - + - + - + - - + + - - + + @@ -247,11 +268,11 @@ - - + + - + @@ -267,7 +288,7 @@ - + @@ -425,10 +446,10 @@ - + - + @@ -446,19 +467,19 @@ @@ -466,7 +487,7 @@ diff --git a/Resources/P2IndustrialControls.xaml b/Resources/P2IndustrialControls.xaml index 5bfb3471f..a5f4d238d 100644 --- a/Resources/P2IndustrialControls.xaml +++ b/Resources/P2IndustrialControls.xaml @@ -25,9 +25,9 @@ - - - + + + @@ -57,13 +57,13 @@ - - + + - + @@ -75,9 +75,9 @@ - - - + + + @@ -92,11 +92,11 @@ - - + + - + @@ -107,21 +107,23 @@ - diff --git a/WorkstationWindowStartupBounds.cs b/WorkstationWindowStartupBounds.cs new file mode 100644 index 000000000..4ca349df6 --- /dev/null +++ b/WorkstationWindowStartupBounds.cs @@ -0,0 +1,32 @@ +using System; +using System.Windows; + +namespace ArIED61850Tester; + +/// +/// First-paint-only bounds for the interactive workstation windows. +/// Never intercept user move, resize, maximize or restore. +/// Hidden IO FAT donor windows use Manual placement and remain untouched. +/// +internal static class WorkstationWindowStartupBounds +{ + internal static void Apply(Window window) + { + if (window is not MainWindow && window is not IoListTestingWindow) + return; + + // The embedded FAT donor is intentionally hidden offscreen. + if (window.WindowStartupLocation == WindowStartupLocation.Manual) + return; + + var work = SystemParameters.WorkArea; // WPF device-independent units. + const double inset = 24d; + var maxWidth = Math.Max(window.MinWidth, work.Width - 2d * inset); + var maxHeight = Math.Max(window.MinHeight, work.Height - 2d * inset); + + window.Width = Math.Clamp(window.Width, window.MinWidth, maxWidth); + window.Height = Math.Clamp(window.Height, window.MinHeight, maxHeight); + window.Left = work.Left + Math.Max(0d, (work.Width - window.Width) / 2d); + window.Top = work.Top + Math.Max(0d, (work.Height - window.Height) / 2d); + } +} diff --git a/tests/ARSAS.Tests/P0CoolBlueWorkstationTests.cs b/tests/ARSAS.Tests/P0CoolBlueWorkstationTests.cs new file mode 100644 index 000000000..e476d6764 --- /dev/null +++ b/tests/ARSAS.Tests/P0CoolBlueWorkstationTests.cs @@ -0,0 +1,80 @@ +using System.Xml.Linq; + +namespace ARSAS.Tests; + +/// +/// P0 visual contract: one cool-blue source across engineering grids, and no +/// automatic main-window maximization after the first frame. +/// +public sealed class P0CoolBlueWorkstationTests +{ + [Fact] + public void AllEngineeringGridsShareCoolBlueSemanticResources() + { + var xaml = XDocument.Parse(Read("Resources/P2BlueSteelGreige.xaml")); + XNamespace wpf = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; + XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml"; + + var colors = new Dictionary + { + ["CoolPanelColor"] = "#F8FBFF", + ["CoolAlternateColor"] = "#EDF5FF", + ["CoolHeaderColor"] = "#E8F2FF", + ["CoolBorderColor"] = "#D7E5F6", + ["CoolHoverColor"] = "#E4F0FF" + }; + foreach (var (name, color) in colors) + Assert.Equal(color, xaml.Descendants(wpf + "Color") + .Single(e => (string?)e.Attribute(x + "Key") == name).Value); + + var modern = xaml.Descendants(wpf + "Style") + .Single(e => (string?)e.Attribute(x + "Key") == "ModernDataGrid"); + Assert.Contains(modern.Elements(wpf + "Setter"), e => + (string?)e.Attribute("Property") == "RowBackground" && + (string?)e.Attribute("Value") == "{StaticResource GridRowPrimary}"); + Assert.Contains(modern.Elements(wpf + "Setter"), e => + (string?)e.Attribute("Property") == "AlternatingRowBackground" && + (string?)e.Attribute("Value") == "{StaticResource GridRowAlternate}"); + + var industrial = XDocument.Parse(Read("Resources/P2IndustrialControls.xaml")); + var header = industrial.Descendants(wpf + "Style") + .Single(e => (string?)e.Attribute(x + "Key") == "IndustrialGridHeader"); + Assert.Contains(header.Elements(wpf + "Setter"), e => + (string?)e.Attribute("Property") == "Background" && + (string?)e.Attribute("Value") == "{DynamicResource GridHeaderSurface}"); + } + + [Fact] + public void WindowGeometryIsUserControlledAfterCenteredNormalStartup() + { + foreach (var path in new[] { "MainWindow.xaml", "IoListTestingWindow.xaml" }) + { + var root = XDocument.Parse(Read(path)).Root!; + Assert.Equal("Normal", (string?)root.Attribute("WindowState")); + Assert.InRange(double.Parse(root.Attribute("MinHeight")!.Value), 600d, 700d); + } + var main = XDocument.Parse(Read("MainWindow.xaml")).Root!; + Assert.Equal("CenterScreen", (string?)main.Attribute("WindowStartupLocation")); + + var adapter = Read("P2BlueSteelGreigeUx.cs"); + Assert.DoesNotContain("WindowState.Maximized", adapter, StringComparison.Ordinal); + Assert.DoesNotContain("ApplyEngineeringGrid", adapter, StringComparison.Ordinal); + + var startup = Read("WorkstationWindowStartupBounds.cs"); + Assert.Contains("SystemParameters.WorkArea", startup, StringComparison.Ordinal); + Assert.Contains("WindowStartupLocation.Manual", startup, StringComparison.Ordinal); + Assert.DoesNotContain("SizeChanged +=", startup, StringComparison.Ordinal); + Assert.DoesNotContain("Activated +=", startup, StringComparison.Ordinal); + Assert.Contains("WorkstationWindowStartupBounds.Apply(window);", Read("App.xaml.cs"), StringComparison.Ordinal); + } + + private static string Read(string path) + { + for (DirectoryInfo? directory = new(AppContext.BaseDirectory); directory != null; directory = directory.Parent) + { + var full = Path.Combine(directory.FullName, path); + if (File.Exists(full)) return File.ReadAllText(full); + } + throw new FileNotFoundException(path); + } +} diff --git a/tests/ARSAS.Tests/P2BlueSteelGreigeThemeTests.cs b/tests/ARSAS.Tests/P2BlueSteelGreigeThemeTests.cs index 930897d0e..68a7d4fc7 100644 --- a/tests/ARSAS.Tests/P2BlueSteelGreigeThemeTests.cs +++ b/tests/ARSAS.Tests/P2BlueSteelGreigeThemeTests.cs @@ -5,7 +5,7 @@ namespace ARSAS.Tests; public sealed class P2BlueSteelGreigeThemeTests { [Fact] - public void Theme_UsesBlueSteelShellAndWarmGreigeZebraRows() + public void Theme_UsesBlueSteelShellAndCoolBlueZebraRows() { var source = File.ReadAllText(FindRepoFile("Resources/P2BlueSteelGreige.xaml")); var document = XDocument.Parse(source); @@ -19,9 +19,15 @@ string ResourceValue(string elementName, string key, string attribute) ?? throw new Xunit.Sdk.XunitException($"Missing {key}.{attribute}"); Assert.Equal("#2A465B", ResourceValue("SolidColorBrush", "BlueSteelNavSurface", "Color")); - Assert.Equal("#F3F0EA", ResourceValue("SolidColorBrush", "GreigeRowA", "Color")); - Assert.Equal("#ECE8E1", ResourceValue("SolidColorBrush", "GreigeRowB", "Color")); - Assert.Equal("#E1DDD5", ResourceValue("SolidColorBrush", "GreigeHeader", "Color")); + Assert.Equal("#F8FBFF", document.Descendants(presentation + "Color") + .Single(node => (string?)node.Attribute(x + "Key") == "CoolPanelColor").Value); + Assert.Equal("#EDF5FF", document.Descendants(presentation + "Color") + .Single(node => (string?)node.Attribute(x + "Key") == "CoolAlternateColor").Value); + Assert.Equal("#E8F2FF", document.Descendants(presentation + "Color") + .Single(node => (string?)node.Attribute(x + "Key") == "CoolHeaderColor").Value); + Assert.Equal("{StaticResource CoolPanelColor}", ResourceValue("SolidColorBrush", "GreigeRowA", "Color")); + Assert.Equal("{StaticResource CoolAlternateColor}", ResourceValue("SolidColorBrush", "GreigeRowB", "Color")); + Assert.Equal("{StaticResource CoolHeaderColor}", ResourceValue("SolidColorBrush", "GreigeHeader", "Color")); var appBackground = document.Descendants(presentation + "LinearGradientBrush") .Single(node => (string?)node.Attribute(x + "Key") == "AppBackgroundGradient") @@ -33,9 +39,9 @@ string ResourceValue(string elementName, string key, string attribute) .Single(node => (string?)node.Attribute(x + "Key") == "ModernDataGrid") .ToString(); Assert.Contains("RowBackground", modernGrid, StringComparison.Ordinal); - Assert.Contains("GreigeRowA", modernGrid, StringComparison.Ordinal); + Assert.Contains("GridRowPrimary", modernGrid, StringComparison.Ordinal); Assert.Contains("AlternatingRowBackground", modernGrid, StringComparison.Ordinal); - Assert.Contains("GreigeRowB", modernGrid, StringComparison.Ordinal); + Assert.Contains("GridRowAlternate", modernGrid, StringComparison.Ordinal); Assert.Contains("AlternationCount", modernGrid, StringComparison.Ordinal); } @@ -53,16 +59,17 @@ public void Startup_InstallsThemeBeforeRelayFasciaAndAppliesWindowAdapter() } [Fact] - public void RuntimeAdapter_KeepsMaximizedWorkstationAndFatZebraRows() + public void RuntimeAdapter_PreservesWindowGeometryAndBlueGridRows() { var source = File.ReadAllText(FindRepoFile("P2BlueSteelGreigeUx.cs")); - Assert.Contains("window.WindowState = WindowState.Maximized", source, StringComparison.Ordinal); + Assert.DoesNotContain("WindowState.Maximized", source, StringComparison.Ordinal); + Assert.DoesNotContain("ApplyEngineeringGrid", source, StringComparison.Ordinal); Assert.Contains("P2MainIedListItemStyle", source, StringComparison.Ordinal); Assert.Contains("P2FatIedListItemStyle", source, StringComparison.Ordinal); Assert.Contains("P2FatDataGridRow", source, StringComparison.Ordinal); - Assert.Contains("GreigeRowA", source, StringComparison.Ordinal); - Assert.Contains("GreigeRowB", source, StringComparison.Ordinal); + Assert.Contains("GridRowPrimary", source, StringComparison.Ordinal); + Assert.Contains("GridRowAlternate", source, StringComparison.Ordinal); Assert.Contains("AlternationCount = 2", source, StringComparison.Ordinal); } diff --git a/tests/ARSAS.Tests/StartupVisualStabilityRegressionTests.cs b/tests/ARSAS.Tests/StartupVisualStabilityRegressionTests.cs index 12e1e92fe..5de6f7953 100644 --- a/tests/ARSAS.Tests/StartupVisualStabilityRegressionTests.cs +++ b/tests/ARSAS.Tests/StartupVisualStabilityRegressionTests.cs @@ -44,8 +44,11 @@ public void WindowAdapter_IsOnceOnlyAndNeverOverridesCanonicalInterFont() [Fact] public void ResponsiveNavigation_OwnsShellHeight_WithoutLateClobber() { + var app = Read("App.xaml.cs"); var main = XDocument.Parse(Read("MainWindow.xaml")); - Assert.Equal("Maximized", (string?)main.Root?.Attribute("WindowState")); + Assert.Equal("Normal", (string?)main.Root?.Attribute("WindowState")); + Assert.Equal("CenterScreen", (string?)main.Root?.Attribute("WindowStartupLocation")); + Assert.Contains("WorkstationWindowStartupBounds.Apply(window);", app, StringComparison.Ordinal); var responsive = Read("MainWindow.NavigationLayoutFix.cs"); var p2 = Read("P2IndustrialWorkstationUx.cs");