From fb5f3ef3655d824c2e578f010f2ab5404ac48860 Mon Sep 17 00:00:00 2001 From: Jason Naylor Date: Wed, 23 Oct 2024 11:31:41 -0700 Subject: [PATCH] Fix LT-21737: Make highlight button work * Generate xhtml with a nodeId attribute that uses the ConfigurableDictionaryNode hash and use that to set highlight * Modify the 'Configure Dictionary' right click menu to use the nodeId attribute to select the correct tree node --- .../DictionaryConfigurationController.cs | 56 +++++-------------- Src/xWorks/DictionaryConfigurationDlg.cs | 17 +----- Src/xWorks/LcmXhtmlGenerator.cs | 29 ++++++++-- Src/xWorks/XhtmlDocView.cs | 23 ++++---- 4 files changed, 53 insertions(+), 72 deletions(-) diff --git a/Src/xWorks/DictionaryConfigurationController.cs b/Src/xWorks/DictionaryConfigurationController.cs index b0507fd293..514c5a268f 100644 --- a/Src/xWorks/DictionaryConfigurationController.cs +++ b/Src/xWorks/DictionaryConfigurationController.cs @@ -1563,9 +1563,9 @@ private static void SetIsEnabledForSubTree(ConfigurableDictionaryNode node, bool /// If no match is found, SelectedNode is not set. Otherwise, the best match found /// is used to set SelectedNode. /// - internal void SetStartingNode(List classList) + internal void SetStartingNode(string nodeId) { - if (classList == null || classList.Count == 0) + if (string.IsNullOrEmpty(nodeId)) return; if (View != null && View.TreeControl != null && @@ -1579,22 +1579,15 @@ internal void SetStartingNode(List classList) var configNode = node.Tag as ConfigurableDictionaryNode; if (configNode == null) continue; - var cssClass = CssGenerator.GetClassAttributeForConfig(configNode); - if (classList[0].Split(' ').Contains(cssClass)) + topNode = FindConfigNode(configNode, nodeId, new List()); + if (topNode != null) { - topNode = configNode; break; } } - if (topNode == null) - return; - // We have a match, so search through the TreeNode tree to find the TreeNode tagged - // with the given configuration node. If found, set that as the SelectedNode. - classList.RemoveAt(0); - var startingConfigNode = FindConfigNode(topNode, classList); foreach (TreeNode node in View.TreeControl.Tree.Nodes) { - var startingTreeNode = FindMatchingTreeNode(node, startingConfigNode); + var startingTreeNode = FindMatchingTreeNode(node, topNode); if (startingTreeNode != null) { View.TreeControl.Tree.SelectedNode = startingTreeNode; @@ -1605,48 +1598,29 @@ internal void SetStartingNode(List classList) } /// - /// Recursively descend the configuration tree, progressively matching nodes against CSS class path. Stop - /// when we run out of both tree and classes. Classes can be skipped if not matched. Running out of tree nodes - /// before running out of classes causes one level of backtracking up the configuration tree to look for a better match. + /// Recursively descend the configuration tree depth first until a matching nodeId is found /// /// LT-17213 Now 'internal static' so DictionaryConfigurationDlg can use it. - internal static ConfigurableDictionaryNode FindConfigNode(ConfigurableDictionaryNode topNode, List classPath) + internal static ConfigurableDictionaryNode FindConfigNode(ConfigurableDictionaryNode topNode, string nodeId, List visited) { - if (classPath.Count == 0) + if (string.IsNullOrEmpty(nodeId) || $"{topNode.GetHashCode()}".Equals(nodeId)) { return topNode; // what we have already is the best we can find. } + visited.Add(topNode); - // If we can't go further down the configuration tree, but still have classes to match, back up one level - // and try matching with the remaining classes. The configuration tree doesn't always map exactly with - // the XHTML tree structure. For instance, in the XHTML, Examples contains instances of Example, each - // of which contains an instance of Translations, which contains instances of Translation. In the configuration - // tree, Examples contains Example and Translations at the same level. - if (topNode.ReferencedOrDirectChildren == null || topNode.ReferencedOrDirectChildren.Count == 0) - { - var match = FindConfigNode(topNode.Parent, classPath); - return ReferenceEquals(match, topNode.Parent) - ? topNode // this is the best we can find. - : match; // we found something better! - } ConfigurableDictionaryNode matchingNode = null; foreach (var node in topNode.ReferencedOrDirectChildren) { - var cssClass = CssGenerator.GetClassAttributeForConfig(node); - // LT-17359 a reference node might have "senses mainentrysubsenses" - if (cssClass == classPath[0].Split(' ')[0]) + if (visited.Contains(node)) + continue; + var match = FindConfigNode(node, nodeId, visited); + if (match != null) { - matchingNode = node; - break; + return match; } } - // If we didn't match, skip this class in the list and try the next class, looking at the same configuration - // node. There are classes in the XHTML that aren't represented in the configuration nodes. ("sensecontent" - // and "sense" among others) - if (matchingNode == null) - matchingNode = topNode; - classPath.RemoveAt(0); - return FindConfigNode(matchingNode, classPath); + return null; } /// diff --git a/Src/xWorks/DictionaryConfigurationDlg.cs b/Src/xWorks/DictionaryConfigurationDlg.cs index 18fe85c0db..b0a8ce2603 100644 --- a/Src/xWorks/DictionaryConfigurationDlg.cs +++ b/Src/xWorks/DictionaryConfigurationDlg.cs @@ -230,28 +230,13 @@ private static ConfigurableDictionaryNode GetTopLevelNode(ConfigurableDictionary return childNode; } - private static bool DoesGeckoElementOriginateFromConfigNode(ConfigurableDictionaryNode configNode, GeckoElement element, - ConfigurableDictionaryNode topLevelNode) - { - Guid dummyGuid; - GeckoElement dummyElement; - var classListForGeckoElement = XhtmlDocView.GetClassListFromGeckoElement(element, out dummyGuid, out dummyElement); - classListForGeckoElement.RemoveAt(0); // don't need the top level class - var nodeToMatch = DictionaryConfigurationController.FindConfigNode(topLevelNode, classListForGeckoElement); - return Equals(nodeToMatch, configNode); - } - private static IEnumerable FindMatchingSpans(ConfigurableDictionaryNode selectedNode, GeckoElement parent, ConfigurableDictionaryNode topLevelNode, LcmCache cache) { var elements = new List(); - var desiredClass = CssGenerator.GetClassAttributeForConfig(selectedNode); - if (ConfiguredLcmGenerator.IsCollectionNode(selectedNode, cache)) - desiredClass = CssGenerator.GetClassAttributeForCollectionItem(selectedNode); foreach (var span in parent.GetElementsByTagName("span")) { - if (span.GetAttribute("class") != null && span.GetAttribute("class").Split(' ')[0] == desiredClass && - DoesGeckoElementOriginateFromConfigNode(selectedNode, span, topLevelNode)) + if (span.GetAttribute("nodeId") != null && span.GetAttribute("nodeId").Equals($"{selectedNode.GetHashCode()}")) { elements.Add(span); } diff --git a/Src/xWorks/LcmXhtmlGenerator.cs b/Src/xWorks/LcmXhtmlGenerator.cs index bf3ad8b81c..d215577dc5 100644 --- a/Src/xWorks/LcmXhtmlGenerator.cs +++ b/Src/xWorks/LcmXhtmlGenerator.cs @@ -16,6 +16,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Web.UI.WebControls; using System.Xml; @@ -549,6 +550,10 @@ public IFragment GenerateWsPrefixWithString(ConfigurableDictionaryNode config, C { xw.WriteStartElement("span"); xw.WriteAttributeString("class", CssGenerator.WritingSystemPrefix); + if (!settings.IsWebExport) + { + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); + } var prefix = ((CoreWritingSystemDefinition)settings.Cache.WritingSystemFactory.get_EngineOrNull(wsId)).Abbreviation; xw.WriteString(prefix); xw.WriteEndElement(); @@ -568,6 +573,7 @@ public IFragment GenerateAudioLinkContent(ConfigurableDictionaryNode config, str { xw.WriteStartElement("audio"); xw.WriteAttributeString("id", safeAudioId); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteStartElement("source"); xw.WriteAttributeString("src", srcAttribute); xw.WriteRaw(""); @@ -589,15 +595,15 @@ public IFragment GenerateAudioLinkContent(ConfigurableDictionaryNode config, str public IFragment WriteProcessedObject(ConfigurableDictionaryNode config, bool isBlock, IFragment elementContent, string className) { - return WriteProcessedContents(isBlock, elementContent, className); + return WriteProcessedContents(config, isBlock, elementContent, className); } public IFragment WriteProcessedCollection(ConfigurableDictionaryNode config, bool isBlock, IFragment elementContent, string className) { - return WriteProcessedContents(isBlock, elementContent, className); + return WriteProcessedContents(config, isBlock, elementContent, className); } - private IFragment WriteProcessedContents(bool asBlock, IFragment xmlContent, string className) + private IFragment WriteProcessedContents(ConfigurableDictionaryNode config, bool asBlock, IFragment xmlContent, string className) { if (!xmlContent.IsNullOrEmpty()) { @@ -643,6 +649,10 @@ public IFragment GenerateGroupingNode(ConfigurableDictionaryNode config, object { xw.WriteStartElement("span"); xw.WriteAttributeString("class", className); + if (!settings.IsWebExport) + { + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); + } var innerBuilder = new StringBuilder(); foreach (var child in config.ReferencedOrDirectChildren) @@ -699,7 +709,9 @@ public void StartMultiRunString(IFragmentWriter writer, ConfigurableDictionaryNo { var xw = ((XmlFragmentWriter)writer).Writer; xw.WriteStartElement("span"); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteAttributeString("lang", writingSystem); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); } public void EndMultiRunString(IFragmentWriter writer) @@ -712,6 +724,7 @@ public void StartBiDiWrapper(IFragmentWriter writer, ConfigurableDictionaryNode { var xw = ((XmlFragmentWriter)writer).Writer; xw.WriteStartElement("span"); // set direction on a nested span to preserve Context's position and direction. + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteAttributeString("dir", rightToLeft ? "rtl" : "ltr"); } @@ -725,6 +738,7 @@ public void StartRun(IFragmentWriter writer, ConfigurableDictionaryNode config, { var xw = ((XmlFragmentWriter)writer).Writer; xw.WriteStartElement("span"); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteAttributeString("lang", writingSystem); } @@ -868,6 +882,7 @@ public void StartEntry(IFragmentWriter writer, ConfigurableDictionaryNode config var xw = ((XmlFragmentWriter)writer).Writer; xw.WriteStartElement("div"); xw.WriteAttributeString("class", className); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteAttributeString("id", "g" + entryGuid); } @@ -890,6 +905,7 @@ public void AddCollection(IFragmentWriter writer, ConfigurableDictionaryNode con var xw = ((XmlFragmentWriter)writer).Writer; xw.WriteStartElement(isBlockProperty ? "div" : "span"); xw.WriteAttributeString("class", className); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteRaw(content.ToString()); xw.WriteEndElement(); } @@ -924,6 +940,7 @@ public IFragment AddImage(ConfigurableDictionaryNode config, string classAttribu xw.WriteAttributeString("class", classAttribute); xw.WriteAttributeString("src", srcAttribute); xw.WriteAttributeString("id", "g" + pictureGuid); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteEndElement(); xw.Flush(); return fragment; @@ -954,6 +971,7 @@ public IFragment GenerateSenseNumber(ConfigurableDictionaryNode config, string f xw.WriteStartElement("span"); xw.WriteAttributeString("class", "sensenumber"); xw.WriteAttributeString("lang", senseNumberWs); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteString(formattedSenseNumber); xw.WriteEndElement(); xw.Flush(); @@ -1047,6 +1065,7 @@ public IFragment AddCollectionItem(ConfigurableDictionaryNode config, bool isBlo { xw.WriteStartElement(isBlock ? "div" : "span"); xw.WriteAttributeString("class", collectionItemClass); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteRaw(content.ToString()); xw.WriteEndElement(); xw.Flush(); @@ -1063,6 +1082,7 @@ public IFragment AddProperty(ConfigurableDictionaryNode config, string className { xw.WriteStartElement(isBlockProperty ? "div" : "span"); xw.WriteAttributeString("class", className); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteString(content); xw.WriteEndElement(); xw.Flush(); @@ -1082,10 +1102,11 @@ public IFragment AddSenseData(ConfigurableDictionaryNode config, IFragment sense // Wrap the number and sense combination in a sensecontent span so that both can be affected by DisplayEachSenseInParagraph xw.WriteStartElement("span"); xw.WriteAttributeString("class", "sensecontent"); - xw.WriteRaw(senseNumberSpan?.ToString()); + xw.WriteRaw(senseNumberSpan?.ToString() ?? string.Empty); xw.WriteStartElement(isBlock ? "div" : "span"); xw.WriteAttributeString("class", className); xw.WriteAttributeString("entryguid", "g" + ownerGuid); + xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}"); xw.WriteRaw(senseContent.ToString()); xw.WriteEndElement(); // element name for property xw.WriteEndElement(); // diff --git a/Src/xWorks/XhtmlDocView.cs b/Src/xWorks/XhtmlDocView.cs index 8266cf3154..a1dee06191 100644 --- a/Src/xWorks/XhtmlDocView.cs +++ b/Src/xWorks/XhtmlDocView.cs @@ -315,7 +315,7 @@ internal static void HandleDomLeftClick(RecordClerk clerk, PropertyTable propert // or the entry being clicked (when the user clicks anywhere in an entry that is not currently selected) var destinationGuid = GetGuidFromEntryLink(element); if (destinationGuid == Guid.Empty) - GetClassListFromGeckoElement(element, out destinationGuid, out _); + GetElementInfoFromGeckoElement(element, out destinationGuid, out _); // If we don't have a destination GUID, the user may have clicked a video player. We can't handle that, // and if we say we did, we will prevent the user from operating the video controls. @@ -481,7 +481,7 @@ internal static void HandleDomRightClick(GeckoWebBrowser browser, DomMouseEventA { Guid topLevelGuid; GeckoElement entryElement; - var classList = GetClassListFromGeckoElement(element, out topLevelGuid, out entryElement); + var classList = GetElementInfoFromGeckoElement(element, out topLevelGuid, out entryElement); var localizedName = DictionaryConfigurationListener.GetDictionaryConfigurationType(propertyTable); var label = string.Format(xWorksStrings.ksConfigure, localizedName); s_contextMenu = new ContextMenuStrip(); @@ -505,28 +505,29 @@ internal static void HandleDomRightClick(GeckoWebBrowser browser, DomMouseEventA /// Returns the class hierarchy for a GeckoElement /// /// LT-17213 Internal for use in DictionaryConfigurationDlg - internal static List GetClassListFromGeckoElement(GeckoElement element, out Guid topLevelGuid, out GeckoElement entryElement) + internal static string GetElementInfoFromGeckoElement(GeckoElement element, out Guid topLevelGuid, out GeckoElement entryElement) { topLevelGuid = Guid.Empty; entryElement = element; - var classList = new List(); + string nearestNodeId = null; if (entryElement.TagName == "body" || entryElement.TagName == "html") - return classList; + return string.Empty; for (; entryElement != null; entryElement = entryElement.ParentElement) { + if (string.IsNullOrEmpty(nearestNodeId)) + { + nearestNodeId = entryElement.GetAttribute("nodeId"); + } var className = entryElement.GetAttribute("class"); - if (string.IsNullOrEmpty(className)) - continue; if (className == "letHead") break; - classList.Insert(0, className); if (entryElement.TagName == "div" && entryElement.ParentElement.TagName == "body") { topLevelGuid = GetGuidFromGeckoDomElement(entryElement); break; // we have the element we want; continuing to loop will get its parent instead } } - return classList; + return nearestNodeId; } /// @@ -598,7 +599,7 @@ private static void RunConfigureDialogAt(object sender, EventArgs e) var tagObjects = (object[])item.Tag; var propertyTable = tagObjects[0] as PropertyTable; var mediator = tagObjects[1] as Mediator; - var classList = tagObjects[2] as List; + var nodeId = tagObjects[2] as string; var guid = (Guid)tagObjects[3]; bool refreshNeeded; using (var dlg = new DictionaryConfigurationDlg(propertyTable)) @@ -611,7 +612,7 @@ private static void RunConfigureDialogAt(object sender, EventArgs e) else if (clerk != null) current = clerk.CurrentObject; var controller = new DictionaryConfigurationController(dlg, propertyTable, mediator, current); - controller.SetStartingNode(classList); + controller.SetStartingNode(nodeId); dlg.Text = String.Format(xWorksStrings.ConfigureTitle, DictionaryConfigurationListener.GetDictionaryConfigurationType(propertyTable)); dlg.HelpTopic = DictionaryConfigurationListener.GetConfigDialogHelpTopic(propertyTable); dlg.ShowDialog(propertyTable.GetValue("window"));