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
27 changes: 18 additions & 9 deletions Src/xWorks/Avalonia/Composer/DetailComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,11 @@ private sealed class CompilerSources
public static ComposedDetail Compose(ILexEntry entry, LcmCache cache, bool showHiddenFields = false,
SlicePluginRegistry plugins = null,
ViewDefinitionOverrideResolver overrides = null,
ISet<string> showAllWritingSystemsFields = null)
ISet<string> showAllWritingSystemsFields = null,
Action<string> writingSystemFocused = null)
=> Compose((ICmObject)entry, cache, "Normal", showHiddenFields, plugins, overrides,
showAllWritingSystemsFields: showAllWritingSystemsFields);
showAllWritingSystemsFields: showAllWritingSystemsFields,
writingSystemFocused: writingSystemFocused);

/// <summary>
/// Compose the structured detail view for ANY record root + starting layout -- the
Expand All @@ -132,11 +134,14 @@ public static ComposedDetail Compose(ILexEntry entry, LcmCache cache, bool showH
/// <param name="showAllWritingSystemsFields">Template StableIds of parts under a
/// transient "Show all right now" reveal: every row of those parts composes with its
/// full writing-system set, ignoring per-field visibility restrictions.</param>
/// <param name="writingSystemFocused">The host's editor-focus callback for plugin rows;
/// null when no host supplies one.</param>
public static ComposedDetail Compose(ICmObject obj, LcmCache cache, string layoutName = "Normal",
bool showHiddenFields = false, SlicePluginRegistry plugins = null,
ViewDefinitionOverrideResolver overrides = null,
string layoutChoiceField = null,
ISet<string> showAllWritingSystemsFields = null)
ISet<string> showAllWritingSystemsFields = null,
Action<string> writingSystemFocused = null)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
if (cache == null) throw new ArgumentNullException(nameof(cache));
Expand All @@ -157,7 +162,7 @@ public static ComposedDetail Compose(ICmObject obj, LcmCache cache, string layou
IDetailEditContext composedContext = null;
var state = new ComposeState(cache, showHiddenFields,
plugins ?? SlicePluginRegistry.Default, () => composedContext, overrides,
showAllWritingSystemsFields);
showAllWritingSystemsFields, writingSystemFocused);
state.EnterModel(root);
foreach (var node in root.Roots)
state.Walk(node, obj, 0);
Expand Down Expand Up @@ -301,6 +306,8 @@ public FieldEditHandler HandlerFor(string stableId)
// receive (resolved when the factory runs, after Compose has built the context).
private readonly SlicePluginRegistry _plugins;
private readonly Func<IDetailEditContext> _editContextAccessor;
// The host's editor-focus callback, handed to plugin rows through the build context.
private readonly Action<string> _writingSystemFocused;
// Parts under the host's transient "Show all right now" reveal (template StableIds);
// every row of those parts composes with its full writing-system set.
private readonly ISet<string> _showAllWsFields;
Expand Down Expand Up @@ -338,12 +345,14 @@ public FieldEditHandler HandlerFor(string stableId)
public ComposeState(LcmCache cache, bool showHiddenFields,
SlicePluginRegistry plugins, Func<IDetailEditContext> editContextAccessor,
ViewDefinitionOverrideResolver overrides = null,
ISet<string> showAllWritingSystemsFields = null)
ISet<string> showAllWritingSystemsFields = null,
Action<string> writingSystemFocused = null)
{
_cache = cache;
_showHidden = showHiddenFields;
_plugins = plugins;
_editContextAccessor = editContextAccessor;
_writingSystemFocused = writingSystemFocused;
_overrides = overrides;
_showAllWsFields = showAllWritingSystemsFields;
_sda = cache.DomainDataByFlid;
Expand Down Expand Up @@ -2704,10 +2713,10 @@ private void AddStructuredText(ViewNode node, ICmObject obj, int depth, int flid
private void AddPluginRow(ViewNode node, ICmObject obj, int depth, ISlicePlugin plugin)
{
// ONE plugin contract -- the build context bundles everything a
// plugin can need (object, node, deferred edit-context accessor, cache); there is
// no
// service-aware marker type test.
var context = new SlicePluginBuildContext(obj, node, _editContextAccessor, _cache);
// plugin can need (object, node, deferred edit-context accessor, cache, focus
// callback); there is no service-aware marker type test.
var context = new SlicePluginBuildContext(obj, node, _editContextAccessor, _cache,
_writingSystemFocused);
Func<Avalonia.Controls.Control> factory = () => plugin.BuildControl(context);
AddField(new DetailField(StableId(node, obj), Localize(node.Label) ?? node.Field,
node.Field, node.WritingSystem, DetailFieldKind.Custom, node.EditorClassification,
Expand Down
6 changes: 4 additions & 2 deletions Src/xWorks/Avalonia/Hosting/RecordEditView.Avalonia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,15 +351,17 @@ private void ShowAvaloniaEntry(ICmObject obj)
composed = lexEntry != null
? DetailComposer.Compose(lexEntry, Cache, showHidden,
overrides: ResolveViewOverride,
showAllWritingSystemsFields: m_showAllWsFields)
showAllWritingSystemsFields: m_showAllWsFields,
writingSystemFocused: OnDetailWritingSystemFocused)
// Non-entry roots compose against the tool's configured layout
// (m_layoutName, default "Normal"); a type-selected layout (m_layoutChoiceField, e.g.
// Notebook RnGenericRec keyed on "Type") resolves to the right variant inside Compose.
: DetailComposer.Compose(obj, Cache,
string.IsNullOrEmpty(m_layoutName) ? "Normal" : m_layoutName, showHidden,
overrides: ResolveViewOverride,
layoutChoiceField: m_layoutChoiceField,
showAllWritingSystemsFields: m_showAllWsFields);
showAllWritingSystemsFields: m_showAllWsFields,
writingSystemFocused: OnDetailWritingSystemFocused);
if (composed != null)
{
detail = composed.Model;
Expand Down
10 changes: 6 additions & 4 deletions Src/xWorks/Avalonia/Plugins/ReversalIndexEntryPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Linq;
using Avalonia.Controls;
using SIL.FieldWorks.Common.FwAvalonia.Detail;
using SIL.FieldWorks.Common.FwAvalonia.Seams;
using SIL.LCModel;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Core.Text;
Expand Down Expand Up @@ -40,6 +39,9 @@ public sealed class ReversalIndexEntryPlugin : ISlicePlugin
public const string ReversalIndexEntrySliceClassName =
"SIL.FieldWorks.XWorks.LexEd.ReversalIndexEntrySlice";

/// <summary>The editor's automation id when the layout node declares none.</summary>
public const string DefaultAutomationId = "ReversalEntriesEditor";

public string LegacyClassName => ReversalIndexEntrySliceClassName;

public Control BuildControl(SlicePluginBuildContext context)
Expand All @@ -64,7 +66,7 @@ public Control BuildControl(SlicePluginBuildContext context)
kind: DetailFieldKind.Text,
editorClassification: node?.EditorClassification
?? SIL.FieldWorks.Common.FwAvalonia.ViewDefinition.EditorClassification.Known,
automationId: node?.AutomationId ?? "ReversalEntriesEditor",
automationId: node?.AutomationId ?? DefaultAutomationId,
localizationKey: node?.LocalizationKey,
routing: node?.Routing ?? SIL.FieldWorks.Common.FwAvalonia.ViewDefinition.HostRouting.Product,
values: rows,
Expand All @@ -74,9 +76,9 @@ public Control BuildControl(SlicePluginBuildContext context)
objectHvo: sense.Hvo);

var reversalContext = new ReversalDetailEditContext(cache, context.EditContext, entryByWsKey);
var automationId = node?.AutomationId ?? "ReversalEntriesEditor";
var automationId = node?.AutomationId ?? DefaultAutomationId;
return new FwMultiWsTextField(field, automationId, reversalContext,
writingSystemFocused: wsTag => WritingSystemKeyboards.Activate(cache, wsTag));
writingSystemFocused: context.WritingSystemFocused);
}
catch (Exception e)
{
Expand Down
13 changes: 11 additions & 2 deletions Src/xWorks/Avalonia/Plugins/SlicePlugins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,22 @@ public interface ISlicePlugin
/// Everything the composer hands a plugin factory, bundled into one contract:
/// the row's object and typed node, the detail view's edit context (resolved lazily through the
/// composer's deferred accessor -- the context object is created during compose, BEFORE the
/// edit context exists; plugin factories run at render time, after), and the cache.
/// edit context exists; plugin factories run at render time, after), the cache, and the
/// host's writing-system focus callback.
/// </summary>
public sealed class SlicePluginBuildContext
{
private readonly Func<IDetailEditContext> _editContextAccessor;

public SlicePluginBuildContext(ICmObject target, ViewNode node,
Func<IDetailEditContext> editContextAccessor, LcmCache cache)
Func<IDetailEditContext> editContextAccessor, LcmCache cache,
Action<string> writingSystemFocused = null)
{
Target = target;
Node = node;
_editContextAccessor = editContextAccessor;
Cache = cache;
WritingSystemFocused = writingSystemFocused;
}

/// <summary>The composed row's own object (the slice's object in legacy terms).</summary>
Expand All @@ -66,6 +69,12 @@ public SlicePluginBuildContext(ICmObject target, ViewNode node,
public IDetailEditContext EditContext => _editContextAccessor?.Invoke();

public LcmCache Cache { get; }

/// <summary>
/// The host's editor-focus callback, raised with the writing-system tag of the plugin
/// editor that gained focus. Null when the host supplies none.
/// </summary>
public Action<string> WritingSystemFocused { get; }
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ public class DetailComposerTests : MemoryOnlyBackendProviderTestBase
public override void TestSetup()
{
base.TestSetup();
// The reversal plugin tests build a real Avalonia editor, which needs the (headless)
// platform; run in isolation, no earlier fixture has initialized it.
FwAvaloniaRuntime.EnsureInitialized();
NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () =>
{
m_entry = Cache.ServiceLocator.GetInstance<ILexEntryFactory>().Create();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Threading;
using Avalonia.VisualTree;
using NUnit.Framework;
using SIL.FieldWorks.Common.FwAvalonia;
using SIL.FieldWorks.Common.FwAvalonia.Detail;
using SIL.FieldWorks.Common.FwUtils;
using SIL.LCModel;
using SIL.LCModel.Infrastructure;
Expand All @@ -28,6 +35,7 @@ public class DetailWritingSystemStateTests : XWorksAppTestBase
{
private PropertyTable m_propertyTable;
private List<ICmObject> m_createdObjects;
private ILexEntry m_entry;
private RecordEditView m_view;
// The real subscriber under test. MockFwXWindow's bare-minimum LoadUI never loads the
// Main.xml listeners, so the fixture creates the one this channel needs itself.
Expand Down Expand Up @@ -66,6 +74,7 @@ public void TearDownWindow()
{
NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, DestroyLexiconTestData);
m_createdObjects = null;
m_entry = null;
m_view = null;
// Unsubscribes from the process-wide Pub/Sub singleton; without it a disposed
// handler would still hear later fixtures' publishes.
Expand Down Expand Up @@ -110,6 +119,64 @@ public void EditorFocus_UpdatesWritingSystemHvoProperty_ThroughPubSub()
"focusing an analysis editor must move the combo property back");
}

// Plugin rows bypass SliceFactory, so the host's focus handler reaches them only
// through the callback Compose threads into SlicePluginBuildContext.
[Test]
public void PluginRowEditorFocus_UpdatesWritingSystemHvoProperty_ThroughPubSub()
{
var vernacular = Cache.ServiceLocator.WritingSystems.DefaultVernacularWritingSystem;
var analysis = Cache.ServiceLocator.WritingSystems.DefaultAnalysisWritingSystem;
var sense = m_entry.SensesOS[0];
NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () =>
{
var repository = Cache.ServiceLocator.GetInstance<IReversalIndexRepository>();
var hadIndex = repository.AllInstances().Any(index => index.WritingSystem == analysis.Id);
var reversalIndex = repository.FindOrCreateIndexForWs(analysis.Handle);
if (!hadIndex)
m_createdObjects.Add(reversalIndex);
var reversalEntry = reversalIndex.FindOrCreateReversalEntry("dwelling");
reversalEntry.SensesRS.Add(sense);
m_createdObjects.Add(reversalEntry);
});

// Compose with the host's handler as the plugin callback, the way ShowAvaloniaEntry
// does, and realize the view with the same handler on the composer-row path.
var composed = DetailComposer.Compose(m_entry, Cache,
writingSystemFocused: m_view.OnDetailWritingSystemFocused);
FwAvaloniaRuntime.EnsureInitialized();
var view = new DataTree(composed.Model, composed.EditContext, m_view.OnDetailWritingSystemFocused);
var window = new Window { Content = view, Width = 520, Height = 360 };
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
// The plugin stamps <row automation id>.<ws tag> on each of its value boxes.
var row = composed.Model.Fields.First(f => f.Kind == DetailFieldKind.Custom
&& f.ObjectHvo == sense.Hvo);
var boxId = (row.AutomationId ?? ReversalIndexEntryPlugin.DefaultAutomationId) + "." + analysis.Id;
var reversalBox = view.GetVisualDescendants().OfType<TextBox>()
.FirstOrDefault(box => AutomationProperties.GetAutomationId(box) == boxId);
Assert.That(reversalBox, Is.Not.Null,
"precondition: the Reversal Entries plugin row realized its analysis-ws editor");

m_view.OnDetailWritingSystemFocused(vernacular.Id);
Assert.That(CurrentWritingSystemHvoProperty(),
Is.EqualTo(vernacular.Handle.ToString(CultureInfo.InvariantCulture)),
"precondition: the combo property starts away from the reversal row's ws");

reversalBox.Focus();
Dispatcher.UIThread.RunJobs();

Assert.That(CurrentWritingSystemHvoProperty(),
Is.EqualTo(analysis.Handle.ToString(CultureInfo.InvariantCulture)),
"focusing a plugin row's editor must publish its ws like every composer-built row");
}
finally
{
window.Close();
}
}

private string CurrentWritingSystemHvoProperty()
{
return m_propertyTable.GetStringProperty(PropertyConstants.WritingSystemHvo, null);
Expand All @@ -133,17 +200,19 @@ private void CreateLexiconTestData()
{
var stemMorphType = GetMorphTypeOrCreateOne("stem");
var nounPartOfSpeech = GetGrammaticalCategoryOrCreateOne("noun", Cache.LangProject.PartsOfSpeechOA);
AddLexeme(m_createdObjects, "ws-state-entry", stemMorphType, "ws state gloss", nounPartOfSpeech);
m_entry = AddLexeme(m_createdObjects, "ws-state-entry", stemMorphType, "ws state gloss",
nounPartOfSpeech);
}

private void DestroyLexiconTestData()
{
if (m_createdObjects == null)
return;
foreach (var obj in m_createdObjects)
// Reverse order: a reversal entry is deleted before the index that owns it.
for (var i = m_createdObjects.Count - 1; i >= 0; i--)
{
if (obj.IsValidObject && obj is ILexEntry)
obj.Delete();
if (m_createdObjects[i].IsValidObject)
m_createdObjects[i].Delete();
}
}
}
Expand Down
Loading