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