// 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();
}
}
}