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.