// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.Options; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.ContextMenu { /// /// Popup menu control. /// /// [HideInEditor] public class ContextMenu : ContextMenuBase { /// /// The items container. /// /// [HideInEditor] protected class ItemsPanel : Panel { private readonly ContextMenu _menu; /// /// Initializes a new instance of the class. /// /// The menu. public ItemsPanel(ContextMenu menu) : base(ScrollBars.Vertical) { _menu = menu; } /// protected override void Arrange() { base.Arrange(); // Arrange controls Margin margin = _menu._itemsMargin; float y = 0; float width = Width - margin.Width; for (int i = 0; i < _children.Count; i++) { if (_children[i] is ContextMenuItem item && item.Visible) { var height = item.Height; item.Bounds = new Rectangle(margin.Left, y, width, height); y += height + margin.Height; } } } } /// /// The items area margin. /// protected Margin _itemsAreaMargin = new Margin(0, 0, 3, 3); /// /// The items margin. /// protected Margin _itemsMargin = new Margin(16, 0, 2, 0); /// /// The items panel. /// protected ItemsPanel _panel; /// /// Gets or sets the items area margin (items container area margin). /// public Margin ItemsAreaMargin { get => _itemsAreaMargin; set { _itemsAreaMargin = value; PerformLayout(); } } /// /// Gets or sets the items margin. /// public Margin ItemsMargin { get => _itemsMargin; set { _itemsMargin = value; PerformLayout(); } } /// /// Gets or sets the minimum popup width. /// public float MinimumWidth { get; set; } /// /// Gets or sets the maximum amount of items in the view. If popup has more items to show it uses a additional scroll panel. /// public int MaximumItemsInViewCount { get; set; } /// /// Gets the items (readonly). /// public IEnumerable Items => _panel.Children.OfType(); /// /// Event fired when user clicks on the button. /// public event Action ButtonClicked; /// /// Gets the context menu items container control. /// public Panel ItemsContainer => _panel; /// /// The auto sort. /// private bool _autosort; /// /// The auto sort property. /// public bool AutoSort { get => _autosort; set { _autosort = value; if (_autosort) SortButtons(); } } /// /// Initializes a new instance of the class. /// public ContextMenu() { MinimumWidth = 10; MaximumItemsInViewCount = 20; _panel = new ItemsPanel(this) { ClipChildren = false, Parent = this, }; } /// /// Sorts all alphabetically. /// /// Overrides property. public void SortButtons(bool force = false) { if (!_autosort && !force) return; _panel.Children.Sort((control, control1) => { if (control is ContextMenuButton cmb && control1 is ContextMenuButton cmb1) return string.Compare(cmb.Text, cmb1.Text, StringComparison.OrdinalIgnoreCase); if (!(control is ContextMenuButton)) return 1; return -1; }); } /// /// Removes all the added items (buttons, separators, etc.). /// public void DisposeAllItems() { for (int i = _panel.ChildrenCount - 1; _panel.ChildrenCount > 0 && i >= 0; i--) { if (_panel.Children[i] is ContextMenuItem) _panel.Children[i].Dispose(); } } /// /// Adds the button. /// /// The text. /// Created context menu item control. public ContextMenuButton AddButton(string text) { var item = new ContextMenuButton(this, text) { Parent = _panel }; SortButtons(); return item; } /// /// Adds the button. /// /// The text. /// The short keys. /// Created context menu item control. public ContextMenuButton AddButton(string text, string shortKeys) { var item = new ContextMenuButton(this, text, shortKeys) { Parent = _panel }; SortButtons(); return item; } /// /// Adds the button. /// /// The text. /// On button clicked event. /// Created context menu item control. public ContextMenuButton AddButton(string text, Action clicked) { var item = new ContextMenuButton(this, text) { Parent = _panel }; item.Clicked += clicked; SortButtons(); return item; } /// /// Adds the button. /// /// The text. /// On button clicked event. /// Created context menu item control. public ContextMenuButton AddButton(string text, Action clicked) { var item = new ContextMenuButton(this, text) { Parent = _panel }; item.ButtonClicked += clicked; SortButtons(); return item; } /// /// Adds the button. /// /// The text. /// The shortKeys. /// On button clicked event. /// Created context menu item control. public ContextMenuButton AddButton(string text, string shortKeys, Action clicked) { var item = new ContextMenuButton(this, text, shortKeys) { Parent = _panel }; item.Clicked += clicked; SortButtons(); return item; } /// /// Adds the button. /// /// The text. /// The input binding. /// On button clicked event. /// Created context menu item control. public ContextMenuButton AddButton(string text, InputBinding binding, Action clicked) { var item = new ContextMenuButton(this, text, binding.ToString()) { Parent = _panel }; item.Clicked += clicked; SortButtons(); return item; } /// /// Gets the child menu (with that name). /// /// The text. /// Created context menu item control or null if missing. public ContextMenuChildMenu GetChildMenu(string text) { for (int i = 0; i < _panel.ChildrenCount; i++) { if (_panel.Children[i] is ContextMenuChildMenu menu && menu.Text == text) return menu; } return null; } /// /// Adds the child menu or gets it if already created (with that name). /// /// The text. /// Created context menu item control. public ContextMenuChildMenu GetOrAddChildMenu(string text) { var item = GetChildMenu(text); if (item == null) { item = new ContextMenuChildMenu(this, text) { Parent = _panel }; } return item; } /// /// Adds the child menu. /// /// The text. /// Created context menu item control. public ContextMenuChildMenu AddChildMenu(string text) { var item = new ContextMenuChildMenu(this, text) { Parent = _panel }; return item; } /// /// Adds the separator. /// public void AddSeparator() { var item = new ContextMenuSeparator(this) { Parent = _panel }; } /// /// Called when button get clicked. /// /// The button. public virtual void OnButtonClicked(ContextMenuButton button) { ButtonClicked?.Invoke(button); } /// public override void Show(Control parent, Float2 location, ContextMenuDirection? direction = null) { // Remove last separator to make context menu look better int lastIndex = _panel.Children.Count - 1; if (lastIndex >= 0 && _panel.Children[lastIndex] is ContextMenuSeparator separator) separator.Dispose(); base.Show(parent, location, direction); } /// public override bool ContainsPoint(ref Float2 location, bool precise) { if (base.ContainsPoint(ref location, precise)) return true; var cLocation = location - Location; for (int i = 0; i < _panel.Children.Count; i++) { if (_panel.Children[i].ContainsPoint(ref cLocation, precise)) return true; } return false; } /// protected override void PerformLayoutAfterChildren() { var prevSize = Size; // Calculate size of the context menu (items only) float maxWidth = 0; float height = _itemsAreaMargin.Height; int itemsLeft = MaximumItemsInViewCount; int overflowItemCount = 0; int itemsCount = 0; for (int i = 0; i < _panel.Children.Count; i++) { if (_panel.Children[i] is ContextMenuItem item && item.Visible) { itemsCount++; if (itemsLeft > 0) { height += item.Height + _itemsMargin.Height; itemsLeft--; } else { overflowItemCount++; } maxWidth = Mathf.Max(maxWidth, item.MinimumWidth); } } if (itemsCount != 0) height -= _itemsMargin.Height; // Remove item margin from top and bottom maxWidth = Mathf.Max(maxWidth + 20, MinimumWidth); // Move child arrows to accommodate scroll bar showing if (overflowItemCount > 0) { foreach (var child in _panel.Children) { if (child is ContextMenuButton item && item.Visible) { item.ExtraAdjustmentAmount = -_panel.VScrollBar.Width; } } } // Resize container Size = new Float2(Mathf.Ceil(maxWidth), Mathf.Ceil(height)); // Arrange items view panel var panelBounds = new Rectangle(Float2.Zero, Size); _itemsAreaMargin.ShrinkRectangle(ref panelBounds); _panel.Bounds = panelBounds; // Check if is visible size get changed if (Visible && prevSize != Size) { // Update window dimensions UpdateWindowSize(); } } /// public override bool OnCharInput(char c) { if (base.OnCharInput(c)) return true; // Find the item that starts with that character if (char.IsLetterOrDigit(c) && _panel.VScrollBar != null && _panel.VScrollBar.Visible) { int startIndex = 0; for (int i = 0; i < _panel.Children.Count; i++) { if (_panel.Children[i] is ContextMenuButton item && item.Visible && item.IsFocused) { // Start searching from the last hit item startIndex = i + 1; break; } } for (int i = startIndex; i < _panel.Children.Count; i++) { if (_panel.Children[i] is ContextMenuButton item && item.Visible) { bool startsWith = false; for (int j = 0; j < item.Text.Length; j++) { var k = item.Text[j]; if (char.ToLower(k) == char.ToLower(c)) { startsWith = true; break; } if (!char.IsWhiteSpace(k) && k != '>') break; } if (startsWith) { // Focus found item item.Focus(); _panel.ScrollViewTo(item); return true; } } } if (startIndex > 0 && startIndex <= _panel.Children.Count) { // No more items found so start from the top if there are matching items _panel.Children[startIndex - 1].Defocus(); return OnCharInput(c); } } return false; } /// public override bool OnKeyDown(KeyboardKeys key) { if (base.OnKeyDown(key)) return true; switch (key) { case KeyboardKeys.ArrowDown: for (int i = 0; i < _panel.Children.Count; i++) { if (_panel.Children[i] is ContextMenuButton item && item.Visible && item.Enabled) { item.Focus(); _panel.ScrollViewTo(item); return true; } } break; case KeyboardKeys.ArrowUp: for (int i = _panel.Children.Count - 1; i >= 0; i--) { if (_panel.Children[i] is ContextMenuButton item && item.Visible && item.Enabled) { item.Focus(); _panel.ScrollViewTo(item); return true; } } break; } return false; } } }