diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ea37a8b..e7e35b69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,32 @@ jobs: shell: powershell run: .\ArIED61850Tester\scripts\verify-source-clean.ps1 + - name: Verify premium P0 UX invariants + shell: powershell + run: | + $main = Get-Content .\ArIED61850Tester\MainWindow.xaml -Raw + $save = Get-Content .\ArIED61850Tester\SaveSclWindow.xaml -Raw + if ($save -notmatch 'x:Name="EditionIndicator"' -or + $save -notmatch 'Content="Edition 2"' -or + $save -notmatch 'Content="Edition 1"') { + throw "Save SCL segmented Edition 2 / Edition 1 selector was not found." + } + if ($save -match '#2563EB #1D4ED8 #EAF1FF - #F5F7FB + #EEF3F9 #E5EAF3 @@ -23,15 +23,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M8,5 L19,12 L8,19 Z + M6,6 H18 V18 H6 Z + M12,20 H21 M16.5,3.5 A2.12,2.12 0 0 1 19.5,6.5 L7,19 L3,20 L4,16 Z + M19,21 H5 A2,2 0 0 1 3,19 V5 A2,2 0 0 1 5,3 H16 L21,8 V19 A2,2 0 0 1 19,21 Z M17,21 V13 H7 V21 M7,3 V8 H15 + M18,6 L6,18 M6,6 L18,18 + M20,11 A8,8 0 1 0 18.3,16 M20,4 V11 H13 + M3,7 H9 L11,9 H21 L19,20 H3 Z M3,7 V5 H9 L11,7 + M14,2 V8 H20 M14,2 H6 A2,2 0 0 0 4,4 V20 A2,2 0 0 0 6,22 H18 A2,2 0 0 0 20,20 V8 Z M9,15 H15 M12,12 L15,15 L12,18 + M14,2 V8 H20 M14,2 H6 A2,2 0 0 0 4,4 V20 A2,2 0 0 0 6,22 H18 A2,2 0 0 0 20,20 V8 Z M10,15 H16 M13,12 L16,15 L13,18 + M12,5 V19 M5,12 H19 + + + @@ -78,7 +140,7 @@ diff --git a/ArIED61850Tester.csproj b/ArIED61850Tester.csproj index 61bdd39f..286f610a 100644 --- a/ArIED61850Tester.csproj +++ b/ArIED61850Tester.csproj @@ -14,9 +14,9 @@ ArIED 61850 ArIED 61850 — IEC 61850 Engineering Workstation Windows IEC 61850 engineering workstation for SCL-assisted project setup, interoperable Edition 1/2 SCL export, live MMS model discovery, independent multi-IED monitoring, reporting diagnostics, sequence of events, and guarded Smart Control. - 1.6.10 - 1.6.10.0 - 1.6.10.0 + 1.6.11 + 1.6.11.0 + 1.6.11.0 iec61850;iec-61850;mms;scl;ied-explorer;multi-ied-monitoring;relay-testing;substation-automation;digital-substation;report-control-block;sequence-of-events;smart-control;wpf;dotnet GPL-3.0-or-later $(ARIEC61850_PROJECT) diff --git a/MainWindow.xaml b/MainWindow.xaml index a1e9a31d..beaa31a3 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ArIED 61850 — Smart IED Explorer & Monitor" Height="900" Width="1480" MinHeight="760" MinWidth="1180" - WindowStartupLocation="CenterScreen" Background="{StaticResource Bg}" + WindowStartupLocation="CenterScreen" Background="{StaticResource AppBackgroundGradient}" FontFamily="Aptos, Segoe UI Variable Text, Segoe UI, Calibri" Icon="Assets/app-icon.ico" Closing="Window_Closing"> @@ -97,14 +97,14 @@ @@ -130,10 +130,8 @@ Style="{StaticResource SoftButton}" Padding="8,7" FontSize="11.5" ToolTip="Fast-connect every saved or SCL-imported IED and start live monitoring for saved selections"> - - - - + + @@ -143,10 +141,8 @@ Style="{StaticResource SoftButton}" Padding="7,7" FontSize="11.2" ToolTip="Import one or more IED endpoints from SCD, CID, ICD, IID, SSD, or XML SCL files"> - - - - + + @@ -156,7 +152,9 @@ Style="{StaticResource PrimaryButton}" Padding="7,7" FontSize="11.2" ToolTip="Add an IEC 61850 IED by IP address"> - + + + @@ -176,16 +174,22 @@ - + + + - - + + - + + + @@ -214,37 +218,33 @@ - - + + + TextTrimming="CharacterEllipsis" TextAlignment="Center" HorizontalAlignment="Stretch"/> + HorizontalAlignment="Center" Margin="0,5,25,0" Panel.ZIndex="10"> @@ -253,9 +253,7 @@ IsEnabled="{Binding IsActionEnabled}" ToolTip="{Binding RemoveToolTip}" Width="25" Height="25" Margin="0,-4,-4,0" HorizontalAlignment="Right" VerticalAlignment="Top" Panel.ZIndex="12"> - - - + - @@ -402,7 +405,7 @@ - + @@ -418,9 +421,19 @@ - + @@ -429,10 +442,11 @@ AutoGenerateColumns="False" MaxHeight="300" MinHeight="52" RowHeight="44" MinRowHeight="42" FrozenColumnCount="1" EnableRowVirtualization="True" EnableColumnVirtualization="True" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" - ScrollViewer.CanContentScroll="True" ScrollViewer.HorizontalScrollBarVisibility="Auto"> + ScrollViewer.CanContentScroll="True" ScrollViewer.HorizontalScrollBarVisibility="Disabled" + CanUserResizeColumns="False" ClipToBounds="True"> - - + + @@ -473,28 +487,28 @@ - - - + + + - - + - - - + - + diff --git a/SaveSclWindow.xaml b/SaveSclWindow.xaml index 1c4141d8..c77d2e7c 100644 --- a/SaveSclWindow.xaml +++ b/SaveSclWindow.xaml @@ -1,18 +1,53 @@ - + Icon="Assets/app-icon.ico" Loaded="Window_Loaded"> + + + + + - + + + + + + + @@ -20,33 +55,116 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SaveSclWindow.xaml.cs b/SaveSclWindow.xaml.cs index 2dd67e56..5092859a 100644 --- a/SaveSclWindow.xaml.cs +++ b/SaveSclWindow.xaml.cs @@ -1,10 +1,17 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; using AR.Iec61850.Scl.Export; namespace ArIED61850Tester; -public sealed class SaveSclDialogViewModel +public sealed class SaveSclDialogViewModel : INotifyPropertyChanged { + private SclSchemaProfileDescriptor _selectedSchemaProfile; + private string _editionHint = string.Empty; + public SaveSclDialogViewModel( string iedName, string sourceDescription, @@ -14,22 +21,63 @@ public SaveSclDialogViewModel( SourceDescription = string.IsNullOrWhiteSpace(sourceDescription) ? "IEC 61850 model" : sourceDescription.Trim(); - SchemaProfiles = - [ - SclSchemaProfiles.Get(SclSchemaProfile.Edition2V31), - SclSchemaProfiles.Get(SclSchemaProfile.Edition1V16) - ]; - SelectedSchemaProfile = SclSchemaProfiles.Get(selectedProfile); + _selectedSchemaProfile = SclSchemaProfiles.Get(selectedProfile); + UpdateHint(); } + public event PropertyChangedEventHandler? PropertyChanged; + public string IedName { get; } public string SourceDescription { get; } - public IReadOnlyList SchemaProfiles { get; } - public SclSchemaProfileDescriptor SelectedSchemaProfile { get; set; } + + public SclSchemaProfileDescriptor SelectedSchemaProfile + { + get => _selectedSchemaProfile; + private set + { + if (_selectedSchemaProfile.Profile == value.Profile) + return; + _selectedSchemaProfile = value; + OnPropertyChanged(); + UpdateHint(); + } + } + + public string EditionHint + { + get => _editionHint; + private set + { + if (string.Equals(_editionHint, value, StringComparison.Ordinal)) + return; + _editionHint = value; + OnPropertyChanged(); + } + } + + public bool IsEdition2 => SelectedSchemaProfile.IsEdition2; + + public void Select(SclSchemaProfile profile) + { + SelectedSchemaProfile = SclSchemaProfiles.Get(profile); + OnPropertyChanged(nameof(IsEdition2)); + } + + private void UpdateHint() + { + EditionHint = SelectedSchemaProfile.IsEdition2 + ? "Recommended for modern engineering exchange. The output is saved as an IID file." + : "Use for legacy engineering systems that require Edition 1. The output is saved as an ICD file."; + } + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public partial class SaveSclWindow : Window { + private const double IndicatorOffset = 192d; + public SaveSclWindow( string iedName, string sourceDescription, @@ -41,6 +89,42 @@ public SaveSclWindow( public SaveSclDialogViewModel ViewModel => (SaveSclDialogViewModel)DataContext; + private void Window_Loaded(object sender, RoutedEventArgs e) + => ApplyEditionVisuals(animate: false); + + private void Edition2_Click(object sender, RoutedEventArgs e) + { + ViewModel.Select(SclSchemaProfile.Edition2V31); + ApplyEditionVisuals(animate: true); + } + + private void Edition1_Click(object sender, RoutedEventArgs e) + { + ViewModel.Select(SclSchemaProfile.Edition1V16); + ApplyEditionVisuals(animate: true); + } + + private void ApplyEditionVisuals(bool animate) + { + var target = ViewModel.IsEdition2 ? 0d : IndicatorOffset; + Edition2Button.Foreground = ViewModel.IsEdition2 ? Brushes.White : new SolidColorBrush(Color.FromRgb(52, 64, 84)); + Edition1Button.Foreground = ViewModel.IsEdition2 ? new SolidColorBrush(Color.FromRgb(52, 64, 84)) : Brushes.White; + + if (!animate) + { + EditionIndicatorTranslate.X = target; + return; + } + + var animation = new DoubleAnimation + { + To = target, + Duration = TimeSpan.FromMilliseconds(185), + EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } + }; + EditionIndicatorTranslate.BeginAnimation(TranslateTransform.XProperty, animation, HandoffBehavior.SnapshotAndReplace); + } + private void Save_Click(object sender, RoutedEventArgs e) => DialogResult = true; } diff --git a/scripts/publish-windows-portable.ps1 b/scripts/publish-windows-portable.ps1 index cf27f3bc..1cecb2e6 100644 --- a/scripts/publish-windows-portable.ps1 +++ b/scripts/publish-windows-portable.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.6.10", + [string]$Version = "1.6.11", [string]$Runtime = "win-x64", [bool]$SingleFile = $true, [bool]$SelfContained = $true, @@ -23,7 +23,7 @@ if ($normalizedVersion.StartsWith("v", [System.StringComparison]::OrdinalIgnoreC $normalizedVersion = $normalizedVersion.Substring(1) } if ($normalizedVersion -notmatch '^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$') { - throw "Invalid version '$Version'. Use a value such as 1.6.10 or v1.6.10." + throw "Invalid version '$Version'. Use a value such as 1.6.11 or v1.6.11." } $outputRoot = Join-Path $root "dist"