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
118 changes: 10 additions & 108 deletions Src/Common/Controls/DetailControls/DataTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4631,119 +4631,21 @@ private void OnJumpToToolAndFilterAnthroItem(string toolToJumpTo)
}

/// <summary>
/// Common logic shared between OnDisplayJumpToTool and OnJumpToTool.
/// forEnableOnly is true when called from OnDisplayJumpToTool.
/// Common logic shared between OnDisplayJumpToTool and OnJumpToTool: the target
/// <see cref="DetailJumpTargets"/> resolves for the current slice, or the root when
/// there is none. A real jump to the notebook first creates the record a text lacks.
/// </summary>
/// <param name="forEnableOnly">True when called from OnDisplayJumpToTool.</param>
internal Guid GetGuidForJumpToTool(Command cmd, bool forEnableOnly, out string tool)
{
tool = XmlUtils.GetMandatoryAttributeValue(cmd.Parameters[0], "tool");
string className = XmlUtils.GetMandatoryAttributeValue(cmd.Parameters[0], "className");
ICmObject targetObject;
if (CurrentSlice == null)
targetObject = Root;
else
targetObject = CurrentSlice.Object;
if (targetObject != null)
{
var owner = targetObject.Owner;
if (tool == "concordance")
{
int flidSlice = 0;
if (CurrentSlice != null && !CurrentSlice.IsHeaderNode)
{
flidSlice = CurrentSlice.Flid;
if (flidSlice == 0 || m_mdc.get_IsVirtual(flidSlice))
return cmd.TargetId;
}
switch (className)
{
case "LexEntry":
if (m_root != null && m_root.ClassID == LexEntryTags.kClassId)
{
if (cmd.Id == "CmdRootEntryJumpToConcordance")
{
return m_root.Guid;
}

if (targetObject.ClassID == LexEntryRefTags.kClassId)
return cmd.TargetId;

if (targetObject.ClassID == LexEntryTags.kClassId)
return targetObject.Guid;

var lexEntry = targetObject.OwnerOfClass<ILexEntry>();
return lexEntry == null ? cmd.TargetId : lexEntry.Guid;
}
break;
case "LexSense":
if (targetObject.ClassID == LexSenseTags.kClassId)
{
if (((ILexSense)targetObject).Entry == m_root)
return targetObject.Guid;
}
break;
case "MoForm":
if (m_cache.ClassIsOrInheritsFrom(targetObject.ClassID, MoFormTags.kClassId))
{
if (flidSlice == MoFormTags.kflidForm)
return targetObject.Guid;
}
break;
}
}
else if (tool == "lexiconEdit")
{
if (owner != null && owner != m_root && owner.ClassID == LexEntryTags.kClassId)
{
return owner.Guid;
}
}
else if (tool == "notebookEdit")
{
if (owner != null &&
owner.ClassID == RnGenericRecTags.kClassId)
return owner.Guid;
if (targetObject is IText)
{
IRnGenericRec referringRecord;
if (NotebookRecordRefersToThisText(targetObject as IText, out referringRecord))
return referringRecord.Guid;

// Text is not already associated with a notebook record. So there's nothing yet to jump to.
// If the user is really doing the jump we need to make it now.
// Otherwise we just need to return something non-null to indicate the jump
// is possible (though this is not currently used).
if (forEnableOnly)
return targetObject.Guid;
// User is really making the jump. Create a notebook record, associate it, and jump.
var newNotebookRec = CreateAndAssociateNotebookRecord();
return newNotebookRec.Guid;
}
// Try TargetId by default
}
else if (tool == "interlinearEdit")
{
if (targetObject.ClassID == TextTags.kClassId)
{
return targetObject.Guid;
}
}
}
return cmd.TargetId;
}

private IRnGenericRec CreateAndAssociateNotebookRecord()
{
if (!(CurrentSlice.Object is IText))
throw new ArgumentException("CurrentSlice.Object ought to be a Text object.");

IText textObj = CurrentSlice.Object as IText;
// Create new Notebook record

((IText)CurrentSlice.Object).AssociateWithNotebook(true);
IRnGenericRec referringRecord;
NotebookRecordRefersToThisText(CurrentSlice.Object as IText, out referringRecord);
return referringRecord;
var target = CurrentSlice == null ? m_root : CurrentSlice.Object;
int? flid = CurrentSlice != null && !CurrentSlice.IsHeaderNode ? CurrentSlice.Flid : (int?)null;
var jump = DetailJumpTargets.Resolve(tool, className, cmd.Id, cmd.TargetId, m_root, target, flid);
if (jump.TextWithoutRecord != null && !forEnableOnly)
return DetailJumpTargets.AssociateWithNotebook(jump.TextWithoutRecord).Guid;
return jump.Guid;
}

internal static bool NotebookRecordRefersToThisText(IText text, out IRnGenericRec referringRecord)
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 @@ -318,4 +318,13 @@
<data name="ksPopupMenusDisabled" xml:space="preserve">
<value>Context menus are disabled in popup windows.</value>
</data>
<data name="ksSwapLexWithAllo" xml:space="preserve">
<value>Swap Lexeme Form with Allomorph</value>
</data>
<data name="ksAlternateForms" xml:space="preserve">
<value>Alternate Forms</value>
</data>
<data name="ksSwapXWithY" xml:space="preserve">
<value>Swap: "{0}"{2}With: "{1}"</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;
using SIL.FieldWorks.Common.Framework.DetailControls.Resources;
using SIL.FieldWorks.Common.Widgets;
using SIL.LCModel;
using XCore;
Expand All @@ -14,7 +15,7 @@
using SIL.LCModel.Core.Text;
using SIL.LCModel.Core.KernelInterfaces;

namespace SIL.FieldWorks.XWorks.LexEd
namespace SIL.FieldWorks.Common.Framework.DetailControls
{
/// <summary>
/// Summary description for SwapLexemeWithAllomorphDlg.
Expand Down Expand Up @@ -132,8 +133,8 @@ public void SetDlgInfo(LcmCache cache, PropertyTable propertyTable, ILexEntry en
// StartPosition = FormStartPosition.Manual;
//}
m_lvAlloOptions.Items[0].Selected = true;
Text = LexEdStrings.ksSwapLexWithAllo;
label2.Text = LexEdStrings.ksAlternateForms;
Text = DetailControlsStrings.ksSwapLexWithAllo;
label2.Text = DetailControlsStrings.ksAlternateForms;

// Determine the help file to use, if any
m_helpTopic = "khtpSwapLexemeWithAllomorph";
Expand Down Expand Up @@ -193,7 +194,7 @@ private void SetBottomMessage()
m_fwTextBoxBottomMsg.WritingSystemCode = userWs;
string sLexVal = m_entry.LexemeFormOA.Form.VernacularDefaultWritingSystem.Text ?? "";
// Treat null value as empty string. This fixes LT-5889, LT-5891, and LT-5914.
string sFmt = LexEdStrings.ksSwapXWithY;
string sFmt = DetailControlsStrings.ksSwapXWithY;
string sWithVal = m_allomorph.Form.VernacularDefaultWritingSystem.Text ?? "";
ITsString tss = TsStringUtils.MakeString(String.Format(sFmt, sLexVal, sWithVal, StringUtils.kChHardLB), userWs);
m_fwTextBoxBottomMsg.Tss = tss;
Expand Down
18 changes: 18 additions & 0 deletions Src/Common/FwAvalonia/Detail/DataTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,24 @@ internal IReadOnlyList<FwReferenceVectorField> VectorRows
public void RequestRowFocusOnRebuild(IReadOnlyList<string> rowAutomationIds)
=> RowFocusRequested = rowAutomationIds;

/// <summary>The object whose first shown row the rebuild that follows should focus;
/// 0 when none was requested.</summary>
internal int ObjectFocusRequested { get; private set; }

/// <summary>
/// Asks the rebuild that follows to give keyboard focus to the first row showing
/// <paramref name="hvo"/>, for a gesture that replaced the focused row's object with
/// a new one (its rows have ids the outgoing view cannot know).
/// </summary>
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.
/// </summary>
internal bool FocusFirstRowOf(int hvo)
=> hvo != 0 && FocusFirstRow(Model.Fields.Where(f => f.ObjectHvo == hvo).Select(f => f.RowAutomationId));

/// <summary>
/// Focuses the first row of <paramref name="rowAutomationIds"/> that is shown and
/// can take focus; false when none can.
Expand Down
22 changes: 17 additions & 5 deletions Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public sealed class Memento
{
public Memento(string automationId, int caretIndex, double verticalOffset = 0,
string vectorAutomationId = null, string vectorItemKey = null, int vectorItemIndex = -1,
bool focusVectorItem = false, IReadOnlyList<string> rowAutomationIds = null)
bool focusVectorItem = false, IReadOnlyList<string> rowAutomationIds = null,
int objectHvo = 0)
{
AutomationId = automationId;
CaretIndex = caretIndex;
Expand All @@ -41,8 +42,15 @@ public Memento(string automationId, int caretIndex, double verticalOffset = 0,
VectorItemIndex = vectorItemIndex;
FocusVectorItem = focusVectorItem;
RowAutomationIds = rowAutomationIds;
ObjectHvo = objectHvo;
}

/// <summary>
/// The object whose first row the outgoing view asked to focus after the rebuild
/// (a gesture replaced the focused row's object); 0 when none.
/// </summary>
public int ObjectHvo { get; }

/// <summary>
/// The automation ids of the rows the outgoing view asked to try focusing after
/// the rebuild, in order (a gesture removed the focused row and maybe its
Expand Down Expand Up @@ -94,10 +102,11 @@ public static Memento Capture(Control root)
var vectorIndex = selectedVector?.SelectedItemIndex ?? -1;
var focusVector = tree?.VectorFocusRequested ?? false;
var focusRow = tree?.RowFocusRequested;
var focusObject = tree?.ObjectFocusRequested ?? 0;

var focusManager = TopLevel.GetTopLevel(root)?.FocusManager;
if (!(focusManager?.GetFocusedElement() is Control focused) || !root.IsVisualAncestorOf(focused))
return new Memento(null, -1, verticalOffset, vectorId, vectorKey, vectorIndex, focusVector, focusRow);
return new Memento(null, -1, verticalOffset, vectorId, vectorKey, vectorIndex, focusVector, focusRow, focusObject);

// The editor itself carries the stable id (e.g. "LexemeFormEditor.vern"); walk up in
// case focus landed on an inner template part.
Expand All @@ -107,11 +116,11 @@ public static Memento Capture(Control root)
if (!string.IsNullOrEmpty(id))
{
return new Memento(id, (focused as TextBox)?.CaretIndex ?? -1, verticalOffset,
vectorId, vectorKey, vectorIndex, focusVector, focusRow);
vectorId, vectorKey, vectorIndex, focusVector, focusRow, focusObject);
}
}

return new Memento(null, -1, verticalOffset, vectorId, vectorKey, vectorIndex, focusVector, focusRow);
return new Memento(null, -1, verticalOffset, vectorId, vectorKey, vectorIndex, focusVector, focusRow, focusObject);
}

// Whether the rebuilt view should focus the vector row's current item: the outgoing view
Expand Down Expand Up @@ -220,7 +229,8 @@ public static bool TryRestoreScroll(Control root, Memento memento)
/// 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). Safe to call before the view is attached.
/// 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.
/// </summary>
public static void RestoreAfterLayout(Control root, Memento memento)
{
Expand All @@ -246,6 +256,8 @@ public static void RestoreAfterLayout(Control root, Memento 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);
}
// 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 Down
6 changes: 6 additions & 0 deletions Src/Common/FwAvalonia/DetailHostControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ public void ShowDetail(DetailModel detail, IDetailEditContext editContext = null
public void FocusRowOnNextShow(IReadOnlyList<string> rowAutomationIds)
=> (CurrentContent as DataTree)?.RequestRowFocusOnRebuild(rowAutomationIds);

/// <summary>
/// Asks the next re-show to give keyboard focus to the first row showing the object
/// with <paramref name="hvo"/>, for a gesture that replaced the focused row's object.
/// </summary>
public void FocusObjectOnNextShow(int hvo) => (CurrentContent as DataTree)?.RequestObjectFocusOnRebuild(hvo);

/// <summary>The detail model shown now; null before the first show.</summary>
public DetailModel ShownDetail => (CurrentContent as DataTree)?.Model;
}
Expand Down
42 changes: 42 additions & 0 deletions Src/Common/FwAvalonia/FwAvaloniaTests/DetailFocusMemoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,48 @@ public void RequestedRowFocus_YieldsToTheRememberedEditor_WhenItSurvives()
Assert.That(FindEditor(second, "GlossEditor.vern").IsFocused, Is.True);
}

// A gesture replaced the focused row's object with a new one, whose rows carry ids the
// outgoing view cannot know: the first row showing that object takes focus.
[AvaloniaTest]
public void RequestedObjectFocus_FocusesTheFirstRowShowingTheObject()
{
var first = NewViewOfForm(8);
var window = new Window { Content = first, Width = 420, Height = 200 };
window.Show();
Dispatcher.UIThread.RunJobs();
FindEditor(first, "LexEntry/identity/#0@8.vern").Focus();
Dispatcher.UIThread.RunJobs();

first.RequestObjectFocusOnRebuild(9);
var memento = DetailFocusMemory.Capture(first);
Assert.That(memento.AutomationId, Is.EqualTo("LexEntry/identity/#0@8.vern"), "precondition: the replaced form's editor had focus");
Assert.That(memento.ObjectHvo, Is.EqualTo(9));

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

Assert.That(FindEditor(second, "LexEntry/identity/#0@8.vern"), Is.Null, "precondition: the remembered editor is gone");
Assert.That(FindEditor(second, "LexEntry/identity/#0@9.vern").IsFocused, Is.True,
"the new form's editor takes focus");
}

// 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)
{
var values = new List<DetailWsValue> { new DetailWsValue("vern", "casa") };
var fields = new List<DetailField>
{
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>()));
}

private static DataTree NewLongView()
{
var roots = new List<ViewNode>();
Expand Down
Loading
Loading