From 2f81e9183fcaf19d46b5f1ca801098dab79b2c79 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 24 Jun 2026 17:50:23 -0400 Subject: [PATCH] Phase-1 follow-up: avalonia-rule-formula-editor (Grammar rule editors) Stacked on the interlinear follow-up. Restore the rule-formula surface and activate it: - add back the 29 rule-formula files (the 5 plugins, RuleFormulaModel/RegionEditor + RuleCellCommands, PhEnvironment + BasicIPASymbol editors, projector/sinks/options + deriver, and all their tests incl. SupportingEditorComposeTests) - restore the 5 plugin registrations in RegionEditorPlugins - restore the 5 rule class names in the burn-down census - FLIP: register the 6 rule tools (PhonologicalRuleEdit, EnvironmentEdit, compoundRuleAdvancedEdit, naturalClassedit, phonemeEdit, AdhocCoprohibEdit) in LexicalEditFeatureCatalog under a new "Grammar rule editors" group (restoring the canonical pre-split active set + parity notes); Phase1FollowUpSurfaceTools is now empty (both edit follow-ups landed; browse table gated separately) - add the 6 rule TestCase rows to RegisteredRecordEditTools_ResolveToAvalonia Rebased onto the squashed phase1-base (post PR #964 review): tool registration is now catalog-driven, so this flip also adds a new "Grammar rule editors" group (6 rows) to the Tools->Options "Manage Individual Features" dialog under UIMode=New. Co-Authored-By: Claude Opus 4.8 (1M context) --- Src/Common/FwAvalonia/FwAvaloniaStrings.cs | 21 ++ .../BasicIPASymbolEditorTests.cs | 64 ++++ .../PhEnvironmentEditorTests.cs | 86 +++++ .../FwAvaloniaTests/RuleCellCommandsTests.cs | 44 +++ .../FwAvaloniaTests/RuleFormulaModelTests.cs | 117 ++++++ .../RuleFormulaRegionEditorTests.cs | 131 +++++++ .../FwAvalonia/LexicalEditFeatureCatalog.cs | 20 +- .../FwAvalonia/LexicalEditSurfaceRegistry.cs | 14 +- .../FwAvalonia/Region/BasicIPASymbolEditor.cs | 102 +++++ .../FwAvalonia/Region/PhEnvironmentEditor.cs | 192 ++++++++++ .../FwAvalonia/Region/RuleCellCommands.cs | 91 +++++ .../FwAvalonia/Region/RuleFormulaModel.cs | 240 ++++++++++++ .../Region/RuleFormulaRegionEditor.cs | 228 ++++++++++++ .../AffixRuleFormulaRegionEditorPlugin.cs | 47 +++ Src/xWorks/BasicIpaSymbolDeriver.cs | 104 ++++++ .../BasicIpaSymbolRegionEditorPlugin.cs | 88 +++++ Src/xWorks/MetaRuleFormulaEditSink.cs | 228 ++++++++++++ .../MetaRuleFormulaRegionEditorPlugin.cs | 55 +++ Src/xWorks/PhEnvironmentRegionEditorPlugin.cs | 94 +++++ Src/xWorks/RegionEditorPlugins.cs | 16 +- Src/xWorks/RuleFormulaEditSink.cs | 350 ++++++++++++++++++ Src/xWorks/RuleFormulaOptions.cs | 64 ++++ Src/xWorks/RuleFormulaProjector.cs | 234 ++++++++++++ Src/xWorks/RuleFormulaRegionEditorPlugin.cs | 63 ++++ .../BasicIpaSymbolEditWorkflowTests.cs | 78 ++++ .../xWorksTests/LexemeEditorBurnDownTests.cs | 18 +- .../PhEnvironmentEditWorkflowTests.cs | 73 ++++ .../xWorksTests/RecordEditViewSwitchTests.cs | 14 +- .../xWorksTests/RuleFormulaCompoundTests.cs | 104 ++++++ .../RuleFormulaEditWorkflowTests.cs | 192 ++++++++++ .../xWorksTests/RuleFormulaMetathesisTests.cs | 151 ++++++++ .../xWorksTests/RuleFormulaOptionsTests.cs | 56 +++ .../xWorksTests/RuleFormulaProjectorTests.cs | 175 +++++++++ .../RuleFormulaRegionEditorPluginTests.cs | 115 ++++++ .../SupportingEditorComposeTests.cs | 65 ++++ 35 files changed, 3706 insertions(+), 28 deletions(-) create mode 100644 Src/Common/FwAvalonia/FwAvaloniaTests/BasicIPASymbolEditorTests.cs create mode 100644 Src/Common/FwAvalonia/FwAvaloniaTests/PhEnvironmentEditorTests.cs create mode 100644 Src/Common/FwAvalonia/FwAvaloniaTests/RuleCellCommandsTests.cs create mode 100644 Src/Common/FwAvalonia/FwAvaloniaTests/RuleFormulaModelTests.cs create mode 100644 Src/Common/FwAvalonia/FwAvaloniaTests/RuleFormulaRegionEditorTests.cs create mode 100644 Src/Common/FwAvalonia/Region/BasicIPASymbolEditor.cs create mode 100644 Src/Common/FwAvalonia/Region/PhEnvironmentEditor.cs create mode 100644 Src/Common/FwAvalonia/Region/RuleCellCommands.cs create mode 100644 Src/Common/FwAvalonia/Region/RuleFormulaModel.cs create mode 100644 Src/Common/FwAvalonia/Region/RuleFormulaRegionEditor.cs create mode 100644 Src/xWorks/AffixRuleFormulaRegionEditorPlugin.cs create mode 100644 Src/xWorks/BasicIpaSymbolDeriver.cs create mode 100644 Src/xWorks/BasicIpaSymbolRegionEditorPlugin.cs create mode 100644 Src/xWorks/MetaRuleFormulaEditSink.cs create mode 100644 Src/xWorks/MetaRuleFormulaRegionEditorPlugin.cs create mode 100644 Src/xWorks/PhEnvironmentRegionEditorPlugin.cs create mode 100644 Src/xWorks/RuleFormulaEditSink.cs create mode 100644 Src/xWorks/RuleFormulaOptions.cs create mode 100644 Src/xWorks/RuleFormulaProjector.cs create mode 100644 Src/xWorks/RuleFormulaRegionEditorPlugin.cs create mode 100644 Src/xWorks/xWorksTests/BasicIpaSymbolEditWorkflowTests.cs create mode 100644 Src/xWorks/xWorksTests/PhEnvironmentEditWorkflowTests.cs create mode 100644 Src/xWorks/xWorksTests/RuleFormulaCompoundTests.cs create mode 100644 Src/xWorks/xWorksTests/RuleFormulaEditWorkflowTests.cs create mode 100644 Src/xWorks/xWorksTests/RuleFormulaMetathesisTests.cs create mode 100644 Src/xWorks/xWorksTests/RuleFormulaOptionsTests.cs create mode 100644 Src/xWorks/xWorksTests/RuleFormulaProjectorTests.cs create mode 100644 Src/xWorks/xWorksTests/RuleFormulaRegionEditorPluginTests.cs create mode 100644 Src/xWorks/xWorksTests/SupportingEditorComposeTests.cs diff --git a/Src/Common/FwAvalonia/FwAvaloniaStrings.cs b/Src/Common/FwAvalonia/FwAvaloniaStrings.cs index 5a1b5b6a89..e42bc59478 100644 --- a/Src/Common/FwAvalonia/FwAvaloniaStrings.cs +++ b/Src/Common/FwAvalonia/FwAvaloniaStrings.cs @@ -497,5 +497,26 @@ public static string StructuredTextParagraphName(string fieldLabel, int paragrap public static string FeatureAnalysesName => Text("FwAvalonia.Feature.AnalysesName", "Words Analyses"); public static string FeatureAnalysesDescription => Text("FwAvalonia.Feature.AnalysesDescription", "The interlinear morph-bundle editor."); + + /// Group heading for the avalonia-rule-formula-editor tool surfaces in the feature-manager dialog. + public static string FeatureGroupGrammarRuleEditors => Text("FwAvalonia.FeatureGroup.GrammarRuleEditors", "Grammar rule editors"); + + public static string FeaturePhonologicalRuleEditName => Text("FwAvalonia.Feature.PhonologicalRuleEditName", "Phonological Rules"); + public static string FeaturePhonologicalRuleEditDescription => Text("FwAvalonia.Feature.PhonologicalRuleEditDescription", "The regular and metathesis rule editors."); + + public static string FeatureEnvironmentEditName => Text("FwAvalonia.Feature.EnvironmentEditName", "Phonological Environments"); + public static string FeatureEnvironmentEditDescription => Text("FwAvalonia.Feature.EnvironmentEditDescription", "The phonological-environment string editor."); + + public static string FeatureCompoundRuleAdvancedEditName => Text("FwAvalonia.Feature.CompoundRuleAdvancedEditName", "Compound Rules"); + public static string FeatureCompoundRuleAdvancedEditDescription => Text("FwAvalonia.Feature.CompoundRuleAdvancedEditDescription", "The compound-rule editor."); + + public static string FeatureNaturalClassEditName => Text("FwAvalonia.Feature.NaturalClassEditName", "Natural Classes"); + public static string FeatureNaturalClassEditDescription => Text("FwAvalonia.Feature.NaturalClassEditDescription", "The natural-class editor."); + + public static string FeaturePhonemeEditName => Text("FwAvalonia.Feature.PhonemeEditName", "Phonemes"); + public static string FeaturePhonemeEditDescription => Text("FwAvalonia.Feature.PhonemeEditDescription", "The Basic IPA symbol editor."); + + public static string FeatureAdhocCoprohibEditName => Text("FwAvalonia.Feature.AdhocCoprohibEditName", "Ad Hoc Co-occurrence Rules"); + public static string FeatureAdhocCoprohibEditDescription => Text("FwAvalonia.Feature.AdhocCoprohibEditDescription", "The ad hoc co-occurrence-rule editor."); } } diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/BasicIPASymbolEditorTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/BasicIPASymbolEditorTests.cs new file mode 100644 index 0000000000..2f7eca59ec --- /dev/null +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/BasicIPASymbolEditorTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Linq; +using Avalonia.Controls; +using Avalonia.Headless.NUnit; +using Avalonia.Threading; +using Avalonia.VisualTree; +using NUnit.Framework; +using SIL.FieldWorks.Common.FwAvalonia.Region; +using FwAvaloniaTests.VisualChecks; +using FwAvaloniaDialogsTests; + +namespace FwAvaloniaTests +{ + /// + /// avalonia-rule-formula-editor (task 3.1) — the Basic IPA Symbol editor control: typing a symbol commits + /// through the sink; with no sink it is read-only. PNG + AssertNoCrowding. + /// + [TestFixture] + public class BasicIPASymbolEditorTests + { + private sealed class FakeSink : IBasicIpaSymbolCommandSink + { + public string LastCommitted; + public bool Commit(string symbol) { LastCommitted = symbol; return true; } + } + + private static TextBox Box(Control root) => root.GetVisualDescendants().OfType().First(); + + [AvaloniaTest] + public void Editable_CommitsTypedSymbol() + { + var sink = new FakeSink(); + var editor = new BasicIPASymbolEditor("p", sink); + var window = new Window { Content = editor, Width = 240, Height = 70 }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + window.UpdateLayout(); + Dispatcher.UIThread.RunJobs(); + + DialogSnapshot.Capture(window, "BasicIPASymbolEditor-01-editable"); + DialogLayoutAssert.AssertNoCrowding(editor); + + Box(editor).Text = "pʰ"; + Assert.That(editor.TryCommit(), Is.True); + Assert.That(sink.LastCommitted, Is.EqualTo("pʰ")); + Assert.That(editor.CommittedText, Is.EqualTo("pʰ")); + } + + [AvaloniaTest] + public void NoSink_IsReadOnly() + { + var editor = new BasicIPASymbolEditor("p", null); + var window = new Window { Content = editor, Width = 240, Height = 70 }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + + Assert.That(editor.IsEditable, Is.False); + Assert.That(Box(editor).IsReadOnly, Is.True); + } + } +} diff --git a/Src/Common/FwAvalonia/FwAvaloniaTests/PhEnvironmentEditorTests.cs b/Src/Common/FwAvalonia/FwAvaloniaTests/PhEnvironmentEditorTests.cs new file mode 100644 index 0000000000..37e984ed98 --- /dev/null +++ b/Src/Common/FwAvalonia/FwAvaloniaTests/PhEnvironmentEditorTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +using System.Linq; +using Avalonia.Controls; +using Avalonia.Headless.NUnit; +using Avalonia.Threading; +using Avalonia.VisualTree; +using NUnit.Framework; +using SIL.FieldWorks.Common.FwAvalonia.Region; +using FwAvaloniaTests.VisualChecks; +using FwAvaloniaDialogsTests; + +namespace FwAvaloniaTests +{ + /// + /// avalonia-rule-formula-editor (task 3.2) — the environment editor control: commit a valid string, + /// reject an invalid one (restore the last committed value + show the error). PNG + AssertNoCrowding. + /// + [TestFixture] + public class PhEnvironmentEditorTests + { + private sealed class FakeSink : IPhEnvironmentCommandSink + { + public string LastCommitted; + // "valid" = anything that is non-empty and balanced-ish; the real recognizer is tested in xWorks. + public bool Validate(string representation) => !(representation ?? "").Contains("X"); + public bool Commit(string representation) { LastCommitted = representation; return true; } + } + + private static (PhEnvironmentEditor Editor, Window Window, FakeSink Sink) Show(string initial) + { + var sink = new FakeSink(); + var editor = new PhEnvironmentEditor(initial, sink); + var window = new Window { Content = editor, Width = 360, Height = 120 }; + window.Show(); + Dispatcher.UIThread.RunJobs(); + window.UpdateLayout(); + Dispatcher.UIThread.RunJobs(); + return (editor, window, sink); + } + + private static TextBox Box(Control root) => + root.GetVisualDescendants().OfType().First(); + + [AvaloniaTest] + public void ValidEdit_Commits() + { + var (editor, window, sink) = Show("/ _ #"); + DialogSnapshot.Capture(window, "PhEnvironmentEditor-01-initial"); + DialogLayoutAssert.AssertNoCrowding(editor); + + Box(editor).Text = "/ a _ #"; + Assert.That(editor.TryCommit(), Is.True); + Assert.That(sink.LastCommitted, Is.EqualTo("/ a _ #")); + Assert.That(editor.CommittedText, Is.EqualTo("/ a _ #")); + } + + [AvaloniaTest] + public void InvalidEdit_IsRejected_AndRestoresLastCommitted() + { + var (editor, window, sink) = Show("/ _ #"); + + Box(editor).Text = "/ X _ #"; // the fake validator rejects anything containing X + Assert.That(editor.TryCommit(), Is.False); + Assert.That(sink.LastCommitted, Is.Null, "an invalid string is never committed"); + Assert.That(Box(editor).Text, Is.EqualTo("/ _ #"), "the box restores the last committed value"); + + DialogSnapshot.Capture(window, "PhEnvironmentEditor-02-invalid"); + DialogLayoutAssert.AssertNoCrowding(editor); + } + + [AvaloniaTest] + public void InsertToolbar_OffersTheLiteralInserts() + { + var (editor, _, _) = Show(string.Empty); + var ids = editor.GetVisualDescendants().OfType