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
100 changes: 29 additions & 71 deletions Src/Common/Controls/DetailControls/Slice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1935,12 +1935,7 @@ private bool InsertObjectIfPossible(int newObjectClassId, int ownerClassId, stri
|| slice.Object.Equals(ContainingDataTree.Root)) // Other cases.
{
// The slice's object has an acceptable type provided it implements the required field.
// See if the current slice's object has the field named.
int flid = slice.GetFlid(fieldName);
var mdc = Cache.MetaDataCacheAccessor as IFwMetaDataCacheManaged;
int flidT = ContainingDataTree.GetFlidIfPossible(ownerClassId, fieldName, mdc);
if (flidT != 0 && flid != flidT)
flid = flidT;
int flid = DetailObjectInsertion.ResolveFlid(slice.Object, fieldName, ownerClassId);
if (flid == 0)
return false;
// Found a suitable slice. Do the insertion.
Expand Down Expand Up @@ -2027,65 +2022,25 @@ int InsertObject(int flid, int newObjectClassId, out ICmObject newObject)

newObject = null;

bool fAbstract = m_cache.DomainDataByFlid.MetaDataCache.GetAbstract(newObjectClassId);
if (fAbstract)
{
// We've been handed an abstract class to insert. Try to determine the desired
// concrete from the context.
if (newObjectClassId == MoFormTags.kClassId && Object is ILexEntry)
{
var entry = (Object as ILexEntry);
newObjectClassId = entry.GetDefaultClassForNewAllomorph();
}
else
{
return -1;
}
}
newObjectClassId = DetailObjectInsertion.ConcreteClass(Object, newObjectClassId);
if (newObjectClassId == 0)
return -1;
// OK, we can add to property flid of the object of slice slice.
int insertionPosition = 0;//leave it at 0 if it does not matter
int hvoOwner = Object.Hvo;
int clidOwner = Object.ClassID;
int clidOfFlid = flid / 1000;
if (clidOwner != clidOfFlid && clidOfFlid == Object.Owner.ClassID)
var owner = DetailObjectInsertion.OwnerOf(Object, flid);
if (owner == null)
return -1;
int hvoOwner = owner.Hvo;
int insertionPosition;
try
{
hvoOwner = Object.Owner.Hvo;
// The current slice's key says where in the property it sits, so the new
// object goes after it. We might not be on the right slice to insert this
// item. See FWR-898.
insertionPosition = DetailObjectInsertion.InsertionPosition(owner, flid, CurrentSliceKeyHvos());
}
int type = GetFieldType(flid);
if (type == (int)CellarPropertyType.OwningSequence)
catch
{
try
{
// We might not be on the right slice to insert this item. See FWR-898.
insertionPosition = Cache.DomainDataByFlid.get_VecSize(hvoOwner, flid);
}
catch
{
return -1;
}
if (ContainingDataTree != null && ContainingDataTree.CurrentSlice != null)
{
ISilDataAccess sda = m_cache.DomainDataByFlid;
int chvo = insertionPosition;
// See if the current slice in any way indicates a position in that property.
object[] key = ContainingDataTree.CurrentSlice.Key;
bool fGotIt = false;
for (int ikey = key.Length - 1; ikey >= 0 && !fGotIt; ikey--)
{
if (!(key[ikey] is int))
continue;
var hvoTarget = (int)key[ikey];
for (int i = 0; i < chvo; i++)
{
if (hvoTarget == sda.get_VecItem(hvoOwner, flid, i))
{
insertionPosition = i + 1; // insert after current object.
fGotIt = true; // break outer loop
break;
}
}
}
}
return -1;
}
var slices = new HashSet<Slice>(ContainingDataTree.Slices);

Expand All @@ -2095,16 +2050,6 @@ int InsertObject(int flid, int newObjectClassId, out ICmObject newObject)
{
dtContainer.SetCurrentObjectFlids(hvoOwner, flid);
var fieldType = (CellarPropertyType) m_cache.MetaDataCacheAccessor.GetFieldType(flid);
switch (fieldType)
{
case CellarPropertyType.OwningCollection:
insertionPosition = -1;
break;

case CellarPropertyType.OwningAtomic:
insertionPosition = -2;
break;
}
using (CmObjectUi uiObj = CmObjectUi.CreateNewUiObject(m_mediator, m_propertyTable, newObjectClassId, hvoOwner, flid, insertionPosition))
{
// If uiObj is null, typically CreateNewUiObject displayed a dialog and the user cancelled.
Expand Down Expand Up @@ -2181,6 +2126,19 @@ int InsertObject(int flid, int newObjectClassId, out ICmObject newObject)
return insertionPosition;
}

// The objects of the current slice's key, deepest first; none without a current slice.
private IEnumerable<int> CurrentSliceKeyHvos()
{
var key = ContainingDataTree?.CurrentSlice?.Key;
if (key == null)
yield break;
for (int ikey = key.Length - 1; ikey >= 0; ikey--)
{
if (key[ikey] is int hvo)
yield return hvo;
}
}

/// <summary>
/// Find a slice nested below this one whose object is hvo and expand it if it is collapsed.
/// </summary>
Expand Down
12 changes: 9 additions & 3 deletions Src/Common/FwAvalonia/Detail/DataTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,17 @@ public void RequestRowFocusOnRebuild(IReadOnlyList<string> rowAutomationIds)
public void RequestObjectFocusOnRebuild(int hvo) => ObjectFocusRequested = hvo;

/// <summary>
/// Focuses the first shown row of the object with <paramref name="hvo"/> that can
/// take focus; false when none can.
/// Focuses the first shown editor row of the object with <paramref name="hvo"/> that
/// can take focus, else its first header row that can; false when none can.
/// </summary>
internal bool FocusFirstRowOf(int hvo)
=> hvo != 0 && FocusFirstRow(Model.Fields.Where(f => f.ObjectHvo == hvo).Select(f => f.RowAutomationId));
{
if (hvo == 0)
return false;
var rows = Model.Fields.Where(f => f.ObjectHvo == hvo).ToList();
return FocusFirstRow(rows.Where(f => f.Kind != DetailFieldKind.Header).Select(f => f.RowAutomationId))
|| FocusFirstRow(rows.Where(f => f.Kind == DetailFieldKind.Header).Select(f => f.RowAutomationId));
}

/// <summary>
/// Focuses the first row of <paramref name="rowAutomationIds"/> that is shown and
Expand Down
41 changes: 31 additions & 10 deletions Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,12 @@ public static bool TryRestoreScroll(Control root, Memento memento)
/// <summary>
/// Restores the memento once the incoming view has laid out: the scroll offset first (an
/// offset set before layout is clamped to zero by the still-empty extent), then the
/// vector row's current item, then focus -- the remembered editor, else that 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), else the first row of the object it asked for (a
/// gesture replaced the row's object). Safe to call before the view is attached.
/// vector row's current item, then focus -- the object the outgoing view asked for (a
/// gesture added or replaced one) first of all; else the remembered editor; else that
/// 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.
/// </summary>
public static void RestoreAfterLayout(Control root, Memento memento)
{
Expand All @@ -250,14 +251,17 @@ public static void RestoreAfterLayout(Control root, Memento memento)
if (needsScroll)
TryRestoreScroll(root, memento);
var vector = RestoreVectorSelection(root, memento);
if (!TryRestoreFocus(root, memento))
// The object a gesture added or replaced takes the keyboard outright. A row
// request yields to a surviving editor, unless the gesture was made from a
// row's menu button, which left focus on that button.
var focused = (memento.ObjectHvo != 0 || RemembersAMenuButton(memento))
&& RestoreRequestedFocus(root, memento);
if (!focused && !TryRestoreFocus(root, memento))
{
if (vector != null && WantsVectorFocus(memento))
vector.FocusItemAt(vector.SelectedItemIndex);
else if (memento.RowAutomationIds != null)
(root as DataTree)?.FocusFirstRow(memento.RowAutomationIds);
else if (memento.ObjectHvo != 0)
(root as DataTree)?.FocusFirstRowOf(memento.ObjectHvo);
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.
Expand All @@ -270,6 +274,23 @@ public static void RestoreAfterLayout(Control root, Memento memento)
root.LayoutUpdated += onLayout;
}

// Whether the remembered control is a row's field-options button or hotlinks link
// rather than an editor.
private static bool RemembersAMenuButton(Memento memento)
=> memento.AutomationId != null
&& (memento.AutomationId.EndsWith(".FieldMenu", StringComparison.Ordinal)
|| memento.AutomationId.EndsWith(".Hotlinks", StringComparison.Ordinal));

// Focuses what the outgoing view asked for: the first surviving row of those named,
// else the first row of the object named; false when it asked for nothing or none can.
private static bool RestoreRequestedFocus(Control root, Memento memento)
{
var tree = root as DataTree;
if (memento.RowAutomationIds != null)
return tree?.FocusFirstRow(memento.RowAutomationIds) == true;
return memento.ObjectHvo != 0 && tree?.FocusFirstRowOf(memento.ObjectHvo) == true;
}

/// <summary>
/// Focuses the control with the memento's automation id inside <paramref name="root"/>
/// (which must already be attached to a TopLevel). Returns false when no match exists --
Expand Down
97 changes: 88 additions & 9 deletions Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This software is licensed under the LGPL, version 2.1 or later
// (http://www.gnu.org/licenses/lgpl-2.1.html)

using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Automation;
Expand Down Expand Up @@ -134,19 +135,97 @@ public void RequestedObjectFocus_FocusesTheFirstRowShowingTheObject()
"the new form's editor takes focus");
}

// 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]
public void RequestedObjectFocus_WinsOverTheRememberedHotlinksLink()
{
var first = NewViewOfForm(8, withHotlinks: true);
var window = new Window { Content = first, Width = 420, Height = 200 };
window.Show();
Dispatcher.UIThread.RunJobs();
var link = first.GetVisualDescendants().OfType<Button>()
.Single(b => AutomationProperties.GetAutomationId(b) == "LexEntry/identity/senses.Hotlinks");
link.Focus();
Dispatcher.UIThread.RunJobs();

first.RequestObjectFocusOnRebuild(9);
var memento = DetailFocusMemory.Capture(first);
Assert.That(memento.AutomationId, Is.EqualTo("LexEntry/identity/senses.Hotlinks"), "precondition: the link had focus");

var second = NewViewOfForm(9, withHotlinks: true);
DetailFocusMemory.RestoreAfterLayout(second, memento);
window.Content = second;
Dispatcher.UIThread.RunJobs();

Assert.That(FindEditor(second, "LexEntry/identity/#0@9.vern").IsFocused, Is.True,
"the new object's editor takes focus, not the surviving link");
}

// An insert made from another row's menu leaves that row's editor focused and alive;
// the new object still takes the keyboard, as a row request would not.
[AvaloniaTest]
public void RequestedObjectFocus_WinsOverASurvivingEditor()
{
var first = NewViewOfForm(8);
var window = new Window { Content = first, Width = 420, Height = 200 };
window.Show();
Dispatcher.UIThread.RunJobs();
FindEditor(first, "LexEntry/identity/#1@3.vern").Focus();
Dispatcher.UIThread.RunJobs();
first.RequestObjectFocusOnRebuild(8);
var memento = DetailFocusMemory.Capture(first);
Assert.That(memento.AutomationId, Is.EqualTo("LexEntry/identity/#1@3.vern"), "precondition: Gloss had focus");

var second = NewViewOfForm(8);
DetailFocusMemory.RestoreAfterLayout(second, memento);
window.Content = second;
Dispatcher.UIThread.RunJobs();

Assert.That(FindEditor(second, "LexEntry/identity/#0@8.vern").IsFocused, Is.True,
"the object asked for, not the surviving Gloss editor");
}

// The object asked for has a header row before its editor rows: its first editor takes
// focus, so typing can start at once; the header only when no editor can.
[AvaloniaTest]
public void RequestedObjectFocus_PrefersTheObjectsFirstEditor_OverItsHeader()
{
var first = NewViewOfForm(8, withHotlinks: true);
var window = new Window { Content = first, Width = 420, Height = 200 };
window.Show();
Dispatcher.UIThread.RunJobs();
first.RequestObjectFocusOnRebuild(3);
var memento = DetailFocusMemory.Capture(first);

var second = NewViewOfForm(8, withHotlinks: true);
DetailFocusMemory.RestoreAfterLayout(second, memento);
window.Content = second;
Dispatcher.UIThread.RunJobs();

Assert.That(FindEditor(second, "LexEntry/identity/#1@3.vern").IsFocused, Is.True,
"the Gloss editor of object 3, not its Senses header");
}

// A view whose Lexeme Form row shows the form with the given hvo, its row ids derived
// from the stable ids as the composer derives them.
private static DataTree NewViewOfForm(int formHvo)
// from the stable ids as the composer derives them; with hotlinks, a Senses header
// row with a hotlinks link comes first.
private static DataTree NewViewOfForm(int formHvo, bool withHotlinks = false)
{
var values = new List<DetailWsValue> { new DetailWsValue("vern", "casa") };
var fields = new List<DetailField>
var fields = new List<DetailField>();
if (withHotlinks)
{
new DetailField("LexEntry/identity/#0@" + formHvo, "Lexeme Form", "Form", "vernacular", DetailFieldKind.Text,
EditorClassification.Known, null, null, HostRouting.Product, values, null, null, objectHvo: formHvo),
new DetailField("LexEntry/identity/#1@3", "Gloss", "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>()));
fields.Add(new DetailField("LexEntry/identity/senses", "Senses", "Senses", null, DetailFieldKind.Header,
EditorClassification.GroupingNone, null, null, HostRouting.Product, null, null, null,
isEditable: false, isCollapsible: true, hotlinksId: "mnuDataTree-Sense-Hotlinks", objectHvo: 3));
}
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));
fields.Add(new DetailField("LexEntry/identity/#1@3", "Gloss", "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>()),
menuRequested: withHotlinks ? request => { } : (Action<DetailMenuRequest>)null);
}

private static DataTree NewLongView()
Expand Down
Loading
Loading