From 0e6065c7a6d6dcef6c51b3ba45545535ec48cabe Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Thu, 16 Jul 2026 15:13:35 -0700 Subject: [PATCH 1/2] Fix LT-22630: Custom first vernacular fields defaulting to analysis The LT-22145 fix made StringSliceView.WsPending force DefaultAnalWs on every keystroke, so typing in custom fields configured for another writing system came out as analysis. WsPending now supplies a writing system only when the field is empty, using the ws configured for the slice and falling back to DefaultAnalWs. Co-Authored-By: Claude Fable 5 --- .../StringSliceUtilsTests.cs | 58 +++++++++++++++++++ .../Controls/DetailControls/StringSlice.cs | 49 ++++++++++++++-- .../DetailControls/StringSliceUtils.cs | 15 +++++ 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 Src/Common/Controls/DetailControls/DetailControlsTests/StringSliceUtilsTests.cs 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..69a542035e 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); 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. From d6a977f1f0c5ef942b0e1715bb34698173712cfb Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Thu, 16 Jul 2026 15:44:30 -0700 Subject: [PATCH 2/2] LT-22630: Make ws chooser match data-entry ws in empty fields The chooser reads the selection's props, which fall back to the UI ws in an empty field even though typing uses the slice's configured ws. Set typing props on an insertion point in an empty field so the chooser and keyboard agree with what typing will produce. Co-Authored-By: Claude Fable 5 --- Src/Common/Controls/DetailControls/StringSlice.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Src/Common/Controls/DetailControls/StringSlice.cs b/Src/Common/Controls/DetailControls/StringSlice.cs index 69a542035e..16a569a3cd 100644 --- a/Src/Common/Controls/DetailControls/StringSlice.cs +++ b/Src/Common/Controls/DetailControls/StringSlice.cs @@ -654,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);