diff --git a/Src/LexText/LexTextControls/PatternView.cs b/Src/LexText/LexTextControls/PatternView.cs index 27bbb76eca..5337604c6b 100644 --- a/Src/LexText/LexTextControls/PatternView.cs +++ b/Src/LexText/LexTextControls/PatternView.cs @@ -69,6 +69,15 @@ protected override EditingHelper CreateEditingHelper() return new PatternEditingHelper(Cache, this); } + /// + /// Activate() is disabled by default in ReadOnlyViews, but a pattern editor does want to + /// show selections so the user can see what a chooser insert/delete will act on. + /// + protected override bool AllowDisplaySelection + { + get { return true; } + } + public void Init(Mediator mediator, PropertyTable propertyTable, int hvo, IPatternControl patternControl, PatternVcBase vc, int rootFrag, ISilDataAccess sda) { CheckDisposed(); diff --git a/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaControlWiringTests.cs b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaControlWiringTests.cs new file mode 100644 index 0000000000..abfecba4a7 --- /dev/null +++ b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaControlWiringTests.cs @@ -0,0 +1,46 @@ +// 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 NUnit.Framework; + +namespace SIL.FieldWorks.XWorks.MorphologyEditor +{ + /// + /// Constructing each real rule formula control must produce a read-only rootsite, since a + /// rule cell is modifiable only by chooser-insert and delete. + /// + [TestFixture] + public class RuleFormulaControlWiringTests + { + [Test] + public void RegRuleFormulaControl_RootSiteIsReadOnly() + { + using (var control = new RegRuleFormulaControl(null)) + { + Assert.That(control.RootSite.ReadOnlyView, Is.True, + "RegRuleFormulaControl must wire up a read-only rootsite"); + } + } + + [Test] + public void MetaRuleFormulaControl_RootSiteIsReadOnly() + { + using (var control = new MetaRuleFormulaControl(null)) + { + Assert.That(control.RootSite.ReadOnlyView, Is.True, + "MetaRuleFormulaControl must wire up a read-only rootsite"); + } + } + + [Test] + public void AffixRuleFormulaControl_RootSiteIsReadOnly() + { + using (var control = new AffixRuleFormulaControl(null)) + { + Assert.That(control.RootSite.ReadOnlyView, Is.True, + "AffixRuleFormulaControl must wire up a read-only rootsite"); + } + } + } +} diff --git a/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaDirectEditReproTests.cs b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaDirectEditReproTests.cs new file mode 100644 index 0000000000..a3ffc56c84 --- /dev/null +++ b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaDirectEditReproTests.cs @@ -0,0 +1,279 @@ +// 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.Windows.Forms; +using NUnit.Framework; +using SIL.LCModel; +using SIL.LCModel.Core.Text; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Infrastructure; +using SIL.FieldWorks.Common.RootSites; +using SIL.FieldWorks.Common.ViewsInterfaces; +using SIL.FieldWorks.LexText.Controls; +using XCore; + +namespace SIL.FieldWorks.XWorks.MorphologyEditor +{ + /// + /// Drives a real IVwRootBox (the managed Views engine), hosted by a live + /// PatternView/RegRuleFormulaVc pair against a real in-memory LcmCache, and calls + /// IVwSelection.ReplaceWithTsString directly -- the same low-level entry point IME + /// composition or drag-and-drop would use, and one PatternView.OnKeyPress never sees + /// because it only reacts to Windows key events. + /// + [TestFixture] + public class RuleFormulaDirectEditReproTests : MemoryOnlyBackendProviderTestBase + { + private Mediator m_mediator; + private PropertyTable m_propertyTable; + private TestPatternView m_view; + + public override void TestSetup() + { + base.TestSetup(); + m_mediator = new Mediator(); + m_propertyTable = new PropertyTable(m_mediator); + m_propertyTable.SetProperty("cache", Cache, false); + } + + public override void TestTearDown() + { + if (m_view != null) + { + m_view.Dispose(); + m_view = null; + } + if (m_propertyTable != null) + { + m_propertyTable.Dispose(); + m_propertyTable = null; + } + if (m_mediator != null) + { + m_mediator.Dispose(); + m_mediator = null; + } + base.TestTearDown(); + } + + /// Minimal no-op IPatternControl -- sufficient because we never drive + /// selection through the chooser/insert/delete UI in this test; we only need + /// PatternView's selection-changed handler not to crash when we install a + /// selection directly. + private class NullPatternControl : IPatternControl + { + public object GetContext(SelectionHelper sel) => null; + public object GetContext(SelectionHelper sel, SelectionHelper.SelLimitType limit) => null; + public object GetItem(SelectionHelper sel, SelectionHelper.SelLimitType limit) => null; + public int GetItemContextIndex(object ctxt, object obj) => -1; + public SelLevInfo[] GetLevelInfo(object ctxt, int index) => null; + public int GetContextCount(object ctxt) => 0; + public object GetNextContext(object ctxt) => null; + public object GetPrevContext(object ctxt) => null; + public int GetFlid(object ctxt) => 0; + } + + /// Exposes the protected layout hook so the view can be laid out + /// headlessly. + private class TestPatternView : PatternView + { + public void CallLayout() + { + OnLayout(new LayoutEventArgs(this, string.Empty)); + } + + public void SimulateKeyDown(Keys key) + { + var e = new KeyEventArgs(key); + OnKeyDown(e); + } + + public bool TestAllowDisplaySelection => AllowDisplaySelection; + } + + private IPhPhoneme CreatePhoneme(string name) + { + IPhPhoneme p = null; + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + Cache.LangProject.PhonologicalDataOA.PhonemeSetsOS.Add( + Cache.ServiceLocator.GetInstance().Create()); + p = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.PhonemeSetsOS[0].PhonemesOC.Add(p); + p.Name.SetVernacularDefaultWritingSystem(name); + }); + return p; + } + + /// + /// Builds a real regular-rule RHS whose left context is a single phoneme, hosts it in a + /// live PatternView/RegRuleFormulaVc pair, and returns the phoneme plus the live view. + /// + private (IPhPhoneme phoneme, TestPatternView view) BuildLiveRuleFormulaView(string phonemeName) + { + IPhPhoneme phoneme = CreatePhoneme(phonemeName); + IPhSegRuleRHS rhs = null; + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + var rule = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.PhonRulesOS.Add(rule); + rhs = Cache.ServiceLocator.GetInstance().Create(); + rule.RightHandSidesOS.Add(rhs); + var segCtxt = Cache.ServiceLocator.GetInstance().Create(); + rhs.LeftContextOA = segCtxt; + segCtxt.FeatureStructureRA = phoneme; + }); + + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var view = new TestPatternView { Cache = Cache, Visible = false, Width = 300, Height = 60 }; + view.Init(m_mediator, m_propertyTable, rhs.Hvo, new NullPatternControl(), vc, RegRuleFormulaVc.kfragRHS, + Cache.MainCacheAccessor); + view.ReadOnlyView = true; + view.CallLayout(); + m_view = view; + return (phoneme, view); + } + + /// + /// Builds a real regular-rule RHS whose left context is a natural class special-cased to + /// display only its abbreviation ("C" or "V"), + /// hosts it in a live PatternView/RegRuleFormulaVc pair, and returns the natural class + /// plus the live view. + /// + private (IPhNaturalClass naturalClass, TestPatternView view) BuildLiveRuleFormulaViewWithNaturalClass(string abbr) + { + IPhNaturalClass nc = null; + IPhSegRuleRHS rhs = null; + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + nc = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.NaturalClassesOS.Add(nc); + nc.Name.SetAnalysisDefaultWritingSystem("Test Class"); + nc.Abbreviation.SetAnalysisDefaultWritingSystem(abbr); + + var rule = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.PhonRulesOS.Add(rule); + rhs = Cache.ServiceLocator.GetInstance().Create(); + rule.RightHandSidesOS.Add(rhs); + var ncCtxt = Cache.ServiceLocator.GetInstance().Create(); + rhs.LeftContextOA = ncCtxt; + ncCtxt.FeatureStructureRA = nc; + // GetNumLines(ncCtxt) must be exactly 1 to hit the "C"/"V" abbreviation-only + // branch; one plus-constraint variable is the cheapest way to make that so. + var constraint = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.FeatConstraintsOS.Add(constraint); + ncCtxt.PlusConstrRS.Add(constraint); + }); + + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var view = new TestPatternView { Cache = Cache, Visible = false, Width = 300, Height = 60 }; + view.Init(m_mediator, m_propertyTable, rhs.Hvo, new NullPatternControl(), vc, RegRuleFormulaVc.kfragRHS, + Cache.MainCacheAccessor); + view.ReadOnlyView = true; + view.CallLayout(); + m_view = view; + return (nc, view); + } + + /// + /// Selects the whole displayed natural-class abbreviation (via its object path from the + /// RHS root, bypassing PatternView.OnKeyPress entirely) and replaces its text directly + /// through IVwSelection.ReplaceWithTsString. A natural class is shared by every rule that + /// references it, so an edit landing here is a project-wide rename, exactly like the + /// phoneme case. + /// + [Test] + public void ReplaceWithTsString_OnNaturalClassAbbreviation_BypassesOnKeyPress_AndShouldNotRenameTheClass() + { + var (naturalClass, view) = BuildLiveRuleFormulaViewWithNaturalClass("C"); + + var levels = new[] + { + new SelLevInfo { tag = PhSimpleContextNCTags.kflidFeatureStructure, ihvo = 0 }, + new SelLevInfo { tag = PhSegRuleRHSTags.kflidLeftContext, ihvo = 0 } + }; + IVwSelection sel = view.RootBox.MakeTextSelInObj(0, levels.Length, levels, 0, null, + true, false, false, /* fWholeObj */ true, /* fInstall */ true); + Assert.That(sel, Is.Not.Null, + "could not construct a selection over the natural class's abbreviation display -- fixture/path assumption is wrong"); + + ITsString corrupted = TsStringUtils.MakeString("CORRUPTED", Cache.DefaultAnalWs); + + UndoableUnitOfWorkHelper.Do("undo", "redo", naturalClass, () => sel.ReplaceWithTsString(corrupted)); + + string abbrAfter = naturalClass.Abbreviation.AnalysisDefaultWritingSystem.Text; + Assert.That(abbrAfter, Is.EqualTo("C"), + "an edit that bypassed PatternView.OnKeyPress altered the real, project-wide " + + "PhNaturalClass.Abbreviation (got '" + abbrAfter + "')"); + } + + /// + /// Selects the whole displayed phoneme (via its object path from the RHS root, bypassing + /// any WM_CHAR-level filtering entirely -- PatternView.OnKeyPress is never invoked here) + /// and replaces its text directly through IVwSelection.ReplaceWithTsString, exactly the + /// kind of call an IME composition commit or a drag-and-drop would make. + /// + [Test] + public void ReplaceWithTsString_OnPhonemeTerminalUnit_BypassesOnKeyPress_AndShouldNotRenameThePhoneme() + { + var (phoneme, view) = BuildLiveRuleFormulaView("p"); + + var levels = new[] + { + new SelLevInfo { tag = PhSimpleContextSegTags.kflidFeatureStructure, ihvo = 0 }, + new SelLevInfo { tag = PhSegRuleRHSTags.kflidLeftContext, ihvo = 0 } + }; + IVwSelection sel = view.RootBox.MakeTextSelInObj(0, levels.Length, levels, 0, null, + true, false, false, /* fWholeObj */ true, /* fInstall */ true); + Assert.That(sel, Is.Not.Null, + "could not construct a selection over the phoneme's terminal-unit display -- fixture/path assumption is wrong"); + + ITsString corrupted = TsStringUtils.MakeString("CORRUPTED", Cache.DefaultVernWs); + + // The rootsite's own low-level text-replacement API, which bypasses the WM_CHAR + // filter. The unit of work is required for any edit to commit, not part of the + // bypass. + UndoableUnitOfWorkHelper.Do("undo", "redo", phoneme, () => sel.ReplaceWithTsString(corrupted)); + + string nameAfter = phoneme.Name.VernacularDefaultWritingSystem.Text; + Assert.That(nameAfter, Is.EqualTo("p"), + "an edit that bypassed PatternView.OnKeyPress altered the real, project-wide " + + "PhPhoneme.Name (got '" + nameAfter + "')"); + } + + /// + /// A read-only rootsite must not prevent PatternView's own Delete-key handling, which + /// removes items through RemoveItemsRequested rather than by editing text. + /// + [Test] + public void DeleteKey_StillRaisesRemoveItemsRequested_WhenRootsiteIsReadOnly() + { + var (_, view) = BuildLiveRuleFormulaView("p"); + Assert.That(view.ReadOnlyView, Is.True, "fixture assumption: the rootsite is read-only"); + + bool removeRequested = false; + view.RemoveItemsRequested += (sender, e) => removeRequested = true; + + view.SimulateKeyDown(Keys.Delete); + + Assert.That(removeRequested, Is.True, + "Delete must still raise RemoveItemsRequested when the rootsite is read-only"); + } + + /// + /// A read-only rootsite suppresses Activate() by default + /// (SimpleRootSite.AllowDisplaySelection), + /// which would hide the selection a chooser insert/delete needs the user to see. + /// + [Test] + public void AllowDisplaySelection_IsTrue_WhenRootsiteIsReadOnly() + { + var (_, view) = BuildLiveRuleFormulaView("p"); + Assert.That(view.ReadOnlyView, Is.True, "fixture assumption: the rootsite is read-only"); + + Assert.That(view.TestAllowDisplaySelection, Is.True, + "the selection must still be shown even though the rootsite is read-only"); + } + } +} diff --git a/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaVcBaseEditabilityTests.cs b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaVcBaseEditabilityTests.cs new file mode 100644 index 0000000000..79a381efa1 --- /dev/null +++ b/Src/LexText/Morphology/MorphologyEditorDllTests/RuleFormulaVcBaseEditabilityTests.cs @@ -0,0 +1,349 @@ +// 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; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using NUnit.Framework; +using SIL.LCModel; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Infrastructure; +using SIL.FieldWorks.Common.ViewsInterfaces; +using XCore; + +namespace SIL.FieldWorks.XWorks.MorphologyEditor +{ + /// + /// The rule formula view is modifiable only by chooser-insert and delete, so + /// RuleFormulaVcBase must mark the natural-class abbreviation and terminal-unit + /// (phoneme/boundary) name fragments non-editable; both bind directly to the + /// referenced object's own live string field. + /// + [TestFixture] + public class RuleFormulaVcBaseEditabilityTests : MemoryOnlyBackendProviderTestBase + { + private Mediator m_mediator; + private PropertyTable m_propertyTable; + + public override void TestSetup() + { + base.TestSetup(); + m_mediator = new Mediator(); + m_propertyTable = new PropertyTable(m_mediator); + } + + public override void TestTearDown() + { + if (m_propertyTable != null) + { + m_propertyTable.Dispose(); + m_propertyTable = null; + } + if (m_mediator != null) + { + m_mediator.Dispose(); + m_mediator = null; + } + base.TestTearDown(); + } + + private IPhNaturalClass CreateNaturalClass(string abbr) + { + IPhNaturalClass nc = null; + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + nc = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.NaturalClassesOS.Add(nc); + nc.Name.SetAnalysisDefaultWritingSystem("Test Class"); + nc.Abbreviation.SetAnalysisDefaultWritingSystem(abbr); + }); + return nc; + } + + private IPhPhoneme CreatePhoneme(string name) + { + IPhPhoneme p = null; + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + Cache.LangProject.PhonologicalDataOA.PhonemeSetsOS.Add( + Cache.ServiceLocator.GetInstance().Create()); + p = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PhonologicalDataOA.PhonemeSetsOS[0].PhonemesOC.Add(p); + p.Name.SetVernacularDefaultWritingSystem(name); + }); + return p; + } + + /// + /// The natural-class abbreviation fragment (kfragNC) is rendered via AddStringAltMember + /// directly against the natural class's own Abbreviation field. + /// + [Test] + public void Display_NaturalClassAbbreviationFragment_IsMarkedNotEditable() + { + IPhNaturalClass nc = CreateNaturalClass("Stp"); + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, nc.Hvo, RuleFormulaVcBase.kfragNC); + + Assert.That(env.StringAltMemberCalls, Is.Not.Empty, + "expected AddStringAltMember to be called for the NC abbreviation fragment"); + foreach (var call in env.StringAltMemberCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "the natural class abbreviation must not be directly editable in the rule formula view"); + } + } + + /// + /// The terminal-unit (phoneme/boundary) fragment (kfragTerminalUnit) is rendered via + /// AddStringAltMember directly against the terminal unit's own Name field -- a live + /// write channel into the phoneme's real, project-wide name. + /// + [Test] + public void Display_TerminalUnitNameFragment_IsMarkedNotEditable() + { + IPhPhoneme phoneme = CreatePhoneme("p"); + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, phoneme.Hvo, RuleFormulaVcBase.kfragTerminalUnit); + + Assert.That(env.StringAltMemberCalls, Is.Not.Empty, + "expected AddStringAltMember to be called for the terminal unit name fragment"); + foreach (var call in env.StringAltMemberCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "the phoneme/boundary name must not be directly editable in the rule formula view"); + } + } + + /// Same defect, exercised through the metathesis-rule view + /// constructor. + [Test] + public void Display_TerminalUnitNameFragment_ViaMetaRuleFormulaVc_IsMarkedNotEditable() + { + IPhPhoneme phoneme = CreatePhoneme("t"); + var vc = new MetaRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, phoneme.Hvo, RuleFormulaVcBase.kfragTerminalUnit); + + Assert.That(env.StringAltMemberCalls, Is.Not.Empty); + foreach (var call in env.StringAltMemberCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "metathesis rule formula view shares the same defect as the base class"); + } + } + + /// Same defect, exercised through the affix-process view constructor. + [Test] + public void Display_TerminalUnitNameFragment_ViaAffixRuleFormulaVc_IsMarkedNotEditable() + { + IPhPhoneme phoneme = CreatePhoneme("k"); + var vc = new AffixRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, phoneme.Hvo, RuleFormulaVcBase.kfragTerminalUnit); + + Assert.That(env.StringAltMemberCalls, Is.Not.Empty); + foreach (var call in env.StringAltMemberCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "affix process rule formula view shares the same defect as the base class"); + } + } + + /// + /// The feature-value line (kfragFeature) is a computed "abbreviation value" string bound + /// to a fake tag, not free text. + /// + [Test] + public void Display_FeatureLineFragment_IsMarkedNotEditable() + { + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, 0, RuleFormulaVcBase.kfragFeature); + + Assert.That(env.AddPropCalls, Is.Not.Empty, + "expected AddProp to be called for the feature-line fragment"); + foreach (var call in env.AddPropCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "the feature-value line must not be directly editable in the rule formula view"); + } + } + + /// The plus-variable line (kfragPlusVariable) is a computed string bound to a + /// fake tag, not free text. + [Test] + public void Display_PlusVariableLineFragment_IsMarkedNotEditable() + { + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, 0, RuleFormulaVcBase.kfragPlusVariable); + + Assert.That(env.AddPropCalls, Is.Not.Empty, + "expected AddProp to be called for the plus-variable fragment"); + foreach (var call in env.AddPropCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "the plus-variable line must not be directly editable in the rule formula view"); + } + } + + /// The minus-variable line (kfragMinusVariable) is a computed string bound to a + /// fake tag, not free text. + [Test] + public void Display_MinusVariableLineFragment_IsMarkedNotEditable() + { + var vc = new RegRuleFormulaVc(Cache, m_propertyTable); + var env = new EditabilityRecordingEnv(); + + vc.Display(env, 0, RuleFormulaVcBase.kfragMinusVariable); + + Assert.That(env.AddPropCalls, Is.Not.Empty, + "expected AddProp to be called for the minus-variable fragment"); + foreach (var call in env.AddPropCalls) + { + Assert.That(call.EditableAtCallTime, Is.EqualTo((int)TptEditable.ktptNotEditable), + "the minus-variable line must not be directly editable in the rule formula view"); + } + } + + /// + /// Records enough of IVwEnv's calls to observe, at the moment AddStringAltMember binds a + /// fragment to a real domain-object field, whether the view constructor had most recently + /// set the ktptEditable property to NotEditable. All other members are unused by the + /// fragments under test and throw if hit, so a future change that routes through a + /// different IVwEnv member will fail loudly rather than silently pass. + /// + private class EditabilityRecordingEnv : IVwEnv + { + public struct Call + { + public int Tag; + public int Ws; + public int EditableAtCallTime; + } + + public struct PropCall + { + public int Tag; + public int Frag; + public int EditableAtCallTime; + } + + public List StringAltMemberCalls = new List(); + public List AddPropCalls = new List(); + + private int m_currentEditable = int.MinValue; // sentinel: never set + + public void AddStringAltMember(int tag, int ws, IVwViewConstructor _vwvc) + { + StringAltMemberCalls.Add(new Call { Tag = tag, Ws = ws, EditableAtCallTime = m_currentEditable }); + } + + public void AddProp(int tag, IVwViewConstructor _vwvc, int frag) + { + AddPropCalls.Add(new PropCall { Tag = tag, Frag = frag, EditableAtCallTime = m_currentEditable }); + } + + public void set_IntProperty(int tpt, int tpv, int nValue) + { + if (tpt == (int)FwTextPropType.ktptEditable) + m_currentEditable = nValue; + } + + public ITsTextProps Props + { + set { /* not relevant to editability of these two fragments */ } + } + + public void get_StringWidth(ITsString _tss, ITsTextProps _ttp, out int dmpx, out int dmpy) + { + dmpx = 0; + dmpy = 0; + } + + public int OpenObject + { + get { throw new NotImplementedException(); } + } + + public int EmbeddingLevel + { + get { return 0; } + } + + public ISilDataAccess DataAccess + { + get { throw new NotImplementedException(); } + } + + public void AddObjProp(int tag, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddObjVec(int tag, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddObjVecItems(int tag, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddReversedObjVecItems(int tag, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddObj(int hvo, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddLazyVecItems(int tag, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddLazyItems(int[] _rghvo, int chvo, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void AddDerivedProp(int[] _rgtag, int ctag, IVwViewConstructor _vwvc, int frag) { throw new NotImplementedException(); } + public void NoteDependency(int[] _rghvo, int[] _rgtag, int chvo) { } + public void NoteStringValDependency(int hvo, int tag, int ws, ITsString _tssVal) { throw new NotImplementedException(); } + public void AddStringProp(int tag, IVwViewConstructor _vwvc) { throw new NotImplementedException(); } + public void AddUnicodeProp(int tag, int ws, IVwViewConstructor _vwvc) { throw new NotImplementedException(); } + public void AddIntProp(int tag) { throw new NotImplementedException(); } + public void AddIntPropPic(int tag, IVwViewConstructor _vc, int frag, int nMin, int nMax) { throw new NotImplementedException(); } + public void AddStringAlt(int tag) { throw new NotImplementedException(); } + public void AddStringAltSeq(int tag, int[] _rgenc, int cws) { throw new NotImplementedException(); } + public void AddString(ITsString _ss) { throw new NotImplementedException(); } + public void AddTimeProp(int tag, uint flags) { throw new NotImplementedException(); } + public int CurrentObject() { throw new NotImplementedException(); } + public void GetOuterObject(int ichvoLevel, out int _hvo, out int _tag, out int _ihvo) { throw new NotImplementedException(); } + public void AddWindow(IVwEmbeddedWindow _ew, int dmpAscent, bool fJustifyRight, bool fAutoShow) { throw new NotImplementedException(); } + public void AddSeparatorBar() { throw new NotImplementedException(); } + public void AddSimpleRect(int rgb, int dmpWidth, int dmpHeight, int dmpBaselineOffset) { throw new NotImplementedException(); } + public void OpenDiv() { throw new NotImplementedException(); } + public void CloseDiv() { throw new NotImplementedException(); } + public void OpenParagraph() { throw new NotImplementedException(); } + public void OpenTaggedPara() { throw new NotImplementedException(); } + public void OpenMappedPara() { throw new NotImplementedException(); } + public void OpenMappedTaggedPara() { throw new NotImplementedException(); } + public void OpenConcPara(int ichMinItem, int ichLimItem, VwConcParaOpts cpoFlags, int dmpAlign) { throw new NotImplementedException(); } + public void OpenOverridePara(int cOverrideProperties, DispPropOverride[] _rgOverrideProperties) { throw new NotImplementedException(); } + public void CloseParagraph() { throw new NotImplementedException(); } + public void OpenInnerPile() { throw new NotImplementedException(); } + public void CloseInnerPile() { throw new NotImplementedException(); } + public void OpenSpan() { throw new NotImplementedException(); } + public void CloseSpan() { throw new NotImplementedException(); } + public void OpenTable(int cCols, VwLength vlWidth, int mpBorder, VwAlignment vwalign, VwFramePosition frmpos, VwRule vwrule, int mpSpacing, int mpPadding, bool fSelectOneCol) { throw new NotImplementedException(); } + public void CloseTable() { throw new NotImplementedException(); } + public void OpenTableRow() { throw new NotImplementedException(); } + public void CloseTableRow() { throw new NotImplementedException(); } + public void OpenTableCell(int nRowSpan, int nColSpan) { throw new NotImplementedException(); } + public void CloseTableCell() { throw new NotImplementedException(); } + public void OpenTableHeaderCell(int nRowSpan, int nColSpan) { throw new NotImplementedException(); } + public void CloseTableHeaderCell() { throw new NotImplementedException(); } + public void MakeColumns(int nColSpan, VwLength vlWidth) { throw new NotImplementedException(); } + public void MakeColumnGroup(int nColSpan, VwLength vlWidth) { throw new NotImplementedException(); } + public void OpenTableHeader() { throw new NotImplementedException(); } + public void CloseTableHeader() { throw new NotImplementedException(); } + public void OpenTableFooter() { throw new NotImplementedException(); } + public void CloseTableFooter() { throw new NotImplementedException(); } + public void OpenTableBody() { throw new NotImplementedException(); } + public void CloseTableBody() { throw new NotImplementedException(); } + public void set_StringProperty(int sp, string bstrValue) { throw new NotImplementedException(); } + public void AddPictureWithCaption(IPicture _pict, int tag, ITsTextProps _ttpCaption, int hvoCmFile, int ws, int dxmpWidth, int dympHeight, IVwViewConstructor _vwvc) { throw new NotImplementedException(); } + public void AddPicture(IPicture _pict, int tag, int dxmpWidth, int dympHeight) { throw new NotImplementedException(); } + public void SetParagraphMark(VwBoundaryMark boundaryMark) { throw new NotImplementedException(); } + public void EmptyParagraphBehavior(int behavior) { throw new NotImplementedException(); } + public bool IsParagraphOpen() { throw new NotImplementedException(); } + } + } +} diff --git a/Src/LexText/Morphology/RuleFormulaControl.cs b/Src/LexText/Morphology/RuleFormulaControl.cs index 88035e5afa..b2dfafdc97 100644 --- a/Src/LexText/Morphology/RuleFormulaControl.cs +++ b/Src/LexText/Morphology/RuleFormulaControl.cs @@ -1150,7 +1150,9 @@ private void InitializeComponent() this.m_view.Location = new System.Drawing.Point(0, 0); this.m_view.Mediator = null; this.m_view.Name = "m_view"; - this.m_view.ReadOnlyView = false; + // A rule formula cell is modifiable only via chooser-insert and delete, never free + // text. + this.m_view.ReadOnlyView = true; this.m_view.ScrollMinSize = new System.Drawing.Size(0, 0); this.m_view.ScrollPosition = new System.Drawing.Point(0, 0); this.m_view.ShowRangeSelAfterLostFocus = false; diff --git a/Src/LexText/Morphology/RuleFormulaVcBase.cs b/Src/LexText/Morphology/RuleFormulaVcBase.cs index b316717761..b3f526bb3d 100644 --- a/Src/LexText/Morphology/RuleFormulaVcBase.cs +++ b/Src/LexText/Morphology/RuleFormulaVcBase.cs @@ -285,6 +285,9 @@ public override void Display(IVwEnv vwenv, int hvo, int frag) break; case kfragNC: + // The text belongs to the referenced natural class, not to the rule, so an + // edit here would rename it for every rule that uses it. + vwenv.set_IntProperty((int)FwTextPropType.ktptEditable, (int)FwTextPropVar.ktpvEnum, (int)TptEditable.ktptNotEditable); int ncWs = WritingSystemServices.ActualWs(m_cache, WritingSystemServices.kwsFirstAnal, hvo, PhNaturalClassTags.kflidAbbreviation); if (ncWs != 0) @@ -303,6 +306,9 @@ public override void Display(IVwEnv vwenv, int hvo, int frag) break; case kfragTerminalUnit: + // The text belongs to the referenced phoneme or boundary marker, so an edit + // here would rename it for every rule that uses it. + vwenv.set_IntProperty((int)FwTextPropType.ktptEditable, (int)FwTextPropVar.ktpvEnum, (int)TptEditable.ktptNotEditable); int tuWs = WritingSystemServices.ActualWs(m_cache, WritingSystemServices.kwsFirstVern, hvo, PhTerminalUnitTags.kflidName); if (tuWs != 0) @@ -320,14 +326,18 @@ public override void Display(IVwEnv vwenv, int hvo, int frag) break; case kfragFeature: + // This is a computed "abbreviation value" line, not free text. + vwenv.set_IntProperty((int)FwTextPropType.ktptEditable, (int)FwTextPropVar.ktpvEnum, (int)TptEditable.ktptNotEditable); vwenv.AddProp(ktagFeature, this, kfragFeatureLine); break; case kfragPlusVariable: + vwenv.set_IntProperty((int)FwTextPropType.ktptEditable, (int)FwTextPropVar.ktpvEnum, (int)TptEditable.ktptNotEditable); vwenv.AddProp(ktagVariable, this, kfragPlusVariableLine); break; case kfragMinusVariable: + vwenv.set_IntProperty((int)FwTextPropType.ktptEditable, (int)FwTextPropVar.ktpvEnum, (int)TptEditable.ktptNotEditable); vwenv.AddProp(ktagVariable, this, kfragMinusVariableLine); break; }