Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@
<recordList owner="MorphologicalData" property="Phonemes"/>
</clerk>
<clerk id="naturalClasses">
<recordList owner="MorphologicalData" property="NaturalClasses"/>
<recordList owner="MorphologicalData" property="NaturalClasses">
<dynamicloaderinfo assemblyPath="MorphologyEditorDll.dll" class="SIL.FieldWorks.XWorks.MorphologyEditor.NaturalClassRecordList"/>
</recordList>
</clerk>
<clerk id="environments">
<recordList owner="MorphologicalData" property="Environments"/>
Expand Down
4 changes: 3 additions & 1 deletion Src/LexText/Morphology/AffixRuleFormulaControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,9 @@ public override void SetMappingNaturalClass(SelectionHelper sel = null)
var natClasses = new HashSet<ICmObject>();
foreach (var nc in m_cache.LangProject.PhonologicalDataOA.NaturalClassesOS)
{
if (nc.ClassID == PhNCFeaturesTags.kClassId)
// Rule mappings only allow feature-based, user-defined natural classes
// to be selected.
if (nc is IPhNCFeatures featNC && IsFeatureBasedNCUserDefined(featNC))
natClasses.Add(nc);
}
var selectedNc = DisplayChooser(MEStrings.ksRuleNCOpt, MEStrings.ksRuleNCChooserLink,
Expand Down
6 changes: 5 additions & 1 deletion Src/LexText/Morphology/AffixRuleFormulaSlice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ public bool OnMappingSetNaturalClass(object args)
public bool OnDisplayMappingJumpToNaturalClass(object commandObject, ref XCore.UIItemDisplayProperties display)
{
CheckDisposed();
bool enable = AffixRuleFormulaControl.IsNCIndexCurrent;
// Note that a mapping's Natural Class is always feature-based.
// For feature-based NCs, we only want to allow jumping to the NC
// if it is user-defined.
var mapping = AffixRuleFormulaControl.CurrentObject as IMoModifyFromInput;
bool enable = mapping != null && RuleFormulaControl.IsFeatureBasedNCUserDefined(mapping.ModificationRA);
display.Enabled = enable;
display.Visible = enable;
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using SIL.LCModel;

namespace SIL.FieldWorks.XWorks.MorphologyEditor
{
/// <summary>
/// Tests to confirm that <see cref="NaturalClassRecordList"/> includes every
/// natural class except a feature-based one generated automatically for a rule.
/// </summary>
[TestFixture]
public class NaturalClassRecordListTests : RuleFormulaVcTestBase
{
private IEnumerable<int> NCRecordList(params ICmObject[] natClasses)
{
return NaturalClassRecordList.ExcludeAutoGenerated(
natClasses.Select(nc => nc.Hvo), Cache);
}
[Test]
public void AutoGeneratedClass_IsExcluded()
{
IPhNCFeatures natClass = AddAutoGeneratedFeatureNaturalClass("s to n",
new Dictionary<string, string> { { "vd", "+" } });

// Since the only NC is autogenerated, the NC record list should be empty
Assert.That(NCRecordList(natClass), Is.Empty);
}

[Test]
public void NamedFeatureClass_IsIncluded()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new Dictionary<string, string> { { "vd", "+" } });

Assert.That(NCRecordList(natClass), Is.EqualTo(new[] { natClass.Hvo }));
}

/// <summary>
/// A feature-based class the user creates starts with no name, so an unnamed class must
/// be included for the user to be able to name it.
/// </summary>
[Test]
public void UnnamedFeatureClass_IsIncluded()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(null, null,
new Dictionary<string, string> { { "vd", "+" } });

Assert.That(NCRecordList(natClass), Is.EqualTo(new[] { natClass.Hvo }));
}

[Test]
public void SegmentClass_IsIncluded()
{
IPhNCSegments natClass = AddSegmentNaturalClass("Vowels", "V");

Assert.That(NCRecordList(natClass), Is.EqualTo(new[] { natClass.Hvo }));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,62 @@ public class RuleFormulaControlNaturalClassNameTests : RuleFormulaVcTestBase
[Test]
public void NoClass_IsNotUserDefined()
{
Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(null), Is.False);
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(null), Is.False);
}

/// <summary>
/// The check answers whether a class carries a name the user gave it, and an unnamed
/// class carries none. Set Phonological Features relies on that answer to keep
/// offering feature edits on unnamed classes (LT-22576).
/// A user defined class can have no name, while an auto-generated one will always have
/// a generated name. Check that if the class has no name, it is flagged as user defined.
/// </summary>
[Test]
public void ClassWithNoName_IsNotUserDefined()
public void ClassWithNoName_IsUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(null, "Vd",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False,
"an unnamed class carries no name the user gave it");
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True,
"an unnamed class must be user defined");
}

/// <summary>
/// A user defined class can have an empty name, while an auto-generated one will always
/// have a nonempty generated name. Check that if the class's name is empty,
/// it is flagged as user defined.
/// </summary>
[Test]
public void ClassWithAnEmptyName_IsNotUserDefined()
public void ClassWithAnEmptyName_IsUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(string.Empty, "Vd",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False,
"an empty name is not a name the user gave the class");
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True,
"a class with an empty name must be user defined");
}

[Test]
public void ClassNamedForARule_IsNotUserDefined()
{
IPhNCFeatures natClass = AddRuleNamedFeatureNaturalClass("s to n",
IPhNCFeatures natClass = AddAutoGeneratedFeatureNaturalClass("s to n",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False);
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.False);
}

/// <summary>
/// Projects hold rule-generated names that carry a prefix ahead of the generated text, so
/// the generated text is recognised wherever it sits in the name.
/// If a project contains a NC whose name was generated but a user has added text before
/// or after the generated name, it is user defined.
/// </summary>
[Test]
public void ClassNamedForARuleBehindAPrefix_IsNotUserDefined()
public void ClassNamedForARuleWithUserAssignedPrefixOrPostfix_IsUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass(null, null,
new FeatVals { { "vd", "+" } });
natClass.Name.SetUserWritingSystem("Phonemes s and t - "
+ string.Format(MEStrings.ksRuleNCFeatsName, "s to n"));

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.False);
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True);
natClass.Name.SetUserWritingSystem(string.Format(MEStrings.ksRuleNCFeatsName, "s to n")
+ " - phonemes s and t");
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True);
}

[Test]
Expand All @@ -79,7 +85,7 @@ public void NamedClass_IsUserDefined()
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd",
new FeatVals { { "vd", "+" } });

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.True);
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True);
}

/// <summary>
Expand All @@ -101,16 +107,52 @@ public void ClassNamedInAnAnalysisWritingSystemOtherThanTheUserOne_IsUserDefined
"the name must reach the check through the analysis writing system, "
+ "not through a placeholder");

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.True);
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True);
}

[Test]
public void NamedClassWithNoFeatures_IsUserDefined()
{
IPhNCFeatures natClass = AddFeatureNaturalClass("Voiced", "Vd", new FeatVals());

Assert.That(RuleFormulaControl.IsFeatureBasedNCNameUserDefined(natClass), Is.True,
Assert.That(RuleFormulaControl.IsFeatureBasedNCUserDefined(natClass), Is.True,
"a name qualifies a class whatever it carries for features");
}

/// <summary>
/// No class is neither user defined nor auto-generated.
/// </summary>
[Test]
public void NullClass_IsNotAutoGenerated()
{
Assert.That(RuleFormulaControl.IsAutoGeneratedNaturalClass(null), Is.False);
}

/// <summary>
/// A translation may put the rule name first, leaving no generated text before the
/// placeholder. Its generated names are still recognized, but ordinary names are not.
/// </summary>
[Test]
public void FormatStartingWithRuleName_RecognisesGeneratedName()
{
const string format = "{0} rule: created automatically";

Assert.That(RuleFormulaControl.IsAutoGeneratedName(string.Format(format, "s to n"), format),
Is.True, "a name generated from the format is recognized");
Assert.That(RuleFormulaControl.IsAutoGeneratedName("Voiced", format), Is.False,
"an empty stub before the rule name must not match every name");
}

/// <summary>
/// A translation lacking the rule name placeholder must not break recognition; no name
/// counts as generated from it.
/// </summary>
[Test]
public void FormatWithoutPlaceholder_RecognisesNothing()
{
const string format = "Created automatically for rule";

Assert.That(RuleFormulaControl.IsAutoGeneratedName(format, format), Is.False);
}
}
}
Loading
Loading