diff --git a/src/DynamoCore/Search/NodeSearchModel.cs b/src/DynamoCore/Search/NodeSearchModel.cs index 34b11c008a9..be6579300a8 100644 --- a/src/DynamoCore/Search/NodeSearchModel.cs +++ b/src/DynamoCore/Search/NodeSearchModel.cs @@ -233,8 +233,8 @@ internal string ProcessNodeCategory(string category, ref SearchElementGroup grou internal IEnumerable Search(string search, LuceneSearchUtility luceneSearchUtility) { - - if (luceneSearchUtility != null) + if (luceneSearchUtility == null) return null; + lock (luceneSearchUtility) { //The DirectoryReader and IndexSearcher have to be assigned after commiting indexing changes and before executing the Searcher.Search() method, otherwise new indexed info won't be reflected luceneSearchUtility.dirReader = luceneSearchUtility.writer != null ? luceneSearchUtility.writer.GetReader(applyAllDeletes: true) : DirectoryReader.Open(luceneSearchUtility.indexDir); @@ -277,7 +277,6 @@ internal IEnumerable Search(string search, LuceneSearchUtilit } return candidates; } - return null; } internal NodeSearchElement FindModelForNodeNameAndCategory(string nodeName, string nodeCategory, string parameters) diff --git a/src/DynamoCoreWpf/Controls/IncanvasSearchControl.xaml.cs b/src/DynamoCoreWpf/Controls/IncanvasSearchControl.xaml.cs index b8f40090618..698ba04d168 100644 --- a/src/DynamoCoreWpf/Controls/IncanvasSearchControl.xaml.cs +++ b/src/DynamoCoreWpf/Controls/IncanvasSearchControl.xaml.cs @@ -70,15 +70,18 @@ private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) private void ExecuteSearchElement(ListBoxItem listBoxItem) { var searchElement = listBoxItem.DataContext as NodeSearchElementViewModel; - if (searchElement != null) - { - searchElement.Position = ViewModel.InCanvasSearchPosition; - searchElement.ClickedCommand?.Execute(null); - Analytics.TrackEvent( - Dynamo.Logging.Actions.Select, - Dynamo.Logging.Categories.InCanvasSearchOperations, - searchElement.FullName); - } + ExecuteSearchElement(searchElement); + } + + private void ExecuteSearchElement(NodeSearchElementViewModel searchElement) + { + if (searchElement == null) return; + searchElement.Position = ViewModel.InCanvasSearchPosition; + searchElement.ClickedCommand?.Execute(null); + Analytics.TrackEvent( + Dynamo.Logging.Actions.Select, + Dynamo.Logging.Categories.InCanvasSearchOperations, + searchElement.FullName); } private void OnMouseEnter(object sender, MouseEventArgs e) @@ -183,15 +186,26 @@ private void OnInCanvasSearchKeyDown(object sender, KeyEventArgs e) switch (key) { - case Key.Escape: + case Key.Escape: OnRequestShowInCanvasSearch(ShowHideFlags.Hide); break; case Key.Enter: - if (HighlightedItem != null && ViewModel.CurrentMode != SearchViewModel.ViewMode.LibraryView) + ViewModel.AfterLastPendingSearch(() => { - ExecuteSearchElement(HighlightedItem); - OnRequestShowInCanvasSearch(ShowHideFlags.Hide); - } + Dispatcher.BeginInvoke(() => + { + var searchElement = HighlightedItem?.DataContext as NodeSearchElementViewModel; + + //if dropdown hasn't yet fully loaded lets assume the user wants the first element + searchElement ??= ViewModel.FilteredResults.FirstOrDefault(); + + if (searchElement != null && ViewModel.CurrentMode != SearchViewModel.ViewMode.LibraryView) + { + ExecuteSearchElement(searchElement); + OnRequestShowInCanvasSearch(ShowHideFlags.Hide); + } + }); + }); break; case Key.Up: index = MoveToNextMember(false, members, highlightedMember); diff --git a/src/DynamoCoreWpf/DynamoCoreWpf.csproj b/src/DynamoCoreWpf/DynamoCoreWpf.csproj index 61c45650e15..407febd8a7d 100644 --- a/src/DynamoCoreWpf/DynamoCoreWpf.csproj +++ b/src/DynamoCoreWpf/DynamoCoreWpf.csproj @@ -1,4 +1,4 @@ - + true @@ -406,6 +406,7 @@ + diff --git a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt index 0e1fc691107..52acbfd52ac 100644 --- a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt +++ b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt @@ -2867,6 +2867,7 @@ Dynamo.ViewModels.RequestBitmapSourceHandler Dynamo.ViewModels.RequestOpenDocumentationLinkHandler Dynamo.ViewModels.RequestPackagePublishDialogHandler Dynamo.ViewModels.SearchViewModel +Dynamo.ViewModels.SearchViewModel.AfterLastPendingSearch(System.Action action) -> void Dynamo.ViewModels.SearchViewModel.BrowserRootCategories.get -> System.Collections.ObjectModel.ObservableCollection Dynamo.ViewModels.SearchViewModel.BrowserVisibility.get -> bool Dynamo.ViewModels.SearchViewModel.BrowserVisibility.set -> void @@ -3674,11 +3675,16 @@ Dynamo.Wpf.Utilities.GroupStyleItemSelector.GroupStyleItemSelector() -> void Dynamo.Wpf.Utilities.JobDebouncer Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken.DebounceQueueToken() -> void -Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken.IsDirty -> bool +Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken.LastExecutionId -> long +Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken.SerialQueue -> Dynamo.Wpf.Utilities.SerialQueue Dynamo.Wpf.Utilities.LibraryDragAndDrop Dynamo.Wpf.Utilities.LibraryDragAndDrop.LibraryDragAndDrop() -> void Dynamo.Wpf.Utilities.MessageBoxService Dynamo.Wpf.Utilities.MessageBoxService.MessageBoxService() -> void +Dynamo.Wpf.Utilities.SerialQueue +Dynamo.Wpf.Utilities.SerialQueue.DispatchAsync(System.Action action) -> void +Dynamo.Wpf.Utilities.SerialQueue.SerialQueue() -> void +Dynamo.Wpf.Utilities.SerialQueue.UnhandledException -> System.Action Dynamo.Wpf.Utilities.WebView2Utilities Dynamo.Wpf.VariableInputNodeViewCustomization Dynamo.Wpf.VariableInputNodeViewCustomization.Dispose() -> void @@ -4344,7 +4350,6 @@ readonly Dynamo.ViewModels.PortViewModel.node -> Dynamo.ViewModels.NodeViewModel readonly Dynamo.ViewModels.PortViewModel.port -> Dynamo.Graph.Nodes.PortModel readonly Dynamo.ViewModels.WorkspaceViewModel.Model -> Dynamo.Graph.Workspaces.WorkspaceModel readonly Dynamo.Wpf.Extensions.ViewLoadedParams.dynamoMenu -> System.Windows.Controls.Menu -readonly Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken.JobLock -> object readonly Dynamo.Wpf.ViewModels.Watch3D.DefaultWatch3DViewModel.dynamoModel -> Dynamo.Interfaces.IDynamoModel readonly Dynamo.Wpf.ViewModels.Watch3D.DefaultWatch3DViewModel.engineManager -> Dynamo.Models.IEngineControllerManager readonly Dynamo.Wpf.ViewModels.Watch3D.DefaultWatch3DViewModel.logger -> Dynamo.Logging.ILogger @@ -5619,7 +5624,8 @@ static Dynamo.Wpf.UI.VisualConfigurations.ErrorTextFontWeight -> System.Windows. static Dynamo.Wpf.UI.VisualConfigurations.LibraryTooltipTextFontWeight -> System.Windows.FontWeight static Dynamo.Wpf.UI.VisualConfigurations.NodeTooltipTextFontWeight -> System.Windows.FontWeight static Dynamo.Wpf.Utilities.CompactBubbleHandler.Process(ProtoCore.Mirror.MirrorData value) -> Dynamo.ViewModels.CompactBubbleViewModel -static Dynamo.Wpf.Utilities.JobDebouncer.EnqueueJobAsync(System.Action job, Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken token) -> System.Threading.Tasks.Task +static Dynamo.Wpf.Utilities.JobDebouncer.EnqueueMandatoryJobAsync(System.Action job, Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken token) -> void +static Dynamo.Wpf.Utilities.JobDebouncer.EnqueueOptionalJobAsync(System.Action job, Dynamo.Wpf.Utilities.JobDebouncer.DebounceQueueToken token) -> void static Dynamo.Wpf.Utilities.MessageBoxService.Show(string msg, string title, bool showRichTextBox, System.Windows.MessageBoxButton button, System.Windows.MessageBoxImage img) -> System.Windows.MessageBoxResult static Dynamo.Wpf.Utilities.MessageBoxService.Show(string msg, string title, System.Windows.MessageBoxButton button, System.Collections.Generic.IEnumerable buttonNames, System.Windows.MessageBoxImage img) -> System.Windows.MessageBoxResult static Dynamo.Wpf.Utilities.MessageBoxService.Show(string msg, string title, System.Windows.MessageBoxButton button, System.Windows.MessageBoxImage img) -> System.Windows.MessageBoxResult diff --git a/src/DynamoCoreWpf/Utilities/JobDebouncer.cs b/src/DynamoCoreWpf/Utilities/JobDebouncer.cs index 23bc0f02ecc..ae7bca37120 100644 --- a/src/DynamoCoreWpf/Utilities/JobDebouncer.cs +++ b/src/DynamoCoreWpf/Utilities/JobDebouncer.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; namespace Dynamo.Wpf.Utilities { @@ -7,30 +6,38 @@ public static class JobDebouncer { public class DebounceQueueToken { - public bool IsDirty = false; - public readonly object JobLock = new(); + public long LastExecutionId = 0; + public SerialQueue SerialQueue = new(); }; /// /// Action is guaranteed to run at most once for every call, and exactly once after the last call. - /// Execution is sequential, and jobs that share a with a newer job will be ignored. + /// Execution is sequential, and optional jobs that share a with a newer optional job will be ignored. /// /// /// /// - public static Task EnqueueJobAsync(Action job, DebounceQueueToken token) + public static void EnqueueOptionalJobAsync(Action job, DebounceQueueToken token) { - token.IsDirty = true; - return Task.Run(() => + lock (token) { - lock (token.JobLock) + token.LastExecutionId++; + var myExecutionId = token.LastExecutionId; + token.SerialQueue.DispatchAsync(() => { - while (token.IsDirty) - { - token.IsDirty = false; - job(); - } - } - }); + if (myExecutionId < token.LastExecutionId) return; + job(); + }); + } + } + public static void EnqueueMandatoryJobAsync(Action job, DebounceQueueToken token) + { + lock (token) + { + token.SerialQueue.DispatchAsync(() => + { + job(); + }); + } } } } diff --git a/src/DynamoCoreWpf/Utilities/SerialQueue.cs b/src/DynamoCoreWpf/Utilities/SerialQueue.cs new file mode 100644 index 00000000000..9cfe7fe9028 --- /dev/null +++ b/src/DynamoCoreWpf/Utilities/SerialQueue.cs @@ -0,0 +1,80 @@ +//https://github.com/gentlee/SerialQueue/blob/master/SerialQueue/SerialQueue.cs +using System; +using System.Threading; + +namespace Dynamo.Wpf.Utilities +{ + public class SerialQueue + { + class LinkedListNode(Action action) + { + public readonly Action Action = action; + public LinkedListNode Next; + } + + public event Action UnhandledException = delegate { }; + + private LinkedListNode _queueFirst; + private LinkedListNode _queueLast; + private bool _isRunning = false; + + public void DispatchAsync(Action action) + { + var newNode = new LinkedListNode(action); + + lock (this) + { + if (_queueFirst == null) + { + _queueFirst = newNode; + _queueLast = newNode; + + if (!_isRunning) + { + _isRunning = true; + ThreadPool.QueueUserWorkItem(Run); + } + } + else + { + _queueLast!.Next = newNode; + _queueLast = newNode; + } + } + } + + private void Run(object _) + { + while (true) + { + LinkedListNode firstNode; + + lock (this) + { + if (_queueFirst == null) + { + _isRunning = false; + return; + } + firstNode = _queueFirst; + _queueFirst = null; + _queueLast = null; + } + + while (firstNode != null) + { + var action = firstNode.Action; + firstNode = firstNode.Next; + try + { + action(); + } + catch (Exception error) + { + UnhandledException.Invoke(action, error); + } + } + } + } + } +} diff --git a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs index f18f2b08a8e..eb2869ae7b9 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs @@ -863,12 +863,6 @@ private static string MakeFullyQualifiedName(string path, string addition) /// internal void SearchAndUpdateResults() { - //Unit tests don't have an Application.Current - (Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher).Invoke(() => - { - searchResults.Clear(); - }); - if (!String.IsNullOrEmpty(SearchText.Trim())) { SearchAndUpdateResults(SearchText); @@ -1178,7 +1172,12 @@ public void OnSearchElementClicked(NodeModel nodeModel, Point position) private static readonly JobDebouncer.DebounceQueueToken DebounceQueueToken = new(); public void Search(object parameter) { - JobDebouncer.EnqueueJobAsync(SearchAndUpdateResults, DebounceQueueToken); + JobDebouncer.EnqueueOptionalJobAsync(SearchAndUpdateResults, DebounceQueueToken); + } + + public void AfterLastPendingSearch(Action action) + { + JobDebouncer.EnqueueMandatoryJobAsync(action, DebounceQueueToken); } internal bool CanSearch(object parameter) diff --git a/src/LibraryViewExtensionWebView2/Handlers/SearchResultDataProvider.cs b/src/LibraryViewExtensionWebView2/Handlers/SearchResultDataProvider.cs index 396aa8d919c..d3f2fbd238c 100644 --- a/src/LibraryViewExtensionWebView2/Handlers/SearchResultDataProvider.cs +++ b/src/LibraryViewExtensionWebView2/Handlers/SearchResultDataProvider.cs @@ -47,7 +47,7 @@ public SearchResultDataProvider(NodeSearchModel model, IconResourceProvider icon public override Stream GetResource(string searchText, out string extension) { var text = Uri.UnescapeDataString(searchText); - var elements = model.Search(text, LuceneSearch.LuceneUtilityNodeSearch); + var elements = string.IsNullOrWhiteSpace(text) ? new List() : model.Search(text, LuceneSearch.LuceneUtilityNodeSearch); extension = "json"; return GetNodeItemDataStream(elements, true); } diff --git a/src/LibraryViewExtensionWebView2/LibraryViewController.cs b/src/LibraryViewExtensionWebView2/LibraryViewController.cs index b85a8ca17a5..c6d9b541fb2 100644 --- a/src/LibraryViewExtensionWebView2/LibraryViewController.cs +++ b/src/LibraryViewExtensionWebView2/LibraryViewController.cs @@ -461,10 +461,17 @@ private void Browser_KeyDown(object sender, KeyEventArgs e) var synteticEventData = new Dictionary { - [Enum.GetName(typeof(ModifiersJS), e.KeyboardDevice.Modifiers)] = "true", ["key"] = e.Key.ToString() }; + foreach(ModifiersJS modifier in Enum.GetValues(typeof(ModifiersJS))) + { + if (((int)e.KeyboardDevice.Modifiers & (int)modifier) != 0) + { + synteticEventData[Enum.GetName(typeof(ModifiersJS), modifier)] = "true"; + } + } + _ = ExecuteScriptFunctionAsync(browser, "eventDispatcher", synteticEventData); } diff --git a/src/LibraryViewExtensionWebView2/ScriptingObject.cs b/src/LibraryViewExtensionWebView2/ScriptingObject.cs index 5320554130c..9ed959badf6 100644 --- a/src/LibraryViewExtensionWebView2/ScriptingObject.cs +++ b/src/LibraryViewExtensionWebView2/ScriptingObject.cs @@ -3,8 +3,12 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Threading; +using Dynamo.Wpf.Utilities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using static Dynamo.Wpf.Utilities.JobDebouncer; namespace Dynamo.LibraryViewExtensionWebView2 { @@ -46,6 +50,8 @@ public string GetBase64StringFromPath(string iconurl) return $"data:image/{ext};base64, {iconAsBase64}"; } + private static readonly JobDebouncer.DebounceQueueToken DebounceQueueToken = new(); + /// /// This method will receive any message sent from javascript and execute a specific code according to the message /// @@ -98,12 +104,19 @@ internal void Notify(string dataFromjs) { var data = simpleRPCPayload["data"] as string; var extension = string.Empty; - var searchStream = controller.searchResultDataProvider.GetResource(data, out extension); - var searchReader = new StreamReader(searchStream); - var results = searchReader.ReadToEnd(); - //send back results to librarie.js - LibraryViewController.ExecuteScriptFunctionAsync(controller.browser, "completeSearch", results); - searchReader.Dispose(); + + var dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; + JobDebouncer.EnqueueOptionalJobAsync(() => { + var searchStream = controller.searchResultDataProvider.GetResource(data, out extension); + var searchReader = new StreamReader(searchStream); + var results = searchReader.ReadToEnd(); + dispatcher.Invoke(() => + { + //send back results to librarie.js + LibraryViewController.ExecuteScriptFunctionAsync(controller.browser, "completeSearch", results); + searchReader.Dispose(); + }); + }, DebounceQueueToken); } //When the html
that contains the sample package is clicked then we will be moved to the next Step in the Guide else if (funcName == "NextStep")