diff --git a/Source/Editor/CustomEditors/CustomEditorPresenter.cs b/Source/Editor/CustomEditors/CustomEditorPresenter.cs
index c7832cfe1..2ccd53e25 100644
--- a/Source/Editor/CustomEditors/CustomEditorPresenter.cs
+++ b/Source/Editor/CustomEditors/CustomEditorPresenter.cs
@@ -8,6 +8,7 @@ using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
+using FlaxEditor.CustomEditors.GUI;
namespace FlaxEditor.CustomEditors
{
@@ -243,11 +244,13 @@ namespace FlaxEditor.CustomEditors
///
protected readonly RootEditor Editor;
- ///
- /// The selected objects list (read-only).
- ///
public readonly ValueContainer Selection = new ValueContainer(ScriptMemberInfo.Null);
+ ///
+ /// The current properties search query.
+ ///
+ public string SearchText = string.Empty;
+
///
/// The undo object used by this editor.
///
@@ -529,6 +532,256 @@ namespace FlaxEditor.CustomEditors
Editor?.RefreshInternal();
}
+ ///
+ /// Applies search filter query to the presenter layout controls.
+ ///
+ public void ApplySearchFilter(string query)
+ {
+ SearchText = query;
+ if (Root == null)
+ return;
+
+ var isQueryEmpty = string.IsNullOrEmpty(query);
+ var groupMatchCache = new Dictionary();
+ UpdateFilter(Root, query, isQueryEmpty, groupMatchCache);
+ UpdatePropertiesListsVisibility(Panel, query);
+ UpdateGroupsVisibility(Panel, query);
+ Panel.PerformLayout();
+ }
+
+ ///
+ /// Updates the visibility of properties lists and drop panels based on the current search query.
+ ///
+ public void UpdateGroupsAndListsVisibility()
+ {
+ if (string.IsNullOrEmpty(SearchText))
+ {
+ RestoreVisibilities(Panel);
+ }
+ else
+ {
+ UpdatePropertiesListsVisibility(Panel, SearchText);
+ UpdateGroupsVisibility(Panel, SearchText);
+ }
+ Panel.PerformLayout();
+ }
+
+ private void RestoreVisibilities(Control control)
+ {
+ if (control is DropPanel dropPanel)
+ {
+ dropPanel.SearchText = string.Empty;
+ dropPanel.Visible = true;
+ }
+ else if (control is PropertiesList list)
+ {
+ list.Visible = true;
+ }
+
+ if (control is ContainerControl container)
+ {
+ for (int i = 0; i < container.ChildrenCount; i++)
+ {
+ RestoreVisibilities(container.GetChild(i));
+ }
+ }
+ }
+
+ private void UpdatePropertiesListsVisibility(Control control, string query)
+ {
+ if (control is PropertiesList list)
+ {
+ if (string.IsNullOrEmpty(query))
+ {
+ list.Visible = true;
+ }
+ else
+ {
+ bool anyVisible = false;
+ foreach (var label in list.Element.Labels)
+ {
+ if (label.Visible)
+ {
+ anyVisible = true;
+ break;
+ }
+ }
+ list.Visible = anyVisible;
+ }
+ }
+
+ if (control is ContainerControl container)
+ {
+ for (int i = 0; i < container.ChildrenCount; i++)
+ {
+ UpdatePropertiesListsVisibility(container.GetChild(i), query);
+ }
+ }
+ }
+
+ private void UpdateGroupsVisibility(Control control, string query)
+ {
+ if (control is DropPanel dropPanel)
+ {
+ dropPanel.SearchText = query;
+ for (int i = 0; i < dropPanel.ChildrenCount; i++)
+ {
+ UpdateGroupsVisibility(dropPanel.GetChild(i), query);
+ }
+
+ if (string.IsNullOrEmpty(query))
+ {
+ dropPanel.Visible = true;
+ }
+ else
+ {
+ dropPanel.Visible = HasVisibleDescendants(dropPanel);
+ }
+ }
+ else if (control is ContainerControl container)
+ {
+ for (int i = 0; i < container.ChildrenCount; i++)
+ {
+ UpdateGroupsVisibility(container.GetChild(i), query);
+ }
+ }
+ }
+
+ private bool HasVisibleDescendants(ContainerControl container)
+ {
+ for (int i = 0; i < container.ChildrenCount; i++)
+ {
+ var child = container.GetChild(i);
+ if (child is PropertyNameLabel label && label.Visible)
+ {
+ return true;
+ }
+ if (child is DropPanel dropPanel && dropPanel.Visible)
+ {
+ return true;
+ }
+ if (child is ContainerControl subContainer && subContainer.Visible && HasVisibleDescendants(subContainer))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private bool IsInMatchingGroup(Control control, string query, Dictionary groupMatchCache)
+ {
+ if (string.IsNullOrEmpty(query))
+ return false;
+
+ var p = control.Parent;
+ while (p != null)
+ {
+ if (p is DropPanel dropPanel)
+ {
+ if (!groupMatchCache.TryGetValue(dropPanel, out bool matches))
+ {
+ var headerText = dropPanel.HeaderText;
+ matches = headerText != null && headerText.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
+ groupMatchCache[dropPanel] = matches;
+ }
+ if (matches)
+ return true;
+ }
+ p = p.Parent;
+ }
+ return false;
+ }
+
+ private bool UpdateFilter(CustomEditor editor, string query, bool forceVisible, Dictionary groupMatchCache)
+ {
+ bool isVisible = false;
+
+ bool labelMatches = false;
+ if (editor.LinkedLabel != null)
+ {
+ editor.LinkedLabel.SearchText = query;
+ if (forceVisible)
+ {
+ labelMatches = true;
+ }
+ else if (!string.IsNullOrEmpty(query))
+ {
+ var labelText = editor.LinkedLabel.Text.ToString();
+ if (labelText.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ labelMatches = true;
+ }
+ else if (IsInMatchingGroup(editor.LinkedLabel, query, groupMatchCache))
+ {
+ labelMatches = true;
+ }
+ }
+ }
+
+ bool isThisEditorVisible = forceVisible || labelMatches;
+
+ bool anyChildVisible = false;
+ bool forceChildrenVisible = isThisEditorVisible;
+
+ foreach (var child in editor.ChildrenEditors)
+ {
+ if (UpdateFilter(child, query, forceChildrenVisible, groupMatchCache))
+ {
+ anyChildVisible = true;
+ }
+ }
+
+ isVisible = isThisEditorVisible || anyChildVisible;
+
+ if (editor.LinkedLabel != null)
+ {
+ SetLabelAndControlsVisible(editor.LinkedLabel, isVisible);
+ }
+
+ if (editor.Style == DisplayStyle.Group && editor.Layout != null && editor.Layout.Control != null)
+ {
+ editor.Layout.Control.Visible = isVisible;
+ }
+
+ return isVisible;
+ }
+
+ private void SetLabelAndControlsVisible(PropertyNameLabel label, bool visible)
+ {
+ label.Visible = visible;
+ var container = label.FirstChildControlContainer ?? label.Parent as PropertiesList;
+ if (container != null)
+ {
+ int startIndex = label.FirstChildControlIndex;
+ if (startIndex >= 0)
+ {
+ int endIndex = container.Children.Count;
+ var labels = container.Element.Labels;
+ int labelIndex = labels.IndexOf(label);
+ if (labelIndex >= 0 && labelIndex < labels.Count - 1)
+ {
+ for (int i = labelIndex + 1; i < labels.Count; i++)
+ {
+ var nextContainer = labels[i].FirstChildControlContainer ?? labels[i].Parent as PropertiesList;
+ if (nextContainer == container)
+ {
+ endIndex = labels[i].FirstChildControlIndex - 1;
+ break;
+ }
+ }
+ }
+
+ for (int i = startIndex; i < endIndex; i++)
+ {
+ if (i < container.Children.Count)
+ {
+ container.Children[i].Visible = visible;
+ }
+ }
+ }
+ }
+ }
+
///
public override ContainerControl ContainerControl => Panel;
}
diff --git a/Source/Editor/CustomEditors/Editors/GenericEditor.cs b/Source/Editor/CustomEditors/Editors/GenericEditor.cs
index a20b41f8c..900fc6c8d 100644
--- a/Source/Editor/CustomEditors/Editors/GenericEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/GenericEditor.cs
@@ -868,6 +868,28 @@ namespace FlaxEditor.CustomEditors.Editors
if (c.LabelIndex != -1 && c.PropertiesList != null && c.PropertiesList.Labels.Count > c.LabelIndex)
{
var label = c.PropertiesList.Labels[c.LabelIndex];
+ if (visible && Presenter != null && !string.IsNullOrEmpty(Presenter.SearchText))
+ {
+ bool match = label.Text.ToString().IndexOf(Presenter.SearchText, StringComparison.OrdinalIgnoreCase) >= 0;
+ if (!match)
+ {
+ var p = label.Parent;
+ while (p != null)
+ {
+ if (p is DropPanel dropPanel)
+ {
+ var headerText = dropPanel.HeaderText;
+ if (headerText != null && headerText.IndexOf(Presenter.SearchText, StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ match = true;
+ break;
+ }
+ }
+ p = p.Parent;
+ }
+ }
+ visible = match;
+ }
label.Visible = visible;
for (int j = label.FirstChildControlIndex; j < c.PropertiesList.Properties.Children.Count; j++)
{
@@ -908,6 +930,10 @@ namespace FlaxEditor.CustomEditors.Editors
_visibleIfCaches = null;
}
}
+ if (Presenter != null && !string.IsNullOrEmpty(Presenter.SearchText))
+ {
+ Presenter.UpdateGroupsAndListsVisibility();
+ }
base.Refresh();
}
diff --git a/Source/Editor/CustomEditors/GUI/PropertyNameLabel.cs b/Source/Editor/CustomEditors/GUI/PropertyNameLabel.cs
index c509dd1d2..3108d0a7e 100644
--- a/Source/Editor/CustomEditors/GUI/PropertyNameLabel.cs
+++ b/Source/Editor/CustomEditors/GUI/PropertyNameLabel.cs
@@ -1,6 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
+using FlaxEditor.Utilities;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -45,6 +46,34 @@ namespace FlaxEditor.CustomEditors.GUI
///
public Color HighlightStripColor;
+ ///
+ /// The active search text query used to highlight matching parts of the label.
+ ///
+ public string SearchText = string.Empty;
+
+ private string _lastSearchText;
+ private string _lastText;
+ private QueryFilterHelper.Range[] _highlightRanges;
+
+ private void UpdateHighlights()
+ {
+ var text = Text?.ToString() ?? string.Empty;
+ if (_lastSearchText == SearchText && _lastText == text)
+ return;
+
+ _lastSearchText = SearchText;
+ _lastText = text;
+
+ if (!string.IsNullOrEmpty(SearchText))
+ {
+ QueryFilterHelper.Match(SearchText, text, out _highlightRanges);
+ }
+ else
+ {
+ _highlightRanges = null;
+ }
+ }
+
///
/// Occurs when label creates the context menu popup for th property. Can be used to add some custom logic per property editor.
///
@@ -83,6 +112,31 @@ namespace FlaxEditor.CustomEditors.GUI
{
Render2D.FillRectangle(new Rectangle(0, 0, 2, Height), HighlightStripColor);
}
+
+ UpdateHighlights();
+
+ if (_highlightRanges != null && _highlightRanges.Length > 0)
+ {
+ var text = Text.ToString();
+ var font = Font?.GetFont();
+ if (font != null)
+ {
+ var style = Style.Current;
+ var color = style.ProgressNormal * 0.6f;
+ var margin = Margin;
+ var textSize = font.MeasureText(text);
+ var textX = margin.Left;
+ var textY = margin.Top + (Height - margin.Height - textSize.Y) * 0.5f;
+
+ for (int i = 0; i < _highlightRanges.Length; i++)
+ {
+ var start = font.GetCharPosition(text, _highlightRanges[i].StartIndex);
+ var end = font.GetCharPosition(text, _highlightRanges[i].EndIndex);
+ var highlightRect = new Rectangle(start.X + textX, textY, end.X - start.X, textSize.Y);
+ Render2D.FillRectangle(highlightRect, color);
+ }
+ }
+ }
}
///
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs
index 1d6d827ab..06e007a17 100644
--- a/Source/Editor/Windows/Assets/PrefabWindow.cs
+++ b/Source/Editor/Windows/Assets/PrefabWindow.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Xml;
using FlaxEditor.Content;
using FlaxEditor.CustomEditors;
+using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Input;
@@ -29,6 +30,8 @@ namespace FlaxEditor.Windows.Assets
private readonly PrefabTree _tree;
private readonly PrefabWindowViewport _viewport;
private readonly CustomEditorPresenter _propertiesEditor;
+ private SearchBox _propertiesSearchBox;
+ private Panel _propertiesScrollingPanel;
private readonly ToolStripButton _saveButton;
private readonly ToolStripButton _toolStripUndo;
@@ -142,7 +145,7 @@ namespace FlaxEditor.Windows.Assets
};
// Split Panel 2
- _split2 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.Vertical)
+ _split2 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
@@ -197,9 +200,33 @@ namespace FlaxEditor.Windows.Assets
_viewport.TransformGizmo.ModeChanged += UpdateToolstrip;
// Prefab properties editor
+ var propHeaderPanel = new ContainerControl
+ {
+ AnchorPreset = AnchorPresets.HorizontalStretchTop,
+ BackgroundColor = Style.Current.Background,
+ IsScrollable = false,
+ Offsets = new Margin(0, 0, 0, 18 + 6),
+ Parent = _split2.Panel2,
+ };
+ _propertiesSearchBox = new SearchBox
+ {
+ AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
+ Parent = propHeaderPanel,
+ Bounds = new Rectangle(4, 4, propHeaderPanel.Width - 8, 18),
+ };
+ _propertiesSearchBox.TextChanged += ApplyPropertiesSearchFilter;
+
+ _propertiesScrollingPanel = new Panel(ScrollBars.Vertical)
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Offsets = new Margin(0, 0, propHeaderPanel.Bottom, 0),
+ Parent = _split2.Panel2,
+ };
+
_propertiesEditor = new CustomEditorPresenter(_undo, null, this);
- _propertiesEditor.Panel.Parent = _split2.Panel2;
+ _propertiesEditor.Panel.Parent = _propertiesScrollingPanel;
_propertiesEditor.Modified += MarkAsEdited;
+ _propertiesEditor.AfterLayout += OnPresenterAfterLayout;
// Toolstrip
_saveButton = _toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save", ref inputOptions.Save);
@@ -580,6 +607,16 @@ namespace FlaxEditor.Windows.Assets
///
public EditorViewport PresenterViewport => _viewport;
+ private void OnPresenterAfterLayout(LayoutElementsContainer layout)
+ {
+ ApplyPropertiesSearchFilter();
+ }
+
+ private void ApplyPropertiesSearchFilter()
+ {
+ _propertiesEditor.ApplySearchFilter(_propertiesSearchBox.Text);
+ }
+
///
EditorViewport ISceneEditingContext.Viewport => Viewport;
}
diff --git a/Source/Editor/Windows/PropertiesWindow.cs b/Source/Editor/Windows/PropertiesWindow.cs
index 03316a221..a62f7508b 100644
--- a/Source/Editor/Windows/PropertiesWindow.cs
+++ b/Source/Editor/Windows/PropertiesWindow.cs
@@ -5,6 +5,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Xml;
using FlaxEditor.CustomEditors;
+using FlaxEditor.CustomEditors.Elements;
+using FlaxEditor.CustomEditors.GUI;
+using FlaxEditor.GUI.Input;
using FlaxEditor.SceneGraph;
using FlaxEditor.Viewport;
using FlaxEngine;
@@ -23,6 +26,8 @@ namespace FlaxEditor.Windows
private readonly Dictionary _actorScrollValues = new Dictionary();
private bool _lockObjects = false;
+ private SearchBox _searchBox;
+ private Panel _scrollingPanel;
///
public override bool UseLayoutData => true;
@@ -66,18 +71,42 @@ namespace FlaxEditor.Windows
///
/// The editor.
public PropertiesWindow(Editor editor)
- : base(editor, true, ScrollBars.Vertical)
+ : base(editor, true, ScrollBars.None)
{
Title = "Properties";
Icon = editor.Icons.Build64;
AutoFocus = true;
+ var headerPanel = new ContainerControl
+ {
+ AnchorPreset = AnchorPresets.HorizontalStretchTop,
+ BackgroundColor = Style.Current.Background,
+ IsScrollable = false,
+ Offsets = new Margin(0, 0, 0, 18 + 6),
+ Parent = this,
+ };
+ _searchBox = new SearchBox
+ {
+ AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
+ Parent = headerPanel,
+ Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18),
+ };
+ _searchBox.TextChanged += ApplySearchFilter;
+
+ _scrollingPanel = new Panel(ScrollBars.Vertical)
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Offsets = new Margin(0, 0, headerPanel.Bottom, 0),
+ Parent = this,
+ };
+
Presenter = new CustomEditorPresenter(editor.Undo, null, this);
- Presenter.Panel.Parent = this;
+ Presenter.Panel.Parent = _scrollingPanel;
Presenter.GetUndoObjects += GetUndoObjects;
Presenter.Features |= FeatureFlags.CacheExpandedGroups;
+ Presenter.AfterLayout += OnPresenterAfterLayout;
- VScrollBar.ValueChanged += OnScrollValueChanged;
+ _scrollingPanel.VScrollBar.ValueChanged += OnScrollValueChanged;
Editor.SceneEditing.SelectionChanged += OnSelectionChanged;
}
@@ -115,7 +144,8 @@ namespace FlaxEditor.Windows
}
}
- _actorScrollValues[Editor.SceneEditing.Selection[0].ID] = VScrollBar.TargetValue;
+ if (_scrollingPanel.VScrollBar != null)
+ _actorScrollValues[Editor.SceneEditing.Selection[0].ID] = _scrollingPanel.VScrollBar.TargetValue;
}
private IEnumerable