From 8b1e0df2225d5678b9285b1465d50c185b42a076 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 8 May 2022 19:42:34 +0200 Subject: [PATCH] Add **Content Search** window to searching Visual Scripts and other assets --- Source/Editor/Modules/ContentFindingModule.cs | 81 +++ Source/Editor/Modules/UIModule.cs | 1 + Source/Editor/Surface/VisjectSurfaceWindow.cs | 2 + .../Assets/VisjectFunctionSurfaceWindow.cs | 2 + .../Windows/Assets/VisualScriptWindow.cs | 2 + .../Windows/Search/ContentSearchWindow.cs | 673 ++++++++++++++++++ 6 files changed, 761 insertions(+) create mode 100644 Source/Editor/Windows/Search/ContentSearchWindow.cs diff --git a/Source/Editor/Modules/ContentFindingModule.cs b/Source/Editor/Modules/ContentFindingModule.cs index 89d5914fa..4fb0ef75f 100644 --- a/Source/Editor/Modules/ContentFindingModule.cs +++ b/Source/Editor/Modules/ContentFindingModule.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using FlaxEditor.Content; +using FlaxEditor.GUI.Docking; using FlaxEditor.SceneGraph; using FlaxEditor.Windows.Search; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Windows.Search; namespace FlaxEditor.Modules { @@ -47,6 +49,7 @@ namespace FlaxEditor.Modules { private List _quickActions; private ContentFinder _finder; + private ContentSearchWindow _searchWin; /// /// Initializes a new instance of the class. @@ -72,9 +75,87 @@ namespace FlaxEditor.Modules _finder = null; } + if (_searchWin != null) + { + _searchWin.Dispose(); + _searchWin = null; + } + base.OnExit(); } + /// + /// Shows the content search window. + /// + public void ShowSearch() + { + // Try to find the currently focused editor window that might call this + DockWindow window = null; + foreach (var editorWindow in Editor.Windows.Windows) + { + if (editorWindow.Visible && editorWindow.ContainsFocus) + { + window = editorWindow; + break; + } + } + + ShowSearch(window); + } + + /// + /// Shows the content search window. + /// + /// The target control to show search for it. + /// The initial query for the search. + public void ShowSearch(Control control, string query = null) + { + // Try to find the owning window + DockWindow window = null; + while (control != null && window == null) + { + window = control as DockWindow; + control = control.Parent; + } + + ShowSearch(window, query); + } + + /// + /// Shows the content search window. + /// + /// The target window to show search next to it. + /// The initial query for the search. + public void ShowSearch(DockWindow window, string query = null) + { + if (_searchWin == null) + _searchWin = new ContentSearchWindow(Editor); + _searchWin.TargetWindow = window; + if (!_searchWin.IsHidden) + { + // Focus + _searchWin.SelectTab(); + _searchWin.Focus(); + } + else if (window != null) + { + // Show docked to the target window + _searchWin.Show(DockState.DockBottom, window); + _searchWin.SearchLocation = ContentSearchWindow.SearchLocations.CurrentAsset; + } + else + { + // Show floating + _searchWin.ShowFloating(); + _searchWin.SearchLocation = ContentSearchWindow.SearchLocations.AllAssets; + } + if (query != null) + { + _searchWin.Query = query; + _searchWin.Search(); + } + } + /// /// Shows the content finder popup. /// diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 3d82b1a17..3d1cc129e 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -511,6 +511,7 @@ namespace FlaxEditor.Modules cm.AddButton("Graphics Quality", Editor.Windows.GraphicsQualityWin.FocusOrShow); cm.AddButton("Game Cooker", Editor.Windows.GameCookerWin.FocusOrShow); cm.AddButton("Profiler", Editor.Windows.ProfilerWin.FocusOrShow); + cm.AddButton("Content Search", Editor.ContentFinding.ShowSearch); cm.AddButton("Visual Script Debugger", Editor.Windows.VisualScriptDebuggerWin.FocusOrShow); cm.AddSeparator(); cm.AddButton("Save window layout", Editor.Windows.SaveLayout); diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index e475c0f2d..0d68f6d2b 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -778,11 +778,13 @@ namespace FlaxEditor.Surface _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); _toolstrip.AddSeparator(); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Setup input actions InputActions.Add(options => options.Undo, _undo.PerformUndo); InputActions.Add(options => options.Redo, _undo.PerformRedo); + InputActions.Add(options => options.Search, Editor.ContentFinding.ShowSearch); } private void OnUndoRedo(IUndoAction action) diff --git a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs index e3261b325..d296c4af7 100644 --- a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs +++ b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs @@ -73,6 +73,7 @@ namespace FlaxEditor.Windows.Assets _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); _toolstrip.AddSeparator(); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Panel @@ -86,6 +87,7 @@ namespace FlaxEditor.Windows.Assets // Setup input actions InputActions.Add(options => options.Undo, _undo.PerformUndo); InputActions.Add(options => options.Redo, _undo.PerformRedo); + InputActions.Add(options => options.Search, Editor.ContentFinding.ShowSearch); } private void OnUndoRedo(IUndoAction action) diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs index b76d2427d..a54ca837c 100644 --- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs +++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs @@ -589,6 +589,7 @@ namespace FlaxEditor.Windows.Assets _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); _toolstrip.AddSeparator(); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/visual/index.html")).LinkTooltip("See documentation to learn more"); @@ -631,6 +632,7 @@ namespace FlaxEditor.Windows.Assets // Setup input actions InputActions.Add(options => options.Undo, _undo.PerformUndo); InputActions.Add(options => options.Redo, _undo.PerformRedo); + InputActions.Add(options => options.Search, Editor.ContentFinding.ShowSearch); InputActions.Add(options => options.DebuggerContinue, OnDebuggerContinue); InputActions.Add(options => options.DebuggerStepOver, OnDebuggerStepOver); InputActions.Add(options => options.DebuggerStepOut, OnDebuggerStepOut); diff --git a/Source/Editor/Windows/Search/ContentSearchWindow.cs b/Source/Editor/Windows/Search/ContentSearchWindow.cs new file mode 100644 index 000000000..d9ef1d335 --- /dev/null +++ b/Source/Editor/Windows/Search/ContentSearchWindow.cs @@ -0,0 +1,673 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FlaxEditor; +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Docking; +using FlaxEditor.GUI.Tree; +using FlaxEditor.Scripting; +using FlaxEditor.Surface; +using FlaxEditor.Windows; +using FlaxEditor.Windows.Assets; +using FlaxEngine.GUI; + +namespace FlaxEngine.Windows.Search +{ + /// + /// The content searching window. Allows to search inside Visual Scripts, Materials, Particles and other assets. + /// + internal class ContentSearchWindow : EditorWindow + { + /// + /// Content searching location types. + /// + public enum SearchLocations + { + /// + /// Searches in the currently opened asset (last focused). + /// + CurrentAsset, + + /// + /// Searches in all opened asset windows. + /// + AllOpenedAssets, + + /// + /// Searches in all asset in the project. + /// + AllAssets, + } + + /// + /// Content searching asset types. + /// + [Flags] + public enum SearchAssetTypes + { + None = 0, + VisualScript = 1 << 0, + Material = 1 << 1, + AnimGraph = 1 << 2, + ParticleEmitter = 1 << 3, + All = VisualScript | Material | AnimGraph | ParticleEmitter, + } + + private sealed class SearchSurfaceContext : ISurfaceContext + { + /// + public string SurfaceName => string.Empty; + + /// + public byte[] SurfaceData { get; set; } + + /// + public void OnContextCreated(VisjectSurfaceContext context) + { + } + } + + private sealed class SearchResultTreeNode : TreeNode + { + public Action Navigate; + + public SearchResultTreeNode() + : base(false, SpriteHandle.Invalid, SpriteHandle.Invalid) + { + } + + public SearchResultTreeNode(SpriteHandle icon) + : base(false, icon, icon) + { + } + + /// + public override bool OnShowTooltip(out string text, out Vector2 location, out Rectangle area) + { + var result = base.OnShowTooltip(out text, out location, out area); + location = new Vector2(ChildrenIndent, HeaderHeight); + return result; + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (IsFocused && key == KeyboardKeys.Return && Navigate != null) + { + Navigate.Invoke(this); + return true; + } + return base.OnKeyDown(key); + } + + /// + protected override bool OnMouseDoubleClickHeader(ref Vector2 location, MouseButton button) + { + if (Navigate != null) + { + Navigate.Invoke(this); + return true; + } + return base.OnMouseDoubleClickHeader(ref location, button); + } + } + + private TextBox _searchBox; + private Tree _resultsTree; + private TreeNode _resultsTreeRoot; + private Label _loadingLabel; + private float _progress; + private CancellationTokenSource _token; + private Task _task; + private bool _searchTextExact; + private string _searchText; + private List _pendingResults = new List(); + private SearchSurfaceContext _searchSurfaceContext = new SearchSurfaceContext(); + private VisjectSurfaceContext _visjectSurfaceContext; + private SurfaceStyle _visjectSurfaceStyle; + + /// + /// The current search location type. + /// + public SearchLocations SearchLocation = SearchLocations.AllAssets; + + /// + /// The current search asset types (flags). + /// + public SearchAssetTypes SearchAssets = SearchAssetTypes.VisualScript; + + /// + /// Gets or sets the current search query text. + /// + public string Query + { + get => _searchBox.Text; + set => _searchBox.Text = value; + } + + /// + /// The target window which called the search tool. Used as a context for search. + /// + public DockWindow TargetWindow; + + /// + public ContentSearchWindow(Editor editor) + : base(editor, true, ScrollBars.Vertical) + { + Title = "Content Search"; + Icon = Editor.Icons.Search64; + + // Setup UI + var topPanel = new ContainerControl + { + AnchorPreset = AnchorPresets.HorizontalStretchTop, + IsScrollable = false, + Offsets = new Margin(0, 0, 0, 22.0f), + BackgroundColor = Style.Current.Background, + Parent = this, + }; + var optionsButton = new Button(2, 2, 60.0f, 18.0f) + { + Text = "Options", + TooltipText = "Change search options", + Parent = topPanel, + }; + optionsButton.ButtonClicked += OnOptionsDropdownClicked; + _searchBox = new TextBox + { + AnchorPreset = AnchorPresets.HorizontalStretchMiddle, + WatermarkText = "Search...", + Parent = topPanel, + Bounds = new Rectangle(optionsButton.Right + 2.0f, 2, topPanel.Width - 4.0f - optionsButton.Width, 18.0f), + }; + _searchBox.TextBoxEditEnd += OnSearchBoxEditEnd; + _resultsTree = new Tree(false) + { + AnchorPreset = AnchorPresets.HorizontalStretchTop, + Offsets = new Margin(0, 0, topPanel.Bottom + 4 - 16.0f, 0), + IsScrollable = true, + Parent = this, + IndexInParent = 0, + }; + _resultsTreeRoot = new TreeNode + { + ChildrenIndent = 0, + BackgroundColorSelected = Color.Transparent, + BackgroundColorHighlighted = Color.Transparent, + BackgroundColorSelectedUnfocused = Color.Transparent, + TextColor = Color.Transparent, + }; + _resultsTreeRoot.Expand(); + _resultsTree.AddChild(_resultsTreeRoot); + _loadingLabel = new Label + { + AnchorPreset = AnchorPresets.HorizontalStretchBottom, + Offsets = new Margin(0, 0, -20.0f, 0.0f), + Parent = this, + Visible = false, + }; + + _visjectSurfaceContext = new VisjectSurfaceContext(null, null, _searchSurfaceContext); + } + + /// + public override void OnUpdate() + { + base.OnUpdate(); + + if (_loadingLabel.Visible) + _loadingLabel.Text = string.Format("Searching {0}%...", (int)_progress); + + lock (_pendingResults) + { + // Add in batches or after search end + if (_pendingResults.Count > 10 || _token == null) + { + LockChildrenRecursive(); + for (int i = 0; i < _pendingResults.Count; i++) + _resultsTreeRoot.AddChild(_pendingResults[i]); + _pendingResults.Clear(); + UnlockChildrenRecursive(); + PerformLayout(); + } + } + } + + /// + public override void OnKeyUp(KeyboardKeys key) + { + if (key == KeyboardKeys.ArrowDown && _resultsTree.Selection.Count == 0 && _resultsTreeRoot.HasAnyVisibleChild && (_searchBox.IsFocused || IsFocused)) + { + // Focus the first item from results + _resultsTree.Select(_resultsTreeRoot.Children[0] as TreeNode); + return; + } + + base.OnKeyUp(key); + } + + /// + public override void OnDestroy() + { + Cancel(); + + base.OnDestroy(); + } + + private void OnOptionsDropdownClicked(Button optionsButton) + { + var menu = new ContextMenu(); + + var m = menu.AddChildMenu("Search Location").ContextMenu; + var entries = new List(); + EnumComboBox.BuildEntriesDefault(typeof(SearchLocations), entries); + foreach (var e in entries) + { + var value = (SearchLocations)e.Value; + var b = m.AddButton(e.Name, () => + { + SearchLocation = value; + Search(); + }); + b.TooltipText = e.Tooltip; + b.Checked = SearchLocation == value; + } + + m = menu.AddChildMenu("Search Assets").ContextMenu; + entries.Clear(); + EnumComboBox.BuildEntriesDefault(typeof(SearchAssetTypes), entries); + foreach (var e in entries) + { + var value = (SearchAssetTypes)e.Value; + var b = m.AddButton(e.Name, () => + { + if (value == SearchAssetTypes.None || value == SearchAssetTypes.All) + SearchAssets = value; + else + SearchAssets ^= value; + Search(); + }); + b.TooltipText = e.Tooltip; + if (value == SearchAssetTypes.None || value == SearchAssetTypes.All) + b.Checked = SearchAssets == value; + else + b.Checked = SearchAssets.HasFlag(value); + } + + menu.Show(Parent, Location + optionsButton.BottomLeft); + } + + private void OnSearchBoxEditEnd(TextBoxBase searchBox) + { + Search(); + Focus(); + } + + private void Cancel() + { + if (_token != null) + { + try + { + // Wait for async end + _token.Cancel(); + _task.Wait(); + } + catch (Exception ex) + { + Editor.LogWarning(ex); + } + finally + { + _token = null; + } + } + lock (_pendingResults) + { + _pendingResults.Clear(); + } + } + + /// + /// Initializes the searching (stops the current one if any active). + /// + public void Search() + { + // Stop + Cancel(); + + // Clear results + _resultsTreeRoot.DisposeChildren(); + _resultsTreeRoot.Expand(); + + // Start async searching + _searchText = _searchBox.Text; + _searchText = _searchText.Trim(); + if (_searchText.Length == 0) + return; + _searchTextExact = _searchText.Length > 2 && _searchText[0] == '\"' && _searchText[_searchText.Length - 1] == '\"'; + if (_searchTextExact) + _searchText = _searchText.Substring(1, _searchText.Length - 2); + _token = new CancellationTokenSource(); + _task = Task.Run(SearchAsync, _token.Token); + _progress = 0.0f; + _loadingLabel.Visible = true; + } + + private void SearchAsync() + { + try + { + SearchAsyncInner(); + } + catch (Exception ex) + { + Editor.LogWarning(ex); + lock (_pendingResults) + _pendingResults.Clear(); + } + finally + { + _visjectSurfaceContext.Clear(); + _progress = 100.0f; + _loadingLabel.Visible = false; + _token = null; + } + } + + private void SearchAsyncInner() + { + // Get the assets for search + Guid[] assets = null; + switch (SearchLocation) + { + case SearchLocations.CurrentAsset: + if (TargetWindow is AssetEditorWindow assetEditorWindow) + assets = new[] { assetEditorWindow.Item.ID }; + break; + case SearchLocations.AllOpenedAssets: + assets = new Guid[Editor.Windows.Windows.Count]; + for (var i = 0; i < Editor.Windows.Windows.Count; i++) + { + if (Editor.Windows.Windows[i] is AssetEditorWindow window) + assets[i] = window.Item.ID; + } + break; + case SearchLocations.AllAssets: + assets = Content.GetAllAssets(); + break; + } + if (assets == null) + return; + + // Build valid asset typenames list + var searchAssets = SearchAssets; + var validTypeNames = new HashSet(); + if (searchAssets.HasFlag(SearchAssetTypes.VisualScript)) + { + validTypeNames.Add(typeof(VisualScript).FullName); + } + if (searchAssets.HasFlag(SearchAssetTypes.Material)) + { + validTypeNames.Add(typeof(Material).FullName); + validTypeNames.Add(typeof(MaterialFunction).FullName); + } + if (searchAssets.HasFlag(SearchAssetTypes.AnimGraph)) + { + validTypeNames.Add(typeof(AnimationGraph).FullName); + validTypeNames.Add(typeof(AnimationGraphFunction).FullName); + } + if (searchAssets.HasFlag(SearchAssetTypes.ParticleEmitter)) + { + validTypeNames.Add(typeof(ParticleEmitter).FullName); + validTypeNames.Add(typeof(ParticleEmitterFunction).FullName); + } + + // Iterate over all assets + for (var i = 0; i < assets.Length && !_token.IsCancellationRequested; i++) + { + var id = assets[i]; + _progress = ((float)i / assets.Length) * 100.0f; + if (!Content.GetAssetInfo(id, out var assetInfo)) + continue; + if (!validTypeNames.Contains(assetInfo.TypeName)) + continue; + // TODO: implement assets indexing or other caching for faster searching + var asset = Content.LoadAsync(id); + if (asset == null) + continue; + + // Search asset contents + if (asset is VisualScript visualScript) + SearchAsyncInnerVisject(asset, visualScript.LoadSurface()); + else if (asset is Material material) + SearchAsyncInnerVisject(asset, material.LoadSurface(false)); + else if (asset is MaterialFunction materialFunction) + SearchAsyncInnerVisject(asset, materialFunction.LoadSurface()); + else if (asset is AnimationGraph animationGraph) + SearchAsyncInnerVisject(asset, animationGraph.LoadSurface()); + else if (asset is AnimationGraphFunction animationGraphFunction) + SearchAsyncInnerVisject(asset, animationGraphFunction.LoadSurface()); + else if (asset is ParticleEmitter particleEmitter) + SearchAsyncInnerVisject(asset, particleEmitter.LoadSurface(false)); + else if (asset is ParticleEmitterFunction particleEmitterFunction) + SearchAsyncInnerVisject(asset, particleEmitterFunction.LoadSurface()); + + // Don't eat whole performance + Thread.Sleep(15); + } + } + + private bool IsSearchMatch(ref string text) + { + if (_searchTextExact) + return text.Equals(_searchText, StringComparison.Ordinal); + return text.IndexOf(_searchText, StringComparison.OrdinalIgnoreCase) != -1; + } + + private void AddAssetSearchResult(ref SearchResultTreeNode assetTreeNode, Asset asset) + { + if (assetTreeNode != null) + return; + assetTreeNode = new SearchResultTreeNode + { + Text = FlaxEditor.Utilities.Utils.GetAssetNamePath(asset.Path), + Tag = asset.ID, + Navigate = OnNavigateAsset, + }; + } + + private void SearchAsyncInnerVisject(Asset asset, byte[] surfaceData) + { + // Load Visject surface from data + if (surfaceData == null || surfaceData.Length == 0) + return; + _searchSurfaceContext.SurfaceData = surfaceData; + if (_visjectSurfaceContext.Load()) + { + Editor.LogError("Failed to load Visject Surface for " + asset.Path); + return; + } + if (_visjectSurfaceStyle == null) + _visjectSurfaceStyle = SurfaceStyle.CreateDefault(Editor); + SearchResultTreeNode assetTreeNode = null; + // TODO: support nested surfaces (eg. in Anim Graph) + + // Search parameters + foreach (var parameter in _visjectSurfaceContext.Parameters) + { + SearchResultTreeNode parameterTreeNode = null; + SearchVisjectMatch(parameter.Value, (matchedValue, matchedText) => + { + AddAssetSearchResult(ref assetTreeNode, asset); + if (parameterTreeNode == null) + parameterTreeNode = new SearchResultTreeNode + { + Text = parameter.Name, + Tag = assetTreeNode.Tag, + Navigate = assetTreeNode.Navigate, + Parent = assetTreeNode, + }; + var valueTreeNode = AddVisjectSearchResult(matchedValue, matchedText); + valueTreeNode.Tag = assetTreeNode.Tag; + valueTreeNode.Navigate = assetTreeNode.Navigate; + valueTreeNode.Parent = parameterTreeNode; + }, "Default Value: "); + } + + // Search nodes + foreach (var node in _visjectSurfaceContext.Nodes) + { + SearchResultTreeNode nodeTreeNode = null; + if (node.Values != null) + { + foreach (var value in node.Values) + { + SearchVisjectMatch(value, (matchedValue, matchedText) => + { + AddAssetSearchResult(ref assetTreeNode, asset); + if (nodeTreeNode == null) + nodeTreeNode = new SearchResultTreeNode + { + Text = node.Title, + TooltipText = node.TooltipText, + Tag = node.ID, + Navigate = OnNavigateVisjectNode, + Parent = assetTreeNode, + }; + var valueTreeNode = AddVisjectSearchResult(matchedValue, matchedText, node.Archetype.ConnectionsHints); + valueTreeNode.Tag = node.ID; + valueTreeNode.Navigate = OnNavigateVisjectNode; + valueTreeNode.Parent = nodeTreeNode; + }); + } + } + } + + if (assetTreeNode != null) + { + assetTreeNode.ExpandAll(); + lock (_pendingResults) + _pendingResults.Add(assetTreeNode); + } + } + + private void SearchVisjectMatch(object value, Action matched, string prefix = null) + { + var text = value as string; + if (value == null || value is byte[]) + return; + if (text == null) + { + if (value is IList asList) + { + // Check element of the list in separate + for (int i = 0; i < asList.Count; i++) + SearchVisjectMatch(asList[i], matched, $"[{i}] "); + return; + } + if (value is IDictionary asDictionary) + { + // Check each pair of the dictionary in separate + foreach (var key in asDictionary.Keys) + { + var isMatch = false; + var keyValue = asDictionary[key]; + object keyMatchedValue = key, valueMatchedValue = keyValue; + string keyMatchedText = null, valueMatchedText = null; + SearchVisjectMatch(key, (matchedValue, matchedText) => + { + isMatch = true; + keyMatchedValue = matchedValue; + keyMatchedText = matchedText; + }); + SearchVisjectMatch(keyValue, (matchedValue, matchedText) => + { + isMatch = true; + valueMatchedValue = matchedValue; + valueMatchedText = matchedText; + }); + if (isMatch) + { + if (keyMatchedText == null) + keyMatchedText = keyMatchedValue?.ToString() ?? string.Empty; + if (valueMatchedText == null) + valueMatchedText = valueMatchedValue?.ToString() ?? string.Empty; + matched(valueMatchedValue, $"[{keyMatchedText}]: {valueMatchedText}"); + } + } + return; + } + + text = value.ToString(); + + // Special case for assets (and Guid converted into asset) to search in asset name path + if (value is Guid asGuid) + { + if (Content.GetAssetInfo(asGuid, out _)) + value = Content.LoadAsync(asGuid); + else + text = Json.JsonSerializer.GetStringID(asGuid); + } + if (value is Asset asAsset) + text = FlaxEditor.Utilities.Utils.GetAssetNamePath(asAsset.Path); + } + if (IsSearchMatch(ref text)) + { + if (prefix != null) + text = prefix + text; + matched(value, text); + } + } + + private SearchResultTreeNode AddVisjectSearchResult(object value, string text, ConnectionsHint connectionsHint = ConnectionsHint.None) + { + var valueType = TypeUtils.GetObjectType(value); + _visjectSurfaceStyle.GetConnectionColor(valueType, connectionsHint, out var valueColor); + return new SearchResultTreeNode(_visjectSurfaceStyle.Icons.BoxClose) + { + Text = text, + TooltipText = valueType.ToString(), + IconColor = valueColor, + }; + } + + private void OnNavigateAsset(SearchResultTreeNode treeNode) + { + var assetId = (Guid)treeNode.Tag; + var contentItem = Editor.ContentDatabase.FindAsset(assetId); + Editor.ContentEditing.Open(contentItem); + } + + private void OnNavigateVisjectNode(SearchResultTreeNode treeNode) + { + var nodeId = (uint)treeNode.Tag; + var assetId = Guid.Empty; + var assetTreeNode = treeNode.Parent; + while (!(assetTreeNode.Tag is Guid)) + assetTreeNode = assetTreeNode.Parent; + assetId = (Guid)assetTreeNode.Tag; + var contentItem = Editor.ContentDatabase.FindAsset(assetId); + if (Editor.ContentEditing.Open(contentItem) is IVisjectSurfaceWindow window) + { + var node = window.VisjectSurface.FindNode(nodeId); + if (node != null) + { + // Focus this node + window.VisjectSurface.FocusNode(node); + } + else + { + // Retry once the surface gets loaded + window.SurfaceLoaded += () => OnNavigateVisjectNode(treeNode); + } + } + } + } +}