// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.GUI.ContextMenu; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI { /// /// Combo box control allows to choose one item or set of items from the provided collection of options. /// /// /// Difference between and is that ComboBox uses native window to show the items list while Dropdown uses a custom panel added to parent window. /// This means that Dropdown will work on all platforms that don't support multiple native windows (eg. Android, PS4, Xbox One). /// /// [HideInEditor] public class ComboBox : Control { /// /// The default height of the control. /// public const float DefaultHeight = 18.0f; /// /// The items. /// protected List _items = new List(); /// /// The item tooltips (optional). /// protected string[] _tooltips; /// /// The popup menu. May be null if has not been used yet. /// protected ContextMenu.ContextMenu _popupMenu; /// /// The mouse down flag. /// protected bool _mouseDown; /// /// The block popup flag. /// protected bool _blockPopup; /// /// The selected indices. /// protected List _selectedIndices = new List(4); /// /// Gets or sets the items collection. /// [EditorOrder(1), Tooltip("The items collection.")] public List Items { get => _items; set => _items = value; } /// /// Gets or sets the items tooltips (optional). /// [NoSerialize, HideInEditor] public string[] Tooltips { get => _tooltips; set => _tooltips = value; } /// /// True if sort items before showing the list, otherwise present them in the unchanged order. /// [EditorOrder(40), Tooltip("If checked, items will be sorted before showing the list, otherwise present them in the unchanged order.")] public bool Sorted { get; set; } /// /// Gets or sets a value indicating whether support multi items selection. /// [EditorOrder(41), Tooltip("If checked, combobox will support multi items selection. Otherwise it will be single item picking.")] public bool SupportMultiSelect { 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. /// [EditorOrder(42), Limit(1, 1000), Tooltip("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 or sets the selected item (returns if no item is being selected or more than one item is selected). /// [HideInEditor, NoSerialize] public string SelectedItem { get => _selectedIndices.Count == 1 ? _items[_selectedIndices[0]] : string.Empty; set => SelectedIndex = _items.IndexOf(value); } /// /// Gets a value indicating whether this combobox has any item selected. /// public bool HasSelection => _selectedIndices.Count != 0; /// /// Gets or sets the index of the selected. If combobox has more than 1 item selected then it returns invalid index (value -1). /// [EditorOrder(2), Tooltip("The index of the selected item from the list.")] public int SelectedIndex { get => _selectedIndices.Count == 1 ? _selectedIndices[0] : -1; set { // Clamp index value = Mathf.Min(value, _items.Count - 1); // Check if index will change if (value != SelectedIndex) { // Select _selectedIndices.Clear(); if (value != -1) _selectedIndices.Add(value); OnSelectedIndexChanged(); } } } /// /// Gets or sets the selection. /// [NoSerialize, HideInEditor] public List Selection { get => _selectedIndices; set { if (value == null) throw new ArgumentNullException(); if (!SupportMultiSelect && value.Count > 1) throw new InvalidOperationException(); for (int i = 0; i < value.Count; i++) { var index = value[i]; if (index < 0 || index >= _items.Count) throw new ArgumentOutOfRangeException(); } if (!_selectedIndices.SequenceEqual(value)) { // Select _selectedIndices.Clear(); _selectedIndices.AddRange(value); OnSelectedIndexChanged(); } } } /// /// Event fired when selected index gets changed. /// public event Action SelectedIndexChanged; /// /// Occurs when popup is showing (before event). Can be used to update items collection before showing it to the user. /// public event Action PopupShowing; /// /// Custom popup creation function. /// public event Func PopupCreate; /// /// Gets the popup menu (it may be null if not used - lazy init). /// public ContextMenu.ContextMenu Popup => _popupMenu; /// /// Gets a value indicating whether this popup menu is opened. /// public bool IsPopupOpened => _popupMenu != null && _popupMenu.IsOpened; /// /// Gets or sets the font used to draw text. /// [EditorDisplay("Style"), EditorOrder(2000)] public FontReference Font { get; set; } /// /// Gets or sets the color of the text. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color TextColor { get; set; } /// /// Gets or sets the color of the border. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BorderColor { get; set; } /// /// Gets or sets the background color when combobox popup is opened. /// [EditorDisplay("Style"), EditorOrder(2010)] public Color BackgroundColorSelected { get; set; } /// /// Gets or sets the border color when combobox popup is opened. /// [EditorDisplay("Style"), EditorOrder(2020)] public Color BorderColorSelected { get; set; } /// /// Gets or sets the background color when combobox is highlighted. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BackgroundColorHighlighted { get; set; } /// /// Gets or sets the border color when combobox is highlighted. /// [EditorDisplay("Style"), EditorOrder(2000)] public Color BorderColorHighlighted { get; set; } /// /// Gets or sets the image used to render combobox drop arrow icon. /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render combobox drop arrow icon.")] public IBrush ArrowImage { get; set; } /// /// Gets or sets the color used to render combobox drop arrow icon. /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render combobox drop arrow icon.")] public Color ArrowColor { get; set; } /// /// Gets or sets the color used to render combobox drop arrow icon (menu is opened). /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render combobox drop arrow icon (menu is opened).")] public Color ArrowColorSelected { get; set; } /// /// Gets or sets the color used to render combobox drop arrow icon (menu is highlighted). /// [EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render combobox drop arrow icon (menu is highlighted).")] public Color ArrowColorHighlighted { get; set; } /// /// Initializes a new instance of the class. /// public ComboBox() : this(0, 0) { } /// /// Initializes a new instance of the class. /// /// The x. /// The y. /// The width. public ComboBox(float x, float y, float width = 120.0f) : base(x, y, width, DefaultHeight) { MaximumItemsInViewCount = 20; var style = Style.Current; Font = new FontReference(style.FontMedium); TextColor = style.Foreground; BackgroundColor = style.BackgroundNormal; BackgroundColorHighlighted = BackgroundColor; BackgroundColorSelected = BackgroundColor; BorderColor = style.BorderNormal; BorderColorHighlighted = style.BorderSelected; BorderColorSelected = BorderColorHighlighted; ArrowImage = new SpriteBrush(style.ArrowDown); ArrowColor = style.Foreground * 0.6f; ArrowColorSelected = style.BackgroundSelected; ArrowColorHighlighted = style.Foreground; } /// /// Clears the items. /// public void ClearItems() { SelectedIndex = -1; _items.Clear(); } /// /// Adds the item. /// /// The item. public void AddItem(string item) { _items.Add(item); } /// /// Adds the items. /// /// The items. public void AddItems(IEnumerable items) { _items.AddRange(items); } /// /// Sets the items. /// /// The items. public void SetItems(IEnumerable items) { SelectedIndex = -1; _items.Clear(); _items.AddRange(items); } /// /// Determines whether the specified item is selected. /// /// The item to check. /// true if the item is selected; otherwise, false. public bool IsSelected(string item) { return IsSelected(_items.IndexOf(item)); } /// /// Determines whether the item at the specified index is selected. /// /// The index. /// true if the item is selected; otherwise, false. public bool IsSelected(int index) { return index != -1 && _selectedIndices.Contains(index); } /// /// Called when selected item index gets changed. /// protected virtual void OnSelectedIndexChanged() { if (_tooltips != null && _tooltips.Length == _items.Count) TooltipText = _selectedIndices.Count == 1 ? _tooltips[_selectedIndices[0]] : null; SelectedIndexChanged?.Invoke(this); } /// /// Called when item is clicked. /// /// The index. protected virtual void OnItemClicked(int index) { if (SupportMultiSelect) { if (_selectedIndices.Contains(index)) _selectedIndices.Remove(index); else _selectedIndices.Add(index); OnSelectedIndexChanged(); } else { SelectedIndex = index; } } /// /// Shows the context menu popup. /// protected void ShowPopup() { // Ensure to have valid menu if (_popupMenu == null) { _popupMenu = OnCreatePopup(); _popupMenu.MaximumItemsInViewCount = MaximumItemsInViewCount; // Bind events _popupMenu.VisibleChanged += cm => { var win = Root; _blockPopup = win != null && new Rectangle(Float2.Zero, Size).Contains(PointFromWindow(win.MousePosition)); if (!_blockPopup) Focus(); }; _popupMenu.ButtonClicked += btn => { OnItemClicked((int)btn.Tag); if (SupportMultiSelect) { // Don't hide in multi-select, so user can edit multiple elements instead of just one UpdateButtons(); _popupMenu?.PerformLayout(); } else { _popupMenu?.Hide(); } }; } // Check if menu hs been already shown if (_popupMenu.Visible) { if (!SupportMultiSelect) _popupMenu.Hide(); return; } PopupShowing?.Invoke(this); // Check if has any items if (_items.Count > 0) { UpdateButtons(); // Show dropdown list _popupMenu.MinimumWidth = Width; _popupMenu.Show(this, new Float2(1, Height)); } } /// /// Updates buttons layout. /// private void UpdateButtons() { if (_popupMenu.Items.Count() != _items.Count) { var itemControls = _popupMenu.Items.ToArray(); foreach (var e in itemControls) e.Dispose(); if (Sorted) _items.Sort(); for (int i = 0; i < _items.Count; i++) { var btn = _popupMenu.AddButton(_items[i]); OnLayoutMenuButton(btn, i, true); btn.Tag = i; } } else { var itemControls = _popupMenu.Items.ToArray(); if (Sorted) _items.Sort(); for (int i = 0; i < _items.Count; i++) { if (itemControls[i] is ContextMenuButton btn) { btn.Text = _items[i]; OnLayoutMenuButton(btn, i, true); } } } } /// /// Called when button is created or updated. Can be used to customize the visuals. /// /// The button. /// The item index. /// true if button is created else it is repainting the button protected virtual void OnLayoutMenuButton(ContextMenuButton button, int index, bool construct = false) { button.Checked = _selectedIndices.Contains(index); if (_tooltips != null && _tooltips.Length > index) button.TooltipText = _tooltips[index]; } /// /// Creates the popup menu. /// protected virtual ContextMenu.ContextMenu OnCreatePopup() { if (PopupCreate != null) return PopupCreate(this); return new ContextMenu.ContextMenu(); } /// public override void OnDestroy() { if (_popupMenu != null) { _popupMenu.Hide(); _popupMenu.Dispose(); _popupMenu = null; } if (IsDisposing) return; _selectedIndices.Clear(); _selectedIndices = null; _items.Clear(); _items = null; _tooltips = null; base.OnDestroy(); } /// public override void Draw() { // Cache data var clientRect = new Rectangle(Float2.Zero, Size); float margin = clientRect.Height * 0.2f; float boxSize = clientRect.Height - margin * 2; bool isOpened = IsPopupOpened; bool enabled = EnabledInHierarchy; Color backgroundColor = BackgroundColor; Color borderColor = BorderColor; Color arrowColor = ArrowColor; if (!enabled) { backgroundColor *= 0.5f; arrowColor *= 0.7f; } else if (isOpened || _mouseDown) { backgroundColor = BackgroundColorSelected; borderColor = BorderColorSelected; arrowColor = ArrowColorSelected; } else if (IsMouseOver || IsNavFocused) { backgroundColor = BackgroundColorHighlighted; borderColor = BorderColorHighlighted; arrowColor = ArrowColorHighlighted; } // Background Render2D.FillRectangle(clientRect, backgroundColor); Render2D.DrawRectangle(clientRect.MakeExpanded(-2.0f), borderColor); // Check if has selected item if (_selectedIndices != null && _selectedIndices.Count > 0) { string text = _selectedIndices.Count == 1 ? _items[_selectedIndices[0]] : "Multiple Values"; // Draw text of the selected item float textScale = Height / DefaultHeight; var textRect = new Rectangle(margin, 0, clientRect.Width - boxSize - 2.0f * margin, clientRect.Height); Render2D.PushClip(textRect); var textColor = TextColor; Render2D.DrawText(Font.GetFont(), text, textRect, enabled ? textColor : textColor * 0.5f, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, textScale); Render2D.PopClip(); } // Arrow ArrowImage?.Draw(new Rectangle(clientRect.Width - margin - boxSize, margin, boxSize, boxSize), arrowColor); } /// public override void OnLostFocus() { base.OnLostFocus(); // Clear flags _mouseDown = false; _blockPopup = false; } /// public override void OnMouseLeave() { // Clear flags _mouseDown = false; _blockPopup = false; base.OnMouseLeave(); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { if (button == MouseButton.Left) { _mouseDown = true; Focus(); return true; } return base.OnMouseDown(location, button); } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (_mouseDown && !_blockPopup) { _mouseDown = false; ShowPopup(); } else { _blockPopup = false; } return true; } /// public override void OnSubmit() { base.OnSubmit(); ShowPopup(); } } }