Add initial properties search to properties wndow and prefab properties panel.

This commit is contained in:
2026-07-03 15:54:03 -05:00
parent 9952d6cbf8
commit 6ba406953b
6 changed files with 463 additions and 11 deletions
@@ -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
/// </summary>
protected readonly RootEditor Editor;
/// <summary>
/// The selected objects list (read-only).
/// </summary>
public readonly ValueContainer Selection = new ValueContainer(ScriptMemberInfo.Null);
/// <summary>
/// The current properties search query.
/// </summary>
public string SearchText = string.Empty;
/// <summary>
/// The undo object used by this editor.
/// </summary>
@@ -529,6 +532,256 @@ namespace FlaxEditor.CustomEditors
Editor?.RefreshInternal();
}
/// <summary>
/// Applies search filter query to the presenter layout controls.
/// </summary>
public void ApplySearchFilter(string query)
{
SearchText = query;
if (Root == null)
return;
var isQueryEmpty = string.IsNullOrEmpty(query);
var groupMatchCache = new Dictionary<DropPanel, bool>();
UpdateFilter(Root, query, isQueryEmpty, groupMatchCache);
UpdatePropertiesListsVisibility(Panel, query);
UpdateGroupsVisibility(Panel, query);
Panel.PerformLayout();
}
/// <summary>
/// Updates the visibility of properties lists and drop panels based on the current search query.
/// </summary>
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<DropPanel, bool> 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<DropPanel, bool> 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;
}
}
}
}
}
/// <inheritdoc />
public override ContainerControl ContainerControl => Panel;
}
@@ -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();
}
@@ -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
/// </summary>
public Color HighlightStripColor;
/// <summary>
/// The active search text query used to highlight matching parts of the label.
/// </summary>
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;
}
}
/// <summary>
/// Occurs when label creates the context menu popup for th property. Can be used to add some custom logic per property editor.
/// </summary>
@@ -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);
}
}
}
}
/// <inheritdoc />
+39 -2
View File
@@ -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
/// <inheritdoc />
public EditorViewport PresenterViewport => _viewport;
private void OnPresenterAfterLayout(LayoutElementsContainer layout)
{
ApplyPropertiesSearchFilter();
}
private void ApplyPropertiesSearchFilter()
{
_propertiesEditor.ApplySearchFilter(_propertiesSearchBox.Text);
}
/// <inheritdoc />
EditorViewport ISceneEditingContext.Viewport => Viewport;
}
+46 -6
View File
@@ -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<Guid, float> _actorScrollValues = new Dictionary<Guid, float>();
private bool _lockObjects = false;
private SearchBox _searchBox;
private Panel _scrollingPanel;
/// <inheritdoc />
public override bool UseLayoutData => true;
@@ -66,18 +71,42 @@ namespace FlaxEditor.Windows
/// </summary>
/// <param name="editor">The editor.</param>
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<object> GetUndoObjects(CustomEditorPresenter customEditorPresenter)
@@ -135,8 +165,18 @@ namespace FlaxEditor.Windows
Presenter.Select(objects);
// Set scroll value of window if it exists
if (Editor.SceneEditing.SelectionCount == 1)
VScrollBar.TargetValue = _actorScrollValues.GetValueOrDefault(Editor.SceneEditing.Selection[0].ID, 0);
if (Editor.SceneEditing.SelectionCount == 1 && _scrollingPanel.VScrollBar != null)
_scrollingPanel.VScrollBar.TargetValue = _actorScrollValues.GetValueOrDefault(Editor.SceneEditing.Selection[0].ID, 0);
}
private void OnPresenterAfterLayout(LayoutElementsContainer layout)
{
ApplySearchFilter();
}
private void ApplySearchFilter()
{
Presenter.ApplySearchFilter(_searchBox.Text);
}
/// <inheritdoc />
+42
View File
@@ -72,6 +72,33 @@ namespace FlaxEngine.GUI
[EditorOrder(10), Tooltip("The text to show on a panel header.")]
public string HeaderText { get; set; }
/// <summary>
/// The active search text query used to highlight matching parts of the header text.
/// </summary>
public string SearchText = string.Empty;
private string _lastSearchText;
private string _lastHeaderText;
private int _highlightIndex = -1;
private void UpdateHighlights()
{
if (_lastSearchText == SearchText && _lastHeaderText == HeaderText)
return;
_lastSearchText = SearchText;
_lastHeaderText = HeaderText;
if (!string.IsNullOrEmpty(SearchText) && !string.IsNullOrEmpty(HeaderText))
{
_highlightIndex = HeaderText.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase);
}
else
{
_highlightIndex = -1;
}
}
/// <summary>
/// Gets or sets the height of the header.
/// </summary>
@@ -411,6 +438,21 @@ namespace FlaxEngine.GUI
Render2D.PushClip(textRect);
Render2D.DrawText(HeaderTextFont.GetFont(), HeaderTextMaterial, HeaderText, textRect, textColor, TextAlignment.Near, TextAlignment.Center);
UpdateHighlights();
if (_highlightIndex >= 0)
{
var font = HeaderTextFont.GetFont();
if (font != null)
{
var highlightColor = Style.Current.ProgressNormal * 0.6f;
var textSize = font.MeasureText(HeaderText);
var textY = (HeaderHeight - textSize.Y) * 0.5f;
var start = font.GetCharPosition(HeaderText, _highlightIndex);
var end = font.GetCharPosition(HeaderText, _highlightIndex + SearchText.Length);
var highlightRect = new Rectangle(start.X + textRect.X, textY, end.X - start.X, textSize.Y);
Render2D.FillRectangle(highlightRect, highlightColor);
}
}
Render2D.PopClip();
if (!_isClosed && EnableContainmentLines)