Skip to content
Merged
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
Expand Up @@ -10,6 +10,7 @@
using SIL.LCModel.Core.WritingSystems;
using SIL.FieldWorks.Common.FwUtils;
using XCore;
using SIL.FieldWorks.Common.Framework.DetailControls.Resources;

namespace SIL.FieldWorks.Common.Framework.DetailControls
{
Expand Down Expand Up @@ -66,6 +67,10 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

/// <summary>The dialog title for the field named <paramref name="fieldLabel"/>.</summary>
public static string TitleFor(string fieldLabel)
=> string.Format(DetailControlsStrings.ksSliceConfigureWssDlgTitle, fieldLabel);

/// <summary>
/// Gets the selected writing system.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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.Linq;
using System.Windows.Forms;
using NUnit.Framework;
using SIL.FieldWorks.Common.DetailRules;
using SIL.FieldWorks.Common.Widgets;
using SIL.LCModel;
using SIL.LCModel.DomainServices;

namespace SIL.FieldWorks.Common.Framework.DetailControls
{
/// <summary>
/// The WinForms Writing Systems menu is answered by the rule the Avalonia detail view also
/// uses. These pin that rule against the view's own writing-system query, so the two detail
/// views cannot offer different writing systems.
/// </summary>
[TestFixture]
public class MultiStringSliceWritingSystemsTests : MemoryOnlyBackendProviderTestBase
{
private const int MoFormFormFlid = 5035001;

[TestCase(WritingSystemServices.kwsAnals, 0)]
[TestCase(WritingSystemServices.kwsVerns, 0)]
[TestCase(WritingSystemServices.kwsAnalVerns, 0)]
// The Pronunciation form, the one shipped part that offers a second set on request.
[TestCase(WritingSystemServices.kwsPronunciations, WritingSystemServices.kwsVerns)]
public void SharedRule_OffersExactlyWhatTheViewOffered(int wsMagic, int optionalMagic)
{
var hvo = Cache.LangProject.Hvo;
using (var host = new Form())
using (var view = new LabeledMultiStringView(hvo, MoFormFormFlid, wsMagic, optionalMagic,
false, true, true))
{
host.Controls.Add(view);
view.InnerView.Cache = Cache;
var spec = WritingSystemFieldSpec.FromMagicIds(hvo, wsMagic, optionalMagic, false);

Assert.That(FieldWritingSystemOptions.Options(Cache, spec).Select(ws => ws.Id),
Is.EqualTo(view.WritingSystemOptions.Select(ws => ws.Id)),
"the menu must offer the writing systems the view offered, in the same order");
Assert.That(FieldWritingSystemOptions.DefaultShown(Cache, spec).Select(ws => ws.Id),
Is.EqualTo(view.GetWritingSystemOptions(false).Select(ws => ws.Id)),
"and default to the same subset when the field stores no selection");
}
}

[Test]
public void SharedRule_OffersMoreThanItShows_WhenTheProjectHasAnUncheckedWritingSystem()
{
var hvo = Cache.LangProject.Hvo;
var spec = WritingSystemFieldSpec.FromMagicIds(hvo, WritingSystemServices.kwsAnals, 0, false);
var offered = FieldWritingSystemOptions.Options(Cache, spec).Select(ws => ws.Id).ToList();
var shown = FieldWritingSystemOptions.DefaultShown(Cache, spec).Select(ws => ws.Id).ToList();

Assert.That(shown, Is.SubsetOf(offered),
"everything shown must be offerable; the menu may offer more than the field shows");
}
}
}
75 changes: 37 additions & 38 deletions Src/Common/Controls/DetailControls/MultiStringSlice.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2015-2017 SIL International
// Copyright (c) 2015-2017 SIL International
// This software is licensed under the LGPL, version 2.1 or later
// (http://www.gnu.org/licenses/lgpl-2.1.html)

Expand All @@ -10,6 +10,7 @@
using System.Xml;
using SIL.LCModel.Core.WritingSystems;
using SIL.FieldWorks.Common.Controls;
using SIL.FieldWorks.Common.DetailRules;
using SIL.FieldWorks.Common.Framework.DetailControls.Resources;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.FieldWorks.Common.FwUtils;
Expand All @@ -28,8 +29,17 @@ namespace SIL.FieldWorks.Common.Framework.DetailControls
/// </summary>
public class MultiStringSlice : ViewPropertySlice, IWritingSystemChooser
{
// The writing-system sets this slice was built for, kept so the Writing Systems menu can
// be answered by the rule both detail views share.
private int m_wsMagic;
private int m_wsMagicOptional;
private bool m_forceIncludeEnglish;

public MultiStringSlice(ICmObject obj, int flid, int ws, int wsOptional, bool forceIncludeEnglish, bool editable, bool spellCheck)
{
m_wsMagic = ws;
m_wsMagicOptional = wsOptional;
m_forceIncludeEnglish = forceIncludeEnglish;
var view = new LabeledMultiStringView(obj.Hvo, flid, ws, wsOptional, forceIncludeEnglish, editable, spellCheck);
Control = view;
#if _DEBUG
Expand Down Expand Up @@ -63,6 +73,9 @@ public override RootSite RootSite
/// </summary>
public void Reuse(ICmObject obj, int flid, int ws, int wsOptional, bool forceIncludeEnglish, bool editable, bool spellCheck)
{
m_wsMagic = ws;
m_wsMagicOptional = wsOptional;
m_forceIncludeEnglish = forceIncludeEnglish;
var view = (LabeledMultiStringView)Control;
Label = null; // new slice normally has this
SetupWssToDisplay();
Expand Down Expand Up @@ -109,10 +122,19 @@ void HandleRightMouseClickedEvent(SimpleRootSite sender, FwRightMouseClickEventA
/// </summary>
/// <returns></returns>
public IEnumerable<CoreWritingSystemDefinition> GetVisibleWritingSystems()
=> FieldWritingSystemOptions.Shown(m_cache, WritingSystemSpec(), StoredWritingSystems());

// The sets this field draws on, as the shared rule takes them.
private WritingSystemFieldSpec WritingSystemSpec()
=> WritingSystemFieldSpec.FromMagicIds(Object?.Hvo ?? 0, m_wsMagic,
m_wsMagicOptional, m_forceIncludeEnglish);

// The selection persisted on this field's part ref, or null when it has none and the
// field shows its default set.
private IReadOnlyList<string> StoredWritingSystems()
{
var singlePropertySequenceValue = StringSliceUtils.GetVisibleWSSPropertyValue(PartRef(),
((LabeledMultiStringView) Control).GetWritingSystemOptions(false));
return StringSliceUtils.GetVisibleWritingSystems(singlePropertySequenceValue, WritingSystemOptionsForDisplay);
var stored = XmlUtils.GetOptionalAttributeValue(PartRef(), "visibleWritingSystems", null);
return stored == null ? null : ChoiceGroup.DecodeSinglePropertySequenceValue(stored);
}

public override void Install(DataTree parent)
Expand Down Expand Up @@ -205,7 +227,7 @@ public bool OnDataTreeWritingSystemsConfigureDlg(object args)
using (var dlg = new ConfigureWritingSystemsDlg(WritingSystemOptionsForDisplay, WritingSystemsSelectedForDisplay,
m_propertyTable.GetValue<IHelpTopicProvider>("HelpTopicProvider")))
{
dlg.Text = String.Format(DetailControlsStrings.ksSliceConfigureWssDlgTitle, Label);
dlg.Text = ConfigureWritingSystemsDlg.TitleFor(Label);
if (dlg.ShowDialog() == DialogResult.OK)
PersistAndRedisplayWssToDisplayForPart(dlg.SelectedWritingSystems);
}
Expand Down Expand Up @@ -251,30 +273,18 @@ public bool OnDisplayWritingSystemOptionsForSlice(object parameter, ref UIListDi
{
CheckDisposed();
display.List.Clear();
m_propertyTable.SetProperty(display.PropertyName, StringSliceUtils.GetVisibleWSSPropertyValue(PartRef(),
((LabeledMultiStringView) Control).GetWritingSystemOptions(false)), false);
AddWritingSystemListWithIcuLocales(display, WritingSystemOptionsForDisplay);
// Options, checkmarks and the cannot-empty-the-set rule all come from the shared
// rule, so this menu and the Avalonia one offer the same list.
var options = FieldWritingSystemOptions.Menu(m_cache, WritingSystemSpec(),
StoredWritingSystems());
m_propertyTable.SetProperty(display.PropertyName,
ChoiceGroup.EncodeSinglePropertySequenceValue(
options.Where(o => o.IsChecked).Select(o => o.Id).ToArray()), false);
foreach (var option in options)
display.List.Add(option.Label, option.Id, null, null, option.CanUncheck);
return true;//we handled this, no need to ask anyone else.
}

/// <summary>
/// stores the list values in terms of icu locale
/// </summary>
/// <param name="display"></param>
/// <param name="list"></param>
private void AddWritingSystemListWithIcuLocales(UIListDisplayProperties display, IEnumerable<CoreWritingSystemDefinition> list)
{
var active = StringSliceUtils.GetVisibleWSSPropertyValue(PartRef(),
((LabeledMultiStringView) Control).GetWritingSystemOptions(false)).Split(',');
foreach (var ws in list)
{
// generally enable all items, but if only one is checked that one is disabled;
// it can't be turned off.
bool enabled = (active.Length != 1 || ws.Id != active[0]);
display.List.Add(ws.DisplayLabel, ws.Id, null, null, enabled);
}
}

/// ------------------------------------------------------------------------------------
/// <summary>
/// Called when property changed.
Expand Down Expand Up @@ -320,18 +330,7 @@ private void PersistAndRedisplayWssToDisplayForPart(string singlePropertySequenc
/// Get the language project's list of pronunciation writing systems into sync with the supplied list.
/// </summary>
private void UpdatePronunciationWritingSystems(IEnumerable<CoreWritingSystemDefinition> newValues)
{
if (newValues.Count() != m_cache.ServiceLocator.WritingSystems.CurrentPronunciationWritingSystems.Count
|| !m_cache.ServiceLocator.WritingSystems.CurrentPronunciationWritingSystems.SequenceEqual(newValues))
{
NonUndoableUnitOfWorkHelper.Do(m_cache.ServiceLocator.GetInstance<IActionHandler>(), () =>
{
m_cache.ServiceLocator.WritingSystems.CurrentPronunciationWritingSystems.Clear();
foreach (CoreWritingSystemDefinition ws in newValues)
m_cache.ServiceLocator.WritingSystems.CurrentPronunciationWritingSystems.Add(ws);
});
}
}
=> PronunciationWritingSystems.Sync(m_cache, newValues);

/// <summary>
/// go through all the data tree slices, finding the slices that refer to the same part as this slice
Expand Down
21 changes: 21 additions & 0 deletions Src/Common/FwAvalonia/Detail/DetailModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,27 @@ public DetailField(
/// </summary>
public bool IsMultiStringRow { get; set; }

/// <summary>
/// The layout's <c>optionalWs</c> spec: writing systems this row can be asked to show
Comment thread
papeh marked this conversation as resolved.
/// beyond its <see cref="WritingSystem"/> set, but never shows by default. Null on every
/// shipped row but the Pronunciation form. Only the Writing Systems menu reads it.
/// </summary>
public string OptionalWritingSystem { get; set; }

/// <summary>
/// The layout's <c>forceIncludeEnglish</c> flag: English joins this row's writing-system
/// options even when the project has not checked it. Only the Writing Systems menu
/// reads it.
/// </summary>
public bool ForceIncludeEnglish { get; set; }

/// <summary>
/// The row's stored per-writing-system selection (the layout's
/// <c>visibleWritingSystems</c>, as the project override holds it), or null when the row
/// stores none and shows its default set. The Writing Systems menu checks these.
/// </summary>
public IReadOnlyList<string> VisibleWritingSystems { get; set; }

/// <summary>
/// How this row's domain decides two option names are the same name, for rows that mint
/// items from typed text. Environments compare with spaces stripped, so the picker must
Expand Down
35 changes: 34 additions & 1 deletion Src/Common/FwAvalonia/FwAvaloniaTests/ViewDefinitionTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2026 SIL International
// 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)

Expand Down Expand Up @@ -49,6 +49,10 @@ public class XmlLayoutImporterTests
<part id='LexEntry-Detail-PerFieldWs'>
<slice label='Form' editor='multistring' field='CitationForm' ws='all analysis'/>
</part>
<part id='LexEntry-Detail-OptionalWs'>
<slice label='Pronunciation' editor='multistring' field='Form'
ws='all pronunciation' optionalWs='all vernacular' forceIncludeEnglish='true'/>
</part>
</bin></PartInventory>";

private static ViewDefinitionModel Import(string layoutXml)
Expand Down Expand Up @@ -195,6 +199,35 @@ public void Import_NoVisibleWritingSystems_LeavesOverrideNull()
"a field with no override shows the full configured set (null = no restriction)");
}

[Test]
public void Import_OptionalWsAndForceIncludeEnglish_RideTheNode()
{
// Both expand what the field's Writing Systems menu may OFFER without changing what
// the field shows. Only the Pronunciation form uses optionalWs in the shipped parts.
var model = Import(@"
<layout class='LexEntry' type='detail' name='OWs'>
<part ref='OptionalWs'/>
</layout>");

var field = model.Roots[0];
Assert.That(field.WritingSystem, Is.EqualTo("all pronunciation"));
Assert.That(field.OptionalWritingSystem, Is.EqualTo("all vernacular"));
Assert.That(field.ForceIncludeEnglish, Is.True);
}

[Test]
public void Import_WithoutOptionalWs_LeavesBothPropertiesUnset()
{
var model = Import(@"
<layout class='LexEntry' type='detail' name='PFW'>
<part ref='PerFieldWs'/>
</layout>");

Assert.That(model.Roots[0].OptionalWritingSystem, Is.Null,
"a part with no optionalWs offers only its own writing systems");
Assert.That(model.Roots[0].ForceIncludeEnglish, Is.False);
}

[Test]
public void Import_DynamicEditor_RaisesInfoDiagnostic()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ private static JObject WriteNode(ViewNode node)
AddIfPresent(o, "hotlinks", node.HotlinksId);
AddIfPresent(o, "helpTopicID", node.HelpTopicId);
AddIfPresent(o, "sliceId", node.SliceId);
AddIfPresent(o, "optionalWs", node.OptionalWritingSystem);
if (node.ForceIncludeEnglish)
o["forceIncludeEnglish"] = true;
// The stored selection travels with the two facts above: options without the
// checked set would round-trip a field into the wrong checkmarks.
if (node.VisibleWritingSystems != null && node.VisibleWritingSystems.Count > 0)
o["visibleWritingSystems"] = new JArray(node.VisibleWritingSystems);
AddIfPresent(o, "ghost", node.GhostField);
AddIfPresent(o, "ghostWs", node.GhostWs);
AddIfPresent(o, "ghostClass", node.GhostClass);
Expand Down Expand Up @@ -216,7 +223,10 @@ private static ViewNode ReadNode(JToken token)
condition: ReadCondition((JObject)o["condition"]),
chooserLinks: ((JArray)o["chooserLinks"])?.Select(ReadChooserLink).ToList(),
helpTopicId: (string)o["helpTopicID"],
sliceId: (string)o["sliceId"]);
sliceId: (string)o["sliceId"],
optionalWritingSystem: (string)o["optionalWs"],
forceIncludeEnglish: (bool?)o["forceIncludeEnglish"] ?? false,
visibleWritingSystems: ((JArray)o["visibleWritingSystems"])?.Select(t => (string)t).ToList());
}

private static T ParseEnum<T>(JObject o, string name, T fallback) where T : struct
Expand Down
20 changes: 19 additions & 1 deletion Src/Common/FwAvalonia/ViewDefinition/ViewDefinitionModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,12 @@ public ViewNode(
bool toggleValue = false,
bool reorder = false,
string helpTopicId = null,
string sliceId = null)
string sliceId = null,
string optionalWritingSystem = null,
bool forceIncludeEnglish = false)
{
OptionalWritingSystem = optionalWritingSystem;
ForceIncludeEnglish = forceIncludeEnglish;
SliceId = sliceId;
HelpTopicId = helpTopicId;
Reorder = reorder;
Expand Down Expand Up @@ -473,6 +477,20 @@ public ViewNode(
/// </summary>
public IReadOnlyList<string> VisibleWritingSystems { get; }

/// <summary>
/// The part's <c>optionalWs</c> attribute: writing systems a multistring field can be
/// asked to show beyond its <see cref="WritingSystem"/> set, but never shows by default.
/// Null when the part has none, which is every shipped part but the Pronunciation form.
/// </summary>
public string OptionalWritingSystem { get; }

/// <summary>
/// The part's <c>forceIncludeEnglish</c> attribute: English joins this field's
/// writing-system options even when the project has not checked it. Set on the reversal
/// name and abbreviation parts.
/// </summary>
public bool ForceIncludeEnglish { get; }

public IReadOnlyList<ViewNode> Children { get; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,9 @@ private static ViewNode CloneWith(ViewNode n, ViewVisibility visibility, string
n.ContextMenuId, n.HotlinksId, n.GhostField, n.GhostWs, n.GhostClass, n.GhostLabel,
n.ForVariant, n.CustomEditorClass, n.CustomEditorAssembly, n.GhostInitMethod, n.Condition,
n.ChooserLinks, n.EnumStringList, visibleWritingSystems, n.ToggleValue, n.Reorder,
helpTopicId: n.HelpTopicId, sliceId: n.SliceId);
helpTopicId: n.HelpTopicId, sliceId: n.SliceId,
optionalWritingSystem: n.OptionalWritingSystem,
forceIncludeEnglish: n.ForceIncludeEnglish);

// Copy a (leaf) node under a new StableId; AutomationId is dropped so the duplicate gets a fresh,
// non-colliding identity (the renderer derives one from the new StableId by convention).
Expand All @@ -323,6 +325,8 @@ private static ViewNode CloneWithId(ViewNode n, string newId)
n.ContextMenuId, n.HotlinksId, n.GhostField, n.GhostWs, n.GhostClass, n.GhostLabel,
n.ForVariant, n.CustomEditorClass, n.CustomEditorAssembly, n.GhostInitMethod, n.Condition,
n.ChooserLinks, n.EnumStringList, n.VisibleWritingSystems, n.ToggleValue, n.Reorder,
helpTopicId: n.HelpTopicId, sliceId: n.SliceId);
helpTopicId: n.HelpTopicId, sliceId: n.SliceId,
optionalWritingSystem: n.OptionalWritingSystem,
forceIncludeEnglish: n.ForceIncludeEnglish);
}
}
Loading
Loading