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 @@ -56,21 +56,9 @@
<!--
AlternateForms
-->
<command id="CmdDataTree-MoveUp-AlternateForm" label="Move Form _Up" message="MoveUpObjectInSequence" icon="MoveUp">
<parameters field="AlternateForms" className="MoForm"/>
</command>
<command id="CmdDataTree-MoveDown-AlternateForm" label="Move Form _Down" message="MoveDownObjectInSequence" icon="MoveDown">
<parameters field="AlternateForms" className="MoForm"/>
</command>
<command id="CmdDataTree-Insert-AlternateForm" label="Insert Allomorph" message="DataTreeInsert">
<parameters field="AlternateForms" className="MoForm"/>
</command>
<command id="CmdDataTree-Delete-AlternateForm" label="Delete AlternateForm" message="DataTreeDelete" icon="Delete">
<parameters field="AlternateForms" className="MoForm"/>
</command>
<command id="CmdDataTree-Merge-AlternateForm" label="Merge AlternateForm into..." message="DataTreeMerge">
<parameters field="AlternateForms" className="MoForm"/>
</command>
<command id="CmdDataTree-Swap-Allomorph" label="Swap Allomorph with Lexeme Form" message="SwapAllomorphWithLexeme">
<parameters field="AlternateForms" className="MoForm"/>
</command>
Expand Down Expand Up @@ -335,13 +323,6 @@
<item label="-" translate="do not translate"/>
<item command="CmdDataTree-Delete-Picture"/>
</menu>
<menu id="mnuDataTree-AlternateForm">
<item command="CmdDataTree-MoveUp-AlternateForm"/>
<item command="CmdDataTree-MoveDown-AlternateForm"/>
<item label="-" translate="do not translate"/>
<item command="CmdDataTree-Merge-AlternateForm"/>
<item command="CmdDataTree-Delete-AlternateForm"/>
</menu>
<menu id="mnuDataTree-Sense">
<item command="CmdDataTree-Insert-Example"/>
<item command="CmdFindExampleSentence"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
header="true" helpTopicID="khtpField-LexEntry-AlternateFormsSection"/>
</part>
<part id="LexEntry-Detail-AlternateForms" type="Detail">
<seq field="AlternateForms" menu="mnuDataTree-AlternateForm" />
<seq field="AlternateForms" />
</part>
<part id="LexEntry-Jt-HeadWordInPara" type="jtview">
<para>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,6 @@
<data name="ksRootNoComponentsMessage" xml:space="preserve">
<value>An entry that has components cannot have a morph type of root, since by definition a root is indivisible. Would you like Language Explorer to delete the references to components in this entry?</value>
</data>
<data name="ksRedoDeleteRef" xml:space="preserve">
<value>Redo Delete Reference</value>
</data>
<data name="ksUndoDeleteRef" xml:space="preserve">
<value>Undo Delete Reference</value>
</data>
<data name="ksRedoUpdatePicture" xml:space="preserve">
<value>Redo Update Picture</value>
</data>
Expand Down
100 changes: 5 additions & 95 deletions Src/Common/Controls/DetailControls/Slice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2320,24 +2320,9 @@ public virtual bool CanDeleteReferenceNow(Command cmd)
public virtual void HandleDeleteReferenceCommand(Command cmd)
{
CheckDisposed();
if (NextSlice != null && NextSlice.Object != null)
if (NextSlice?.Object is ILexEntryRef ler && ContainingDataTree.Root is ILexEntry root)
{
var ler = NextSlice.Object as ILexEntryRef;
if (ler != null)
{
UndoableUnitOfWorkHelper.Do(DetailControlsStrings.ksUndoDeleteRef, DetailControlsStrings.ksRedoDeleteRef, ler, () =>
{
ler.ComponentLexemesRS.Remove(ContainingDataTree.Root);
// probably not needed, but safe...
if (ler.PrimaryLexemesRS.Contains(ContainingDataTree.Root))
ler.PrimaryLexemesRS.Remove(ContainingDataTree.Root);

ILexEntry entry;
entry = (ILexEntry)m_cache.ServiceLocator.GetObject(ler.OwningEntry.Hvo);
if (entry.EntryRefsOS.Contains(ler))
entry.EntryRefsOS.Remove(ler);
});
}
VariantBackReferences.DeleteReference(root, ler);
}
}

Expand Down Expand Up @@ -2385,38 +2370,9 @@ public virtual int Flid
}
}

private bool FromVariantBackRefField
{
get
{
var rootObj = ContainingDataTree.Root;
int clidRoot = rootObj.ClassID;
return clidRoot == LexEntryTags.kClassId &&
Object != null && Object != rootObj &&
Object.Owner != null && Object.Owner != rootObj &&
(Object.ClassID == clidRoot || Object.Owner.ClassID == clidRoot);
}
}
private bool FromVariantBackRefField => BackRefObject != null;

private ICmObject BackRefObject
{
get
{
var rootObj = ContainingDataTree.Root;
int clidRoot = rootObj.ClassID;
if (clidRoot == LexEntryTags.kClassId &&
Object != null && Object != rootObj &&
Object.Owner != null && Object.Owner != rootObj)
{
if (Object.ClassID == clidRoot)
return Object;

if (Object.Owner.ClassID == clidRoot)
return Object.Owner;
}
return null;
}
}
private ICmObject BackRefObject => VariantBackReferences.BackReferencedEntry(ContainingDataTree.Root, Object);

/// <summary>
/// is it possible to do a deletion menu command on this slice right now?
Expand All @@ -2435,53 +2391,7 @@ public bool GetCanMergeNow()
{
CheckDisposed();

var obj = GetObjectForMenusToOperateOn();
if (obj == null)
return false;

var owner = obj.Owner;
int flid = obj.OwningFlid;
// No support yet for atomic properties.
if (!Cache.IsVectorProperty(flid))
return false;

// Special handling for allomorphs, as they can be merged into the lexeme form.
int clsid = obj.ClassID;
if (flid == LexEntryTags.kflidAlternateForms)
{
// We can merge an alternate with the lexeme form,
// if it is the same class.
if (clsid == ((ILexEntry) owner).LexemeFormOA.ClassID)
return true;
}
// A subsense can always merge into its owning sense.
if (flid == LexSenseTags.kflidSenses)
return true;

int vectorSize = Cache.DomainDataByFlid.get_VecSize(owner.Hvo, flid);
if (owner.IsFieldRequired(flid)
&& vectorSize < 2)
return false;

// Check now to see if there are any other objects of the same class in the flid,
// since only objects of the same class can be merged.
int[] contents;
int chvoMax = Cache.DomainDataByFlid.get_VecSize(owner.Hvo, flid);
using (ArrayPtr arrayPtr = MarshalEx.ArrayToNative<int>(chvoMax))
{
Cache.DomainDataByFlid.VecProp(owner.Hvo, flid, chvoMax, out chvoMax, arrayPtr);
contents = MarshalEx.NativeToArray<int>(arrayPtr, chvoMax);
}
foreach (int hvoInner in contents)
{
var innerObj = m_cache.ServiceLocator.GetInstance<ICmObjectRepository>().GetObject(hvoInner);
if (innerObj != obj && clsid == innerObj.ClassID)
{
return true;
}
}

return false;
return DetailObjectMerge.CanMergeNow(GetObjectForMenusToOperateOn());
}

/// <summary></summary>
Expand Down
17 changes: 12 additions & 5 deletions Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ public static bool TryRestoreScroll(Control root, Memento memento)
/// current item when the memento asks for it (a move or removal made from the row keeps
/// the keyboard in the row); else the first surviving row the outgoing view asked for
/// (a gesture removed the focused row), which comes before the remembered editor only
/// when that is a row's menu button. Safe to call before the view is attached.
/// when that is a row's menu button. The scroll offset yields to the row of the object
/// asked for. Safe to call before the view is attached.
/// </summary>
public static void RestoreAfterLayout(Control root, Memento memento)
{
Expand Down Expand Up @@ -263,12 +264,18 @@ public static void RestoreAfterLayout(Control root, Memento memento)
else
RestoreRequestedFocus(root, memento);
}
// A bring-into-view request queued by a focus change is applied by the next
// arrange; the remembered offset is re-asserted after it.
// After the next arrange: re-assert the remembered offset, or bring the row
// of the object the gesture chose into view wherever it sits.
if (needsScroll)
{
Avalonia.Threading.Dispatcher.UIThread.Post(() => TryRestoreScroll(root, memento),
Avalonia.Threading.DispatcherPriority.Background);
var showsRequestedObject = focused && memento.ObjectHvo != 0;
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
if (showsRequestedObject)
(TopLevel.GetTopLevel(root)?.FocusManager?.GetFocusedElement() as Control)?.BringIntoView();
else
TryRestoreScroll(root, memento);
}, Avalonia.Threading.DispatcherPriority.Background);
}
};
root.LayoutUpdated += onLayout;
Expand Down
44 changes: 44 additions & 0 deletions Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,35 @@ public void RequestedObjectFocus_FocusesTheFirstRowShowingTheObject()
"the new form's editor takes focus");
}

// A gesture replaced an object whose row sits far from where the user had scrolled (a
// merge into the lexeme form from an allomorph near the bottom): the focused row shows
// instead of the remembered offset.
[AvaloniaTest]
public void RequestedObjectFocus_ScrollsToTheObjectsRow_InsteadOfKeepingTheOffset()
{
var first = NewLongViewOfForm(8);
var window = new Window { Content = first, Width = 420, Height = 200 };
window.Show();
Dispatcher.UIThread.RunJobs();
FindScroller(first).Offset = new Avalonia.Vector(0, 400);
Dispatcher.UIThread.RunJobs();
Assert.That(FindScroller(first).Offset.Y, Is.EqualTo(400).Within(0.5), "precondition: scrolled well down");

first.RequestObjectFocusOnRebuild(9);
var memento = DetailFocusMemory.Capture(first);
var second = NewLongViewOfForm(9);
DetailFocusMemory.RestoreAfterLayout(second, memento);
window.Content = second;
Dispatcher.UIThread.RunJobs();
second.UpdateLayout();
Dispatcher.UIThread.RunJobs();

Assert.That(FindEditor(second, "LexEntry/identity/#0@9.vern").IsFocused, Is.True, "the new form's editor takes focus");
// The editor, not the row, is brought into view: a few pixels of row padding stay
// above.
Assert.That(FindScroller(second).Offset.Y, Is.LessThan(10), "the view scrolled up to the editor's row");
}

// A gesture made from a header's hotlinks link added an object: the link survives the
// re-show and is remembered, but the object asked for takes focus over it.
[AvaloniaTest]
Expand Down Expand Up @@ -228,6 +257,21 @@ private static DataTree NewViewOfForm(int formHvo, bool withHotlinks = false)
menuRequested: withHotlinks ? request => { } : (Action<DetailMenuRequest>)null);
}

// A long view whose first row shows the form with the given hvo; the rest show object 3.
private static DataTree NewLongViewOfForm(int formHvo)
{
var values = new List<DetailWsValue> { new DetailWsValue("vern", "casa") };
var fields = new List<DetailField>();
fields.Add(new DetailField("LexEntry/identity/#0@" + formHvo, "Lexeme Form", "Form", "vernacular", DetailFieldKind.Text,
EditorClassification.Known, null, null, HostRouting.Product, values, null, null, objectHvo: formHvo));
for (var i = 1; i < 60; i++)
{
fields.Add(new DetailField("LexEntry/identity/#" + i + "@3", "Field " + i, "Gloss", "analysis", DetailFieldKind.Text,
EditorClassification.Known, null, null, HostRouting.Product, values, null, null, objectHvo: 3));
}
return new DataTree(new DetailModel("LexEntry", "identity", fields, new List<ViewDiagnostic>()));
}

private static DataTree NewLongView()
{
var roots = new List<ViewNode>();
Expand Down
69 changes: 69 additions & 0 deletions Src/FdoUi/DetailRules/DetailObjectMerge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 SIL.FieldWorks.Common.FwUtils;
using SIL.LCModel;

// Rules both detail views share. Nothing here may depend on WinForms, Avalonia, or a slice
// or control type, so the rules outlive WinForms. DetailRulesBoundaryTests enforces this.
namespace SIL.FieldWorks.Common.DetailRules
{
/// <summary>
/// The Merge command of a detail row (message DataTreeMerge): whether the row's object
/// has something to merge into now, and the menu text that says so. Execution stays with
/// <c>CmObjectUi.MergeUnderlyingObject</c>, which asks for the survivor.
/// </summary>
public static class DetailObjectMerge
{
/// <summary>The strings-table key of the text appended to a disabled Merge
/// item.</summary>
public const string CannotMergeKey = "(cannot merge this)";

/// <summary>
/// Whether <paramref name="target"/> can merge into another object now: it sits in an
/// owning vector that holds another object of its class, or an allomorph of the lexeme
/// form's class, or a subsense (which merges into its sense). A required vector keeps
/// its last item.
/// </summary>
public static bool CanMergeNow(ICmObject target)
{
if (target == null || !target.IsValidObject)
return false;
var owner = target.Owner;
var flid = target.OwningFlid;
var cache = target.Cache;
if (owner == null || !cache.IsVectorProperty(flid))
return false;
var classId = target.ClassID;
if (flid == LexEntryTags.kflidAlternateForms)
{
var lexemeForm = ((ILexEntry)owner).LexemeFormOA;
if (lexemeForm != null && lexemeForm.ClassID == classId)
return true;
}
if (flid == LexSenseTags.kflidSenses)
return true;
var sda = cache.DomainDataByFlid;
var count = sda.get_VecSize(owner.Hvo, flid);
if (owner.IsFieldRequired(flid) && count < 2)
return false;
var repository = cache.ServiceLocator.ObjectRepository;
for (var i = 0; i < count; i++)
{
var hvo = sda.get_VecItem(owner.Hvo, flid, i);
if (hvo != target.Hvo && repository.GetObject(hvo).ClassID == classId)
return true;
}
return false;
}

/// <summary>The Merge item's text: the configured label, with the cannot-merge note
/// appended when <paramref name="canMerge"/> is false.</summary>
public static string MergeLabel(string label, bool canMerge)
{
var text = label ?? string.Empty;
return canMerge ? text : text + StringTable.Table.GetString(CannotMergeKey);
}
}
}
Loading
Loading