// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.GUI
{
///
/// The custom combobox for enum editing. Supports some special cases for flag enums.
///
///
public class EnumComboBox : ComboBox
{
///
/// The enum type.
///
protected readonly Type _enumType;
///
/// The enum entries. The same order as items in combo box.
///
protected readonly List _entries = new List();
///
/// The cached value from the UI.
///
protected long _cachedValue;
///
/// True if has value cached, otherwise false.
///
protected bool _hasValueCached;
///
/// The enum entry.
///
public struct Entry
{
///
/// The name.
///
public string Name;
///
/// The tooltip text.
///
public string Tooltip;
///
/// The value.
///
public long Value;
///
/// Initializes a new instance of the struct.
///
/// The name.
/// The tooltip.
/// The value.
public Entry(string name, long value, string tooltip = null)
{
Name = name;
Tooltip = tooltip;
Value = value;
}
}
///
/// Custom extension delegate used to build enum element entries layout.
///
/// The type.
/// The output entries collection.
public delegate void BuildEntriesDelegate(Type type, List entries);
///
/// Gets a value indicating whether this enum has flags.
///
public bool IsFlags { get; }
///
/// Gets or sets the value of the enum (may not be int).
///
public object EnumTypeValue
{
get => Enum.ToObject(_enumType, Value);
set => Value = Convert.ToInt64(value);
}
///
/// Gets or sets the value.
///
public long Value
{
get => _cachedValue;
set
{
// Skip if won't change
if (_cachedValue == value && _hasValueCached)
return;
// Single value
for (int i = 0; i < _entries.Count; i++)
{
if (_entries[i].Value == value)
{
SelectedIndex = i;
return;
}
}
if (IsFlags)
{
// Collection of flags
var selection = new List();
for (int i = 0; i < _entries.Count; i++)
{
var e = _entries[i].Value;
if (e != 0 && (e & value) == e)
{
selection.Add(i);
}
}
Selection = selection;
}
else
{
SelectedIndex = -1;
}
}
}
///
/// Occurs when value gets changed.
///
public event Action ValueChanged;
///
/// Occurs when value gets changed.
///
public event Action EnumValueChanged;
///
/// Initializes a new instance of the class.
///
/// The enum type.
/// The custom entries layout builder. Allows to hide existing or add different enum values to editor.
/// The formatting mode.
public EnumComboBox(Type type, BuildEntriesDelegate customBuildEntriesDelegate = null, EnumDisplayAttribute.FormatMode formatMode = EnumDisplayAttribute.FormatMode.Default)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (!type.IsEnum)
throw new ArgumentException(string.Format("Invalid enum type {0}.", type.FullName));
_enumType = type;
IsFlags = _enumType.GetCustomAttribute() != null;
SupportMultiSelect = IsFlags;
MaximumItemsInViewCount = 15;
SelectedIndexChanged += ComboBoxOnSelectedIndexChanged;
if (customBuildEntriesDelegate != null)
customBuildEntriesDelegate(type, _entries);
else
BuildEntriesDefault(type, _entries, formatMode);
var hasTooltips = false;
var entries = CollectionsMarshal.AsSpan(_entries);
for (int i = 0; i < _entries.Count; i++)
{
ref var e = ref entries[i];
_items.Add(e.Name);
hasTooltips |= e.Tooltip != null;
}
if (hasTooltips)
{
var tooltips = new string[_entries.Count];
for (int i = 0; i < _entries.Count; i++)
{
ref var e = ref entries[i];
tooltips[i] = e.Tooltip;
}
_tooltips = tooltips;
}
}
private void ComboBoxOnSelectedIndexChanged(ComboBox comboBox)
{
CacheValue();
OnValueChanged();
}
///
/// Called when value gets changed.
///
protected virtual void OnValueChanged()
{
ValueChanged?.Invoke();
EnumValueChanged?.Invoke(this);
}
///
/// Caches the selected UI enum value.
///
protected void CacheValue()
{
long value = 0;
if (IsFlags)
{
var selection = Selection;
for (int i = 0; i < selection.Count; i++)
{
var index = selection[i];
value |= _entries[index].Value;
}
}
else
{
var selectedIndex = SelectedIndex;
if (selectedIndex != -1)
value = _entries[selectedIndex].Value;
}
_cachedValue = value;
_hasValueCached = true;
}
///
/// Builds the default entries for the given enum type.
///
/// The type.
/// The output entries.
/// The formatting mode.
public static void BuildEntriesDefault(Type type, List entries, EnumDisplayAttribute.FormatMode formatMode = EnumDisplayAttribute.FormatMode.Default)
{
FieldInfo[] fields = type.GetFields();
entries.Capacity = Mathf.Max(fields.Length - 1, entries.Capacity);
for (int i = 0; i < fields.Length; i++)
{
var field = fields[i];
if (field.Name.Equals("value__", StringComparison.Ordinal))
continue;
var attributes = field.GetCustomAttributes(false);
if (attributes.Any(x => x is HideInEditorAttribute))
continue;
string name;
var nameAttr = (EditorDisplayAttribute)attributes.FirstOrDefault(x => x is EditorDisplayAttribute);
if (nameAttr != null)
{
name = nameAttr.Name;
}
else
{
switch (formatMode)
{
case EnumDisplayAttribute.FormatMode.Default:
name = Utilities.Utils.GetPropertyNameUI(field.Name);
break;
case EnumDisplayAttribute.FormatMode.None:
name = field.Name;
break;
default: throw new ArgumentOutOfRangeException(nameof(formatMode), formatMode, null);
}
}
string tooltip = Editor.Instance.CodeDocs.GetTooltip(new ScriptMemberInfo(field), attributes);
entries.Add(new Entry(name, Convert.ToInt64(field.GetRawConstantValue()), tooltip));
}
}
///
protected override void OnItemClicked(int index)
{
if (IsFlags)
{
var entries = _entries;
// Special case if clicked enum with zero value
if (entries[index].Value == 0)
{
SelectedIndex = index;
return;
}
// Calculate value that will be set after change
long valueAfter = 0;
bool isSelected = _selectedIndices.Contains(index);
long selectedValue = entries[index].Value;
for (int i = 0; i < _selectedIndices.Count; i++)
{
int selectedIndex = _selectedIndices[i];
if (selectedIndex != index && (isSelected || (entries[selectedIndex].Value & selectedValue) == 0))
valueAfter |= entries[selectedIndex].Value;
}
if (!isSelected)
valueAfter |= selectedValue;
// Skip if value won't change
if (Value == valueAfter)
{
return;
}
// Build new selection
for (int i = 0; i < entries.Count; i++)
{
if (entries[i].Value == valueAfter)
{
SelectedIndex = i;
return;
}
}
_selectedIndices.Clear();
for (int i = 0; i < entries.Count; i++)
{
var e = entries[i].Value;
if (e != 0 && (e & valueAfter) == e)
{
_selectedIndices.Add(i);
}
}
OnSelectedIndexChanged();
return;
}
base.OnItemClicked(index);
}
}
}