// 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 { /// /// The custom context menu that shows a list of items and supports searching by name and query results highlighting. /// /// public class ItemsListContextMenu : ContextMenuBase { /// /// The single list item control. /// /// public class Item : Control { /// /// The is mouse down flag. /// protected bool _isMouseDown; /// /// The search query highlights. /// protected List _highlights; /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Occurs when items gets clicked by the user. /// public event Action Clicked; /// /// Initializes a new instance of the class. /// public Item() : base(0, 0, 120, 12) { } /// /// Initializes a new instance of the class. /// /// The item name. /// The item tag object. public Item(string name, object tag = null) : base(0, 0, 120, 12) { Name = name; Tag = tag; } /// /// Updates the filter. /// /// The filter text. 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(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; } } } /// /// Gets the text rectangle. /// /// The output rectangle. protected virtual void GetTextRect(out Rectangle rect) { rect = new Rectangle(2, 0, Width - 4, Height); } /// 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); } /// public override bool OnMouseDown(Vector2 location, MouseButton button) { if (button == MouseButton.Left) { _isMouseDown = true; } return base.OnMouseDown(location, button); } /// public override bool OnMouseUp(Vector2 location, MouseButton button) { if (button == MouseButton.Left && _isMouseDown) { _isMouseDown = false; Clicked?.Invoke(this); } return base.OnMouseUp(location, button); } /// public override void OnMouseLeave() { _isMouseDown = false; base.OnMouseLeave(); } /// 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; /// /// Event fired when any item in this popup menu gets clicked. /// public event Action ItemClicked; /// /// The panel control where you should add your items. /// public readonly VerticalPanel ItemsPanel; /// /// Initializes a new instance of the class. /// /// The control width. /// The control height. 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(); } /// /// Adds the item to the view and registers for the click event. /// /// The item. public void AddItem(Item item) { item.Parent = ItemsPanel; item.Clicked += OnClickItem; } /// /// Called when user clicks on an item. /// /// The item. public void OnClickItem(Item item) { Hide(); ItemClicked?.Invoke(item); } /// /// Resets the view. /// 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); } /// protected override void OnShow() { // Prepare ResetView(); Focus(); _waitingForInput = true; base.OnShow(); } /// public override void Hide() { Focus(null); base.Hide(); } /// 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); } } }