Files
FlaxEngine/Source/Editor/GUI/ItemsListContextMenu.cs
2021-01-02 14:28:49 +01:00

329 lines
10 KiB
C#

// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Utilities;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.GUI
{
/// <summary>
/// The custom context menu that shows a list of items and supports searching by name and query results highlighting.
/// </summary>
/// <seealso cref="ContextMenuBase" />
public class ItemsListContextMenu : ContextMenuBase
{
/// <summary>
/// The single list item control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
public class Item : Control
{
/// <summary>
/// The is mouse down flag.
/// </summary>
protected bool _isMouseDown;
/// <summary>
/// The search query highlights.
/// </summary>
protected List<Rectangle> _highlights;
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Occurs when items gets clicked by the user.
/// </summary>
public event Action<Item> Clicked;
/// <summary>
/// Initializes a new instance of the <see cref="Item"/> class.
/// </summary>
public Item()
: base(0, 0, 120, 12)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Item"/> class.
/// </summary>
/// <param name="name">The item name.</param>
/// <param name="tag">The item tag object.</param>
public Item(string name, object tag = null)
: base(0, 0, 120, 12)
{
Name = name;
Tag = tag;
}
/// <summary>
/// Updates the filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
public void UpdateFilter(string filterText)
{
if (string.IsNullOrWhiteSpace(filterText))
{
// Clear filter
_highlights?.Clear();
Visible = true;
}
else
{
if (QueryFilterHelper.Match(filterText, Name, out var ranges))
{
// Update highlights
if (_highlights == null)
_highlights = new List<Rectangle>(ranges.Length);
else
_highlights.Clear();
var style = Style.Current;
var font = style.FontSmall;
for (int i = 0; i < ranges.Length; i++)
{
var start = font.GetCharPosition(Name, ranges[i].StartIndex);
var end = font.GetCharPosition(Name, ranges[i].EndIndex);
_highlights.Add(new Rectangle(start.X + 2, 0, end.X - start.X, Height));
}
Visible = true;
}
else
{
// Hide
_highlights?.Clear();
Visible = false;
}
}
}
/// <summary>
/// Gets the text rectangle.
/// </summary>
/// <param name="rect">The output rectangle.</param>
protected virtual void GetTextRect(out Rectangle rect)
{
rect = new Rectangle(2, 0, Width - 4, Height);
}
/// <inheritdoc />
public override void Draw()
{
var style = Style.Current;
GetTextRect(out var textRect);
base.Draw();
// Overlay
if (IsMouseOver)
Render2D.FillRectangle(new Rectangle(Vector2.Zero, Size), style.BackgroundHighlighted);
// Draw all highlights
if (_highlights != null)
{
var color = style.ProgressNormal * 0.6f;
for (int i = 0; i < _highlights.Count; i++)
{
var rect = _highlights[i];
rect.Location += textRect.Location;
rect.Height = textRect.Height;
Render2D.FillRectangle(rect, color);
}
}
// Draw name
Render2D.DrawText(style.FontSmall, Name, textRect, Enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center);
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_isMouseDown = true;
}
return base.OnMouseDown(location, button);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left && _isMouseDown)
{
_isMouseDown = false;
Clicked?.Invoke(this);
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
_isMouseDown = false;
base.OnMouseLeave();
}
/// <inheritdoc />
public override int Compare(Control other)
{
if (other is Item otherItem)
return string.Compare(Name, otherItem.Name, StringComparison.Ordinal);
return base.Compare(other);
}
}
private readonly TextBox _searchBox;
private bool _waitingForInput;
/// <summary>
/// Event fired when any item in this popup menu gets clicked.
/// </summary>
public event Action<Item> ItemClicked;
/// <summary>
/// The panel control where you should add your items.
/// </summary>
public readonly VerticalPanel ItemsPanel;
/// <summary>
/// Initializes a new instance of the <see cref="ItemsListContextMenu"/> class.
/// </summary>
/// <param name="width">The control width.</param>
/// <param name="height">The control height.</param>
public ItemsListContextMenu(float width = 320, float height = 220)
{
// Context menu dimensions
Size = new Vector2(width, height);
// Search box
_searchBox = new TextBox(false, 1, 1)
{
Parent = this,
Width = Width - 3,
WatermarkText = "Search...",
};
_searchBox.TextChanged += OnSearchFilterChanged;
// Panel with scrollbar
var scrollPanel = new Panel(ScrollBars.Vertical)
{
Parent = this,
AnchorPreset = AnchorPresets.StretchAll,
Bounds = new Rectangle(0, _searchBox.Bottom + 1, Width, Height - _searchBox.Bottom - 2),
};
// Items list panel
ItemsPanel = new VerticalPanel
{
Parent = scrollPanel,
AnchorPreset = AnchorPresets.HorizontalStretchTop,
IsScrollable = true,
};
}
private void OnSearchFilterChanged()
{
if (IsLayoutLocked)
return;
LockChildrenRecursive();
var items = ItemsPanel.Children;
for (int i = 0; i < items.Count; i++)
{
if (items[i] is Item item)
item.UpdateFilter(_searchBox.Text);
}
UnlockChildrenRecursive();
PerformLayout(true);
_searchBox.Focus();
}
/// <summary>
/// Adds the item to the view and registers for the click event.
/// </summary>
/// <param name="item">The item.</param>
public void AddItem(Item item)
{
item.Parent = ItemsPanel;
item.Clicked += OnClickItem;
}
/// <summary>
/// Called when user clicks on an item.
/// </summary>
/// <param name="item">The item.</param>
public void OnClickItem(Item item)
{
Hide();
ItemClicked?.Invoke(item);
}
/// <summary>
/// Resets the view.
/// </summary>
public void ResetView()
{
LockChildrenRecursive();
var items = ItemsPanel.Children;
for (int i = 0; i < items.Count; i++)
{
if (items[i] is Item item)
item.UpdateFilter(null);
}
_searchBox.Clear();
UnlockChildrenRecursive();
PerformLayout(true);
}
/// <inheritdoc />
protected override void OnShow()
{
// Prepare
ResetView();
Focus();
_waitingForInput = true;
base.OnShow();
}
/// <inheritdoc />
public override void Hide()
{
Focus(null);
base.Hide();
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (key == KeyboardKeys.Escape)
{
Hide();
return true;
}
if (_waitingForInput)
{
_waitingForInput = false;
_searchBox.Focus();
return _searchBox.OnKeyDown(key);
}
return base.OnKeyDown(key);
}
}
}