From 95ae04b790771e8febfd6e6203a8a63ca2b3cf72 Mon Sep 17 00:00:00 2001 From: jlewin Date: Mon, 17 Jun 2019 18:12:34 -0700 Subject: [PATCH] HelpDocs --- .../CustomWidgets/TreeView/TreeView.cs | 22 +++ MatterControlLib/LuceneHelpSearch.cs | 136 ++++++++++++++++++ MatterControlLib/MatterControlLib.csproj | 3 + .../LeftClipFlowLayoutWidget.cs | 10 ++ .../PartPreviewWindow/MainViewWidget.cs | 128 +++++++++++++++++ .../SystemWindowExtension.cs | 103 ++++++++----- .../View3D/PrinterBar/PrinterActionsBar.cs | 1 + .../View3D/PrinterBar/PrinterConnectButton.cs | 3 - MatterControlLib/SetupWizard/HelpTreePanel.cs | 42 ++++-- 9 files changed, 397 insertions(+), 51 deletions(-) create mode 100644 MatterControlLib/LuceneHelpSearch.cs diff --git a/MatterControlLib/CustomWidgets/TreeView/TreeView.cs b/MatterControlLib/CustomWidgets/TreeView/TreeView.cs index 41e015fa1..92f36b069 100644 --- a/MatterControlLib/CustomWidgets/TreeView/TreeView.cs +++ b/MatterControlLib/CustomWidgets/TreeView/TreeView.cs @@ -220,6 +220,17 @@ namespace MatterHackers.MatterControl.CustomWidgets { OnBeforeSelect(null); + if (_selectedNode != null) + { + // Collapse the old tree + foreach (var ancestor in _selectedNode.Parents()) + { + ancestor.Expanded = false; + } + + _selectedNode.Expanded = false; + } + // if the current selection (before change) is !null than clear its background color if (_selectedNode != null) { @@ -229,11 +240,22 @@ namespace MatterHackers.MatterControl.CustomWidgets // change the selection _selectedNode = value; + if (_selectedNode != null) + { + // Expand the new tree + foreach (var ancestor in _selectedNode.Parents()) + { + ancestor.Expanded = true; + } + } + if (_selectedNode != null) { _selectedNode.HighlightRegion.BackgroundColor = theme.AccentMimimalOverlay; } + + OnAfterSelect(null); } } diff --git a/MatterControlLib/LuceneHelpSearch.cs b/MatterControlLib/LuceneHelpSearch.cs new file mode 100644 index 000000000..f6379e033 --- /dev/null +++ b/MatterControlLib/LuceneHelpSearch.cs @@ -0,0 +1,136 @@ + +using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Documents; +using Lucene.Net.Index; +using Lucene.Net.QueryParsers.Classic; +using Lucene.Net.Search; +using Lucene.Net.Store; +using Lucene.Net.Util; +using MatterHackers.Agg.Platform; +using MatterHackers.MatterControl; +using MatterHackers.MatterControl.DataStorage; +using MatterHackers.MatterControl.PartPreviewWindow; +using Newtonsoft.Json; + +namespace MatterControlLib +{ + public class LuceneHelpSearch + { + private static IndexWriter writer; + private static StandardAnalyzer analyzer; + + static LuceneHelpSearch() + { + // Ensures index backwards compatibility + var AppLuceneVersion = LuceneVersion.LUCENE_48; + + var indexLocation = System.IO.Path.Combine(ApplicationDataStorage.Instance.ApplicationTempDataPath, "LuceneIndex"); + System.IO.Directory.CreateDirectory(indexLocation); + + var dir = FSDirectory.Open(indexLocation); + + // create an analyzer to process the text + analyzer = new StandardAnalyzer(AppLuceneVersion); + + // create an index writer + var indexConfig = new IndexWriterConfig(AppLuceneVersion, analyzer); + + writer = new IndexWriter(dir, indexConfig); + + //IndexDocuments(); + } + + public LuceneHelpSearch() + { + } + + + private static void ProcessHelpTree(HelpArticle context, Dictionary helpArticles) + { + helpArticles[context.Path] = context; + + foreach (var child in context.Children) + { + ProcessHelpTree(child, helpArticles); + } + } + + private static void IndexDocuments() + { + Dictionary helpArticles; + + // Clear existing + writer.DeleteAll(); + + // Build index from help-docs.zip + using (var file = AggContext.StaticData.OpenStream(System.IO.Path.Combine("OemSettings", "help-docs.zip"))) + using (var zip = new ZipArchive(file, ZipArchiveMode.Read)) + { + + var tocEntry = zip.Entries.FirstOrDefault(e => e.FullName == "toc.json"); + + using (var docStream = tocEntry.Open()) + { + var reader = new System.IO.StreamReader(docStream); + var tocText = reader.ReadToEnd(); + + var rootHelpArticle = JsonConvert.DeserializeObject(tocText); + + helpArticles = new Dictionary(); + + // Walk the documents tree building up a dictionary of article paths to articles + ProcessHelpTree(rootHelpArticle, helpArticles); + } + + foreach (var entry in zip.Entries) + { + if (entry.FullName.ToLower().EndsWith(".md")) + { + using (var docStream = entry.Open()) + { + var reader = new System.IO.StreamReader(docStream); + + string text = reader.ReadToEnd(); + + var doc = new Document(); + + // StringField indexes but doesn't tokenise + doc.Add(new StringField("name", helpArticles[entry.FullName].Name, Field.Store.YES)); + doc.Add(new StringField("path", entry.FullName, Field.Store.YES)); + doc.Add(new TextField("body", text, Field.Store.NO)); + + writer.AddDocument(doc); + writer.Flush(triggerMerge: false, applyAllDeletes: false); + } + } + } + } + + writer.Commit(); + } + + public IEnumerable Search(string text) + { + var parser = new QueryParser(LuceneVersion.LUCENE_48, "body", analyzer); + var query = parser.Parse(text); + + // re-use the writer to get real-time updates + var searcher = new IndexSearcher(writer.GetReader(applyAllDeletes: true)); + + var hits = searcher.Search(query, 20 /* top 20 */).ScoreDocs; + + return hits.Select(hit => + { + var foundDoc = searcher.Doc(hit.Doc); + return new HelpSearchResult() + { + Name = foundDoc.Get("name"), + Path = foundDoc.Get("path") + }; + }); + } + } +} diff --git a/MatterControlLib/MatterControlLib.csproj b/MatterControlLib/MatterControlLib.csproj index 5b5c01ac3..c535ef137 100644 --- a/MatterControlLib/MatterControlLib.csproj +++ b/MatterControlLib/MatterControlLib.csproj @@ -85,6 +85,9 @@ + + + diff --git a/MatterControlLib/PartPreviewWindow/LeftClipFlowLayoutWidget.cs b/MatterControlLib/PartPreviewWindow/LeftClipFlowLayoutWidget.cs index 1610dd478..3b670b6fb 100644 --- a/MatterControlLib/PartPreviewWindow/LeftClipFlowLayoutWidget.cs +++ b/MatterControlLib/PartPreviewWindow/LeftClipFlowLayoutWidget.cs @@ -45,6 +45,16 @@ namespace MatterHackers.MatterControl.PartPreviewWindow if (gradientBackground != null) { graphics2D.Render(gradientBackground, this.LocalBounds.Left, 0); + + var bounds = this.LocalBounds; + + if (bounds.Width > gradientBackground.Width) + { + // Fill anything outside of the gradient region with the opaque background + graphics2D.FillRectangle( + new RectangleDouble(gradientBackground.Width, bounds.Bottom, bounds.Right, bounds.Top), + this.BackgroundColor); + } } else { diff --git a/MatterControlLib/PartPreviewWindow/MainViewWidget.cs b/MatterControlLib/PartPreviewWindow/MainViewWidget.cs index 27dc9dc13..3b908ef89 100644 --- a/MatterControlLib/PartPreviewWindow/MainViewWidget.cs +++ b/MatterControlLib/PartPreviewWindow/MainViewWidget.cs @@ -32,11 +32,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using MatterControlLib; using MatterHackers.Agg; using MatterHackers.Agg.Platform; using MatterHackers.Agg.UI; using MatterHackers.DataConverters3D; using MatterHackers.Localizations; +using MatterHackers.MatterControl.CustomWidgets; using MatterHackers.MatterControl.Library; using MatterHackers.MatterControl.PartPreviewWindow.PlusTab; using MatterHackers.MatterControl.PrintLibrary; @@ -45,6 +47,24 @@ using MatterHackers.VectorMath; namespace MatterHackers.MatterControl.PartPreviewWindow { + public class HelpSearchResult + { + public string Name { get; set; } + + public string Path { get; set; } + } + + public class HelpSearchResultRow : SettingsRow + { + public HelpSearchResultRow(HelpSearchResult searchResult, ThemeConfig theme) + : base(searchResult.Name, null, theme) + { + this.SearchResult = searchResult; + } + + public HelpSearchResult SearchResult { get; } + } + public class MainViewWidget : FlowLayoutWidget { private EventHandler unregisterEvents; @@ -75,6 +95,83 @@ namespace MatterHackers.MatterControl.PartPreviewWindow Padding = new BorderDouble(left: 8) }; + GuiWidget searchButton = null; + + var searchPanel = new SearchInputBox(theme) + { + Width = 200, + HAnchor = HAnchor.Absolute, + Visible = false, + Padding = new BorderDouble(10, 3) + }; + searchPanel.searchInput.ActualTextEditWidget.EnterPressed += (s, e) => + { + searchPanel.BackgroundColor = theme.SectionBackgroundColor; + + // TODO: + //System.Diagnostics.Debugger.Break(); + + var rightPanel = new VerticalResizeContainer(theme, GrabBarSide.Left) + { + HAnchor = HAnchor.Absolute, + VAnchor = VAnchor.Absolute, + Width = 500, + Height = 200, + BackgroundColor = theme.SectionBackgroundColor + }; + rightPanel.BoundsChanged += (s2, e2) => + { + if (rightPanel.Parent != null) + { + rightPanel.Position = new Vector2(rightPanel.Parent.Width - rightPanel.Width, rightPanel.Position.Y); + } + }; + + var searcher = new LuceneHelpSearch(); + + foreach (var searchResult in searcher.Search(searchPanel.searchInput.Text)) + { + var resultsRow = new HelpSearchResultRow(searchResult, theme); + resultsRow.Click += this.ResultsRow_Click; + + rightPanel.AddChild(resultsRow); + } + + this.Parents().FirstOrDefault().ShowRightSplitPopup( + new MatePoint(searchPanel), + new MatePoint(rightPanel)); + }; + searchPanel.ResetButton.Click += (s, e) => + { + searchPanel.BackgroundColor = Color.Transparent; + searchPanel.Visible = false; + searchButton.Visible = true; + searchPanel.searchInput.Text = ""; + + // TODO: + //this.ClearSearch(); + }; + + extensionArea.AddChild(searchPanel); + + searchButton = theme.CreateSearchButton(); + searchButton.Name = "App Search Button"; + searchButton.Click += (s, e) => + { + if (searchPanel.Visible) + { + // TODO + var a = 2; + } + else + { + searchPanel.Visible = true; + searchButton.Visible = false; + searchPanel.searchInput.Focus(); + } + }; + extensionArea.AddChild(searchButton); + tabControl = new ChromeTabs(extensionArea, theme) { VAnchor = VAnchor.Stretch, @@ -265,6 +362,37 @@ namespace MatterHackers.MatterControl.PartPreviewWindow ApplicationController.Instance.MainView = this; } + private void ResultsRow_Click(object sender, MouseEventArgs e) + { + var helpDocsTab = this.tabControl.AllTabs.FirstOrDefault(t => t.Key == "HelpDocs") as ChromeTab; + if (helpDocsTab == null) + { + var helpTreePanel = new HelpTreePanel(theme) + { + HAnchor = HAnchor.Stretch, + VAnchor = VAnchor.Stretch + }; + + helpDocsTab = new ChromeTab("HelpDocs", "Help".Localize(), tabControl, helpTreePanel, theme, hasClose: false) + { + MinimumSize = new Vector2(0, theme.TabButtonHeight), + Name = "Library Tab", + Padding = new BorderDouble(15, 0), + }; + + this.TabControl.AddTab(helpDocsTab); + } + + this.TabControl.ActiveTab = helpDocsTab; + + if (helpDocsTab.TabContent is HelpTreePanel treePanel) + { + treePanel.ActiveNodePath = (sender as HelpSearchResultRow).SearchResult.Path; + } + + + } + private void SetInitialTab() { // Initial tab selection - workspace load will reset if applicable diff --git a/MatterControlLib/PartPreviewWindow/SystemWindowExtension.cs b/MatterControlLib/PartPreviewWindow/SystemWindowExtension.cs index e51ae2475..2d13da8fb 100644 --- a/MatterControlLib/PartPreviewWindow/SystemWindowExtension.cs +++ b/MatterControlLib/PartPreviewWindow/SystemWindowExtension.cs @@ -197,7 +197,29 @@ namespace MatterHackers.MatterControl.PartPreviewWindow } } + private static void RightHorizontalSplitPopup(SystemWindow systemWindow, MatePoint anchor, MatePoint popup, RectangleDouble altBounds) + { + // Calculate left for right aligned split + Vector2 popupPosition = new Vector2(systemWindow.Width - popup.Widget.Width, 0); + + Vector2 anchorLeft = anchor.Widget.Parent.TransformToScreenSpace(anchor.Widget.Position); + + popup.Widget.Height = anchorLeft.Y; + + popup.Widget.Position = popupPosition; + } + public static void ShowPopup(this SystemWindow systemWindow, MatePoint anchor, MatePoint popup, RectangleDouble altBounds = default(RectangleDouble), int borderWidth = 1) + { + ShowPopup(systemWindow, anchor, popup, altBounds, borderWidth, BestPopupPosition); + } + + public static void ShowRightSplitPopup(this SystemWindow systemWindow, MatePoint anchor, MatePoint popup, RectangleDouble altBounds = default(RectangleDouble), int borderWidth = 1) + { + ShowPopup(systemWindow, anchor, popup, altBounds, borderWidth, RightHorizontalSplitPopup); + } + + public static void ShowPopup(this SystemWindow systemWindow, MatePoint anchor, MatePoint popup, RectangleDouble altBounds, int borderWidth, Action layoutHelper) { var hookedParents = new HashSet(); @@ -216,44 +238,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow { if (anchor.Widget?.Parent != null) { - // Calculate left aligned screen space position (using widgetRelativeTo.parent) - Vector2 anchorLeft = anchor.Widget.Parent.TransformToScreenSpace(anchor.Widget.Position); - anchorLeft += new Vector2(altBounds.Left, altBounds.Bottom); - - Vector2 popupPosition = anchorLeft; - - var bounds = altBounds == default(RectangleDouble) ? anchor.Widget.LocalBounds : altBounds; - - Vector2 xPosition = GetXAnchor(anchor.Mate, popup.Mate, popup.Widget, bounds); - - Vector2 screenPosition; - - screenPosition = anchorLeft + xPosition; - - // Constrain - if (screenPosition.X + popup.Widget.Width > systemWindow.Width - || screenPosition.X < 0) - { - xPosition = GetXAnchor(anchor.AltMate, popup.AltMate, popup.Widget, bounds); - } - - popupPosition += xPosition; - - Vector2 yPosition = GetYAnchor(anchor.Mate, popup.Mate, popup.Widget, bounds); - - screenPosition = anchorLeft + yPosition; - - // Constrain - if (anchor.AltMate != null - && (screenPosition.Y + popup.Widget.Height > systemWindow.Height - || screenPosition.Y < 0)) - { - yPosition = GetYAnchor(anchor.AltMate, popup.AltMate, popup.Widget, bounds); - } - - popupPosition += yPosition; - - popup.Widget.Position = popupPosition; + layoutHelper.Invoke(systemWindow, anchor, popup, altBounds); } } @@ -336,6 +321,48 @@ namespace MatterHackers.MatterControl.PartPreviewWindow popup.Widget.Invalidate(); } + private static void BestPopupPosition(SystemWindow systemWindow, MatePoint anchor, MatePoint popup, RectangleDouble altBounds) + { + // Calculate left aligned screen space position (using widgetRelativeTo.parent) + Vector2 anchorLeft = anchor.Widget.Parent.TransformToScreenSpace(anchor.Widget.Position); + anchorLeft += new Vector2(altBounds.Left, altBounds.Bottom); + + Vector2 popupPosition = anchorLeft; + + var bounds = altBounds == default(RectangleDouble) ? anchor.Widget.LocalBounds : altBounds; + + Vector2 xPosition = GetXAnchor(anchor.Mate, popup.Mate, popup.Widget, bounds); + + Vector2 screenPosition; + + screenPosition = anchorLeft + xPosition; + + // Constrain + if (screenPosition.X + popup.Widget.Width > systemWindow.Width + || screenPosition.X < 0) + { + xPosition = GetXAnchor(anchor.AltMate, popup.AltMate, popup.Widget, bounds); + } + + popupPosition += xPosition; + + Vector2 yPosition = GetYAnchor(anchor.Mate, popup.Mate, popup.Widget, bounds); + + screenPosition = anchorLeft + yPosition; + + // Constrain + if (anchor.AltMate != null + && (screenPosition.Y + popup.Widget.Height > systemWindow.Height + || screenPosition.Y < 0)) + { + yPosition = GetYAnchor(anchor.AltMate, popup.AltMate, popup.Widget, bounds); + } + + popupPosition += yPosition; + + popup.Widget.Position = popupPosition; + } + private static Vector2 GetYAnchor(MateOptions anchor, MateOptions popup, GuiWidget popupWidget, RectangleDouble bounds) { if (anchor.Top && popup.Bottom) diff --git a/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterActionsBar.cs b/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterActionsBar.cs index 61f094ac5..2edf75138 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterActionsBar.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterActionsBar.cs @@ -114,6 +114,7 @@ namespace MatterHackers.MatterControl.PartPreviewWindow break; } } + // make sure the buttons state is set correctly printer.Connection.CommunicationStateChanged += SetPrintButtonStyle; startPrintButton.Closed += (s, e) => printer.Connection.CommunicationStateChanged -= SetPrintButtonStyle; diff --git a/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterConnectButton.cs b/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterConnectButton.cs index 9b77480cb..81248d663 100644 --- a/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterConnectButton.cs +++ b/MatterControlLib/PartPreviewWindow/View3D/PrinterBar/PrinterConnectButton.cs @@ -33,9 +33,6 @@ using MatterHackers.Agg.UI; using MatterHackers.Localizations; using MatterHackers.MatterControl.CustomWidgets; using MatterHackers.MatterControl.PrinterCommunication; -using MatterHackers.MatterControl.PrinterControls.PrinterConnections; -using MatterHackers.MatterControl.SlicerConfiguration; -using MatterHackers.SerialPortCommunication.FrostedSerial; namespace MatterHackers.MatterControl.ActionBar { diff --git a/MatterControlLib/SetupWizard/HelpTreePanel.cs b/MatterControlLib/SetupWizard/HelpTreePanel.cs index 77f2e5c81..4d3c5e143 100644 --- a/MatterControlLib/SetupWizard/HelpTreePanel.cs +++ b/MatterControlLib/SetupWizard/HelpTreePanel.cs @@ -40,6 +40,19 @@ using MatterHackers.MatterControl.PrintLibrary; namespace MatterHackers.MatterControl { + public class HelpArticleTreeNode : TreeNode + { + public HelpArticleTreeNode(HelpArticle helpArticle, ThemeConfig theme) + : base (theme, useIcon: false) + { + this.HelpArticle = helpArticle; + this.Text = helpArticle.Name; + this.Tag = helpArticle; + } + + public HelpArticle HelpArticle { get; } + } + public class HelpTreePanel : SearchableTreePanel { private string guideKey = null; @@ -318,13 +331,13 @@ namespace MatterHackers.MatterControl private TreeNode initialSelection = null; private TreeNode rootNode; + private Dictionary nodesByPath = new Dictionary(); + private TreeNode ProcessTree(HelpArticle container) { - var treeNode = new TreeNode(theme, false) - { - Text = container.Name, - Tag = container - }; + var treeNode = new HelpArticleTreeNode(container, theme); + + nodesByPath[container.Path] = treeNode; foreach (var item in container.Children.OrderBy(i => i.Children.Count == 0).ThenBy(i => i.Name)) { @@ -334,12 +347,9 @@ namespace MatterHackers.MatterControl } else { - var newNode = new TreeNode(theme, false) - { - Text = item.Name, - Tag = item + var newNode = new HelpArticleTreeNode(item, theme); - }; + nodesByPath[item.Path] = newNode; if (item.Name == guideKey || (guideKey != null @@ -356,6 +366,18 @@ namespace MatterHackers.MatterControl return treeNode; } + public string ActiveNodePath + { + get => treeView.SelectedNode?.Tag as string; + set + { + if (nodesByPath.TryGetValue(value, out HelpArticleTreeNode treeNode)) + { + treeView.SelectedNode = treeNode; + } + } + } + public Color ChildBorderColor { get; private set; } private void AddContent(GuiWidget column, string text, bool left, bool bold)