From af9cb4ff08381653edede8d5a71071a47742f6de Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 13:56:16 +0700 Subject: [PATCH 01/18] chore: stage atomic control model patch --- .p0/Models__SignalDefinition.cs.patch | 266 ++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 .p0/Models__SignalDefinition.cs.patch diff --git a/.p0/Models__SignalDefinition.cs.patch b/.p0/Models__SignalDefinition.cs.patch new file mode 100644 index 00000000..e15c6f12 --- /dev/null +++ b/.p0/Models__SignalDefinition.cs.patch @@ -0,0 +1,266 @@ +--- a/Models/SignalDefinition.cs ++++ b/Models/SignalDefinition.cs +@@ -1,6 +1,8 @@ + using System.Text.RegularExpressions; + + namespace ArIED61850Tester.Models; ++ ++public sealed record ControlCommandClaim(long Sequence, string RequestedValue, string ActionLabel); + + public class SignalDefinition : ObservableObject + { +@@ -24,7 +26,11 @@ + private string? _deferredControlCurrentValue; + private string _controlSetPointText = string.Empty; + private string _controlLastResult = string.Empty; +- private bool _controlIsBusy; ++ private readonly object _controlCommandSync = new(); ++ private bool _controlInspectionBusy; ++ private bool _controlCommandBusy; ++ private long _nextControlCommandSequence; ++ private long _activeControlCommandSequence; + private bool _controlInterlockCheck = true; + private bool _controlSynchroCheck; + private bool _controlTestMode; +@@ -133,7 +139,7 @@ + set + { + var normalized = NormalizeControlDisplayValue(value); +- if (ControlIsBusy) ++ if (ControlCommandBusy) + { + _deferredControlCurrentValue = normalized; + return; +@@ -167,30 +173,19 @@ + Set(ref _controlLastResult, normalized); + } + } +- public bool ControlIsBusy ++ public bool ControlInspectionBusy + { +- get => _controlIsBusy; ++ get => _controlInspectionBusy; + set + { +- if (!Set(ref _controlIsBusy, value)) ++ if (!Set(ref _controlInspectionBusy, value)) + return; +- +- Raise(nameof(ControlCanConfirm)); +- +- if (value) +- { +- _deferredControlCurrentValue = null; +- return; +- } +- +- if (_deferredControlCurrentValue != null) +- { +- var deferred = _deferredControlCurrentValue; +- _deferredControlCurrentValue = null; +- ApplyControlCurrentValue(deferred); +- } ++ Raise(nameof(ControlIsBusy)); + } + } ++ ++ public bool ControlCommandBusy => _controlCommandBusy; ++ public bool ControlIsBusy => _controlInspectionBusy || _controlCommandBusy; + + public bool ControlInterlockCheck + { +@@ -215,28 +210,178 @@ + public string ControlPendingAction => _controlPendingAction; + public string ControlPendingConfirmationLabel => + string.IsNullOrWhiteSpace(_controlPendingAction) ? "Confirm command" : $"Confirm {_controlPendingAction}"; +- public bool ControlCanConfirm => _controlConfirmationPending && ControlSupportsOperate && !ControlIsBusy; +- +- public void StageControlConfirmation(string requestedValue, string actionLabel) ++ public bool ControlCanConfirm => _controlConfirmationPending && ControlSupportsOperate && !ControlCommandBusy; ++ ++ public bool TryStageControlConfirmation(string requestedValue, string actionLabel, out string rejectionReason) + { + var normalizedValue = requestedValue?.Trim() ?? string.Empty; +- if (string.IsNullOrWhiteSpace(normalizedValue)) +- return; +- +- Set(ref _controlPendingValue, normalizedValue, nameof(ControlPendingValue)); +- if (Set(ref _controlPendingAction, actionLabel?.Trim() ?? string.Empty, nameof(ControlPendingAction))) +- Raise(nameof(ControlPendingConfirmationLabel)); +- if (Set(ref _controlConfirmationPending, true, nameof(ControlConfirmationPending))) +- Raise(nameof(ControlCanConfirm)); ++ var normalizedAction = actionLabel?.Trim() ?? string.Empty; ++ lock (_controlCommandSync) ++ { ++ if (!ControlSupportsOperate) ++ { ++ rejectionReason = "the live control model does not permit operation"; ++ return false; ++ } ++ if (_controlCommandBusy) ++ { ++ rejectionReason = "another command already owns this control object"; ++ return false; ++ } ++ if (string.IsNullOrWhiteSpace(normalizedValue) || string.IsNullOrWhiteSpace(normalizedAction)) ++ { ++ rejectionReason = "the requested command value or action is empty"; ++ return false; ++ } ++ ++ _controlPendingValue = normalizedValue; ++ _controlPendingAction = normalizedAction; ++ _controlConfirmationPending = true; ++ } ++ ++ Raise(nameof(ControlPendingValue)); ++ Raise(nameof(ControlPendingAction)); ++ Raise(nameof(ControlPendingConfirmationLabel)); ++ Raise(nameof(ControlConfirmationPending)); ++ Raise(nameof(ControlCanConfirm)); ++ rejectionReason = string.Empty; ++ return true; ++ } ++ ++ public bool TryClaimControlConfirmation(out ControlCommandClaim? claim, out string rejectionReason) ++ { ++ lock (_controlCommandSync) ++ { ++ if (!_controlConfirmationPending) ++ { ++ claim = null; ++ rejectionReason = "no pending confirmation exists"; ++ return false; ++ } ++ if (!ControlSupportsOperate) ++ { ++ claim = null; ++ rejectionReason = "the live control model no longer permits operation"; ++ return false; ++ } ++ if (_controlCommandBusy) ++ { ++ claim = null; ++ rejectionReason = "another command already owns this control object"; ++ return false; ++ } ++ if (string.IsNullOrWhiteSpace(_controlPendingValue)) ++ { ++ claim = null; ++ rejectionReason = "the pending command value is empty"; ++ return false; ++ } ++ ++ var sequence = ++_nextControlCommandSequence; ++ _activeControlCommandSequence = sequence; ++ _controlCommandBusy = true; ++ claim = new ControlCommandClaim(sequence, _controlPendingValue, _controlPendingAction); ++ _controlPendingValue = string.Empty; ++ _controlPendingAction = string.Empty; ++ _controlConfirmationPending = false; ++ } ++ ++ PublishCommandClaimed(); ++ rejectionReason = string.Empty; ++ return true; ++ } ++ ++ public bool TryBeginDirectControlCommand( ++ string requestedValue, ++ string actionLabel, ++ out ControlCommandClaim? claim, ++ out string rejectionReason) ++ { ++ var normalizedValue = requestedValue?.Trim() ?? string.Empty; ++ var normalizedAction = actionLabel?.Trim() ?? normalizedValue; ++ lock (_controlCommandSync) ++ { ++ if (!ControlSupportsOperate) ++ { ++ claim = null; ++ rejectionReason = "the live control model does not permit operation"; ++ return false; ++ } ++ if (_controlCommandBusy) ++ { ++ claim = null; ++ rejectionReason = "another command already owns this control object"; ++ return false; ++ } ++ if (string.IsNullOrWhiteSpace(normalizedValue)) ++ { ++ claim = null; ++ rejectionReason = "the requested command value is empty"; ++ return false; ++ } ++ ++ var sequence = ++_nextControlCommandSequence; ++ _activeControlCommandSequence = sequence; ++ _controlCommandBusy = true; ++ claim = new ControlCommandClaim(sequence, normalizedValue, normalizedAction); ++ _controlPendingValue = string.Empty; ++ _controlPendingAction = string.Empty; ++ _controlConfirmationPending = false; ++ } ++ ++ PublishCommandClaimed(); ++ rejectionReason = string.Empty; ++ return true; ++ } ++ ++ public bool CompleteControlCommand(ControlCommandClaim claim) ++ { ++ string? deferred = null; ++ lock (_controlCommandSync) ++ { ++ if (!_controlCommandBusy || _activeControlCommandSequence != claim.Sequence) ++ return false; ++ ++ _activeControlCommandSequence = 0; ++ _controlCommandBusy = false; ++ deferred = _deferredControlCurrentValue; ++ _deferredControlCurrentValue = null; ++ } ++ ++ Raise(nameof(ControlCommandBusy)); ++ Raise(nameof(ControlIsBusy)); ++ Raise(nameof(ControlCanConfirm)); ++ if (deferred != null) ++ ApplyControlCurrentValue(deferred); ++ return true; + } + + public void ClearControlConfirmation() + { +- Set(ref _controlPendingValue, string.Empty, nameof(ControlPendingValue)); +- if (Set(ref _controlPendingAction, string.Empty, nameof(ControlPendingAction))) +- Raise(nameof(ControlPendingConfirmationLabel)); +- if (Set(ref _controlConfirmationPending, false, nameof(ControlConfirmationPending))) +- Raise(nameof(ControlCanConfirm)); ++ lock (_controlCommandSync) ++ { ++ _controlPendingValue = string.Empty; ++ _controlPendingAction = string.Empty; ++ _controlConfirmationPending = false; ++ } ++ ++ Raise(nameof(ControlPendingValue)); ++ Raise(nameof(ControlPendingAction)); ++ Raise(nameof(ControlPendingConfirmationLabel)); ++ Raise(nameof(ControlConfirmationPending)); ++ Raise(nameof(ControlCanConfirm)); ++ } ++ ++ private void PublishCommandClaimed() ++ { ++ _deferredControlCurrentValue = null; ++ Raise(nameof(ControlPendingValue)); ++ Raise(nameof(ControlPendingAction)); ++ Raise(nameof(ControlPendingConfirmationLabel)); ++ Raise(nameof(ControlConfirmationPending)); ++ Raise(nameof(ControlCommandBusy)); ++ Raise(nameof(ControlIsBusy)); ++ Raise(nameof(ControlCanConfirm)); + } + + private bool CanExposeControlActions => !_controlModelResolved || ControlSupportsOperate; From 0a535cb803d84d1d08a0821dd60aecab1cb03945 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 13:57:00 +0700 Subject: [PATCH 02/18] chore: stage atomic confirmation workflow patch --- .p0/MainWindow.xaml.cs.patch | 208 +++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 .p0/MainWindow.xaml.cs.patch diff --git a/.p0/MainWindow.xaml.cs.patch b/.p0/MainWindow.xaml.cs.patch new file mode 100644 index 00000000..a77a6bc6 --- /dev/null +++ b/.p0/MainWindow.xaml.cs.patch @@ -0,0 +1,208 @@ +--- a/MainWindow.xaml.cs ++++ b/MainWindow.xaml.cs +@@ -970,17 +970,20 @@ + await Task.WhenAll(candidates.Select(async signal => + { + await throttle.WaitAsync(_applicationCancellation.Token); +- signal.ControlIsBusy = true; ++ signal.ControlInspectionBusy = true; + try + { + var capabilities = await _runtime.InspectControlAsync( + device.DeviceId, + signal, + _applicationCancellation.Token); +- signal.ControlCurrentValue = capabilities.CurrentValue; +- signal.ControlLastResult = capabilities.SupportsOperate +- ? capabilities.ControlModelText +- : "Control unavailable"; ++ if (!signal.ControlCommandBusy) ++ { ++ signal.ControlCurrentValue = capabilities.CurrentValue; ++ signal.ControlLastResult = capabilities.SupportsOperate ++ ? capabilities.ControlModelText ++ : "Control unavailable"; ++ } + } + catch (OperationCanceledException) + { +@@ -992,7 +995,7 @@ + } + finally + { +- signal.ControlIsBusy = false; ++ signal.ControlInspectionBusy = false; + throttle.Release(); + } + })); +@@ -1006,29 +1009,48 @@ + { + if (sender is not Button button || button.Tag is not SignalDefinition signal || !signal.IsPositionControl) + return; +- if (signal.ControlIsBusy || !signal.ControlSupportsOperate) +- return; +- ++ ++ var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice; + var requestedValue = button.CommandParameter?.ToString()?.Trim() ?? string.Empty; + var actionLabel = button.Content?.ToString()?.Trim() ?? string.Empty; +- if (string.IsNullOrWhiteSpace(requestedValue) || string.IsNullOrWhiteSpace(actionLabel)) +- return; +- +- signal.StageControlConfirmation(requestedValue, actionLabel); ++ AddLog("INFO", device?.Name ?? "IED", ++ $"Control confirmation stage click received: {signal.ObjectReference}; action={actionLabel}; value={requestedValue}; commandBusy={signal.ControlCommandBusy}; inspectionBusy={signal.ControlInspectionBusy}."); ++ ++ if (!signal.TryStageControlConfirmation(requestedValue, actionLabel, out var rejectionReason)) ++ { ++ signal.ControlLastResult = $"Command not staged: {rejectionReason}."; ++ AddLog("WARN", device?.Name ?? "IED", ++ $"Control confirmation stage rejected: {signal.ObjectReference}; reason={rejectionReason}."); ++ SetStatus($"{device?.Name ?? "IED"}: {signal.Name} command not staged — {rejectionReason}."); ++ return; ++ } ++ ++ AddLog("INFO", device?.Name ?? "IED", ++ $"Control confirmation staged: {signal.ObjectReference}; action={actionLabel}; value={requestedValue}."); ++ SetStatus($"{device?.Name ?? "IED"}: review {signal.Name} — {signal.ControlPendingConfirmationLabel}, or Cancel."); ++ } ++ ++ private async void ControlConfirmAction_Click(object sender, RoutedEventArgs e) ++ { ++ if (sender is not Button button || button.Tag is not SignalDefinition signal) ++ return; ++ + var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice; +- SetStatus($"{device?.Name ?? "IED"}: review {signal.Name} — {signal.ControlPendingConfirmationLabel}, or Cancel."); +- } +- +- private async void ControlConfirmAction_Click(object sender, RoutedEventArgs e) +- { +- if (sender is not Button button || button.Tag is not SignalDefinition signal) +- return; +- if (!signal.ControlCanConfirm || string.IsNullOrWhiteSpace(signal.ControlPendingValue)) +- return; +- +- var requestedValue = signal.ControlPendingValue; +- signal.ClearControlConfirmation(); +- await ExecuteQuickControlAsync(signal, requestedValue); ++ AddLog("INFO", device?.Name ?? "IED", ++ $"Confirm click received: {signal.ObjectReference}; pending={signal.ControlConfirmationPending}; action={signal.ControlPendingAction}; value={signal.ControlPendingValue}; commandBusy={signal.ControlCommandBusy}; inspectionBusy={signal.ControlInspectionBusy}."); ++ ++ if (!signal.TryClaimControlConfirmation(out var claim, out var rejectionReason) || claim == null) ++ { ++ signal.ControlLastResult = $"Confirmation rejected: {rejectionReason}."; ++ AddLog("WARN", device?.Name ?? "IED", ++ $"Confirm rejected: {signal.ObjectReference}; reason={rejectionReason}."); ++ SetStatus($"{device?.Name ?? "IED"}: {signal.Name} confirmation rejected — {rejectionReason}."); ++ return; ++ } ++ ++ AddLog("INFO", device?.Name ?? "IED", ++ $"Confirm accepted: {signal.ObjectReference}; sequence={claim.Sequence}; action={claim.ActionLabel}; value={claim.RequestedValue}."); ++ await ExecuteClaimedControlAsync(signal, claim); + } + + private void ControlCancelAction_Click(object sender, RoutedEventArgs e) +@@ -1039,6 +1061,8 @@ + var action = signal.ControlPendingAction; + signal.ClearControlConfirmation(); + var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice; ++ AddLog("INFO", device?.Name ?? "IED", ++ $"Control confirmation cancelled: {signal.ObjectReference}; action={action}."); + SetStatus($"{device?.Name ?? "IED"}: {signal.Name} {action} cancelled before dispatch."); + } + +@@ -1056,27 +1080,37 @@ + return; + } + +- await ExecuteQuickControlAsync(signal, requestedValue); +- } +- +- private async Task ExecuteQuickControlAsync(SignalDefinition signal, string requestedValue) ++ var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice; ++ if (!signal.TryBeginDirectControlCommand( ++ requestedValue, ++ button.Content?.ToString()?.Trim() ?? requestedValue, ++ out var claim, ++ out var rejectionReason) || claim == null) ++ { ++ signal.ControlLastResult = $"Command rejected: {rejectionReason}."; ++ AddLog("WARN", device?.Name ?? "IED", ++ $"Direct control rejected: {signal.ObjectReference}; reason={rejectionReason}."); ++ SetStatus($"{device?.Name ?? "IED"}: {signal.Name} command rejected — {rejectionReason}."); ++ return; ++ } ++ ++ await ExecuteClaimedControlAsync(signal, claim); ++ } ++ ++ private async Task ExecuteClaimedControlAsync(SignalDefinition signal, ControlCommandClaim claim) + { + var device = _signalOwners.TryGetValue(signal, out var owner) ? owner : SelectedDevice; + if (device == null) +- return; +- +- if (signal.ControlIsBusy) + { +- SetStatus($"{device.Name}: {signal.Name} command is already in progress."); ++ signal.CompleteControlCommand(claim); + return; + } + + var clickStopwatch = Stopwatch.StartNew(); +- signal.ControlIsBusy = true; +- signal.ControlLastResult = $"Dispatching {requestedValue}…"; +- SetStatus($"{device.Name}: dispatching {signal.Name} = {requestedValue}…"); ++ signal.ControlLastResult = $"Dispatching {claim.RequestedValue}…"; ++ SetStatus($"{device.Name}: dispatching {signal.Name} = {claim.RequestedValue}…"); + AddLog("INFO", device.Name, +- $"Control click accepted: {signal.ObjectReference} value={requestedValue}; test={signal.ControlTestMode}; interlock={signal.ControlInterlockCheck}; synchro={signal.ControlSynchroCheck}."); ++ $"Dispatch ownership acquired: {signal.ObjectReference}; sequence={claim.Sequence}; value={claim.RequestedValue}; test={signal.ControlTestMode}; interlock={signal.ControlInterlockCheck}; synchro={signal.ControlSynchroCheck}."); + await Dispatcher.Yield(DispatcherPriority.Render); + + try +@@ -1091,15 +1125,14 @@ + return; + } + +- // ExecuteControlAsync owns one live status preflight and the complete control +- // sequence. The old UI path performed a second status read before this call, +- // which added queue latency and created a stale-value race. ++ AddLog("INFO", device.Name, ++ $"MMS command submitted: {signal.ObjectReference}; sequence={claim.Sequence}; value={claim.RequestedValue}."); + var result = await _runtime.ExecuteControlAsync( + device.DeviceId, + new Iec61850ControlCommandRequest + { + Signal = signal, +- ValueText = requestedValue, ++ ValueText = claim.RequestedValue, + InterlockCheck = signal.ControlInterlockCheck, + SynchroCheck = signal.ControlSynchroCheck, + TestMode = signal.ControlTestMode, +@@ -1119,7 +1152,7 @@ + SetStatus($"{device.Name}: {signal.Name} — {signal.ControlLastResult}"); + clickStopwatch.Stop(); + AddLog(result.IsSuccess ? "INFO" : "WARN", device.Name, +- $"Control UI timing: {signal.ObjectReference}; click-to-result={clickStopwatch.Elapsed.TotalMilliseconds:0.###} ms; engine-total={result.TotalElapsedText}; serviceAccepted={result.ServiceAccepted}; stage={result.Stage}."); ++ $"Control UI timing: {signal.ObjectReference}; sequence={claim.Sequence}; click-to-result={clickStopwatch.Elapsed.TotalMilliseconds:0.###} ms; engine-total={result.TotalElapsedText}; serviceAccepted={result.ServiceAccepted}; stage={result.Stage}."); + } + catch (OperationCanceledException) + { +@@ -1135,8 +1168,12 @@ + } + finally + { +- signal.ControlIsBusy = false; +- signal.ClearControlConfirmation(); ++ if (!signal.CompleteControlCommand(claim)) ++ { ++ AddLog("ERROR", device.Name, ++ $"Control ownership release mismatch: {signal.ObjectReference}; sequence={claim.Sequence}."); ++ MarkDiagnosticAlert(); ++ } + } + } + From 4df92b12ccd310bae30e2dd87feab77ef685a6d1 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 13:57:22 +0700 Subject: [PATCH 03/18] chore: stage command panel busy-state patch --- .p0/MainWindow.CommandPanelUx.cs.patch | 47 ++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .p0/MainWindow.CommandPanelUx.cs.patch diff --git a/.p0/MainWindow.CommandPanelUx.cs.patch b/.p0/MainWindow.CommandPanelUx.cs.patch new file mode 100644 index 00000000..94876c66 --- /dev/null +++ b/.p0/MainWindow.CommandPanelUx.cs.patch @@ -0,0 +1,47 @@ +--- a/MainWindow.CommandPanelUx.cs ++++ b/MainWindow.CommandPanelUx.cs +@@ -253,7 +253,7 @@ + { + // One MMS association is serialized. Do not queue background ctlModel + // inspection while an operator command owns the session. +- if (device.CommandSignals.Any(signal => signal.ControlIsBusy)) ++ if (device.CommandSignals.Any(signal => signal.ControlCommandBusy)) + continue; + + var candidates = device.Signals +@@ -272,7 +272,7 @@ + await Task.WhenAll(candidates.Select(async signal => + { + await throttle.WaitAsync(_applicationCancellation.Token); +- signal.ControlIsBusy = true; ++ signal.ControlInspectionBusy = true; + try + { + var capabilities = await _runtime.InspectControlAsync( +@@ -280,10 +280,13 @@ + signal, + _applicationCancellation.Token); + +- signal.ControlCurrentValue = capabilities.CurrentValue; +- signal.ControlLastResult = capabilities.SupportsOperate +- ? string.Empty +- : "Not available"; ++ if (!signal.ControlCommandBusy) ++ { ++ signal.ControlCurrentValue = capabilities.CurrentValue; ++ signal.ControlLastResult = capabilities.SupportsOperate ++ ? string.Empty ++ : "Not available"; ++ } + } + catch (OperationCanceledException) + { +@@ -297,7 +300,7 @@ + } + finally + { +- signal.ControlIsBusy = false; ++ signal.ControlInspectionBusy = false; + throttle.Release(); + } + })); From 3fe295bb3cbb41693b72e3c63fc8e1fd48eac717 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 13:57:31 +0700 Subject: [PATCH 04/18] chore: stage command feedback ownership patch --- .p0/MainWindow.ControlDiagnostics.cs.patch | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .p0/MainWindow.ControlDiagnostics.cs.patch diff --git a/.p0/MainWindow.ControlDiagnostics.cs.patch b/.p0/MainWindow.ControlDiagnostics.cs.patch new file mode 100644 index 00000000..a4fd08a5 --- /dev/null +++ b/.p0/MainWindow.ControlDiagnostics.cs.patch @@ -0,0 +1,22 @@ +--- a/MainWindow.ControlDiagnostics.cs ++++ b/MainWindow.ControlDiagnostics.cs +@@ -379,7 +379,7 @@ + { + state.StableConfirmed = true; + SetStableFeedbackResult(state); +- if (!state.Signal.ControlIsBusy) ++ if (!state.Signal.ControlCommandBusy) + RemovePositionCommand(state.Key); + return; + } +@@ -413,8 +413,8 @@ + return; + } + +- if (propertyName == nameof(SignalDefinition.ControlIsBusy) && +- !state.Signal.ControlIsBusy && state.StableConfirmed) ++ if (propertyName == nameof(SignalDefinition.ControlCommandBusy) && ++ !state.Signal.ControlCommandBusy && state.StableConfirmed) + { + SetStableFeedbackResult(state); + RemovePositionCommand(state.Key); From fdf8e706a4d956eb1f77eeaa1d844dbf45f0fde5 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 13:57:44 +0700 Subject: [PATCH 05/18] chore: stage single confirm button patch --- .p0/MainWindow.xaml.patch | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .p0/MainWindow.xaml.patch diff --git a/.p0/MainWindow.xaml.patch b/.p0/MainWindow.xaml.patch new file mode 100644 index 00000000..7173af44 --- /dev/null +++ b/.p0/MainWindow.xaml.patch @@ -0,0 +1,38 @@ +--- a/MainWindow.xaml ++++ b/MainWindow.xaml +@@ -544,32 +544,9 @@ + + + +- +- ++ -- -+ - +