Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix LT-21737: Make highlight button work #199

Merged
merged 3 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading