diff --git a/EngineeringNavigationRoute.cs b/EngineeringNavigationRoute.cs
new file mode 100644
index 000000000..9af4453a5
--- /dev/null
+++ b/EngineeringNavigationRoute.cs
@@ -0,0 +1,35 @@
+namespace ArIED61850Tester;
+
+///
+/// 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.
+///
+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
+ };
+}
diff --git a/MainWindow.IedSmvQuickStart.cs b/MainWindow.IedSmvQuickStart.cs
index 6e602641e..55cbeda2b 100644
--- a/MainWindow.IedSmvQuickStart.cs
+++ b/MainWindow.IedSmvQuickStart.cs
@@ -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)
{
diff --git a/MainWindow.NavigationLayoutFix.cs b/MainWindow.NavigationLayoutFix.cs
index bf1ff226b..f087445d1 100644
--- a/MainWindow.NavigationLayoutFix.cs
+++ b/MainWindow.NavigationLayoutFix.cs
@@ -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]
@@ -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,
@@ -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.
@@ -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)
@@ -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);
diff --git a/MainWindow.xaml b/MainWindow.xaml
index c2ee8877a..24bc6d710 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -109,47 +109,47 @@
-