630 lines
21 KiB
C#
630 lines
21 KiB
C#
// Copyright (c) 2012-2024 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
|
|
{
|
|
/// <summary>
|
|
/// Combo box control allows to choose one item or set of items from the provided collection of options.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Difference between <see cref="ComboBox"/> and <see cref="Dropdown"/> 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).
|
|
/// </remarks>
|
|
/// <seealso cref="FlaxEngine.GUI.Control" />
|
|
[HideInEditor]
|
|
public class ComboBox : Control
|
|
{
|
|
/// <summary>
|
|
/// The default height of the control.
|
|
/// </summary>
|
|
public const float DefaultHeight = 18.0f;
|
|
|
|
/// <summary>
|
|
/// The items.
|
|
/// </summary>
|
|
protected List<string> _items = new List<string>();
|
|
|
|
/// <summary>
|
|
/// The item tooltips (optional).
|
|
/// </summary>
|
|
protected string[] _tooltips;
|
|
|
|
/// <summary>
|
|
/// The popup menu. May be null if has not been used yet.
|
|
/// </summary>
|
|
protected ContextMenu.ContextMenu _popupMenu;
|
|
|
|
/// <summary>
|
|
/// The mouse down flag.
|
|
/// </summary>
|
|
protected bool _mouseDown;
|
|
|
|
/// <summary>
|
|
/// The block popup flag.
|
|
/// </summary>
|
|
protected bool _blockPopup;
|
|
|
|
/// <summary>
|
|
/// The selected indices.
|
|
/// </summary>
|
|
protected List<int> _selectedIndices = new List<int>(4);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the items collection.
|
|
/// </summary>
|
|
[EditorOrder(1), Tooltip("The items collection.")]
|
|
public List<string> Items
|
|
{
|
|
get => _items;
|
|
set => _items = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the items tooltips (optional).
|
|
/// </summary>
|
|
[NoSerialize, HideInEditor]
|
|
public string[] Tooltips
|
|
{
|
|
get => _tooltips;
|
|
set => _tooltips = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if sort items before showing the list, otherwise present them in the unchanged order.
|
|
/// </summary>
|
|
[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; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether support multi items selection.
|
|
/// </summary>
|
|
[EditorOrder(41), Tooltip("If checked, combobox will support multi items selection. Otherwise it will be single item picking.")]
|
|
public bool SupportMultiSelect { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the maximum amount of items in the view. If popup has more items to show it uses a additional scroll panel.
|
|
/// </summary>
|
|
[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; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the selected item (returns <see cref="string.Empty"/> if no item is being selected or more than one item is selected).
|
|
/// </summary>
|
|
[HideInEditor, NoSerialize]
|
|
public string SelectedItem
|
|
{
|
|
get => _selectedIndices.Count == 1 ? _items[_selectedIndices[0]] : string.Empty;
|
|
set => SelectedIndex = _items.IndexOf(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this combobox has any item selected.
|
|
/// </summary>
|
|
public bool HasSelection => _selectedIndices.Count != 0;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the index of the selected. If combobox has more than 1 item selected then it returns invalid index (value -1).
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the selection.
|
|
/// </summary>
|
|
[NoSerialize, HideInEditor]
|
|
public List<int> 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event fired when selected index gets changed.
|
|
/// </summary>
|
|
public event Action<ComboBox> SelectedIndexChanged;
|
|
|
|
/// <summary>
|
|
/// Occurs when popup is showing (before event). Can be used to update items collection before showing it to the user.
|
|
/// </summary>
|
|
public event Action<ComboBox> PopupShowing;
|
|
|
|
/// <summary>
|
|
/// Custom popup creation function.
|
|
/// </summary>
|
|
public event Func<ComboBox, ContextMenu.ContextMenu> PopupCreate;
|
|
|
|
/// <summary>
|
|
/// Gets the popup menu (it may be null if not used - lazy init).
|
|
/// </summary>
|
|
public ContextMenu.ContextMenu Popup => _popupMenu;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this popup menu is opened.
|
|
/// </summary>
|
|
public bool IsPopupOpened => _popupMenu != null && _popupMenu.IsOpened;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the font used to draw text.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000)]
|
|
public FontReference Font { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the color of the text.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000)]
|
|
public Color TextColor { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the color of the border.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000)]
|
|
public Color BorderColor { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the background color when combobox popup is opened.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2010)]
|
|
public Color BackgroundColorSelected { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the border color when combobox popup is opened.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2020)]
|
|
public Color BorderColorSelected { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the background color when combobox is highlighted.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000)]
|
|
public Color BackgroundColorHighlighted { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the border color when combobox is highlighted.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000)]
|
|
public Color BorderColorHighlighted { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the image used to render combobox drop arrow icon.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000), Tooltip("The image used to render combobox drop arrow icon.")]
|
|
public IBrush ArrowImage { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the color used to render combobox drop arrow icon.
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render combobox drop arrow icon.")]
|
|
public Color ArrowColor { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the color used to render combobox drop arrow icon (menu is opened).
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render combobox drop arrow icon (menu is opened).")]
|
|
public Color ArrowColorSelected { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the color used to render combobox drop arrow icon (menu is highlighted).
|
|
/// </summary>
|
|
[EditorDisplay("Style"), EditorOrder(2000), Tooltip("The color used to render combobox drop arrow icon (menu is highlighted).")]
|
|
public Color ArrowColorHighlighted { get; set; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ComboBox"/> class.
|
|
/// </summary>
|
|
public ComboBox()
|
|
: this(0, 0)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ComboBox"/> class.
|
|
/// </summary>
|
|
/// <param name="x">The x.</param>
|
|
/// <param name="y">The y.</param>
|
|
/// <param name="width">The width.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the items.
|
|
/// </summary>
|
|
public void ClearItems()
|
|
{
|
|
SelectedIndex = -1;
|
|
_items.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
public void AddItem(string item)
|
|
{
|
|
_items.Add(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the items.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
public void AddItems(IEnumerable<string> items)
|
|
{
|
|
_items.AddRange(items);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the items.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
public void SetItems(IEnumerable<string> items)
|
|
{
|
|
SelectedIndex = -1;
|
|
_items.Clear();
|
|
_items.AddRange(items);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the specified item is selected.
|
|
/// </summary>
|
|
/// <param name="item">The item to check.</param>
|
|
/// <returns><c>true</c> if the item is selected; otherwise, <c>false</c>.</returns>
|
|
public bool IsSelected(string item)
|
|
{
|
|
return IsSelected(_items.IndexOf(item));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the item at the specified index is selected.
|
|
/// </summary>
|
|
/// <param name="index">The index.</param>
|
|
/// <returns><c>true</c> if the item is selected; otherwise, <c>false</c>.</returns>
|
|
public bool IsSelected(int index)
|
|
{
|
|
return index != -1 && _selectedIndices.Contains(index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when selected item index gets changed.
|
|
/// </summary>
|
|
protected virtual void OnSelectedIndexChanged()
|
|
{
|
|
if (_tooltips != null && _tooltips.Length == _items.Count)
|
|
TooltipText = _selectedIndices.Count == 1 ? _tooltips[_selectedIndices[0]] : null;
|
|
SelectedIndexChanged?.Invoke(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when item is clicked.
|
|
/// </summary>
|
|
/// <param name="index">The index.</param>
|
|
protected virtual void OnItemClicked(int index)
|
|
{
|
|
if (SupportMultiSelect)
|
|
{
|
|
if (_selectedIndices.Contains(index))
|
|
_selectedIndices.Remove(index);
|
|
else
|
|
_selectedIndices.Add(index);
|
|
OnSelectedIndexChanged();
|
|
}
|
|
else
|
|
{
|
|
SelectedIndex = index;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the context menu popup.
|
|
/// </summary>
|
|
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));
|
|
|
|
// Adjust menu position if it is not the down direction
|
|
if (_popupMenu.Direction == ContextMenuDirection.RightUp)
|
|
{
|
|
var position = _popupMenu.RootWindow.Window.Position;
|
|
_popupMenu.RootWindow.Window.Position = new Float2(position.X, position.Y - Height);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates buttons layout.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when button is created or updated. Can be used to customize the visuals.
|
|
/// </summary>
|
|
/// <param name="button">The button.</param>
|
|
/// <param name="index">The item index.</param>
|
|
/// <param name="construct">true if button is created else it is repainting the button</param>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the popup menu.
|
|
/// </summary>
|
|
protected virtual ContextMenu.ContextMenu OnCreatePopup()
|
|
{
|
|
if (PopupCreate != null)
|
|
return PopupCreate(this);
|
|
return new ContextMenu.ContextMenu();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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 ? (_selectedIndices[0] >= 0 && _selectedIndices[0] < _items.Count ? _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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLostFocus()
|
|
{
|
|
base.OnLostFocus();
|
|
|
|
// Clear flags
|
|
_mouseDown = false;
|
|
_blockPopup = false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnMouseLeave()
|
|
{
|
|
// Clear flags
|
|
_mouseDown = false;
|
|
_blockPopup = false;
|
|
|
|
base.OnMouseLeave();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDown(Float2 location, MouseButton button)
|
|
{
|
|
if (button == MouseButton.Left)
|
|
{
|
|
_mouseDown = true;
|
|
Focus();
|
|
return true;
|
|
}
|
|
|
|
return base.OnMouseDown(location, button);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseUp(Float2 location, MouseButton button)
|
|
{
|
|
if (_mouseDown && !_blockPopup)
|
|
{
|
|
_mouseDown = false;
|
|
ShowPopup();
|
|
}
|
|
else
|
|
{
|
|
_blockPopup = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnSubmit()
|
|
{
|
|
base.OnSubmit();
|
|
|
|
ShowPopup();
|
|
}
|
|
}
|
|
}
|