From b6ba149e17aae7c28690a8b8c749d1e3921ffa1f Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 14 Aug 2026 09:14:25 -0400 Subject: [PATCH 1/3] feat: record Avalonia UI mode in crash reports Add the persisted UIMode and UIModeDisabledTools values to the ErrorReporter property bag alongside the other environment info, so a crash report shows whether Avalonia UI was active and which tools opted out, without duplicating anything the trace covers. --- Src/xWorks/FwXWindow.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Src/xWorks/FwXWindow.cs b/Src/xWorks/FwXWindow.cs index 8423d592d5..5fb2d610e8 100644 --- a/Src/xWorks/FwXWindow.cs +++ b/Src/xWorks/FwXWindow.cs @@ -558,6 +558,10 @@ internal static void SeedUIModeProperties(PropertyTable propertyTable, string se propertyTable.SetProperty(UIFrameworkResolver.UIModeDisabledToolsPropertyName, settingsDisabledTools ?? string.Empty, false); propertyTable.SetPropertyPersistence(UIFrameworkResolver.UIModeDisabledToolsPropertyName, false); + + // Record for crash reports; everything else about the crash comes from the trace. + ErrorReporter.AddProperty("AvaloniaUIMode", UIFrameworkResolver.NormalizeUIMode(settingsUiMode)); + ErrorReporter.AddProperty("AvaloniaDisabledTools", settingsDisabledTools ?? string.Empty); } /// ------------------------------------------------------------------------------------ From cc9725bab338356f2e275162fa522d8c83531868 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 14 Aug 2026 10:59:56 -0400 Subject: [PATCH 2/3] test: cover Avalonia crash-report properties in UI-mode seeding Assert that SeedUIModeProperties records AvaloniaUIMode and AvaloniaDisabledTools on ErrorReporter, so a regression in that wiring surfaces as a test failure instead of only in a real crash report. --- .../Avalonia/Hosting/FwXWindowUIModeSeedingTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Src/xWorks/xWorksTests/Avalonia/Hosting/FwXWindowUIModeSeedingTests.cs b/Src/xWorks/xWorksTests/Avalonia/Hosting/FwXWindowUIModeSeedingTests.cs index 2b7b06b2f7..28ea309a21 100644 --- a/Src/xWorks/xWorksTests/Avalonia/Hosting/FwXWindowUIModeSeedingTests.cs +++ b/Src/xWorks/xWorksTests/Avalonia/Hosting/FwXWindowUIModeSeedingTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using SIL.FieldWorks.Common.FwAvalonia; +using SIL.Reporting; using XCore; namespace SIL.FieldWorks.XWorks @@ -33,6 +34,8 @@ public void TearDown() { m_propertyTable.Dispose(); m_mediator.Dispose(); + ErrorReport.Properties.Remove("AvaloniaUIMode"); + ErrorReport.Properties.Remove("AvaloniaDisabledTools"); } [TestCase("New", "New")] @@ -61,5 +64,14 @@ public void SeedUIModeProperties_SeedsDisabledTools_AndNullBecomesEmpty() Assert.That(m_propertyTable.GetStringProperty( UIFrameworkResolver.UIModeDisabledToolsPropertyName, null), Is.EqualTo("")); } + + [Test] + public void SeedUIModeProperties_RecordsModeAndDisabledToolsForCrashReports() + { + FwXWindow.SeedUIModeProperties(m_propertyTable, "New", "lexiconEdit,posEdit"); + + Assert.That(ErrorReport.Properties["AvaloniaUIMode"], Is.EqualTo("New")); + Assert.That(ErrorReport.Properties["AvaloniaDisabledTools"], Is.EqualTo("lexiconEdit,posEdit")); + } } } From aa06e676695eebac69760e312282d6a96e79881e Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 14 Aug 2026 14:52:04 -0400 Subject: [PATCH 3/3] fix: keep Avalonia crash-report properties live, not startup-only Addresses review feedback on PR #1066: AvaloniaUIMode and AvaloniaDisabledTools only reflected the values seeded at window construction, so a crash after an in-session UI-mode toggle showed the stale startup values instead of what was active at crash time. LexOptionsDlg.m_btnOK_Click and the new AvaloniaOptionsDialogLauncher ApplyUiModeLive/ApplyDisabledToolsLive helpers now call ErrorReporter.AddProperty alongside the existing PropertyTable broadcast, so the crash-report value tracks every live change instead of only the initial seed. Covered by new tests in LexOptionsDlgTests and AvaloniaOptionsDialogLauncherTests. Co-Authored-By: Claude Sonnet 5 --- .../Avalonia/AvaloniaOptionsDialogLauncher.cs | 28 ++++++++----- Src/LexText/LexTextControls/LexOptionsDlg.cs | 3 ++ .../AvaloniaOptionsDialogLauncherTests.cs | 42 +++++++++++++++++++ .../Avalonia/LexOptionsDlgTests.cs | 10 +++++ 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/Src/LexText/LexTextControls/Avalonia/AvaloniaOptionsDialogLauncher.cs b/Src/LexText/LexTextControls/Avalonia/AvaloniaOptionsDialogLauncher.cs index 5a4a3f2150..c0deabcd4a 100644 --- a/Src/LexText/LexTextControls/Avalonia/AvaloniaOptionsDialogLauncher.cs +++ b/Src/LexText/LexTextControls/Avalonia/AvaloniaOptionsDialogLauncher.cs @@ -335,14 +335,7 @@ private static OptionsPayload Apply(LcmCache cache, Mediator mediator, PropertyT // LexOptionsDlg's m_pendingUiModeDisabledTools apply. Live like the mode itself — no restart. var newDisabledTools = state.UIModeDisabledTools ?? string.Empty; if ((settings.UIModeDisabledTools ?? string.Empty) != newDisabledTools) - { - settings.UIModeDisabledTools = newDisabledTools; - if (propertyTable != null) - { - propertyTable.SetProperty(UIFrameworkResolver.UIModeDisabledToolsPropertyName, newDisabledTools, true); - propertyTable.SetPropertyPersistence(UIFrameworkResolver.UIModeDisabledToolsPropertyName, false); - } - } + ApplyDisabledToolsLive(propertyTable, settings, newDisabledTools); // User-interface language: switch the current thread's UI culture live (matches // LexOptionsDlg.m_btnOK_Click), then persist registry value + project WS + reload the string @@ -492,8 +485,9 @@ private static void UninstallPlugin(XmlDocument managerDoc, Mediator mediator, s } // Applies a UI-mode change live: persist into settings + mirror+broadcast through the PropertyTable - // so the open views (RecordBrowseView/RecordEditView) re-resolve without a restart. - private static void ApplyUiModeLive(PropertyTable propertyTable, FwApplicationSettingsBase settings, string mode) + // so the open views (RecordBrowseView/RecordEditView) re-resolve without a restart, and updates the + // crash-report value to match so a report reflects the mode active at crash time, not just at startup. + internal static void ApplyUiModeLive(PropertyTable propertyTable, FwApplicationSettingsBase settings, string mode) { var norm = NormalizeUiMode(mode); settings.UIMode = norm; @@ -502,6 +496,20 @@ private static void ApplyUiModeLive(PropertyTable propertyTable, FwApplicationSe propertyTable.SetProperty(UIModePropertyName, norm, true); propertyTable.SetPropertyPersistence(UIModePropertyName, false); } + ErrorReporter.AddProperty("AvaloniaUIMode", norm); + } + + // Applies a disabled-tools change live: persist into settings + mirror+broadcast through the + // PropertyTable, and keep the crash-report value in step, mirroring ApplyUiModeLive. + internal static void ApplyDisabledToolsLive(PropertyTable propertyTable, FwApplicationSettingsBase settings, string disabledTools) + { + settings.UIModeDisabledTools = disabledTools; + if (propertyTable != null) + { + propertyTable.SetProperty(UIFrameworkResolver.UIModeDisabledToolsPropertyName, disabledTools, true); + propertyTable.SetPropertyPersistence(UIFrameworkResolver.UIModeDisabledToolsPropertyName, false); + } + ErrorReporter.AddProperty("AvaloniaDisabledTools", disabledTools); } internal static string NormalizeUiMode(string mode) => diff --git a/Src/LexText/LexTextControls/LexOptionsDlg.cs b/Src/LexText/LexTextControls/LexOptionsDlg.cs index 82f770566f..e915ba082f 100644 --- a/Src/LexText/LexTextControls/LexOptionsDlg.cs +++ b/Src/LexText/LexTextControls/LexOptionsDlg.cs @@ -19,6 +19,7 @@ using SIL.LCModel.Utils; using SIL.LCModel; using SIL.PlatformUtilities; +using SIL.Reporting; using SIL.Settings; using SIL.Utils; using XCore; @@ -154,6 +155,8 @@ private void m_btnOK_Click(object sender, EventArgs e) m_propertyTable.SetProperty(UIModePropertyName, newUiMode, true); m_propertyTable.SetPropertyPersistence(UIModePropertyName, false); } + // Keep the crash-report value in step with a live toggle, not just the startup seed. + ErrorReporter.AddProperty("AvaloniaUIMode", newUiMode); } m_sNewUserWs = m_userInterfaceChooser.NewUserWs; diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/AvaloniaOptionsDialogLauncherTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/AvaloniaOptionsDialogLauncherTests.cs index 123109df41..1b57eaf87b 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/AvaloniaOptionsDialogLauncherTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/AvaloniaOptionsDialogLauncherTests.cs @@ -25,6 +25,48 @@ namespace LexTextControlsTests [TestFixture] public class AvaloniaOptionsDialogLauncherTests { + [TearDown] + public void TearDown() + { + ErrorReport.Properties.Remove("AvaloniaUIMode"); + ErrorReport.Properties.Remove("AvaloniaDisabledTools"); + } + + // ----- ApplyUiModeLive / ApplyDisabledToolsLive (the crash-report mirror behind Apply) ----- + + [Test] + public void ApplyUiModeLive_PersistsBroadcastsAndRecordsForCrashReports() + { + var settings = new TestFwApplicationSettings { UIMode = "Legacy" }; + using (var mediator = new Mediator()) + using (var propertyTable = new PropertyTable(mediator)) + { + SIL.FieldWorks.LexText.Controls.AvaloniaOptionsDialogLauncher.ApplyUiModeLive( + propertyTable, settings, "new"); + + Assert.That(settings.UIMode, Is.EqualTo("New")); + Assert.That(propertyTable.GetStringProperty("UIMode", "Legacy"), Is.EqualTo("New")); + Assert.That(ErrorReport.Properties["AvaloniaUIMode"], Is.EqualTo("New")); + } + } + + [Test] + public void ApplyDisabledToolsLive_PersistsBroadcastsAndRecordsForCrashReports() + { + var settings = new TestFwApplicationSettings { UIMode = "New" }; + using (var mediator = new Mediator()) + using (var propertyTable = new PropertyTable(mediator)) + { + SIL.FieldWorks.LexText.Controls.AvaloniaOptionsDialogLauncher.ApplyDisabledToolsLive( + propertyTable, settings, "lexiconEdit,posEdit"); + + Assert.That(settings.UIModeDisabledTools, Is.EqualTo("lexiconEdit,posEdit")); + Assert.That(propertyTable.GetStringProperty("UIModeDisabledTools", null), + Is.EqualTo("lexiconEdit,posEdit")); + Assert.That(ErrorReport.Properties["AvaloniaDisabledTools"], Is.EqualTo("lexiconEdit,posEdit")); + } + } + // ----- UIModeGates.ShouldUseAvaloniaUI (the shared New-mode gate, hoisted to FwUtils so checking // it never loads the Avalonia assemblies) ----- diff --git a/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/LexOptionsDlgTests.cs b/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/LexOptionsDlgTests.cs index a68f3b5ccb..2121ec06ba 100644 --- a/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/LexOptionsDlgTests.cs +++ b/Src/LexText/LexTextControls/LexTextControlsTests/Avalonia/LexOptionsDlgTests.cs @@ -14,6 +14,12 @@ namespace LexTextControlsTests [Apartment(System.Threading.ApartmentState.STA)] public class LexOptionsDlgTests { + [TearDown] + public void TearDown() + { + ErrorReport.Properties.Remove("AvaloniaUIMode"); + } + [Test] public void OkClick_SavesUIModeAndMirrorsItIntoPropertyTable() { @@ -39,6 +45,8 @@ public void OkClick_SavesUIModeAndMirrorsItIntoPropertyTable() Assert.That(propertyTable.GetStringProperty("UIMode", "Legacy"), Is.EqualTo("New")); // UIMode flips live (RecordEditView settles and re-resolves on the spot) — no restart needed. Assert.That(dlg.RestartPromptCount, Is.EqualTo(0)); + Assert.That(ErrorReport.Properties["AvaloniaUIMode"], Is.EqualTo("New"), + "a live toggle must update the crash-report value, not just the startup seed"); } } @@ -61,6 +69,8 @@ public void OkClick_LeavesLegacyWhenUserDoesNotChangeSelection() Assert.That(settings.SaveCalls, Is.EqualTo(1)); Assert.That(propertyTable.GetStringProperty("UIMode", "Legacy"), Is.EqualTo("Legacy")); Assert.That(dlg.RestartPromptCount, Is.EqualTo(0)); + Assert.That(ErrorReport.Properties.ContainsKey("AvaloniaUIMode"), Is.False, + "an unchanged selection must not touch the crash-report value"); } }