Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
419fa64
Add ARIEC reconciliation presentation mapping
masarray Aug 13, 2026
89b52d7
Consume ARIEC reconciliation verdicts in FAT binding
masarray Aug 13, 2026
5d929e0
Lock FAT false-missing reconciliation behavior
masarray Aug 13, 2026
3ec94f4
Repin ARIEC61850 P1.2 baseline
masarray Aug 14, 2026
451b898
Present ARIEC P1.2 reconciliation evidence
masarray Aug 14, 2026
7390bc2
Add async cached ARIEC reconciliation lifecycle
masarray Aug 14, 2026
316caad
Consume cached reconciliation in FAT binding
masarray Aug 14, 2026
5e5b242
Refresh ARIEC reconciliation asynchronously after FAT discovery
masarray Aug 14, 2026
578083a
Cover P1.2 status evidence and async reconciliation cache
masarray Aug 14, 2026
896ab2c
Order FAT reconciliation immediately after discovery
masarray Aug 14, 2026
f3c3fb9
Repin ARSAS to ARIEC P1.3 connected reconciliation
masarray Aug 14, 2026
5ff420d
Consume ARIEC P1.3 reconciliation presentation status
masarray Aug 14, 2026
3f8c7d5
Test ARIEC P1.3 alternate discovery and probe budget presentation
masarray Aug 14, 2026
77c5e17
Inject high-level reconciliation producer into FAT cache
masarray Aug 14, 2026
67546ca
Open native client for session-owned reconciliation extension
masarray Aug 14, 2026
e839d74
Bridge ARIEC connected reconciliation through native session owner
masarray Aug 14, 2026
85d303d
Use ARIEC connected facade as FAT reconciliation producer
masarray Aug 14, 2026
3851e40
Expose safe native reconciliation owner presence
masarray Aug 14, 2026
1fdb309
Keep offline reconciliation model-only and connected FAT session-bound
masarray Aug 14, 2026
7167a1d
Test ARIEC connected reconciliation through native owner
masarray Aug 14, 2026
2bf04af
Test native endpoint owner resolution for connected reconciliation
masarray Aug 14, 2026
7d75222
Add fast live signal workspace search
masarray Aug 14, 2026
9df0318
Harden live signal search installation
masarray Aug 14, 2026
8bf54dc
Avoid MainWindow visual helper collision
masarray Aug 14, 2026
02ac69c
Match live signal search to IED Explorer style
masarray Aug 14, 2026
874a05b
Add full-string millisecond timestamp presentation
masarray Aug 14, 2026
d3a7285
Round live timestamps and show full precision tooltip
masarray Aug 14, 2026
204f979
Test rounded live timestamp string presentation
masarray Aug 14, 2026
60db87f
Qualify WPF tooltip type in explorer timestamp style
masarray Aug 14, 2026
dc36d99
Sync ARIEC61850 COMTRADE FileOpen recovery baseline
masarray Aug 14, 2026
33fd65f
noop
masarray Aug 14, 2026
66ac389
Add FAT grid header select-all checkbox
masarray Aug 14, 2026
6cccd08
Add fault-record header select-all checkbox
masarray Aug 14, 2026
a5f5124
Merge branch 'integrate/ariec61850-reconciliation-status' of https://…
masarray Aug 14, 2026
27c6e07
Pin ARIEC COMTRADE success baseline #71
masarray Aug 14, 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 FaultRecordWindow.HeaderSelection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;

namespace ArIED61850Tester;

/// <summary>
/// Adds a tri-state select-all checkbox to the fault-record Get column. Selecting all
/// affects only rows that are currently eligible for download; clearing always clears
/// every row so stale disabled selections cannot remain hidden.
/// </summary>
public partial class FaultRecordWindow
{
private static readonly bool FaultRecordHeaderSelectionClassHandlerRegistered =
RegisterFaultRecordHeaderSelectionClassHandler();

private readonly HashSet<FaultRecordRow> _faultRecordHeaderObservedRows = new();
private bool _faultRecordHeaderSelectionInstallScheduled;
private bool _faultRecordHeaderSelectionInstalled;
private bool _faultRecordHeaderBulkUpdate;
private CheckBox? _faultRecordHeaderSelectionCheckBox;

private static bool RegisterFaultRecordHeaderSelectionClassHandler()
{
EventManager.RegisterClassHandler(
typeof(FaultRecordWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(OnFaultRecordHeaderSelectionWindowLoaded),
handledEventsToo: true);
return true;
}

private static void OnFaultRecordHeaderSelectionWindowLoaded(object sender, RoutedEventArgs e)
{
if (sender is not FaultRecordWindow window ||
window._faultRecordHeaderSelectionInstalled ||
window._faultRecordHeaderSelectionInstallScheduled)
{
return;
}

window._faultRecordHeaderSelectionInstallScheduled = true;
window.Dispatcher.BeginInvoke(
DispatcherPriority.ContextIdle,
new Action(() =>
{
window._faultRecordHeaderSelectionInstallScheduled = false;
window.EnsureFaultRecordHeaderSelection();
}));
}

private void EnsureFaultRecordHeaderSelection()
{
if (_faultRecordHeaderSelectionInstalled)
{
RefreshFaultRecordHeaderSelection();
return;
}

var column = FaultRecordsGrid.Columns.FirstOrDefault(candidate =>
string.Equals(candidate.Header?.ToString(), "Get", StringComparison.OrdinalIgnoreCase));
if (column == null)
return;

var headerCheckBox = new CheckBox
{
Width = 16,
Height = 16,
IsThreeState = true,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Cursor = Cursors.Hand,
Focusable = true,
ToolTip = "Check / uncheck all downloadable fault records"
};
AutomationProperties.SetName(headerCheckBox, "Toggle all downloadable fault records");
headerCheckBox.Click += FaultRecordHeaderSelectionCheckBox_Click;

column.Header = headerCheckBox;
_faultRecordHeaderSelectionCheckBox = headerCheckBox;
_faultRecordHeaderSelectionInstalled = true;

PropertyChanged += FaultRecordHeaderSelectionWindow_PropertyChanged;
Records.CollectionChanged += FaultRecordHeaderSelectionRecords_CollectionChanged;
Closed += FaultRecordHeaderSelectionWindow_Closed;
RewireFaultRecordHeaderRows();
RefreshFaultRecordHeaderSelection();
}

private void FaultRecordHeaderSelectionCheckBox_Click(object sender, RoutedEventArgs e)
{
if (IsBusy)
{
RefreshFaultRecordHeaderSelection();
return;
}

var target = GetFaultRecordHeaderSelectionState() != true;
_faultRecordHeaderBulkUpdate = true;
try
{
foreach (var row in Records)
{
if (target)
row.IsSelected = row.CanSelectForDownload;
else
row.IsSelected = false;
}
}
finally
{
_faultRecordHeaderBulkUpdate = false;
}

RaiseSelectionState();
RefreshFaultRecordHeaderSelection();
}

private void FaultRecordHeaderSelectionWindow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
=> RefreshFaultRecordHeaderSelection();

private void FaultRecordHeaderSelectionRecords_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RewireFaultRecordHeaderRows();
RefreshFaultRecordHeaderSelection();
}

private void RewireFaultRecordHeaderRows()
{
var current = Records.ToHashSet();
foreach (var stale in _faultRecordHeaderObservedRows.Where(row => !current.Contains(row)).ToArray())
{
stale.PropertyChanged -= FaultRecordHeaderSelectionRow_PropertyChanged;
_faultRecordHeaderObservedRows.Remove(stale);
}

foreach (var row in Records)
{
if (!_faultRecordHeaderObservedRows.Add(row))
continue;

row.PropertyChanged += FaultRecordHeaderSelectionRow_PropertyChanged;
}
}

private void FaultRecordHeaderSelectionRow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_faultRecordHeaderBulkUpdate)
return;

RefreshFaultRecordHeaderSelection();
}

private void RefreshFaultRecordHeaderSelection()
{
var header = _faultRecordHeaderSelectionCheckBox;
if (header == null)
return;

var eligibleCount = Records.Count(row => row.CanSelectForDownload);
header.IsEnabled = !IsBusy && eligibleCount > 0;
header.IsChecked = GetFaultRecordHeaderSelectionState();
}

private bool? GetFaultRecordHeaderSelectionState()
{
var eligible = Records.Where(row => row.CanSelectForDownload).ToArray();
if (eligible.Length == 0)
return false;

var selected = eligible.Count(row => row.IsSelected);
if (selected == 0)
return false;
if (selected == eligible.Length)
return true;
return null;
}

private void FaultRecordHeaderSelectionWindow_Closed(object? sender, EventArgs e)
{
PropertyChanged -= FaultRecordHeaderSelectionWindow_PropertyChanged;
Records.CollectionChanged -= FaultRecordHeaderSelectionRecords_CollectionChanged;
Closed -= FaultRecordHeaderSelectionWindow_Closed;

foreach (var row in _faultRecordHeaderObservedRows)
row.PropertyChanged -= FaultRecordHeaderSelectionRow_PropertyChanged;
_faultRecordHeaderObservedRows.Clear();

if (_faultRecordHeaderSelectionCheckBox != null)
_faultRecordHeaderSelectionCheckBox.Click -= FaultRecordHeaderSelectionCheckBox_Click;
}
}
38 changes: 38 additions & 0 deletions Iec61850TimestampPresentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,44 @@ public static string FormatMilliseconds(DateTimeOffset? value, string format, st
public static string FormatMilliseconds(DateTime value, string format)
=> RoundToNearestMillisecond(value).ToString(format, CultureInfo.InvariantCulture);

/// <summary>
/// Formats a full-resolution timestamp string for a compact live-grid display while
/// leaving the source string untouched for evidence, search and full-precision tooltips.
/// Non-timestamp values are returned unchanged rather than guessed.
/// </summary>
public static string FormatMilliseconds(
string? value,
string format = "yyyy-MM-dd HH:mm:ss.fff",
string missing = "-")
{
var text = (value ?? string.Empty).Trim();
if (text.Length == 0 || text == "-")
return text.Length == 0 ? missing : text;

// Relay timestamps in the live workspace normally have no explicit offset.
// Parse those as DateTime first so presentation never invents or shifts a zone.
if (DateTime.TryParse(
text,
CultureInfo.InvariantCulture,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.RoundtripKind,
out var dateTime))
{
return FormatMilliseconds(dateTime, format);
}

// Preserve explicit-offset timestamps when they are supplied by another surface.
if (DateTimeOffset.TryParse(
text,
CultureInfo.InvariantCulture,
DateTimeStyles.AllowWhiteSpaces,
out var dateTimeOffset))
{
return FormatMilliseconds(dateTimeOffset, format);
}

return text;
}

private static long MillisecondDelta(long ticks)
{
var remainder = ticks % TicksPerMillisecond;
Expand Down
Loading
Loading