diff --git a/IoListTestingWindow.EmbeddedEngineeringHost.cs b/IoListTestingWindow.EmbeddedEngineeringHost.cs index 5e434facc..38a163cfb 100644 --- a/IoListTestingWindow.EmbeddedEngineeringHost.cs +++ b/IoListTestingWindow.EmbeddedEngineeringHost.cs @@ -28,6 +28,21 @@ internal static void RegisterEmbeddedEngineeringFatHost() handledEventsToo: true); } + internal void PrepareForEmbeddedEngineeringHost() + { + // WPF refuses Window.Show() when ShowActivated=false while WindowState=Maximized. + // The production FAT XAML historically starts maximized, therefore normalize the + // invisible donor BEFORE Show(). Its exact center is re-parented into MainWindow on + // Loaded; the donor itself never needs a maximized native HWND. + WindowState = WindowState.Normal; + ShowActivated = false; + ShowInTaskbar = false; + Opacity = 0d; + WindowStartupLocation = WindowStartupLocation.Manual; + Left = -32000d; + Top = -32000d; + } + private static void EmbeddedEngineeringFatHost_Loaded(object sender, RoutedEventArgs e) { if (sender is not IoListTestingWindow window || @@ -45,9 +60,7 @@ window.Owner is not MainWindow owner || // MainWindow's legacy launcher still calls Show() on this Window. Make that bootstrap // surface invisible immediately; the actual production visual is moved into Engineering // on the next Loaded-priority dispatcher turn, before ContextIdle command-panel work. - window.ShowActivated = false; - window.ShowInTaskbar = false; - window.Opacity = 0d; + window.PrepareForEmbeddedEngineeringHost(); window.Dispatcher.BeginInvoke( DispatcherPriority.Loaded, @@ -64,14 +77,13 @@ private void TryMountIntoEngineering(MainWindow owner) { EnsureProductionFatPresentationForEmbeddedHost(); DisableLegacyEmbeddedCommandPanel(); - var surface = DetachProductionFatCentralWorkspace(); - if (surface == null) - return; + var surface = DetachProductionFatCentralWorkspace() + ?? throw new InvalidOperationException("Production FAT center could not be detached from its donor window."); _engineeringEmbeddedSurface = surface; _engineeringEmbeddedMounted = owner.MountProductionFatWorkspace(this, surface); if (!_engineeringEmbeddedMounted) - return; + throw new InvalidOperationException("Engineering FAT host rejected the production workspace surface."); // The central FAT view now belongs to MainWindow. Keep this Window loaded and // hidden because existing controller/session/event code is intentionally reused. @@ -79,9 +91,16 @@ private void TryMountIntoEngineering(MainWindow owner) } catch (Exception ex) { + // Never leave an invisible off-screen donor as a silent blank FAT failure. + // Restore the historical standalone presentation so the operator has a usable + // production FAT surface and a visible diagnostic if embedding itself fails. + WindowState = WindowState.Maximized; Opacity = 1d; ShowInTaskbar = true; ShowActivated = true; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + Left = double.NaN; + Top = double.NaN; MessageBox.Show( owner, $"ARSAS could not embed the production FAT workspace. The standalone FAT window will remain available.\n\n{ex.Message}", @@ -190,22 +209,51 @@ private void DisableLegacyEmbeddedCommandPanel() internal void SelectEngineeringDeviceForEmbeddedFat(Iec61850MonitorDevice? device) { - if (!_engineeringEmbeddedMounted || device == null) + if (!_engineeringEmbeddedMounted) return; - var match = Project.Ieds.FirstOrDefault(ied => - (!string.IsNullOrWhiteSpace(ied.LiveDeviceId) && - ied.LiveDeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase)) || - (!string.IsNullOrWhiteSpace(ied.IpAddress) && - ied.IpAddress.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase)) || - ied.IedName.Equals(device.SclIedName, StringComparison.OrdinalIgnoreCase) || - ied.IedName.Equals(device.Name, StringComparison.OrdinalIgnoreCase)); - if (match == null || ReferenceEquals(SelectedIed, match)) + // M3 viewed-device contract: the persistent Engineering IED Explorer owns + // what the FAT grid displays. SelectedIed/Session.SelectContext changes only that + // projection; an active capture remains latched inside its per-IED controller. + // Clearing the Explorer selection or selecting an IED outside this FAT projection + // must clear the FAT view instead of silently leaving the previous IED on screen. + if (device == null) + { + if (CanSelectIed) + SelectedIed = null; return; + } + + // Resolve by strongest Engineering identity first. Do not use one OR predicate: + // a weak fallback on an earlier project row must never beat an exact live DeviceId. + var match = Project.Ieds.FirstOrDefault(_ => false); + if (!string.IsNullOrWhiteSpace(device.DeviceId)) + { + match = Project.Ieds.FirstOrDefault(ied => + !string.IsNullOrWhiteSpace(ied.LiveDeviceId) && + ied.LiveDeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase)); + } + + if (match == null && !string.IsNullOrWhiteSpace(device.SclIedName)) + { + match = Project.Ieds.FirstOrDefault(ied => + ied.IedName.Equals(device.SclIedName, StringComparison.OrdinalIgnoreCase)); + } + + if (match == null && !string.IsNullOrWhiteSpace(device.Name)) + { + match = Project.Ieds.FirstOrDefault(ied => + ied.IedName.Equals(device.Name, StringComparison.OrdinalIgnoreCase)); + } + + if (match == null && !string.IsNullOrWhiteSpace(device.IpAddress)) + { + match = Project.Ieds.FirstOrDefault(ied => + !string.IsNullOrWhiteSpace(ied.IpAddress) && + ied.IpAddress.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase)); + } - // Do not retarget an active production FAT transaction/session. When idle, the - // persistent Engineering IED Explorer is the navigation/selection authority. - if (!CanSelectIed) + if (ReferenceEquals(SelectedIed, match) || !CanSelectIed) return; SelectedIed = match; diff --git a/IoListTestingWindow.xaml b/IoListTestingWindow.xaml index 48b1a7bb5..1b0803998 100644 --- a/IoListTestingWindow.xaml +++ b/IoListTestingWindow.xaml @@ -7,7 +7,7 @@ Height="900" Width="1500" MinHeight="760" MinWidth="1180" WindowStartupLocation="CenterOwner" WindowState="Maximized" Background="{StaticResource AppBackgroundGradient}" - FontFamily="Aptos, Segoe UI Variable Text, Segoe UI, Calibri" + FontFamily="Inter, Segoe UI Variable Text, Segoe UI, Calibri" Icon="Assets/app-icon.ico" Closing="Window_Closing" ContentRendered="IoListTestingWindow_ContentRendered"> @@ -17,11 +17,11 @@ - + - - + + - + @@ -76,24 +76,24 @@ - - - - + + -