diff --git a/Src/Common/Controls/DetailControls/DataTree.cs b/Src/Common/Controls/DetailControls/DataTree.cs
index 65adc8f8c6..b96980dbe0 100644
--- a/Src/Common/Controls/DetailControls/DataTree.cs
+++ b/Src/Common/Controls/DetailControls/DataTree.cs
@@ -5,11 +5,11 @@
using SIL.FieldWorks.Common.Controls;
using SIL.FieldWorks.Common.Framework.DetailControls.Resources;
using SIL.FieldWorks.Common.FwUtils;
-using static SIL.FieldWorks.Common.FwUtils.FwUtils;
using SIL.FieldWorks.Common.RootSites;
using SIL.LCModel;
using SIL.LCModel.Core.Cellar;
using SIL.LCModel.Core.KernelInterfaces;
+using SIL.LCModel.Core.Text;
using SIL.LCModel.Core.WritingSystems;
using SIL.LCModel.DomainServices;
using SIL.LCModel.Infrastructure;
@@ -28,6 +28,7 @@
using System.Windows.Forms;
using System.Xml;
using XCore;
+using static SIL.FieldWorks.Common.FwUtils.FwUtils;
namespace SIL.FieldWorks.Common.Framework.DetailControls
{
@@ -1047,6 +1048,243 @@ public virtual void ShowObject(ICmObject root, string layoutName, string layoutC
}
}
+ ///
+ /// Jump to the slice that contains the given field object and value.
+ ///
+ private void JumpToField(object arguments)
+ {
+ var array = (object[])arguments;
+ int fieldHvo = (int)array[0];
+ string fieldName = PlainFieldName((string)array[1]);
+ string fieldValue = (string)array[2];
+ ICmObject fieldObj = Cache.ServiceLocator.GetInstance().GetObject(fieldHvo);
+ // fieldObj.fieldName == fieldValue.
+ bool found = false;
+ if (!String.IsNullOrEmpty(fieldName))
+ {
+ // Try matching object and field first.
+ foreach (Slice slice in Slices)
+ {
+ if (slice.IsHeaderNode)
+ {
+ continue;
+ }
+ int sliceFlid = GetFlid(slice);
+ string sliceFieldName = sliceFlid == 0 ? "" : m_cache.MetaDataCacheAccessor.GetFieldName(sliceFlid);
+ if (sliceFieldName == fieldName && slice.Object == fieldObj)
+ {
+ found = true;
+ }
+ // Look for special cases.
+ else if (fieldName == "ComplexFormEntryRefs" && sliceFieldName == "PrimaryLexemes" &&
+ slice.Object is ILexEntryRef ler && ler.OwningEntry == fieldObj)
+ {
+ found = true;
+ }
+ else if (fieldName == "ConfigReferencedEntries" && sliceFieldName == "ComponentLexemes" &&
+ slice.Object == fieldObj)
+ {
+ found = true;
+ }
+ else if (fieldName == "DefinitionOrGloss" && (sliceFieldName == "Definition" || sliceFieldName == "Gloss") &&
+ slice.Object == fieldObj &&
+ SliceMatchesValue(slice, fieldValue))
+ {
+ found = true;
+ }
+ else if (fieldName == "LookupComplexEntryType" && sliceFieldName == "ComplexEntryTypes" &&
+ slice.Object is ILexEntryRef ler2 && ler2.OwningEntry == fieldObj)
+ {
+ found = true;
+ }
+ else if ((fieldName == "MLHeadWord" || fieldName == "HeadWordRef") &&
+ (sliceFieldName == "Form" || sliceFieldName == "CitationForm") &&
+ (slice.Object == fieldObj || fieldObj is ILexEntry lexEntry && lexEntry.LexemeFormOA == slice.Object) &&
+ SliceMatchesValue(slice, fieldValue))
+ {
+ found = true;
+ }
+ else if (fieldName == "MLPartOfSpeech" && sliceFieldName == "MorphoSyntaxAnalysis" &&
+ slice is MSAReferenceComboBoxSlice && slice.Object is ILexSense sense && sense.MorphoSyntaxAnalysisRA == fieldObj)
+ {
+ found = true;
+ }
+ else if (fieldName == "MorphTypes" && sliceFieldName == "MorphType" &&
+ slice.Object is IMoAffixForm affix && fieldObj is IMoMorphSynAnalysis msa && msa.MorphTypes.Contains(affix.MorphTypeRA))
+ {
+ found = true;
+ }
+ else if (fieldName == "MorphTypes" && sliceFieldName == "MorphTypes" &&
+ slice.Object is IMoAffixForm affix2 && affix2.MorphTypeRA == fieldObj)
+ {
+ found = true;
+ }
+ else if (fieldName == "ReverseAbbr" && sliceFieldName == "ComplexEntryTypes" &&
+ slice.Object is ILexEntryRef ler3 && ler3.ComplexEntryTypesRS.Contains(fieldObj))
+ {
+ found = true;
+ }
+ else if (fieldName == "ReverseAbbr" && sliceFieldName == "VariantEntryTypes" &&
+ slice.Object is ILexEntryRef ler4 && ler4.EntryTypes.Contains(fieldObj))
+ {
+ found = true;
+ }
+ if (found)
+ {
+ m_currentSliceNew = slice;
+ break;
+ }
+ }
+ }
+ if (!found)
+ {
+ Slice objectSlice = null;
+ Slice valueSlice = null;
+ // Look for the closest matching slice.
+ foreach (Slice slice in Slices)
+ {
+ if (slice.IsHeaderNode)
+ {
+ continue;
+ }
+ int sliceFlid = GetFlid(slice);
+ string sliceFieldName = sliceFlid == 0 ? "" : m_cache.MetaDataCacheAccessor.GetFieldName(sliceFlid);
+ if ((fieldName == "MLHeadWord" || fieldName == "HeadWordRef") && sliceFieldName == "Form" &&
+ (slice.Object == fieldObj || fieldObj is ILexEntry lexEntry && lexEntry.LexemeFormOA == slice.Object))
+ {
+ // MLHeadWord, and HeadWordRef default to Form if the value doesn't match CitationForm or Form.
+ m_currentSliceNew = slice;
+ found = true;
+ break;
+ }
+ // Try matching object and/or value.
+ if (slice.Object == fieldObj && SliceMatchesValue(slice, fieldValue))
+ {
+ m_currentSliceNew = slice;
+ found = true;
+ break;
+ }
+ if (objectSlice == null && fieldObj != Root && slice.Object == fieldObj)
+ {
+ objectSlice = slice;
+ }
+ if (valueSlice == null && SliceMatchesValue(slice, fieldValue))
+ {
+ valueSlice = slice;
+ }
+ }
+ // Prefer matching value over matching object.
+ if (!found && valueSlice != null)
+ {
+ m_currentSliceNew = valueSlice;
+ found = true;
+ }
+ else if (!found && objectSlice != null)
+ {
+ m_currentSliceNew = objectSlice;
+ found = true;
+ }
+ }
+ if (found)
+ {
+ // Set the current slice.
+ m_fCurrentContentControlObjectTriggered = true;
+ OnReadyToSetCurrentSlice(false);
+ }
+ }
+
+ private string PlainFieldName(string fieldname)
+ {
+ if (fieldname.EndsWith("OA") || fieldname.EndsWith("OS") || fieldname.EndsWith("OC")
+ || fieldname.EndsWith("RA") || fieldname.EndsWith("RS") || fieldname.EndsWith("RC"))
+ {
+ return fieldname.Substring(0, fieldname.Length - 2);
+ }
+ return fieldname;
+ }
+
+ private int GetFlid(Slice slice)
+ {
+ if (slice.Flid != 0)
+ {
+ return slice.Flid;
+ }
+ if (slice is ViewPropertySlice vpSlice)
+ {
+ return vpSlice.FieldId;
+ }
+ return 0;
+ }
+
+
+ ///
+ /// Does the slice's display text match the given text?
+ ///
+ private bool SliceMatchesValue(Slice slice, string text)
+ {
+ try
+ {
+ if (slice is MultiStringSlice)
+ {
+ ITsMultiString multiString = m_cache.DomainDataByFlid.get_MultiStringProp(slice.Object.Hvo, GetFlid(slice));
+ if (MultiStringMatchesValue(multiString, text))
+ {
+ return true;
+ }
+ for (int i = 0; i < multiString.StringCount; i++)
+ {
+ ITsString tsString = multiString.GetStringFromIndex(i, out int ws);
+ if (tsString.Text == text)
+ {
+ return true;
+ }
+ }
+ }
+ else if (slice is MorphTypeAtomicReferenceSlice && slice.Object is IMoAffixForm affix)
+ {
+ IMoMorphType morphType = affix.MorphTypeRA;
+ if (MultiStringMatchesValue(morphType.Name, text) || MultiStringMatchesValue(morphType.Abbreviation, text))
+ {
+ return true;
+ }
+
+ }
+ else if (slice is PossibilityReferenceVectorSlice)
+ {
+ int[] hvos = SetupContents(GetFlid(slice), slice.Object);
+ for (int i = 0; i < hvos.Length; i++)
+ {
+ ICmObject obj = m_cache.ServiceLocator.GetInstance().GetObject(hvos[i]);
+ if (obj is ICmPossibility possibility)
+ {
+ if (MultiStringMatchesValue(possibility.Name, text) || MultiStringMatchesValue(possibility.Abbreviation, text))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ catch { }
+ return false;
+ }
+
+ private bool MultiStringMatchesValue(ITsMultiString multiString, string text)
+ {
+ if (multiString != null)
+ {
+ for (int i = 0; i < multiString.StringCount; i++)
+ {
+ ITsString tsString = multiString.GetStringFromIndex(i, out int ws);
+ if (TsStringUtils.NormalizeNfd(tsString.Text) == TsStringUtils.NormalizeNfd(text))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
private void SetCurrentSliceNewFromObject(ICmObject obj)
{
foreach (Slice slice in Slices)
@@ -1248,6 +1486,7 @@ protected override void Dispose(bool disposing)
if (disposing)
{
Subscriber.Unsubscribe(EventConstants.PostponePropChanged, PostponePropChanged);
+ Subscriber.Unsubscribe(EventConstants.JumpToField, JumpToField);
// Do this first, before setting m_fDisposing to true.
if (m_sda != null)
@@ -3711,6 +3950,7 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu
RestorePreferences();
Subscriber.Subscribe(EventConstants.PostponePropChanged, PostponePropChanged);
+ Subscriber.Subscribe(EventConstants.JumpToField, JumpToField);
}
public IxCoreColleague[] GetMessageTargets()
diff --git a/Src/Common/FwUtils/EventConstants.cs b/Src/Common/FwUtils/EventConstants.cs
index 2c162065b8..165813687e 100644
--- a/Src/Common/FwUtils/EventConstants.cs
+++ b/Src/Common/FwUtils/EventConstants.cs
@@ -21,6 +21,7 @@ public static class EventConstants
public const string GetToolForList = "GetToolForList";
public const string HandleLocalHotlink = "HandleLocalHotlink";
public const string ItemDataModified = "ItemDataModified";
+ public const string JumpToField = "JumpToField";
public const string JumpToPopupLexEntry = "JumpToPopupLexEntry";
public const string JumpToRecord = "JumpToRecord";
public const string LinkFollowed = "LinkFollowed";
diff --git a/Src/xWorks/ConfiguredLcmGenerator.cs b/Src/xWorks/ConfiguredLcmGenerator.cs
index 52b3de8b74..d302e9c60f 100644
--- a/Src/xWorks/ConfiguredLcmGenerator.cs
+++ b/Src/xWorks/ConfiguredLcmGenerator.cs
@@ -23,7 +23,9 @@
using SIL.Reporting;
using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -404,6 +406,11 @@ internal static IFragment GenerateContentForEntry(ICmObject entry, ConfigurableD
return settings.ContentGenerator.CreateFragment();
}
+ if (entry is ILexEntry)
+ {
+ // Record the guid of the source entry for JumpToField.
+ settings.ConfigSource[configuration] = entry.Guid;
+ }
var nodeList = BuildNodeList(new List(), configuration);
var pieces = configuration.ReferencedOrDirectChildren
.Select(childNode => new ConfigFragment(childNode, GenerateContentForFieldByReflection(entry, BuildNodeList(nodeList, childNode), publicationDecorator,
@@ -493,6 +500,11 @@ internal static IFragment GenerateContentForFieldByReflection(object field, List
bool fUseReverseSubField = false)
{
var config = nodeList.Last();
+ if (field is ICmObject fieldObj)
+ {
+ // Record the guid of the source field for JumpToField.
+ settings.ConfigSource[config] = fieldObj.Guid;
+ }
if (!config.IsEnabled)
{
@@ -2161,6 +2173,11 @@ private static IFragment GenerateCollectionItemContent(List ConfigSource = new ConcurrentDictionary();
+ public bool WriteConfigSource = true;
+
public LcmCache Cache { get; }
public ReadOnlyPropertyTable PropertyTable { get; }
public bool UseRelativePaths { get; }
diff --git a/Src/xWorks/LcmJsonGenerator.cs b/Src/xWorks/LcmJsonGenerator.cs
index cead3b45a8..8b9d84e509 100644
--- a/Src/xWorks/LcmJsonGenerator.cs
+++ b/Src/xWorks/LcmJsonGenerator.cs
@@ -600,6 +600,7 @@ public static List SavePublishedJsonWithStyles(int[] entriesToSave, Dict
// could contain different data for unique names. The unique names can be generated
// in different orders.
displayXhtmlSettings.StylesGenerator = settings.StylesGenerator;
+ displayXhtmlSettings.WriteConfigSource = false;
var entryContents = new Tuple[entryCount];
var entryActions = new List();
diff --git a/Src/xWorks/LcmXhtmlGenerator.cs b/Src/xWorks/LcmXhtmlGenerator.cs
index e103f98844..458c6cd9e7 100644
--- a/Src/xWorks/LcmXhtmlGenerator.cs
+++ b/Src/xWorks/LcmXhtmlGenerator.cs
@@ -40,7 +40,6 @@ public class LcmXhtmlGenerator : ILcmContentGenerator
public const int EntriesPerPage = 1000;
#endif
-
///
/// Saves the generated content in the Temp directory, to a unique but discoverable and somewhat stable location.
///
@@ -570,6 +569,12 @@ private void WriteNodeId(XmlWriter xw, ConfigurableDictionaryNode config, Config
if (settings != null && (settings.IsWebExport || settings.IsXhtmlExport))
return;
xw.WriteAttributeString("nodeId", $"{config.GetNodeId()}");
+ if (settings.WriteConfigSource && settings.ConfigSource.TryGetValue(config, out Guid guid))
+ {
+ // Write out the source guid for JumpToField to use.
+ xw.WriteAttributeString("sourceGuid", $"{guid}");
+ xw.WriteAttributeString("sourceField", $"{config.FieldDescription}");
+ }
}
public IFragment GenerateAudioLinkContent(ConfigurableDictionaryNode config, ConfiguredLcmGenerator.GeneratorSettings settings, string classname,
diff --git a/Src/xWorks/XhtmlDocView.cs b/Src/xWorks/XhtmlDocView.cs
index 3665499bfc..dd2f10317c 100644
--- a/Src/xWorks/XhtmlDocView.cs
+++ b/Src/xWorks/XhtmlDocView.cs
@@ -2,34 +2,34 @@
// 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.Diagnostics;
-using System.Drawing;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Windows.Forms;
-using System.Xml;
-using System.Xml.Linq;
using Gecko;
using Gecko.DOM;
using SIL.CommandLineProcessing;
using SIL.FieldWorks.Common.Framework;
using SIL.FieldWorks.Common.FwUtils;
-using static SIL.FieldWorks.Common.FwUtils.FwUtils;
using SIL.FieldWorks.Common.Widgets;
-using SIL.LCModel;
-using SIL.LCModel.DomainServices;
using SIL.FieldWorks.FwCoreDlgControls;
using SIL.FieldWorks.FwCoreDlgs;
using SIL.IO;
+using SIL.LCModel;
+using SIL.LCModel.DomainServices;
using SIL.LCModel.Utils;
using SIL.Progress;
using SIL.Utils;
using SIL.Windows.Forms.HtmlBrowser;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Windows.Forms;
+using System.Xml;
+using System.Xml.Linq;
using XCore;
+using static SIL.FieldWorks.Common.FwUtils.FwUtils;
namespace SIL.FieldWorks.XWorks
{
@@ -491,6 +491,10 @@ internal static void HandleDomRightClick(GeckoWebBrowser browser, DomMouseEventA
s_contextMenu.Items.Add(item);
item.Click += RunConfigureDialogAt;
item.Tag = new object[] { propertyTable, mediator, nodeId, topLevelGuid };
+ var item2 = new DisposableToolStripMenuItem(xWorksStrings.ksJumpToField);
+ s_contextMenu.Items.Add(item2);
+ item2.Click += JumpToFieldAt;
+ item2.Tag = new object[] { propertyTable, mediator, entryElement, element };
if (e.CtrlKey) // show hidden menu item for tech support
{
item = new DisposableToolStripMenuItem(xWorksStrings.ksInspect);
@@ -641,6 +645,82 @@ private static void RunDiagnosticsDialogAt(object sender, EventArgs e)
}
}
+ private static void JumpToFieldAt(object sender, EventArgs e)
+ {
+ var item = (ToolStripMenuItem)sender;
+ var tagObjects = (object[])item.Tag;
+ var propertyTable = tagObjects[0] as PropertyTable;
+ var mediator = tagObjects[1] as Mediator;
+ var cache = propertyTable.GetValue("cache");
+ GeckoElement entryElement = tagObjects[2] as GeckoElement;
+ GeckoElement fieldElement = tagObjects[3] as GeckoElement;
+ // Find the field object that contains fieldElement.
+ ICmObject fieldObj = null;
+ string fieldName = null;
+ if (fieldElement.HasAttribute("class") && fieldElement.GetAttribute("class") == "semanticdomains")
+ {
+ // sourceGuid is stored on the first child.
+ fieldElement = (GeckoElement)fieldElement.FirstChild;
+ }
+ for (GeckoElement element = fieldElement; element != null; element = element.ParentElement)
+ {
+ if (element.HasAttribute("sourceGuid"))
+ {
+ Guid fieldGuid = new Guid(element.GetAttribute("sourceGuid"));
+ if (cache.ServiceLocator.GetInstance().TryGetObject(fieldGuid, out fieldObj))
+ {
+ fieldName = element.GetAttribute("sourceField");
+ if (fieldObj is IMoInflAffixSlot ||
+ (fieldObj is ICmPossibility && (fieldName == "Name" || fieldName == "Abbreviation")))
+ {
+ // Use the enclosing field.
+ fieldObj = null;
+ continue;
+ }
+
+ break;
+ }
+ }
+ }
+ if (fieldObj != null)
+ {
+ ILexEntry entryLexEntry = GetGeckoLexEntry(entryElement, cache);
+ ILexEntry fieldLexEntry = GetGeckoLexEntry(fieldElement, cache);
+ if (entryLexEntry != null && fieldLexEntry != null && fieldLexEntry != entryLexEntry)
+ {
+#pragma warning disable 618 // suppress obsolete warning
+ mediator.SendMessage("JumpToRecord", fieldLexEntry.Hvo);
+#pragma warning restore 618
+ }
+ // Jump to field on idle to allow JumpToRecord to finish.
+ void JumpToField(object sender, EventArgs args)
+ {
+ Application.Idle -= JumpToField;
+ // Jump to the slice with the given field.
+ object[] arguments = new object[] { fieldObj.Hvo, fieldName, fieldElement.TextContent };
+ Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToField, arguments));
+ }
+ Application.Idle += JumpToField;
+
+ }
+ }
+
+ private static ILexEntry GetGeckoLexEntry(GeckoElement firstElement, LcmCache cache)
+ {
+ for (GeckoElement element = firstElement; element != null; element = element.ParentElement)
+ {
+ if (element.HasAttribute("sourceGuid"))
+ {
+ Guid guid = new Guid(element.GetAttribute("sourceGuid"));
+ if (cache.ServiceLocator.GetInstance().TryGetObject(guid, out ILexEntry lexEntry))
+ {
+ return lexEntry;
+ }
+ }
+ }
+ return null;
+ }
+
public override int Priority
{
get { return (int)ColleaguePriority.High; }
diff --git a/Src/xWorks/xWorks.csproj b/Src/xWorks/xWorks.csproj
index fd72c6047d..692d3416ec 100644
--- a/Src/xWorks/xWorks.csproj
+++ b/Src/xWorks/xWorks.csproj
@@ -1,4 +1,4 @@
-
+
xWorks
@@ -96,4 +96,17 @@
+
+
+ True
+ True
+ xWorksStrings.resx
+
+
+
+
+ ResXFileCodeGenerator
+ xWorksStrings.Designer.cs
+
+
\ No newline at end of file
diff --git a/Src/xWorks/xWorksStrings.Designer.cs b/Src/xWorks/xWorksStrings.Designer.cs
index 2dc95b8780..b6968932ee 100644
--- a/Src/xWorks/xWorksStrings.Designer.cs
+++ b/Src/xWorks/xWorksStrings.Designer.cs
@@ -1547,6 +1547,15 @@ internal static string ksInvalidFieldInFilterOrSorter {
}
}
+ ///
+ /// Looks up a localized string similar to Jump to field.
+ ///
+ internal static string ksJumpToField {
+ get {
+ return ResourceManager.GetString("ksJumpToField", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Lexical Relation Types:.
///
diff --git a/Src/xWorks/xWorksStrings.resx b/Src/xWorks/xWorksStrings.resx
index 94dfbc51d1..e71e1c9af4 100644
--- a/Src/xWorks/xWorksStrings.resx
+++ b/Src/xWorks/xWorksStrings.resx
@@ -1331,4 +1331,7 @@ See USFM documentation for help.
Batch {0} failed after {1} retries ({2}). Upload aborted.
Error message when a batch fails after all retry attempts. {0} is batch number, {1} is max retries, {2} is HTTP status code.
+
+ Jump to field
+
\ No newline at end of file