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