From efe6cb1f7e6af91414fb39fd044f376e9a2881d6 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Tue, 24 Mar 2026 08:57:39 -0700 Subject: [PATCH 01/12] Fix LT-22333: Click on field in Preview pane and go to field --- .../Controls/DetailControls/DataTree.cs | 79 +++++++++++++++++++ Src/Common/FwUtils/EventConstants.cs | 1 + Src/xWorks/ConfigurableDictionaryNode.cs | 5 ++ Src/xWorks/ConfiguredLcmGenerator.cs | 5 ++ Src/xWorks/LcmXhtmlGenerator.cs | 5 ++ Src/xWorks/XhtmlDocView.cs | 33 ++++++++ Src/xWorks/xWorks.csproj | 15 +++- Src/xWorks/xWorksStrings.Designer.cs | 9 +++ Src/xWorks/xWorksStrings.resx | 3 + 9 files changed, 154 insertions(+), 1 deletion(-) diff --git a/Src/Common/Controls/DetailControls/DataTree.cs b/Src/Common/Controls/DetailControls/DataTree.cs index 65adc8f8c6..b2f197272f 100644 --- a/Src/Common/Controls/DetailControls/DataTree.cs +++ b/Src/Common/Controls/DetailControls/DataTree.cs @@ -1047,6 +1047,83 @@ 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 fieldValue = (string)array[1]; + ICmObject fieldObj = Cache.ServiceLocator.GetInstance().GetObject(fieldHvo); + bool found = false; + // Try matching fieldObject and fieldValue first. + foreach (Slice slice in Slices) + { + if (slice.Object == fieldObj && SliceMatchesText(slice, fieldValue)) + { + m_fSetCurrentSliceNew = true; + } + if (slice is MSAReferenceComboBoxSlice && slice.Object is ILexSense sense && sense.MorphoSyntaxAnalysisRA == fieldObj) + { + m_fSetCurrentSliceNew = true; + } + + if (m_fSetCurrentSliceNew && !slice.IsHeaderNode) + { + m_fSetCurrentSliceNew = false; + m_currentSliceNew = slice; + found = true; + break; + } + } + if (!found) + { + // Just match fieldObject. + foreach (Slice slice in Slices) + { + if (slice.Object == fieldObj) + { + m_fSetCurrentSliceNew = true; + } + + if (m_fSetCurrentSliceNew && !slice.IsHeaderNode) + { + m_fSetCurrentSliceNew = false; + m_currentSliceNew = slice; + found = true; + break; + } + } + } + if (found) + { + // Set the current slice. + m_fCurrentContentControlObjectTriggered = true; + OnReadyToSetCurrentSlice(false); + } + } + + /// + /// Does the slice's display text match the given text? + /// + private bool SliceMatchesText(Slice slice, string text) + { + if (slice is MultiStringSlice) + { + ITsMultiString multiString = m_cache.DomainDataByFlid.get_MultiStringProp(slice.Object.Hvo, slice.Flid); + for (int i = 0; i < multiString.StringCount; i++) + { + ITsString tsString = multiString.GetStringFromIndex(i, out int ws); + if (tsString.Text == text) + { + return true; + } + } + } + return false; + } + private void SetCurrentSliceNewFromObject(ICmObject obj) { foreach (Slice slice in Slices) @@ -1248,6 +1325,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 +3789,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/ConfigurableDictionaryNode.cs b/Src/xWorks/ConfigurableDictionaryNode.cs index 2b9921c694..20d73d06cf 100644 --- a/Src/xWorks/ConfigurableDictionaryNode.cs +++ b/Src/xWorks/ConfigurableDictionaryNode.cs @@ -224,6 +224,11 @@ public List ReferencedOrDirectChildren get { return ReferencedNode == null ? Children : ReferencedNode.Children; } // REVIEW (Hasso) 2016.03: optimize by caching } + /// + /// The Guid of the node source. + /// + internal Guid SourceGuid { get; set; } + /// If node is a HeadWord node. internal bool IsHeadWord => CSSClassNameOverride == "headword" || CSSClassNameOverride == "mainheadword" || CSSClassNameOverride == "headword-classified"; diff --git a/Src/xWorks/ConfiguredLcmGenerator.cs b/Src/xWorks/ConfiguredLcmGenerator.cs index 52b3de8b74..493666eb85 100644 --- a/Src/xWorks/ConfiguredLcmGenerator.cs +++ b/Src/xWorks/ConfiguredLcmGenerator.cs @@ -493,6 +493,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. + config.SourceGuid = fieldObj.Guid; + } if (!config.IsEnabled) { diff --git a/Src/xWorks/LcmXhtmlGenerator.cs b/Src/xWorks/LcmXhtmlGenerator.cs index e103f98844..04c2aae5d5 100644 --- a/Src/xWorks/LcmXhtmlGenerator.cs +++ b/Src/xWorks/LcmXhtmlGenerator.cs @@ -570,6 +570,11 @@ private void WriteNodeId(XmlWriter xw, ConfigurableDictionaryNode config, Config if (settings != null && (settings.IsWebExport || settings.IsXhtmlExport)) return; xw.WriteAttributeString("nodeId", $"{config.GetNodeId()}"); + if (config.SourceGuid != null) + { + // Write out the source guid for JumpToField to use. + xw.WriteAttributeString("sourceGuid", $"{config.SourceGuid.ToString()}"); + } } public IFragment GenerateAudioLinkContent(ConfigurableDictionaryNode config, ConfiguredLcmGenerator.GeneratorSettings settings, string classname, diff --git a/Src/xWorks/XhtmlDocView.cs b/Src/xWorks/XhtmlDocView.cs index 3665499bfc..fc31a326f6 100644 --- a/Src/xWorks/XhtmlDocView.cs +++ b/Src/xWorks/XhtmlDocView.cs @@ -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, element }; if (e.CtrlKey) // show hidden menu item for tech support { item = new DisposableToolStripMenuItem(xWorksStrings.ksInspect); @@ -641,6 +645,35 @@ 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 fieldElement = tagObjects[2] as GeckoElement; + // Find the field object that contains fieldElement. + ICmObject fieldObj = null; + 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)) + { + break; + } + } + } + if (fieldObj != null) + { + // Jump to the slice with the field object and text value. + object[] arguments = new object[] { fieldObj.Hvo, fieldElement.TextContent }; + Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToField, arguments)); + } + } + 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 From 8cab59ed7c4fc7da22e82a3416974ecff8b46441 Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Thu, 26 Mar 2026 08:37:11 -0700 Subject: [PATCH 02/12] Include field name --- .../Controls/DetailControls/DataTree.cs | 86 +++++++++++++++++-- Src/xWorks/LcmXhtmlGenerator.cs | 1 + Src/xWorks/XhtmlDocView.cs | 41 +++++---- 3 files changed, 104 insertions(+), 24 deletions(-) diff --git a/Src/Common/Controls/DetailControls/DataTree.cs b/Src/Common/Controls/DetailControls/DataTree.cs index b2f197272f..8a8aca8674 100644 --- a/Src/Common/Controls/DetailControls/DataTree.cs +++ b/Src/Common/Controls/DetailControls/DataTree.cs @@ -1054,17 +1054,38 @@ private void JumpToField(object arguments) { var array = (object[])arguments; int fieldHvo = (int)array[0]; - string fieldValue = (string)array[1]; + string fieldName = (string)array[1]; + string fieldValue = (string)array[2]; ICmObject fieldObj = Cache.ServiceLocator.GetInstance().GetObject(fieldHvo); + int flid = 0; + try + { + // Some field names are synthetic, like "DefinitionOrGloss". + // In this case, we will try to match the value. + flid = m_cache.MetaDataCacheAccessor.GetFieldId2(fieldObj.ClassID, PlainFieldName(fieldName), true); + } + catch { } bool found = false; - // Try matching fieldObject and fieldValue first. + // Try matching object and field first. foreach (Slice slice in Slices) { - if (slice.Object == fieldObj && SliceMatchesText(slice, fieldValue)) + if (slice.IsHeaderNode) + { + continue; + } + if (slice is MorphTypeAtomicReferenceSlice && slice.Object is IMoAffixForm affix && affix.MorphTypeRA == fieldObj) + { + m_fSetCurrentSliceNew = true; + } + else if (slice is MSAReferenceComboBoxSlice && slice.Object is ILexSense sense && sense.MorphoSyntaxAnalysisRA == fieldObj) + { + m_fSetCurrentSliceNew = true; + } + else if (slice.Object is IMoStemAllomorph && slice.Object.Owner == fieldObj && fieldName == "MLHeadWord" && SliceMatchesText(slice, fieldValue)) { m_fSetCurrentSliceNew = true; } - if (slice is MSAReferenceComboBoxSlice && slice.Object is ILexSense sense && sense.MorphoSyntaxAnalysisRA == fieldObj) + else if (slice.Object == fieldObj && ((flid != 0 && slice.Flid == flid) || (flid == 0 && SliceMatchesText(slice, fieldValue)))) { m_fSetCurrentSliceNew = true; } @@ -1079,14 +1100,13 @@ private void JumpToField(object arguments) } if (!found) { - // Just match fieldObject. + // Try matching just object. foreach (Slice slice in Slices) { if (slice.Object == fieldObj) { m_fSetCurrentSliceNew = true; } - if (m_fSetCurrentSliceNew && !slice.IsHeaderNode) { m_fSetCurrentSliceNew = false; @@ -1104,14 +1124,64 @@ private void JumpToField(object arguments) } } + 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; + } + + /// /// Does the slice's display text match the given text? /// private bool SliceMatchesText(Slice slice, string text) { - if (slice is MultiStringSlice) + try + { + if (slice is MultiStringSlice) + { + ITsMultiString multiString = m_cache.DomainDataByFlid.get_MultiStringProp(slice.Object.Hvo, slice.Flid); + if (MultiStringMatchesText(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 PossibilityReferenceVectorSlice) + { + int[] hvos = SetupContents(slice.Flid, 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 (MultiStringMatchesText(possibility.Name, text) || MultiStringMatchesText(possibility.Abbreviation, text)) + { + return true; + } + } + } + } + } + catch { } + return false; + } + + private bool MultiStringMatchesText(ITsMultiString multiString, string text) + { + if (multiString != null) { - ITsMultiString multiString = m_cache.DomainDataByFlid.get_MultiStringProp(slice.Object.Hvo, slice.Flid); for (int i = 0; i < multiString.StringCount; i++) { ITsString tsString = multiString.GetStringFromIndex(i, out int ws); diff --git a/Src/xWorks/LcmXhtmlGenerator.cs b/Src/xWorks/LcmXhtmlGenerator.cs index 04c2aae5d5..6e0e789810 100644 --- a/Src/xWorks/LcmXhtmlGenerator.cs +++ b/Src/xWorks/LcmXhtmlGenerator.cs @@ -574,6 +574,7 @@ private void WriteNodeId(XmlWriter xw, ConfigurableDictionaryNode config, Config { // Write out the source guid for JumpToField to use. xw.WriteAttributeString("sourceGuid", $"{config.SourceGuid.ToString()}"); + xw.WriteAttributeString("sourceField", $"{config.FieldDescription}"); } } diff --git a/Src/xWorks/XhtmlDocView.cs b/Src/xWorks/XhtmlDocView.cs index fc31a326f6..d287992e72 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 { @@ -655,6 +655,7 @@ private static void JumpToFieldAt(object sender, EventArgs e) GeckoElement fieldElement = tagObjects[2] as GeckoElement; // Find the field object that contains fieldElement. ICmObject fieldObj = null; + string fieldName = null; for (GeckoElement element = fieldElement; element != null; element = element.ParentElement) { if (element.HasAttribute("sourceGuid")) @@ -662,14 +663,22 @@ private static void JumpToFieldAt(object sender, EventArgs e) Guid fieldGuid = new Guid(element.GetAttribute("sourceGuid")); if (cache.ServiceLocator.GetInstance().TryGetObject(fieldGuid, out fieldObj)) { + if (fieldObj is ICmPossibility) + { + // Use the enclosing field. + fieldObj = null; + continue; + } + + fieldName = element.GetAttribute("sourceField"); break; } } } if (fieldObj != null) { - // Jump to the slice with the field object and text value. - object[] arguments = new object[] { fieldObj.Hvo, fieldElement.TextContent }; + // Jump to the slice with the given field. + object[] arguments = new object[] { fieldObj.Hvo, fieldName, fieldElement.TextContent }; Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToField, arguments)); } } From 1402d9b622d4f8ef61260b639a5efa849d5a941b Mon Sep 17 00:00:00 2001 From: John Maxwell Date: Fri, 27 Mar 2026 13:35:27 -0700 Subject: [PATCH 03/12] Add JumpToRecord for subentry fields --- Src/xWorks/ConfiguredLcmGenerator.cs | 12 ++++++- Src/xWorks/XhtmlDocView.cs | 48 ++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/Src/xWorks/ConfiguredLcmGenerator.cs b/Src/xWorks/ConfiguredLcmGenerator.cs index 493666eb85..ae88ddef54 100644 --- a/Src/xWorks/ConfiguredLcmGenerator.cs +++ b/Src/xWorks/ConfiguredLcmGenerator.cs @@ -404,6 +404,8 @@ internal static IFragment GenerateContentForEntry(ICmObject entry, ConfigurableD return settings.ContentGenerator.CreateFragment(); } + // Record the guid of the source entry for JumpToField. + configuration.SourceGuid = entry.Guid; var nodeList = BuildNodeList(new List(), configuration); var pieces = configuration.ReferencedOrDirectChildren .Select(childNode => new ConfigFragment(childNode, GenerateContentForFieldByReflection(entry, BuildNodeList(nodeList, childNode), publicationDecorator, @@ -495,7 +497,7 @@ internal static IFragment GenerateContentForFieldByReflection(object field, List var config = nodeList.Last(); if (field is ICmObject fieldObj) { - // Record the guid of the source. + // Record the guid of the source field for JumpToField. config.SourceGuid = fieldObj.Guid; } @@ -2166,6 +2168,11 @@ private static IFragment GenerateCollectionItemContent(List