From c5412f08ebd2b95cf4c6f16c08ff820c7ee38c3c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Fri, 23 Oct 2026 18:43:49 +0700 Subject: [PATCH 01/11] ux(write): compact IECScout-style inline scalar editor with Material button icons and stable Write caption --- BrowserDataWriteWindow.xaml | 170 ++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 64 deletions(-) diff --git a/BrowserDataWriteWindow.xaml b/BrowserDataWriteWindow.xaml index 63664c816..d082b5334 100644 --- a/BrowserDataWriteWindow.xaml +++ b/BrowserDataWriteWindow.xaml @@ -1,85 +1,127 @@ - + + + M167-120q-21 5-36.5-10.5T120-167l40-191 198 198-191 40Zm191-40L160-358l458-458q23-23 57-23t57 23l84 84q23 23 23 57t-23 57L358-160Zm317-600L261-346l85 85 414-414-85-85Z + M256-200l-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + - + - - + From 380db0951819ebcb60778dae666a23546cf91ef7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Fri, 23 Oct 2026 18:44:30 +0700 Subject: [PATCH 02/11] ux(write): use directly vectorized Google Material paths for write, cancel and reread --- BrowserDataWriteWindow.xaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/BrowserDataWriteWindow.xaml b/BrowserDataWriteWindow.xaml index d082b5334..eadeff6d7 100644 --- a/BrowserDataWriteWindow.xaml +++ b/BrowserDataWriteWindow.xaml @@ -9,8 +9,6 @@ Loaded="Window_Loaded" Closed="Window_Closed"> - M167-120q-21 5-36.5-10.5T120-167l40-191 198 198-191 40Zm191-40L160-358l458-458q23-23 57-23t57 23l84 84q23 23 23 57t-23 57L358-160Zm317-600L261-346l85 85 414-414-85-85Z - M256-200l-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z + + + + + + + + + - + + + + + - + + + + + + - - - - - - - - - - - - - - - + + \ No newline at end of file From 292035ad2dc0c628c8d09e27e49d7e7d545e687e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Fri, 23 Oct 2026 18:48:50 +0700 Subject: [PATCH 06/11] ux(write): bounded live typed previews, Boolean choices and current-value prefills for multi-DA rows --- BrowserBatchWriteWindow.xaml.cs | 110 ++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/BrowserBatchWriteWindow.xaml.cs b/BrowserBatchWriteWindow.xaml.cs index da9bd4413..ee1c5f58e 100644 --- a/BrowserBatchWriteWindow.xaml.cs +++ b/BrowserBatchWriteWindow.xaml.cs @@ -103,11 +103,42 @@ private void SetBusy(bool busy) WriteButton.IsEnabled = !busy && _prepared; } - private void Window_Loaded(object sender, RoutedEventArgs e) + private async void Window_Loaded(object sender, RoutedEventArgs e) { var work = SystemParameters.WorkArea; - Height = Math.Max(MinHeight, Math.Min(580, work.Height - 26)); - Width = Math.Max(MinWidth, Math.Min(760, work.Width - 26)); + Height = Math.Max(MinHeight, Math.Min(515, work.Height - 26)); + Width = Math.Max(MinWidth, Math.Min(940, work.Width - 26)); + await PrimeTypedRowsAsync(); + } + + // The first bounded visible batch is read-only hydrated on open, like + // IEDScout. No blank "new value" textbox and NO hidden Write/Operate. + // Larger DOs can use Read on an explicitly selected, unhydrated row. + private async Task PrimeTypedRowsAsync() + { + if (_busy || !CurrentAssociation) return; + var candidates = Rows.Where(r => r.Eligible).Take(24).ToArray(); + SetBusy(true); + try + { + var done = 0; + foreach (var row in candidates) + { + if (!CurrentAssociation || _lifetime.IsCancellationRequested) break; + try + { + var preview = await _runtime.PreviewBrowserDataAttributeWriteAsync( + _device, row.Reference!.Value, _lifetime.Token); + if (!CurrentAssociation) break; + row.ApplyLivePreview(preview, preserveExplicitEdit: false); + done++; + } + catch (OperationCanceledException) { break; } + catch (Exception ex) { row.Status = "Read failed: " + ex.Message; } + } + StatusLabel.Text = $"Read {done}/{candidates.Length} typed DA(s). Choose a value; select its checkbox and press Read, then Write."; + } + finally { SetBusy(false); } } private BrowserBatchWriteRow[] CheckedRows() @@ -124,10 +155,18 @@ private async void Inspect_Click(object sender, RoutedEventArgs e) WriteButton.IsEnabled = false; var selected = CheckedRows(); if (selected.Length is 0 or > IedBrowserTypedWriteBatch.MaxSelectedAttributes || - selected.Any(r => !r.Eligible || !r.HasEdited) || !CurrentAssociation) + selected.Any(r => !r.Eligible) || !CurrentAssociation) { - StatusLabel.Text = - "Select 1–64 eligible DAs, explicitly edit each new value and keep the same online association. No Write sent."; + StatusLabel.Text = "Select 1–64 eligible attributes on the current IED; nothing was written."; + return; + } + + // The first Read for an uninspected row populates its type-aware + // editor without forcing the operator to invent a value in advance. + var firstRead = selected.Any(r => r.Preview is null); + if (!firstRead && selected.Any(r => !r.HasEdited)) + { + StatusLabel.Text = "Choose a different value for every selected DA before Write."; return; } @@ -141,15 +180,11 @@ private async void Inspect_Click(object sender, RoutedEventArgs e) if (!CurrentAssociation) throw new InvalidOperationException("IED association changed during inspection."); StatusLabel.Text = $"Read-only inspect {i + 1}/{selected.Length}: {row.ReferenceText}"; - row.Preview = null; var preview = await _runtime.PreviewBrowserDataAttributeWriteAsync( _device, row.Reference!.Value, _lifetime.Token); if (!CurrentAssociation) throw new InvalidOperationException("IED association changed during inspection."); - row.Preview = preview.IsAvailable ? preview : null; - row.Type = preview.Type?.MmsType ?? "unverified"; - row.Old = preview.CurrentText; - row.Status = preview.Message; + row.ApplyLivePreview(preview, preserveExplicitEdit: !firstRead); if (row.Preview is null) { StatusLabel.Text = "One DA has no valid live typed Read/GVA proof. No Write sent."; @@ -157,6 +192,11 @@ private async void Inspect_Click(object sender, RoutedEventArgs e) } } + if (firstRead) + { + StatusLabel.Text = "Live values loaded. Choose a new value and click Read to verify changes."; + return; + } if (!TryFreezePlan(selected, out _, out var rejection)) { StatusLabel.Text = rejection + " No Write sent."; @@ -306,6 +346,18 @@ public sealed class BrowserBatchWriteRow : INotifyPropertyChanged private string _status; private string _type = "Inspect first"; private string _old = "—"; + private bool _canEdit; + private bool _isBoolean; + public IReadOnlyList BooleanOptions { get; } = new[] { "false", "true" }; + public string ShortName => + Reference?.Item.Split((char)36).LastOrDefault() ?? ReferenceText; + public bool CanEdit => _canEdit; + public Visibility BooleanEditorVisibility => + _canEdit && _isBoolean ? Visibility.Visible : Visibility.Collapsed; + public Visibility TextEditorVisibility => + _canEdit && !_isBoolean ? Visibility.Visible : Visibility.Collapsed; + public Visibility PlaceholderVisibility => + _canEdit ? Visibility.Collapsed : Visibility.Visible; public MmsObjectReference? Reference { get; } public string ReferenceText { get; } @@ -326,6 +378,42 @@ public BrowserBatchWriteRow( _status = reason; } + public void ApplyLivePreview( + IedBrowserNativeWritePreview preview, bool preserveExplicitEdit) + { + Preview = preview.IsAvailable && preview.Type is not null && + preview.OldValue is not null ? preview : null; + Type = preview.Type?.MmsType ?? "unverified"; + Old = preview.CurrentText; + _isBoolean = preview.Type?.MmsType == "boolean"; + _canEdit = Eligible && Preview is not null && + (preview.Type!.MmsType is "boolean" or "integer" or "unsigned" or + "visible-string" or "mms-string" || + preview.Type.MmsType == "floating-point" && preview.Type.Size == 32); + Status = Preview is null ? preview.Message : + _canEdit ? "Ready to edit" : "Unsupported live MMS editor"; + if (!preserveExplicitEdit) + { + // Use the exact current wire value; never guess a textual enum + // description or coerce a Boolean 0/1 behind the operator. + _newValue = preview.OldValue?.Value switch + { + bool b => b ? "true" : "false", + string str => str, + long or ulong or int or uint or float or double => + Convert.ToString(preview.OldValue.Value, + System.Globalization.CultureInfo.InvariantCulture) ?? "", + _ => "" + }; + HasEdited = false; + Changed(nameof(NewValue)); + } + Changed(nameof(CanEdit)); + Changed(nameof(BooleanEditorVisibility)); + Changed(nameof(TextEditorVisibility)); + Changed(nameof(PlaceholderVisibility)); + } + public bool Selected { get => _selected; From c0a826f016259541142e824285315344564354be Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Fri, 23 Oct 2026 18:49:32 +0700 Subject: [PATCH 07/11] ux(write): keep Browser toolbar Write caption and button geometry stable on all row selections --- MainWindow.IedBrowser.TypedDataWrite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MainWindow.IedBrowser.TypedDataWrite.cs b/MainWindow.IedBrowser.TypedDataWrite.cs index b132804fe..96e8e852a 100644 --- a/MainWindow.IedBrowser.TypedDataWrite.cs +++ b/MainWindow.IedBrowser.TypedDataWrite.cs @@ -127,8 +127,8 @@ private void RefreshIedBrowserWriteButton() var selectedDo = ctlModel is null && selected is null ? BrowserWritableSelectedDo() : null; IedBrowserWriteButton.IsEnabled = ctlModel is not null || selected is not null || selectedDo is not null; - IedBrowserWriteButton.Content = ctlModel is not null ? "Write ctlModel…" : - selectedDo is not null ? "Write DAs…" : "Write DA…"; + // The button always reads "Write". Selection changes the action/tooltip, + // never the caption, width or Material pencil visual. IedBrowserWriteButton.ToolTip = ctlModel is not null ? "Read live ctlModel, choose IEC enum 0–4, confirm a guarded CF Write and verify readback." : selected is not null From e0cac1b3f8485da4ddbc67bda5bebea123e0a28f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Fri, 23 Oct 2026 18:49:52 +0700 Subject: [PATCH 08/11] ux(write): one consistent Browser Write button with user's exact Google Material edit path --- MainWindow.xaml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/MainWindow.xaml b/MainWindow.xaml index 96b976935..99cdc4003 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -2050,10 +2050,18 @@ Style="{StaticResource SoftButton}" Margin="0,0,6,0" IsEnabled="False" Click="IedBrowserControl_Click" ToolTip="Only enabled when this online DO has an existing live-verified native IEC Control descriptor."/> -