Skip to content

Commit

Permalink
Fix LT-21737: Make highlight button work
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jasonleenaylor committed Nov 8, 2024
1 parent 18fdf13 commit 271aabe
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 219 deletions.
5 changes: 3 additions & 2 deletions Src/xWorks/ConfigurableDictionaryNode.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2014-2017 SIL International
// Copyright (c) 2014-2017 SIL International
// This software is licensed under the LGPL, version 2.1 or later
// (http://www.gnu.org/licenses/lgpl-2.1.html)

Expand Down Expand Up @@ -334,7 +334,8 @@ internal ConfigurableDictionaryNode DeepCloneUnderParent(ConfigurableDictionaryN

public override int GetHashCode()
{
return Parent == null ? DisplayLabel.GetHashCode() : DisplayLabel.GetHashCode() ^ Parent.GetHashCode();
object hashingObject = DisplayLabel ?? FieldDescription;
return Parent == null ? hashingObject.GetHashCode() : hashingObject.GetHashCode() ^ Parent.GetHashCode();
}

public override bool Equals(object other)
Expand Down
68 changes: 21 additions & 47 deletions Src/xWorks/DictionaryConfigurationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1558,14 +1558,12 @@ private static void SetIsEnabledForSubTree(ConfigurableDictionaryNode node, bool
}

/// <summary>
/// Search the TreeNode tree to find a starting node based on matching the "class"
/// attributes of the generated XHTML tracing back from the XHTML element clicked.
/// If no match is found, SelectedNode is not set. Otherwise, the best match found
/// is used to set SelectedNode.
/// Search the TreeNode tree to find a starting node based on nodeId attribute - a hash of a ConfigurableDictionaryNode
/// generated into the xhtml. If nothing is found SelectedNode is not set.
/// </summary>
internal void SetStartingNode(List<string> classList)
internal void SetStartingNode(string nodeId)
{
if (classList == null || classList.Count == 0)
if (string.IsNullOrEmpty(nodeId))
return;
if (View != null &&
View.TreeControl != null &&
Expand All @@ -1579,22 +1577,15 @@ internal void SetStartingNode(List<string> 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<ConfigurableDictionaryNode>());
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;
Expand All @@ -1605,48 +1596,31 @@ internal void SetStartingNode(List<string> classList)
}

/// <summary>
/// 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
/// </summary>
/// <remarks>LT-17213 Now 'internal static' so DictionaryConfigurationDlg can use it.</remarks>
internal static ConfigurableDictionaryNode FindConfigNode(ConfigurableDictionaryNode topNode, List<string> classPath)
internal static ConfigurableDictionaryNode FindConfigNode(ConfigurableDictionaryNode topNode, string nodeId, List<ConfigurableDictionaryNode> 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)
if (topNode.ReferencedOrDirectChildren != null)
{
var cssClass = CssGenerator.GetClassAttributeForConfig(node);
// LT-17359 a reference node might have "senses mainentrysubsenses"
if (cssClass == classPath[0].Split(' ')[0])
foreach (var node in topNode.ReferencedOrDirectChildren)
{
matchingNode = node;
break;
if (visited.Contains(node))
continue;
var match = FindConfigNode(node, nodeId, visited);
if (match != null)
{
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;
}

/// <summary>
Expand Down
17 changes: 1 addition & 16 deletions Src/xWorks/DictionaryConfigurationDlg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GeckoElement> FindMatchingSpans(ConfigurableDictionaryNode selectedNode, GeckoElement parent,
ConfigurableDictionaryNode topLevelNode, LcmCache cache)
{
var elements = new List<GeckoElement>();
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);
}
Expand Down
32 changes: 28 additions & 4 deletions Src/xWorks/LcmXhtmlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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("");
Expand All @@ -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())
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -699,6 +709,7 @@ public void StartMultiRunString(IFragmentWriter writer, ConfigurableDictionaryNo
{
var xw = ((XmlFragmentWriter)writer).Writer;
xw.WriteStartElement("span");
xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}");
xw.WriteAttributeString("lang", writingSystem);
}

Expand All @@ -712,6 +723,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");
}

Expand All @@ -725,6 +737,11 @@ public void StartRun(IFragmentWriter writer, ConfigurableDictionaryNode config,
{
var xw = ((XmlFragmentWriter)writer).Writer;
xw.WriteStartElement("span");
// When generating an error node config is null
if (config != null)
{
xw.WriteAttributeString("nodeId", $"{config.GetHashCode()}");
}
xw.WriteAttributeString("lang", writingSystem);
}

Expand Down Expand Up @@ -868,6 +885,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);
}

Expand All @@ -890,6 +908,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();
}
Expand Down Expand Up @@ -924,6 +943,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;
Expand Down Expand Up @@ -954,6 +974,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();
Expand Down Expand Up @@ -1047,6 +1068,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();
Expand All @@ -1063,6 +1085,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();
Expand All @@ -1082,10 +1105,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(); // </span>
Expand Down
23 changes: 12 additions & 11 deletions Src/xWorks/XhtmlDocView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -505,28 +505,29 @@ internal static void HandleDomRightClick(GeckoWebBrowser browser, DomMouseEventA
/// Returns the class hierarchy for a GeckoElement
/// </summary>
/// <remarks>LT-17213 Internal for use in DictionaryConfigurationDlg</remarks>
internal static List<string> 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>();
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;
}

/// <summary>
Expand Down Expand Up @@ -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<string>;
var nodeId = tagObjects[2] as string;
var guid = (Guid)tagObjects[3];
bool refreshNeeded;
using (var dlg = new DictionaryConfigurationDlg(propertyTable))
Expand All @@ -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<IWin32Window>("window"));
Expand Down
16 changes: 8 additions & 8 deletions Src/xWorks/xWorksTests/ConfiguredXHTMLGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9479,14 +9479,14 @@ public void GenerateContentForEntry_LexicalReferencesOrderedCorrectly([Values(tr
}
CssGeneratorTests.PopulateFieldsForTesting(mainEntryNode);
var settings = DefaultSettings;
const string antAbbrSpan = "<span class=\"ownertype_abbreviation\"><span lang=\"en\">ant</span></span>";
const string whSpan = "<span class=\"ownertype_abbreviation\"><span lang=\"en\">wh</span></span>";
const string ptSpan = "<span class=\"ownertype_abbreviation\"><span lang=\"en\">pt</span></span>";
const string antNameSpan = "<span class=\"ownertype_name\"><span lang=\"en\">Antonym</span></span>";
const string femmeSpan = "<span class=\"headword\"><span lang=\"fr\">femme</span></span>";
var garçonSpan = TsStringUtils.Compose("<span class=\"headword\"><span lang=\"fr\">garçon</span></span>");
var bêteSpan = TsStringUtils.Compose("<span class=\"headword\"><span lang=\"fr\">bête</span></span>");
const string trucSpan = "<span class=\"headword\"><span lang=\"fr\">truc</span></span>";
string antAbbrSpan = $"<span class=\"ownertype_abbreviation\"><span nodeId=\"{relAbbrNode.GetHashCode()}\" lang=\"en\">ant</span></span>";
string whSpan = $"<span class=\"ownertype_abbreviation\"><span nodeId=\"{relAbbrNode.GetHashCode()}\" lang=\"en\">wh</span></span>";
string ptSpan = $"<span class=\"ownertype_abbreviation\"><span nodeId=\"{relAbbrNode.GetHashCode()}\" lang=\"en\">pt</span></span>";
string antNameSpan = $"<span class=\"ownertype_name\"><span nodeId=\"{relNameNode.GetHashCode()}\" lang=\"en\">Antonym</span></span>";
string femmeSpan = $"<span class=\"headword\"><span nodeId=\"{refHeadwordNode.GetHashCode()}\" lang=\"fr\">femme</span></span>";
var garçonSpan = TsStringUtils.Compose($"<span class=\"headword\"><span nodeId=\"{refHeadwordNode.GetHashCode()}\" lang=\"fr\">garçon</span></span>");
var bêteSpan = TsStringUtils.Compose($"<span class=\"headword\"><span nodeId=\"{refHeadwordNode.GetHashCode()}\" lang=\"fr\">bête</span></span>");
string trucSpan = $"<span class=\"headword\"><span nodeId=\"{refHeadwordNode.GetHashCode()}\" lang=\"fr\">truc</span></span>";
//SUT
//Console.WriteLine(LcmXhtmlGenerator.SavePreviewHtmlWithStyles(new[] { manEntry.Hvo, familyEntry.Hvo, girlEntry.Hvo, individualEntry.Hvo }, null,
// new DictionaryConfigurationModel { Parts = new List<ConfigurableDictionaryNode> { mainEntryNode } }, m_mediator)); // full output for diagnostics
Expand Down
Loading

0 comments on commit 271aabe

Please sign in to comment.