diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/StringSliceUtilsTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/StringSliceUtilsTests.cs new file mode 100644 index 0000000000..cb89fd58c6 --- /dev/null +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/StringSliceUtilsTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2026 SIL Global +// 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.Common.Framework.DetailControls +{ + /// + /// Unit tests for StringSliceUtils. + /// + [TestFixture] + public class StringSliceUtilsTests + { + private const int kwsVern = 101; + private const int kwsEmptyDefault = 102; + private const int kwsAnal = 103; + + /// + /// A slice created with a specific ws (e.g. a custom field configured as First + /// Vernacular) should use that ws for typing in an empty field (LT-22630). + /// + [Test] + public void GetWsForEmptyField_PrefersSpecifiedWs() + { + Assert.That(StringSliceUtils.GetWsForEmptyField(kwsVern, kwsEmptyDefault, kwsAnal), + Is.EqualTo(kwsVern)); + } + + /// + /// With no ws specified, the 'wsempty' configuration default should win. + /// + [Test] + public void GetWsForEmptyField_FallsBackToEmptyDefault() + { + Assert.That(StringSliceUtils.GetWsForEmptyField(-1, kwsEmptyDefault, kwsAnal), + Is.EqualTo(kwsEmptyDefault)); + } + + /// + /// With nothing configured, the first analysis ws is the last resort (LT-22145). + /// + [Test] + public void GetWsForEmptyField_FallsBackToDefaultAnalysis() + { + Assert.That(StringSliceUtils.GetWsForEmptyField(-1, 0, kwsAnal), Is.EqualTo(kwsAnal)); + } + + /// + /// Magic ws constants are negative and must not be treated as real ws handles. + /// + [Test] + public void GetWsForEmptyField_IgnoresNonPositiveValues() + { + Assert.That(StringSliceUtils.GetWsForEmptyField(-6, -3, kwsAnal), Is.EqualTo(kwsAnal)); + } + } +} diff --git a/Src/Common/Controls/DetailControls/StringSlice.cs b/Src/Common/Controls/DetailControls/StringSlice.cs index 2bc0c7b352..16a569a3cd 100644 --- a/Src/Common/Controls/DetailControls/StringSlice.cs +++ b/Src/Common/Controls/DetailControls/StringSlice.cs @@ -382,8 +382,10 @@ public override void Display(IVwEnv vwenv, int hvo, int frag) #region RootSite implementation /// - /// This is a RootSiteControl that displays a non-multilingual string slice - /// Data entry should always default to the DefaultAnalWs writing system according to LT-22145 + /// A RootSiteControl that displays a single string: a plain (non-multilingual) + /// string property, a Unicode property, or one alternative of a multilingual + /// property. When the field is empty, data entry uses the writing system + /// configured for the slice, defaulting to first analysis (LT-22145, LT-22630). /// class StringSliceView : RootSiteControl, INotifyControlInCurrentSlice { @@ -391,6 +393,8 @@ class StringSliceView : RootSiteControl, INotifyControlInCurrentSlice readonly int m_hvoObj; readonly int m_flid; readonly int m_ws = -1; // -1 signifies not a multilingual property + int m_wsDefault; // ws to use in an empty field, from the 'wsempty' config attribute + CellarPropertyType m_propType; IVwViewConstructor m_vc; public StringSliceView(int hvo, int flid, int ws) @@ -424,17 +428,53 @@ public int DefaultWs set { CheckDisposed(); + m_wsDefault = value; if (m_vc is StringSliceVc) (m_vc as StringSliceVc).DefaultWs = value; } } + /// + /// When the field is empty there is no adjacent text to give newly typed characters a + /// writing system, so supply the one configured for the slice (LT-22145, LT-22630). + /// Once the field has content, -1 lets typing inherit from the surrounding text. + /// Sets are ignored so input-language changes can't hijack an empty field. + /// public override int WsPending { - // Ignore requests to set pending writing system, this slice always deals with the DefaultAnalWs. // ReSharper disable once ValueParameterNotUsed set { } - get => Cache != null ? Cache.DefaultAnalWs : -1; + get + { + if (Cache == null || m_obj == null || !FieldIsEmpty()) + return -1; + return StringSliceUtils.GetWsForEmptyField(m_ws, m_wsDefault, Cache.DefaultAnalWs); + } + } + + /// + /// Is the string property this slice edits currently empty? + /// Unicode and unrecognized property types report false. + /// + private bool FieldIsEmpty() + { + if (!Cache.ServiceLocator.IsValidObjectId(m_hvoObj)) + return false; + var sda = m_cache.DomainDataByFlid; + switch (m_propType) + { + case CellarPropertyType.String: + var tss = sda.get_StringProp(m_hvoObj, m_flid); + return tss == null || tss.Length == 0; + case CellarPropertyType.MultiString: + case CellarPropertyType.MultiUnicode: + if (m_ws <= 0) + return false; + var tssAlt = sda.get_MultiStringAlt(m_hvoObj, m_flid, m_ws); + return tssAlt == null || tssAlt.Length == 0; + default: + return false; + } } #region IDisposable override @@ -569,6 +609,7 @@ public override void MakeRoot() m_obj = m_cache.ServiceLocator.GetInstance().GetObject(m_hvoObj); CellarPropertyType type = (CellarPropertyType)m_cache.DomainDataByFlid.MetaDataCache.GetFieldType(m_flid); + m_propType = type; if (type == CellarPropertyType.Unicode) { m_vc = new UnicodeStringSliceVc(m_flid, m_ws, m_cache); @@ -613,6 +654,18 @@ protected override void HandleSelectionChange(IVwRootBox prootb, IVwSelection vw { s_fProcessingSelectionChanged = true; + // An insertion point in an empty field has no text to supply a ws, so its + // props fall back to the UI ws. Give it the slice's data-entry ws so the + // ws chooser and keyboard match what typing will produce (LT-22630). + if (!vwselNew.IsRange && FieldIsEmpty()) + { + var propsBldr = TsStringUtils.MakePropsBldr(); + propsBldr.SetIntPropValues((int)FwTextPropType.ktptWs, + (int)FwTextPropVar.ktpvDefault, + StringSliceUtils.GetWsForEmptyField(m_ws, m_wsDefault, Cache.DefaultAnalWs)); + vwselNew.SetTypingProps(propsBldr.GetTextProps()); + } + // If the selection is entirely formattable ("IsSelectionInOneFormattableProp"), we don't need to do // the following selection truncation. var hlpr = SelectionHelper.Create(vwselNew, this); diff --git a/Src/Common/Controls/DetailControls/StringSliceUtils.cs b/Src/Common/Controls/DetailControls/StringSliceUtils.cs index 4093f8ca6d..d9eb091fa3 100644 --- a/Src/Common/Controls/DetailControls/StringSliceUtils.cs +++ b/Src/Common/Controls/DetailControls/StringSliceUtils.cs @@ -36,6 +36,21 @@ public static string EncodeWssToDisplayPropertyValue(IEnumerable + /// Pick the writing system for text typed into an empty string field: the ws the slice + /// was created with (e.g. a custom field's First Vernacular), else the configured + /// empty-field default (the 'wsempty' attribute), else the first analysis ws (LT-22145). + /// See LT-22630. + /// + public static int GetWsForEmptyField(int wsSpecified, int wsEmptyDefault, int wsDefaultAnal) + { + if (wsSpecified > 0) + return wsSpecified; + if (wsEmptyDefault > 0) + return wsEmptyDefault; + return wsDefaultAnal; + } + /// /// Get the writing systems we should actually display right now. That is, from the ones /// that are currently possible, select any we've previously configured to show.