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");