Files
FlaxEngine/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs

512 lines
19 KiB
C#

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Input;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
using FlaxEngine.Utilities;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit key-value dictionaries.
/// </summary>
public class DictionaryEditor : CustomEditor
{
/// <summary>
/// The custom implementation of the dictionary items labels that can be used to remove items or edit keys.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.GUI.PropertyNameLabel" />
private class DictionaryItemLabel : PropertyNameLabel
{
private DictionaryEditor _editor;
private object _key;
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryItemLabel"/> class.
/// </summary>
/// <param name="editor">The editor.</param>
/// <param name="key">The key.</param>
public DictionaryItemLabel(DictionaryEditor editor, object key)
: base(key?.ToString() ?? "<null>")
{
_editor = editor;
_key = key;
SetupContextMenu += OnSetupContextMenu;
}
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
if (menu.Items.Any())
menu.AddSeparator();
menu.AddButton("Remove", OnRemoveClicked).Enabled = !_editor._readOnly;
menu.AddButton("Edit", OnEditClicked).Enabled = _editor._canEditKeys;
}
private void OnRemoveClicked(ContextMenuButton button)
{
_editor.Remove(_key);
}
private void OnEditClicked(ContextMenuButton button)
{
var keyType = _editor.Values.Type.GetGenericArguments()[0];
if (keyType == typeof(string) || keyType.IsPrimitive)
{
// Edit as text
var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false);
popup.Validate += (renamePopup, value) =>
{
object newKey;
if (keyType.IsPrimitive)
newKey = JsonSerializer.Deserialize(value, keyType);
else
newKey = value;
return !((IDictionary)_editor.Values[0]).Contains(newKey);
};
popup.Renamed += renamePopup =>
{
object newKey;
if (keyType.IsPrimitive)
newKey = JsonSerializer.Deserialize(renamePopup.Text, keyType);
else
newKey = renamePopup.Text;
_editor.ChangeKey(_key, newKey);
_key = newKey;
Text = _key.ToString();
};
}
else if (keyType.IsEnum)
{
// Edit via enum picker
var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false);
var picker = new EnumComboBox(keyType)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = popup,
EnumTypeValue = _key,
};
picker.ValueChanged += () =>
{
popup.Hide();
object newKey = picker.EnumTypeValue;
if (!((IDictionary)_editor.Values[0]).Contains(newKey))
{
_editor.ChangeKey(_key, newKey);
_key = newKey;
Text = _key.ToString();
}
};
}
else
{
// Generic editor
var popup = ContextMenuBase.ShowEmptyMenu(Parent, Rectangle.Margin(Bounds, Margin));
var presenter = new CustomEditorPresenter(null);
presenter.Panel.AnchorPreset = AnchorPresets.StretchAll;
presenter.Panel.IsScrollable = false;
presenter.Panel.Parent = popup;
presenter.Select(_key);
presenter.Modified += () =>
{
popup.Hide();
object newKey = presenter.Selection[0];
_editor.ChangeKey(_key, newKey);
_key = newKey;
Text = _key?.ToString();
};
}
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && _editor._canEditKeys)
{
OnEditClicked(null);
return true;
}
return base.OnMouseDoubleClick(location, button);
}
/// <inheritdoc />
public override void OnDestroy()
{
_editor = null;
_key = null;
base.OnDestroy();
}
}
private IntValueBox _sizeBox;
private Color _background;
private int _elementsCount;
private bool _readOnly;
private bool _notNullItems;
private bool _canEditKeys;
private bool _keyEdited;
private CollectionAttribute.DisplayType _displayType;
/// <summary>
/// Gets the length of the collection.
/// </summary>
public int Count => (Values[0] as IDictionary)?.Count ?? 0;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// No support for different collections for now
if (HasDifferentValues || HasDifferentTypes)
return;
var type = Values.Type;
var size = Count;
var argTypes = type.GetGenericArguments();
var keyType = argTypes[0];
var valueType = argTypes[1];
_canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum || keyType.IsValueType;
_background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor;
_readOnly = false;
_notNullItems = false;
_displayType = CollectionAttribute.DisplayType.Header;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
Type overrideEditorType = null;
float spacing = 0.0f;
var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute);
if (collection != null)
{
_canEditKeys &= collection.CanReorderItems;
_readOnly = collection.ReadOnly;
_notNullItems = collection.NotNullItems;
if (collection.BackgroundColor.HasValue)
_background = collection.BackgroundColor.Value;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
_displayType = collection.Display;
}
if (attributes != null && attributes.Any(x => x is ReadOnlyAttribute))
{
_readOnly = true;
_canEditKeys = false;
}
// Size
if (layout.ContainerControl is DropPanel dropPanel)
{
var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height;
var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top;
_sizeBox = new IntValueBox(size)
{
MinValue = 0,
MaxValue = _notNullItems ? size : ushort.MaxValue,
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height),
Parent = dropPanel,
};
var label = new Label
{
Text = "Size",
AnchorPreset = AnchorPresets.TopRight,
Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height),
Parent = dropPanel
};
if (_readOnly || !_canEditKeys)
{
_sizeBox.IsReadOnly = true;
_sizeBox.Enabled = false;
}
else
{
_sizeBox.EditEnd += OnSizeChanged;
}
}
// Elements
if (size > 0)
{
var panel = layout.VerticalPanel();
panel.Panel.BackgroundColor = _background;
var keysEnumerable = ((IDictionary)Values[0]).Keys.OfType<object>();
var keys = keysEnumerable as object[] ?? keysEnumerable.ToArray();
var valuesType = new ScriptType(valueType);
// Use separate layout cells for each collection items to improve layout updates for them in separation
var useSharedLayout = valueType.IsPrimitive || valueType.IsEnum;
for (int i = 0; i < size; i++)
{
if (i > 0 && i < size && spacing > 0)
{
panel.Space(spacing);
}
var key = keys.ElementAt(i);
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
var property = panel.AddPropertyItem(new DictionaryItemLabel(this, key));
var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel();
itemLayout.Object(new DictionaryValueContainer(valuesType, key, Values), overrideEditor);
if (_readOnly && itemLayout.Children.Count > 0)
GenericEditor.OnReadOnlyProperty(itemLayout);
}
}
_elementsCount = size;
// Add/Remove buttons
if (!_readOnly && _canEditKeys)
{
var area = layout.Space(20);
var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16)
{
Text = "+",
TooltipText = "Add new item",
AnchorPreset = AnchorPresets.TopRight,
Parent = area.ContainerControl,
Enabled = !_notNullItems,
};
addButton.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count + 1);
};
var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16)
{
Text = "-",
TooltipText = "Remove last item",
AnchorPreset = AnchorPresets.TopRight,
Parent = area.ContainerControl,
Enabled = size > 0,
};
removeButton.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count - 1);
};
}
}
/// <summary>
/// Rebuilds the parent layout if its collection.
/// </summary>
public void RebuildParentCollection()
{
if (ParentEditor is DictionaryEditor dictionaryEditor)
{
dictionaryEditor.RebuildParentCollection();
dictionaryEditor.RebuildLayout();
return;
}
if (ParentEditor is CollectionEditor collectionEditor)
{
collectionEditor.RebuildParentCollection();
collectionEditor.RebuildLayout();
}
}
private void OnSizeChanged()
{
if (IsSetBlocked)
return;
Resize(_sizeBox.Value);
}
/// <summary>
/// Removes the item of the specified key. It supports undo.
/// </summary>
/// <param name="key">The key of the item to remove.</param>
private void Remove(object key)
{
if (IsSetBlocked)
return;
// Allocate new collection
var dictionary = Values[0] as IDictionary;
var type = Values.Type;
var newValues = (IDictionary)type.CreateInstance();
// Copy all keys/values except the specified one
if (dictionary != null)
{
foreach (var e in dictionary.Keys)
{
if (Equals(e, key))
continue;
newValues[e] = dictionary[e];
}
}
SetValue(newValues);
}
/// <summary>
/// Changes the key of the item.
/// </summary>
/// <param name="oldKey">The old key value.</param>
/// <param name="newKey">The new key value.</param>
protected void ChangeKey(object oldKey, object newKey)
{
var dictionary = (IDictionary)Values[0];
var newValues = (IDictionary)Values.Type.CreateInstance();
foreach (var e in dictionary.Keys)
{
if (Equals(e, oldKey))
newValues[newKey] = dictionary[e];
else
newValues[e] = dictionary[e];
}
SetValue(newValues);
_keyEdited = true; // TODO: use custom UndoAction to rebuild UI after key modification
}
/// <summary>
/// Resizes collection to the specified new size.
/// </summary>
/// <param name="newSize">The new size.</param>
protected void Resize(int newSize)
{
var dictionary = Values[0] as IDictionary;
var oldSize = dictionary?.Count ?? 0;
if (oldSize == newSize)
return;
// Allocate new collection
var type = Values.Type;
var argTypes = type.GetGenericArguments();
var keyType = argTypes[0];
var valueType = argTypes[1];
var newValues = (IDictionary)type.CreateInstance();
// Copy all keys/values
int itemsLeft = newSize;
if (dictionary != null)
{
foreach (var e in dictionary.Keys)
{
if (itemsLeft == 0)
break;
newValues[e] = dictionary[e];
itemsLeft--;
}
}
// Insert new items (find unique keys)
int newItemsLeft = newSize - oldSize;
while (newItemsLeft-- > 0)
{
object newKey = null;
if (keyType.IsPrimitive)
{
long uniqueKey = 0;
bool isUnique;
do
{
isUnique = true;
foreach (var e in newValues.Keys)
{
var asLong = Convert.ToInt64(e);
if (asLong.Equals(uniqueKey))
{
uniqueKey++;
isUnique = false;
break;
}
}
} while (!isUnique);
newKey = Convert.ChangeType(uniqueKey, keyType);
}
else if (keyType.IsEnum)
{
var enumValues = Enum.GetValues(keyType);
int uniqueKeyIndex = 0;
bool isUnique;
do
{
isUnique = true;
foreach (var e in newValues.Keys)
{
if (Equals(e, enumValues.GetValue(uniqueKeyIndex)))
{
uniqueKeyIndex++;
isUnique = false;
break;
}
}
} while (!isUnique && uniqueKeyIndex < enumValues.Length);
newKey = enumValues.GetValue(uniqueKeyIndex);
}
else if (keyType == typeof(string))
{
string uniqueKey = "Key";
bool isUnique;
do
{
isUnique = true;
foreach (var e in newValues.Keys)
{
if (string.Equals((string)e, uniqueKey, StringComparison.InvariantCulture))
{
uniqueKey += "*";
isUnique = false;
break;
}
}
} while (!isUnique);
newKey = uniqueKey;
}
else
{
newKey = TypeUtils.GetDefaultValue(new ScriptType(keyType));
}
newValues[newKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
}
SetValue(newValues);
}
/// <inheritdoc />
public override void Refresh()
{
if (_keyEdited)
{
_keyEdited = false;
RebuildLayout();
RebuildParentCollection();
}
base.Refresh();
// No support for different collections for now
if (HasDifferentValues || HasDifferentTypes)
return;
// Check if collection has been resized (by UI or from external source)
if (Count != _elementsCount)
{
RebuildLayout();
RebuildParentCollection();
}
}
}
}