Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Unit tests for StringSliceUtils.
/// </summary>
[TestFixture]
public class StringSliceUtilsTests
{
private const int kwsVern = 101;
private const int kwsEmptyDefault = 102;
private const int kwsAnal = 103;

/// <summary>
/// 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).
/// </summary>
[Test]
public void GetWsForEmptyField_PrefersSpecifiedWs()
{
Assert.That(StringSliceUtils.GetWsForEmptyField(kwsVern, kwsEmptyDefault, kwsAnal),
Is.EqualTo(kwsVern));
}

/// <summary>
/// With no ws specified, the 'wsempty' configuration default should win.
/// </summary>
[Test]
public void GetWsForEmptyField_FallsBackToEmptyDefault()
{
Assert.That(StringSliceUtils.GetWsForEmptyField(-1, kwsEmptyDefault, kwsAnal),
Is.EqualTo(kwsEmptyDefault));
}

/// <summary>
/// With nothing configured, the first analysis ws is the last resort (LT-22145).
/// </summary>
[Test]
public void GetWsForEmptyField_FallsBackToDefaultAnalysis()
{
Assert.That(StringSliceUtils.GetWsForEmptyField(-1, 0, kwsAnal), Is.EqualTo(kwsAnal));
}

/// <summary>
/// Magic ws constants are negative and must not be treated as real ws handles.
/// </summary>
[Test]
public void GetWsForEmptyField_IgnoresNonPositiveValues()
{
Assert.That(StringSliceUtils.GetWsForEmptyField(-6, -3, kwsAnal), Is.EqualTo(kwsAnal));
}
}
}
61 changes: 57 additions & 4 deletions Src/Common/Controls/DetailControls/StringSlice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,15 +382,19 @@ public override void Display(IVwEnv vwenv, int hvo, int frag)

#region RootSite implementation
/// <summary>
/// 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).
/// </summary>
class StringSliceView : RootSiteControl, INotifyControlInCurrentSlice
{
ICmObject m_obj;
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)
Expand Down Expand Up @@ -424,17 +428,53 @@ public int DefaultWs
set
{
CheckDisposed();
m_wsDefault = value;
if (m_vc is StringSliceVc)
(m_vc as StringSliceVc).DefaultWs = value;
}
}

/// <summary>
/// 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.
/// </summary>
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);
}
}

/// <summary>
/// Is the string property this slice edits currently empty?
/// Unicode and unrecognized property types report false.
/// </summary>
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

Expand Down Expand Up @@ -569,6 +609,7 @@ public override void MakeRoot()
m_obj = m_cache.ServiceLocator.GetInstance<ICmObjectRepository>().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);
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions Src/Common/Controls/DetailControls/StringSliceUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ public static string EncodeWssToDisplayPropertyValue(IEnumerable<CoreWritingSyst
return ChoiceGroup.EncodeSinglePropertySequenceValue(wsIds);
}

/// <summary>
/// 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.
/// </summary>
public static int GetWsForEmptyField(int wsSpecified, int wsEmptyDefault, int wsDefaultAnal)
{
if (wsSpecified > 0)
return wsSpecified;
if (wsEmptyDefault > 0)
return wsEmptyDefault;
return wsDefaultAnal;
}

/// <summary>
/// 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.
Expand Down
Loading