diff --git a/Src/LexText/Interlinear/ITextDllTests/RawTextPaneTests.cs b/Src/LexText/Interlinear/ITextDllTests/RawTextPaneTests.cs new file mode 100644 index 0000000000..b69678a682 --- /dev/null +++ b/Src/LexText/Interlinear/ITextDllTests/RawTextPaneTests.cs @@ -0,0 +1,136 @@ +// 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 System.Xml; +using NUnit.Framework; +using SIL.LCModel; +using SIL.LCModel.Core.Text; +using SIL.LCModel.Infrastructure; +using XCore; + +namespace SIL.FieldWorks.IText +{ + /// + /// Tests that the Baseline pane's ShowInvisibleSpaces and ClickInvisibleSpace handlers + /// apply each other's effect directly instead of broadcasting the other property. + /// + [TestFixture] + public class RawTextPaneTests : MemoryOnlyBackendProviderTestBase + { + private IStText m_stText; + private Mediator m_mediator; + private PropertyTable m_propertyTable; + private TestRawTextPane m_pane; + + [OneTimeSetUp] + public override void FixtureSetup() + { + base.FixtureSetup(); + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + m_stText = Cache.ServiceLocator.GetInstance().Create(); + Cache.ServiceLocator.GetInstance().Create().ContentsOA = m_stText; + m_stText.AddNewTextPara(null).Contents = + TsStringUtils.MakeString("xxxa xxxb xxxc.", Cache.DefaultVernWs); + }); + } + + [SetUp] + public override void TestSetup() + { + base.TestSetup(); + m_mediator = new Mediator(); + m_mediator.AddColleague(new ListeningColleague()); + m_propertyTable = new PropertyTable(m_mediator); + // Reading a missing property stores and broadcasts its default + m_propertyTable.SetProperty("ShowInvisibleSpaces", false, false); + m_propertyTable.SetProperty("ClickInvisibleSpace", false, false); + m_pane =new TestRawTextPane(Cache, m_mediator, m_propertyTable, m_stText.Hvo); + } + + [TearDown] + public override void TestTearDown() + { + m_pane.Dispose(); + m_propertyTable.Dispose(); + m_mediator.Dispose(); + base.TestTearDown(); + } + + [Test] + public void ClickInvisibleSpaceOn_SpacesHidden_ShowsSpacesWithoutBroadcast() + { + // SUT + ChooseMenuItem("ClickInvisibleSpace", true); + + Assert.That(m_propertyTable.GetBoolProperty("ShowInvisibleSpaces", false), Is.True); + Assert.That(m_pane.SpacesShown, Is.True, "the pane must show the spaces itself"); + Assert.That(m_mediator.JobItems, Is.Zero, "ShowInvisibleSpaces must change without a broadcast"); + } + + [Test] + public void ShowInvisibleSpacesOff_ClickInsertsOn_EndsClickInsertsWithoutBroadcast() + { + ChooseMenuItem("ShowInvisibleSpaces", true); + ChooseMenuItem("ClickInvisibleSpace", true); + + // SUT + ChooseMenuItem("ShowInvisibleSpaces", false); + + Assert.That(m_propertyTable.GetBoolProperty("ClickInvisibleSpace", true), Is.False); + Assert.That(m_pane.SpacesShown, Is.False); + Assert.That(m_mediator.JobItems, Is.Zero, "ClickInvisibleSpace must change without a broadcast"); + + // The pane must also have recorded that click-insert is off, or turning it on + // again would be taken for no change and leave the spaces hidden. + ChooseMenuItem("ClickInvisibleSpace", true); + Assert.That(m_propertyTable.GetBoolProperty("ShowInvisibleSpaces", false), Is.True); + Assert.That(m_pane.SpacesShown, Is.True); + } + + /// + /// Sets the property as the menu item's click does and delivers the broadcast to the + /// pane, so a test only counts the broadcasts the pane makes itself. + /// + private void ChooseMenuItem(string property, bool value) + { + m_propertyTable.SetProperty(property, value, false); + m_pane.OnPropertyChanged(property); + } + + /// + /// A colleague registered so the Mediator queues broadcasts: with no colleague it + /// discards them before queueing, and the job count would prove nothing. + /// + private sealed class ListeningColleague : IxCoreColleague + { + public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configurationParameters) { } + public IxCoreColleague[] GetMessageTargets() => new IxCoreColleague[] { this }; + public bool ShouldNotCall => false; + public int Priority => (int)ColleaguePriority.High; + } + + /// + /// A laid-out pane on the given text with an insertion point, so a redraw can keep + /// the selection. + /// + private sealed class TestRawTextPane : RawTextPane + { + internal TestRawTextPane(LcmCache cache, Mediator mediator, PropertyTable propertyTable, int hvoText) + { + Cache = cache; + m_mediator = mediator; + m_propertyTable = propertyTable; + Width = 300; + Height = 300; + SetRoot(hvoText); + OnLayout(new LayoutEventArgs(this, "Bounds")); + RootBox.MakeSimpleSel(true, true, false, true); + } + + internal bool SpacesShown => ((ShowSpaceDecorator)RootBox.DataAccess).ShowSpaces; + } + } +} diff --git a/Src/LexText/Interlinear/RawTextPane.cs b/Src/LexText/Interlinear/RawTextPane.cs index 934232229d..8b52db9a16 100644 --- a/Src/LexText/Interlinear/RawTextPane.cs +++ b/Src/LexText/Interlinear/RawTextPane.cs @@ -349,13 +349,7 @@ public override void OnPropertyChanged(string name) break; case "ShowInvisibleSpaces": newVal = ShowInvisibleSpaces; - if (newVal != m_showSpaceDa.ShowSpaces) - { - m_showSpaceDa.ShowSpaces = newVal; - var saveSelection = SelectionHelper.Create(this); - m_rootb.Reconstruct(); - saveSelection.SetSelection(true); - } + ApplyShowInvisibleSpaces(newVal); if (!newVal && ClickInvisibleSpace) TurnOffClickInvisibleSpace(); break; @@ -380,20 +374,41 @@ private void SetParaToReparse(IStTxtPara para) NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => para.ParseIsCurrent = false); } + /// + /// Turns on ShowInvisibleSpaces and redraws here; the set does not broadcast, + /// because this pane is its only listener. + /// private void TurnOnShowInvisibleSpaces() { - if (m_propertyTable != null) - { - m_propertyTable.SetProperty("ShowInvisibleSpaces", true, true); - } + if (m_propertyTable == null) + return; + m_propertyTable.SetProperty("ShowInvisibleSpaces", true, false); + ApplyShowInvisibleSpaces(true); } + /// + /// Turns off ClickInvisibleSpace and records it here; the set does not broadcast, + /// because this pane is its only listener. + /// private void TurnOffClickInvisibleSpace() { - if (m_propertyTable != null) - { - m_propertyTable.SetProperty("ClickInvisibleSpace", false, true); - } + if (m_propertyTable == null) + return; + m_propertyTable.SetProperty("ClickInvisibleSpace", false, false); + m_fClickInsertsZws = false; + } + + /// + /// Redraws the text, keeping the selection, when invisible spaces are shown or hidden. + /// + private void ApplyShowInvisibleSpaces(bool show) + { + if (show == m_showSpaceDa.ShowSpaces) + return; + m_showSpaceDa.ShowSpaces = show; + var saveSelection = SelectionHelper.Create(this); + m_rootb.Reconstruct(); + saveSelection?.SetSelection(true); } private bool ShowInvisibleSpaces