Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
1bdd40d
feat(fat): add native FAT persistent state model
masarray Sep 9, 2026
aa09d19
feat(fat): add tolerant per-IED JSON state store
masarray Sep 9, 2026
b4a7cbb
refactor(fat): bind native FAT rows to Explorer signal authority
masarray Sep 9, 2026
69e278b
refactor(fat): reconcile against canonical Explorer signal scope
masarray Sep 9, 2026
2d5a962
feat(fat): add native continuous FAT workspace tab
masarray Sep 9, 2026
aecc453
Harden native FAT state identity and legacy migration
masarray Sep 9, 2026
19505db
Add immutable native FAT report snapshot models
masarray Sep 9, 2026
2c84084
Build deterministic native FAT report snapshots
masarray Sep 9, 2026
e365da2
Cover native FAT continuity and report snapshot regressions
masarray Sep 9, 2026
88c7cbd
Drive native FAT report preview from immutable evidence snapshots
masarray Sep 9, 2026
3bfc3ea
Preserve unreadable native FAT evidence files
masarray Sep 9, 2026
628d83d
Verify damaged native FAT files remain recoverable
masarray Sep 9, 2026
babd994
Flush native FAT evidence before application shutdown
masarray Sep 9, 2026
b2b412a
Add native FAT PDF evidence export
masarray Sep 9, 2026
42dafa5
Wire native FAT PDF export into engineering workspace
masarray Sep 9, 2026
8fd1c8e
Cover native FAT PDF evidence export
masarray Sep 9, 2026
8b7af6a
Harden FAT persistence across IED switches and shutdown
masarray Sep 9, 2026
eec5507
Keep native FAT scope synchronized with Explorer signal selection
masarray Sep 9, 2026
2c3eac3
Add auditable native FAT history inspector
masarray Sep 9, 2026
7dfec8b
Cover Explorer selection continuity in native FAT regression tests
masarray Sep 9, 2026
ea736d0
Pivot FAT tab to production workspace host
masarray Sep 9, 2026
4272309
Embed production FAT workspace in Engineering tab
masarray Sep 9, 2026
624b4fe
Fix production FAT dock state assignment
masarray Sep 9, 2026
537cad3
Merge current main into production FAT pivot
masarray Sep 9, 2026
11b6c8b
FAT: project Engineering static DataSet into production workspace
masarray Sep 9, 2026
cdc9052
FAT: adopt Engineering-owned SCL runtime workspaces
masarray Sep 9, 2026
c12c4c3
FAT: auto bootstrap from Engineering static DataSet
masarray Sep 9, 2026
d8fdb62
FAT: align seventh tab with Engineering nav language
masarray Sep 9, 2026
088066b
FAT: resolve all seven nav buttons by visual identity
masarray Sep 9, 2026
fc6b351
test: guard Engineering-owned FAT tab bootstrap and nav parity
masarray Sep 9, 2026
6df6eae
Fix FAT static scope duplicate manual overlays
masarray Sep 9, 2026
1edad87
Keep automatic FAT bootstrap on static DataSet authority
masarray Sep 9, 2026
27935b1
Keep Engineering visible during FAT bootstrap
masarray Sep 9, 2026
4dfc834
Add FAT P0 field regression coverage
masarray Sep 9, 2026
a214719
FAT M1: remove duplicate late navigation parity owner
masarray Sep 9, 2026
c6a856e
FAT M0: lock engineering authority and navigation regression contract
masarray Sep 9, 2026
ee1b83b
FAT M0: freeze Engineering workstation architecture contract
masarray Sep 9, 2026
661f359
FAT M1: add guarded canonical-shell migration
masarray Sep 9, 2026
d56fdab
FAT M1: run guarded canonical-shell migration
masarray Sep 9, 2026
4fd3d6f
FAT M1: make seven-destination shell canonical
github-actions[bot] Sep 9, 2026
7085d3f
FAT M1: keep navigation ownership in canonical MainWindow
masarray Sep 9, 2026
1710c52
FAT M1: lock canonical seven-destination shell ownership
masarray Sep 9, 2026
f5d0458
ci: guarded M1 seven-slot consistency fix
masarray Sep 10, 2026
f2df6d0
ci: fix guarded M1 migration newline matching
masarray Sep 10, 2026
b3058ce
fix: complete canonical seven-slot navigation ownership
github-actions[bot] Sep 10, 2026
acf76cd
docs: lock canonical seven-slot FAT navigation invariant
masarray Sep 10, 2026
1a0d3db
test: lock seven-cell responsive navigation geometry
masarray Sep 10, 2026
954795a
test: bind MainWindow navigation clamp to canonical FAT index
masarray Sep 10, 2026
4850435
chore: stage guarded M2 permanent FAT host migration
masarray Sep 10, 2026
b648186
chore: run guarded M2 permanent FAT host migration
masarray Sep 10, 2026
2c1a2ac
chore: correct guarded M2 launcher occurrence check
masarray Sep 10, 2026
523d8ab
chore: rerun guarded M2 migration with precise launcher target
masarray Sep 10, 2026
b885965
feat: complete M2 permanent production FAT host
github-actions[bot] Sep 10, 2026
581e3b4
docs: record M2 permanent FAT host verification contract
masarray Sep 10, 2026
cc0f855
test: make M2 permanent-host regression line-ending stable
masarray Sep 10, 2026
2f82570
test: align P0 no-flicker contract with M2 background prewarm
masarray Sep 10, 2026
6f67d92
chore: stage guarded M3 capture target latch migration
masarray Sep 10, 2026
a921fc8
chore: run guarded M3 capture target latch migration
masarray Sep 10, 2026
3d6a019
feat: start M3 selected IED binding with capture target latch
github-actions[bot] Sep 10, 2026
9556d6a
test: lock M3 viewed IED and capture target separation
masarray Sep 10, 2026
398b00f
chore: stage guarded M3 deterministic viewed IED binding
masarray Sep 10, 2026
ea3cb29
chore: run guarded M3 deterministic viewed IED binding migration
masarray Sep 10, 2026
ae0f0f3
feat: make M3 Engineering IED view binding deterministic
github-actions[bot] Sep 10, 2026
dd1343a
test: lock final M3 Explorer-to-FAT binding contract
masarray Sep 10, 2026
effc8f9
M3: infer production IED plan type for selected-device binding
masarray Sep 10, 2026
ec4b65c
chore: stage M3 persistence ownership guard
masarray Sep 10, 2026
e216eae
chore: run guarded M3 persistence migration
masarray Sep 10, 2026
b52843a
M3: fail closed on per-IED persistence restore
github-actions[bot] Sep 10, 2026
10dcbbc
M3: lock fail-closed per-IED restore contract
masarray Sep 10, 2026
8d1eb7e
M5: add DeviceId-aware persisted IED ownership guard
masarray Sep 10, 2026
73435c8
chore: stage guarded M5 persistence authority migration
masarray Sep 10, 2026
0c3e13d
chore: run guarded M5 persistence authority migration
masarray Sep 10, 2026
a9d6dd1
M5: bind persisted FAT ownership to Engineering DeviceId
github-actions[bot] Sep 10, 2026
4751621
M5: lock DeviceId ownership and read-only reconciliation
masarray Sep 10, 2026
f6ae335
docs: record M3 closure and M5 persistence invariants
masarray Sep 10, 2026
348c5b8
docs: clarify M5 reconciliation is pre-session and non-producing
masarray Sep 10, 2026
2fe9188
docs: mark M3 code and CI gate complete
masarray Sep 10, 2026
178b68d
M4: add fail-closed production FAT capture target adapter
masarray Sep 10, 2026
9ba4be8
chore: stage guarded M4 production adapter wiring
masarray Sep 10, 2026
9dcc6db
chore: run guarded M4 production adapter wiring
masarray Sep 10, 2026
2e37628
M4: add production capture lease and continue isolation regressions
masarray Sep 10, 2026
59d1d10
M4: latch production FAT target across prepare and continue
github-actions[bot] Sep 10, 2026
7a1570b
FAT M6: lock embedded production report preview parity
masarray Sep 10, 2026
8c0d2aa
FAT M7: remove obsolete native side-panel report preview
masarray Sep 10, 2026
4d8d773
FAT M7: guard single production preview authority
masarray Sep 10, 2026
bd6c083
FAT M7: remove obsolete native preview history inspector
masarray Sep 10, 2026
df62112
FAT M7: guard removal of obsolete preview inspector stack
masarray Sep 10, 2026
e373118
FAT M7: align M3 latch regression with production controller adapter
masarray Sep 10, 2026
025447a
FAT M7: keep cleanup guard on the current preflight authority
masarray Sep 10, 2026
aa11604
M7 retire duplicate native FAT workspace authority
masarray Sep 10, 2026
b65ccf9
M7 mount production FAT directly in canonical XAML slot
masarray Sep 10, 2026
b5a3421
M7 remove obsolete native FAT export authority
masarray Sep 10, 2026
a3972d5
M7 update canonical FAT shell regression contract
masarray Sep 10, 2026
166a6f6
M7 lock single production FAT authority regression
masarray Sep 10, 2026
1044d6e
M7 remove retired native FAT explorer reconciler
masarray Sep 10, 2026
8baacf5
M7 remove retired native FAT persistence safety runtime
masarray Sep 10, 2026
5269fed
M7 guard retired native FAT runtime fragments
masarray Sep 10, 2026
352883c
M7 align M6 preview regression with canonical FAT tab authority
masarray Sep 10, 2026
b025b09
Fix FAT hidden donor WPF Show lifecycle
masarray Sep 10, 2026
fe86547
Restore on-demand FAT bootstrap and protect Engineering acquisition
masarray Sep 10, 2026
ba4e437
Preserve Static DataSet report-only authority when FAT opens
masarray Sep 10, 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
196 changes: 196 additions & 0 deletions IoListTestingWindow.EmbeddedEngineeringHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using ArIED61850Tester.Models;

namespace ArIED61850Tester;

/// <summary>
/// Hosts the proven IoListTestingWindow production workspace inside MainWindow's FAT tab.
/// The production Window remains loaded-but-hidden so its existing session controller,
/// auto-capture lifecycle, persistence and report-preview code stay the single FAT authority.
/// Only its central workspace + FAT status footer are re-parented into Engineering.
/// </summary>
public partial class IoListTestingWindow
{
private bool _engineeringEmbeddedMountQueued;
private bool _engineeringEmbeddedMounted;
private FrameworkElement? _engineeringEmbeddedSurface;

[ModuleInitializer]
internal static void RegisterEmbeddedEngineeringFatHost()
{
EventManager.RegisterClassHandler(
typeof(IoListTestingWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(EmbeddedEngineeringFatHost_Loaded),
handledEventsToo: true);
}

internal void PrepareForEmbeddedEngineeringHost()
{
// WPF rejects Show() when a Window is both non-activating and Maximized.
// The donor only exists long enough to create the proven production visual tree,
// so normalize it before the hidden/off-screen bootstrap and let MainWindow own size.
if (WindowState == WindowState.Maximized)
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 ||
!ReferenceEquals(e.OriginalSource, window) ||
window._engineeringEmbeddedMounted ||
window._engineeringEmbeddedMountQueued ||
window.Owner is not MainWindow owner ||
!owner.ProductionFatTabReady)
{
return;
}

window._engineeringEmbeddedMountQueued = true;

// 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.PrepareForEmbeddedEngineeringHost();

window.Dispatcher.BeginInvoke(
DispatcherPriority.Loaded,
new Action(() => window.TryMountIntoEngineering(owner)));
}

private void TryMountIntoEngineering(MainWindow owner)
{
_engineeringEmbeddedMountQueued = false;
if (_engineeringEmbeddedMounted || !IsLoaded || !ReferenceEquals(Owner, owner) || !owner.ProductionFatTabReady)
return;

try
{
EnsureProductionFatPresentationForEmbeddedHost();
DisableLegacyEmbeddedCommandPanel();
var surface = DetachProductionFatCentralWorkspace();
if (surface == null)
return;

_engineeringEmbeddedSurface = surface;
_engineeringEmbeddedMounted = owner.MountProductionFatWorkspace(this, surface);
if (!_engineeringEmbeddedMounted)
return;

// The central FAT view now belongs to MainWindow. Keep this Window loaded and
// hidden because existing controller/session/event code is intentionally reused.
Hide();
}
catch (Exception ex)
{
Opacity = 1d;
ShowInTaskbar = true;
ShowActivated = true;
MessageBox.Show(
owner,
$"ARSAS could not embed the production FAT workspace. The standalone FAT window will remain available.\n\n{ex.Message}",
"FAT workspace host",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}

private void EnsureProductionFatPresentationForEmbeddedHost()
{
// P1 normally installs this during Loaded. Calling it explicitly is safe/idempotent
// and guarantees the first embedded frame is the same V2 FAT grid as the old Window.
InstallFatV2WorkspaceUx();

// PrintPreview historically installs from OnContentRendered. The bootstrap Window is
// intentionally transparent, so install the exact same production preview explicitly
// before the central workspace is detached. This preserves full-center mode switching.
if (!_printPreviewInstalled)
{
InstallPerIedPrintPreview();
PropertyChanged += PrintPreviewWindow_PropertyChanged;
Session.PropertyChanged += PrintPreviewSession_PropertyChanged;
Closed += PrintPreviewWindow_Closed;
_printPreviewInstalled = true;
}

// The embedded center already declares WorkspacePreviewToggle in XAML. Make that
// visible button the production toggle authority instead of the hidden Window header.
if (WorkspacePreviewToggle != null)
_printPreviewToggle = WorkspacePreviewToggle;
}

private void DisableLegacyEmbeddedCommandPanel()
{
// Engineering already owns one shared Command Dock. Prevent the old FAT window's
// duplicate command panel from being created/refreshed while embedded. A non-null
// sentinel makes the queued legacy installer return immediately; null row/summary
// references make its queued refresh a no-op. No command runtime semantics change.
DetachFatCommandDevice();
if (_fatCommandPanelShell?.Parent is Grid existingHost)
{
var row = Grid.GetRow(_fatCommandPanelShell);
existingHost.Children.Remove(_fatCommandPanelShell);
if (row >= 0 && row < existingHost.RowDefinitions.Count)
existingHost.RowDefinitions[row].Height = new GridLength(0);
if (row - 1 >= 0 && row - 1 < existingHost.RowDefinitions.Count)
existingHost.RowDefinitions[row - 1].Height = new GridLength(0);
}

_fatCommandPanelShell ??= new Border { Visibility = Visibility.Collapsed };
_fatCommandRows = null;
_fatCommandSummary = null;
_fatCommandEmptyState = null;
}

private FrameworkElement? DetachProductionFatCentralWorkspace()
{
if (Content is not Grid root)
return null;

var middle = root.Children
.OfType<Grid>()
.FirstOrDefault(child => Grid.GetRow(child) == 2);
var workspaceBorder = middle?.Children
.OfType<Border>()
.FirstOrDefault(child => Grid.GetColumn(child) == 2);
if (middle == null || workspaceBorder == null)
return null;

var footer = root.Children
.OfType<Border>()
.FirstOrDefault(child => Grid.GetRow(child) == 4);

middle.Children.Remove(workspaceBorder);
if (footer != null)
root.Children.Remove(footer);

var host = new Grid
{
DataContext = this,
Margin = new Thickness(0)
};
host.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
if (footer != null)
host.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

Grid.SetRow(workspaceBorder, 0);
host.Children.Add(workspaceBorder);
if (footer != null)
{
Grid.SetRow(footer, 1);
host.Children.Add(footer);
}

return host;
}
}
38 changes: 28 additions & 10 deletions IoListTestingWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,28 @@ private void SetAddingFatIeds(bool value)

private async void StartSession_Click(object sender, RoutedEventArgs e)
{
var selectedIed = SelectedIed;
if (selectedIed?.IsPreparing == true)
// M4 freezes both the IED owner and exact evidence scope before any asynchronous
// Engineering preparation. Explorer navigation may continue, but it cannot redirect
// this transaction or silently widen the production capture scope.
var requestedIed = SelectedIed;
if (requestedIed?.IsPreparing == true)
return;

var preflight = IoTestSessionPreflight.Validate(selectedIed);
if (!preflight.Succeeded)
IoFatCaptureTargetLease captureLease;
try
{
captureLease = IoFatProductionControllerAdapter.LatchStartTarget(Project, requestedIed);
}
catch (InvalidOperationException ex)
{
ShowActionResult(preflight, "FAT session scope is not ready");
ShowActionResult(
IoTestSessionActionResult.Failure(ex.Message),
"FAT session scope is not ready");
return;
}

PreparationStatusText = $"Connecting {selectedIed!.IedName} · {selectedIed.IpAddress}:102";
var selectedIed = captureLease.Ied;
PreparationStatusText = $"Connecting {selectedIed.IedName} · {selectedIed.IpAddress}:102";
RaisePreparationProperties();
try
{
Expand All @@ -155,7 +165,10 @@ private async void StartSession_Click(object sender, RoutedEventArgs e)
}
}

var result = Session.Start(selectedIed);
// Revalidate the frozen identity/configuration only after Engineering has
// completed connection preparation. The adapter delegates to the unchanged
// production session controller, which remains the sole evidence writer.
var result = IoFatProductionControllerAdapter.StartLatched(Project, Session, captureLease);
ShowActionResult(result, "FAT evidence session could not start");
RaiseStatusProperties();
if (result.Succeeded)
Expand Down Expand Up @@ -187,23 +200,28 @@ private async void StartSession_Click(object sender, RoutedEventArgs e)

private void PauseSession_Click(object sender, RoutedEventArgs e)
{
var result = Session.Pause();
var targetIed = SelectedIed;
var result = Session.Pause(targetIed);
ShowActionResult(result, "FAT session could not pause");
if (result.Succeeded)
Storage?.SaveNow();
}

private void ResumeSession_Click(object sender, RoutedEventArgs e)
{
var result = Session.Resume();
// Continue is explicitly IED-targeted. A later Explorer change cannot make the
// production controller rebind this active evidence session to another device.
var targetIed = SelectedIed;
var result = Session.Resume(targetIed);
ShowActionResult(result, "FAT session could not resume");
if (result.Succeeded)
Storage?.ScheduleSave();
}

private void StopSession_Click(object sender, RoutedEventArgs e)
{
var result = Session.Stop();
var targetIed = SelectedIed;
var result = Session.Stop(targetIed);
ShowActionResult(result, "FAT session could not stop");
if (result.Succeeded)
Storage?.SaveNow();
Expand Down
5 changes: 4 additions & 1 deletion MainWindow.IoTesting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ private Task ShowIoTestingWorkspaceAsync(IoTestWorkspaceLaunchResult launch, int
var controller = launch.Session;
var persistence = launch.Workspace;
var window = new IoListTestingWindow(launch.Project, controller, persistence) { Owner = this };
if (ProductionFatTabReady)
window.PrepareForEmbeddedEngineeringHost();
RegisterLoadedIoFatWindow(window);
_activeIoTestSessionController = controller;
Interlocked.Exchange(ref _ioTestObservationSequence, DateTime.UtcNow.Ticks);
Expand Down Expand Up @@ -525,7 +527,8 @@ void WindowClosed(object? sender, EventArgs args)
}

window.Closed += WindowClosed;
Hide();
if (!ProductionFatTabReady)
Hide();
window.Show();
return Task.CompletedTask;
}
Expand Down
28 changes: 28 additions & 0 deletions MainWindow.NativeFatWorkspace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Windows.Threading;

namespace ArIED61850Tester;

/// <summary>
/// M7 compatibility bridge for the canonical seventh Engineering destination.
///
/// MainWindow.xaml owns the FAT tab and navigation button. Production FAT is mounted
/// into that permanent XAML slot by MainWindow.ProductionFatTab.cs. This file deliberately
/// owns no FAT rows, capture commands, evidence state, persistence, preview, export, or timers.
/// It retains only the shared slot index and a deferred navigation-geometry refresh used by
/// the canonical MainWindow shell.
/// </summary>
public partial class MainWindow
{
private const int NativeFatWorkspaceIndex = 6;

private void QueueNativeFatNavigationGeometry()
{
if (!IsLoaded)
return;

Dispatcher.BeginInvoke(
DispatcherPriority.ApplicationIdle,
new Action(() => UpdateNavigationVisuals(MainTabs.SelectedIndex, animate: false)));
}
}
15 changes: 8 additions & 7 deletions MainWindow.NavigationLayoutFix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace ArIED61850Tester;
/// <summary>
/// Owns the responsive geometry of the MainWindow workflow header.
///
/// The original XAML used a 760 px shell split into six equal columns while the
/// The original XAML used a 760 px shell split into seven equal columns while the
/// selection pill moved in hard-coded 150 px steps. That was barely large enough for
/// short labels and clipped "IEC 61850 Explorer" / "GOOSE Subscriber" once the center
/// workspace switch and live connection/status chips were also present. This behavior
Expand Down Expand Up @@ -93,7 +93,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"))
if (button.Name is not ("NavExplorerButton" or "NavLiveButton" or "NavEventsButton" or "NavAlarmButton" or "NavGooseButton" or "NavDiagnosticsButton" or "NavNativeFatButton"))
return;

// A repeated click on the already-selected tab does not raise SelectionChanged,
Expand Down Expand Up @@ -195,7 +195,8 @@ private static void ApplyResponsiveLayout(MainWindow window)
window.FindName("NavEventsButton") as Button,
window.FindName("NavAlarmButton") as Button,
window.FindName("NavGooseButton") as Button,
window.FindName("NavDiagnosticsButton") as Button
window.FindName("NavDiagnosticsButton") as Button,
window.FindName("NavNativeFatButton") as Button
];

private static void UpdatePillGeometry(MainWindow window, double shellWidth)
Expand All @@ -204,9 +205,9 @@ private static void UpdatePillGeometry(MainWindow window, double shellWidth)
return;

// Border padding owns 10 px horizontally. The nav grid itself is divided into
// six equal star columns, so this is the exact width used by each button cell.
// seven equal star columns, so this is the exact width used by each button cell.
var contentWidth = Math.Max(0d, shellWidth - 10d);
var cellWidth = contentWidth / 6d;
var cellWidth = contentWidth / 7d;
pill.Width = Math.Max(1d, cellWidth - 2d);
pill.Height = 36;
pill.HorizontalAlignment = HorizontalAlignment.Left;
Expand Down Expand Up @@ -279,8 +280,8 @@ private static void PositionPill(MainWindow window, bool animate)
if (contentWidth <= 0d)
contentWidth = Math.Max(0d, shell.Width - shell.Padding.Left - shell.Padding.Right);

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

translate.BeginAnimation(TranslateTransform.XProperty, null);
Expand Down
Loading
Loading