Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b91694b
feat(gui): centralize eight operator navigation routes; preserve lega…
masarray Oct 15, 2026
a5bcb91
feat(gui): centralize eight operator navigation routes; preserve lega…
masarray Oct 15, 2026
76c5096
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
345af16
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
143d013
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
1ba1ee7
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
04627d4
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
1a4e2bb
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
7c03029
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
d8c2a27
feat(gui): eight-tab operator navigation and existing SMV viewer work…
masarray Oct 15, 2026
c47d280
test(gui): synchronize operator-approved tab contracts with canonical…
masarray Oct 15, 2026
41067e2
test(gui): synchronize operator-approved tab contracts with canonical…
masarray Oct 15, 2026
10453f6
test(gui): synchronize operator-approved tab contracts with canonical…
masarray Oct 15, 2026
0829cee
test(gui): synchronize operator-approved tab contracts with canonical…
masarray Oct 15, 2026
6401627
test(gui): synchronize operator-approved tab contracts with canonical…
masarray Oct 15, 2026
3aec3e3
test(gui): synchronize operator-approved tab contracts with canonical…
masarray Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions EngineeringNavigationRoute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace ArIED61850Tester;

/// <summary>
/// Operator header order is intentionally independent of the stable WPF
/// workspace indices. Preserve legacy bindings and passive monitors while
/// removing the redundant Live Monitor destination from operator navigation.
/// The legacy live view at index 1 has no navigable header button.
/// </summary>
public static class EngineeringNavigationRoute
{
public const int SmvWorkspaceIndex = 8;

// Visible header: Browser, Devices, Events, Annunciator, GOOSE, SMV,
// Signal Testing, Diagnostics. Values are real MainTabs indices.
private static readonly int[] WorkspaceByButton = [7, 0, 2, 3, 4, 8, 6, 5];

public static int TabForButton(int button)
=> button is >= 0 and < 8
? WorkspaceByButton[button]
: throw new ArgumentOutOfRangeException(nameof(button));

public static int ButtonForTab(int tab)
=> tab switch
{
7 => 0, // IED Browser
0 or 1 => 1, // Device Explorer and unreachable legacy Live Monitor
2 => 2, // Event Logs
3 => 3, // Annunciator
4 => 4, // GOOSE Sniff
8 => 5, // SMV
6 => 6, // Signal Testing
5 => 7, // Diagnostics
_ => 1 // Unspecified internal workspace is never shown as SMV
};
}
19 changes: 19 additions & 0 deletions MainWindow.IedSmvQuickStart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,26 @@ private void IedSmvQuickStart_Click(object sender, RoutedEventArgs e)
if (sender is not Button { CommandParameter: Iec61850MonitorDevice device })
return;

OpenSmvWorkspaceForDevice(device);
}

private void ActivateSelectedSmvWorkspace()
{
if (SelectedDevice is { } device)
OpenSmvWorkspaceForDevice(device);
else if (SmvWorkspaceStatus is not null)
SmvWorkspaceStatus.Text = "Select an IED in Device Explorer, then open SMV capture.";
}

private void OpenSmvCapture_Click(object sender, RoutedEventArgs e)
=> ActivateSelectedSmvWorkspace();

private void OpenSmvWorkspaceForDevice(Iec61850MonitorDevice device)
{
SelectedDevice = device;
if (SmvWorkspaceStatus is not null)
SmvWorkspaceStatus.Text =
"SMV capture for " + device.Name + " · existing verified viewer";

if (_activeSmvViewerWindow is { IsLoaded: true } existing)
{
Expand Down
38 changes: 19 additions & 19 deletions MainWindow.NavigationLayoutFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,26 @@ internal static class MainWindowNavigationLayoutFix

private static readonly string[] FullLabels =
[
"IEC 61850 Explorer",
"Live Monitor",
"Event Log",
"Alarm",
"GOOSE Subscriber",
"Diagnostics",
"FAT",
"IED Browser"
"IED Browser",
"Device Explorer",
"Event Logs",
"Annunciator",
"GOOSE Sniff",
"SMV",
"Signal Testing",
"Diagnostics"
];

private static readonly string[] CompactLabels =
[
"Explorer",
"Live",
"IED Browser",
"Devices",
"Events",
"Alarm",
"GOOSE",
"Diagnostics",
"FAT",
"Browser"
"SMV",
"Testing",
"Diagnostics"
];

[ModuleInitializer]
Expand Down Expand Up @@ -98,7 +98,7 @@ private static void OnMainWindowButtonClick(object sender, RoutedEventArgs e)
if (sender is not MainWindow window || e.Source is not Button button)
return;

if (button.Name is not ("NavExplorerButton" or "NavLiveButton" or "NavEventsButton" or "NavAlarmButton" or "NavGooseButton" or "NavDiagnosticsButton" or "NavNativeFatButton" or "NavIedBrowserButton"))
if (button.Name is not ("NavIedBrowserButton" or "NavExplorerButton" or "NavEventsButton" or "NavAlarmButton" or "NavGooseButton" or "NavSmvButton" or "NavNativeFatButton" or "NavDiagnosticsButton"))
return;

// A repeated click on the already-selected tab does not raise SelectionChanged,
Expand Down Expand Up @@ -160,7 +160,7 @@ private static void ApplyResponsiveLayout(MainWindow window)

// Diagnostics owns a StackPanel containing its text plus the red alert
// badge. Never replace that Content tree while changing layout density.
if (index != 5 && index < labels.Length)
if (index != 7 && index < labels.Length)
{
// Preserve the operator SVG glyphs across Loaded/resize. Replacing
// Content here would silently erase icons declared in MainWindow.xaml.
Expand Down Expand Up @@ -189,14 +189,14 @@ private static void ApplyResponsiveLayout(MainWindow window)
private static Button?[] GetNavigationButtons(MainWindow window)
=>
[
window.FindName("NavIedBrowserButton") as Button,
window.FindName("NavExplorerButton") as Button,
window.FindName("NavLiveButton") as Button,
window.FindName("NavEventsButton") as Button,
window.FindName("NavAlarmButton") as Button,
window.FindName("NavGooseButton") as Button,
window.FindName("NavDiagnosticsButton") as Button,
window.FindName("NavSmvButton") as Button,
window.FindName("NavNativeFatButton") as Button,
window.FindName("NavIedBrowserButton") as Button
window.FindName("NavDiagnosticsButton") as Button
];

private static void UpdatePillGeometry(MainWindow window, double shellWidth)
Expand Down Expand Up @@ -240,7 +240,7 @@ private static void PositionPill(MainWindow window, bool animate)
contentWidth = Math.Max(0d, shell.Width - shell.Padding.Left - shell.Padding.Right);

var cellWidth = contentWidth / 8d;
var target = Math.Clamp(tabs.SelectedIndex, 0, 7) * cellWidth;
var target = EngineeringNavigationRoute.ButtonForTab(tabs.SelectedIndex) * cellWidth;
pill.Width = Math.Max(1d, cellWidth - 2d);

translate.BeginAnimation(TranslateTransform.XProperty, null);
Expand Down
55 changes: 39 additions & 16 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,47 +109,47 @@
<DropShadowEffect BlurRadius="14" ShadowDepth="3" Opacity="0.18" Color="#2563EB"/>
</Border.Effect>
</Border>
<Button x:Name="NavExplorerButton" Grid.Column="0" Tag="0" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="IEC 61850 Explorer">
<Button x:Name="NavExplorerButton" Grid.Column="1" Tag="1" TabIndex="1" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Device Explorer">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorIecTopologyIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
</Viewbox>
<TextBlock Text="IEC 61850 Explorer" VerticalAlignment="Center"/>
<TextBlock Text="Device Explorer" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="NavLiveButton" Grid.Column="1" Tag="1" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Live Monitor">
<Button x:Name="NavSmvButton" Grid.Column="5" Tag="5" TabIndex="5" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="SMV · Sampled Values">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorLiveMonitorIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
</Viewbox>
<TextBlock Text="Live Monitor" VerticalAlignment="Center"/>
<TextBlock Text="SMV" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="NavEventsButton" Grid.Column="2" Tag="2" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Event Log">
<Button x:Name="NavEventsButton" Grid.Column="2" Tag="2" TabIndex="2" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Event Logs">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorEventLogIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
</Viewbox>
<TextBlock Text="Event Log" VerticalAlignment="Center"/>
<TextBlock Text="Event Logs" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="NavAlarmButton" Grid.Column="3" Tag="3" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Alarm">
<Button x:Name="NavAlarmButton" Grid.Column="3" Tag="3" TabIndex="3" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Annunciator">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorAlarmIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
</Viewbox>
<TextBlock Text="Alarm" VerticalAlignment="Center"/>
<TextBlock Text="Annunciator" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="NavGooseButton" Grid.Column="4" Tag="4" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="GOOSE Subscriber">
<Button x:Name="NavGooseButton" Grid.Column="4" Tag="4" TabIndex="4" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="GOOSE Sniff">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorIecWirelessIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
</Viewbox>
<TextBlock Text="GOOSE Subscriber" VerticalAlignment="Center"/>
<TextBlock Text="GOOSE Sniff" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="NavDiagnosticsButton" Grid.Column="5" Tag="5" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}">
<Button x:Name="NavDiagnosticsButton" Grid.Column="7" Tag="7" TabIndex="7" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorDiagnosticsIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
Expand All @@ -163,15 +163,15 @@
</Border>
</StackPanel>
</Button>
<Button x:Name="NavNativeFatButton" Grid.Column="6" Tag="6" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="FAT">
<Button x:Name="NavNativeFatButton" Grid.Column="6" Tag="6" TabIndex="6" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="Signal Testing">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorFatIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
</Viewbox>
<TextBlock Text="FAT" VerticalAlignment="Center"/>
<TextBlock Text="Signal Testing" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button x:Name="NavIedBrowserButton" Grid.Column="7" Tag="7" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="IED Browser">
<Button x:Name="NavIedBrowserButton" Grid.Column="0" Tag="0" TabIndex="0" Click="NavButton_Click" Style="{StaticResource SegmentedNavButton}" ToolTip="IED Browser">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Viewbox Width="15" Height="15" Margin="0,0,5,0" IsHitTestVisible="False">
<Path Data="{StaticResource OperatorIedBrowserIcon}" Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Stretch="Uniform"/>
Expand All @@ -191,7 +191,7 @@
Style="{StaticResource HiddenTabControl}"
SelectionChanged="MainTabs_SelectionChanged">
<!-- IEC 61850 EXPLORER -->
<TabItem Header="IEC 61850 Explorer">
<TabItem Header="Device Explorer">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="258"/>
Expand Down Expand Up @@ -930,7 +930,7 @@
</TabItem>

<!-- GLOBAL MULTI-IED LIVE MONITOR -->
<TabItem Header="Live Monitor">
<TabItem Header="Internal Live Monitor" Visibility="Collapsed" IsEnabled="False">
<Border Style="{StaticResource WorkspaceCard}" Padding="12">
<DockPanel>
<Grid DockPanel.Dock="Top" Margin="0,0,0,10">
Expand Down Expand Up @@ -2284,6 +2284,29 @@
</Grid>
</Border>
</TabItem>

<!-- Sampled Values uses the existing, hardware-backed SMV viewer/capture workflow.
The host is navigation/status only, not a duplicate SMV decoder. -->
<TabItem x:Name="SmvWorkspaceTab" Header="SMV">
<Border Style="{StaticResource WorkspaceCard}" Padding="12">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel>
<TextBlock Text="Sampled Values" Style="{StaticResource WorkspaceTitle}"/>
<TextBlock Text="Capture and inspect IEC 61850-9-2 Sampled Values with the existing native SMV viewer."
Style="{StaticResource WorkspaceSubtitle}" Margin="0,2,0,0"/>
</StackPanel>
<Button Content="Open SMV Capture" Style="{StaticResource PrimaryButton}"
Margin="16,0,0,0" Padding="12,6"
Click="OpenSmvCapture_Click"
ToolTip="Reuse the current SMV viewer and preserve the active snapshot"/>
</StackPanel>
<TextBlock x:Name="SmvWorkspaceStatus" Margin="0,14,0,0" FontSize="12"
Foreground="{StaticResource Muted}"
Text="Select an IED in Device Explorer to launch the SMV capture workspace."/>
</DockPanel>
</Border>
</TabItem>
</TabControl>

<Border Grid.Row="2" Margin="0,10,0,0" Background="#EAF1FF" BorderBrush="#CFE0FF" BorderThickness="1" CornerRadius="14" Padding="11,7">
Expand Down
19 changes: 11 additions & 8 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -818,9 +818,8 @@ private void NavButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button button || !int.TryParse(button.Tag?.ToString(), out var index))
return;
index = Math.Clamp(index, 0, IedBrowserWorkspaceIndex);
MainTabs.SelectedIndex = index;
UpdateNavigationVisuals(index, animate: true);
MainTabs.SelectedIndex = EngineeringNavigationRoute.TabForButton(index);
UpdateNavigationVisuals(MainTabs.SelectedIndex, animate: true);
}

private void MainTabs_SelectionChanged(object sender, SelectionChangedEventArgs e)
Expand Down Expand Up @@ -858,6 +857,10 @@ private void MainTabs_SelectionChanged(object sender, SelectionChangedEventArgs
{
ClearDiagnosticAlert();
}
else if (MainTabs.SelectedIndex == EngineeringNavigationRoute.SmvWorkspaceIndex)
{
ActivateSelectedSmvWorkspace();
}

UpdateNavigationVisuals(MainTabs.SelectedIndex, animate: true);
}
Expand All @@ -867,9 +870,9 @@ private void UpdateNavigationVisuals(int index, bool animate)
if (WorkflowPillTranslate == null)
return;

index = Math.Clamp(index, 0, IedBrowserWorkspaceIndex);
index = EngineeringNavigationRoute.ButtonForTab(index);

// MainWindow is the single owner of all seven Engineering destinations.
// MainWindow is the single owner of all eight operator destinations.
// Keep the same density used by the proven workstation shell while allowing
// enough width for the Explorer label and the new canonical FAT sibling.
var availableWidth = ActualWidth > 0d ? ActualWidth : 1480d;
Expand Down Expand Up @@ -898,14 +901,14 @@ private void UpdateNavigationVisuals(int index, bool animate)

var buttons = new[]
{
NavIedBrowserButton,
NavExplorerButton,
NavLiveButton,
NavEventsButton,
NavAlarmButton,
NavGooseButton,
NavDiagnosticsButton,
NavSmvButton,
NavNativeFatButton,
NavIedBrowserButton
NavDiagnosticsButton
};
for (var i = 0; i < buttons.Length; i++)
buttons[i].Foreground = i == index ? Brushes.White : new SolidColorBrush(Color.FromRgb(71, 84, 103));
Expand Down
2 changes: 1 addition & 1 deletion P2IndustrialWorkstationUx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ private static bool Contains(string? value, string query)
private static void DecorateMainNavigation(MainWindow window)
{
ApplyNavigationTemplate(window.FindName("NavExplorerButton") as Button, window, "IconExplorerNavContent");
ApplyNavigationTemplate(window.FindName("NavLiveButton") as Button, window, "IconMonitorNavContent");
ApplyNavigationTemplate(window.FindName("NavSmvButton") as Button, window, "IconMonitorNavContent");
ApplyNavigationTemplate(window.FindName("NavEventsButton") as Button, window, "IconEventsNavContent");
ApplyNavigationTemplate(window.FindName("NavGooseButton") as Button, window, "IconGooseNavContent");
ApplyNavigationTemplate(window.FindName("NavDiagnosticsButton") as Button, window, "IconDiagnosticsNavContent");
Expand Down
Loading
Loading