From fb2bae2198627ae596495bc77ee562a6b21d4f47 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Mon, 12 Dec 2022 18:31:46 +0100 Subject: [PATCH 1/4] Exposed functionality for Add New and Go To buttons. --- Scripts/Editor/Core/CollectionCustomEditor.cs | 5 ++++- Scripts/Editor/Core/CollectionItemDropdown.cs | 6 ++---- Scripts/Editor/Core/CollectionItemPropertyDrawer.cs | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Scripts/Editor/Core/CollectionCustomEditor.cs b/Scripts/Editor/Core/CollectionCustomEditor.cs index efd109d..f3cc1a3 100644 --- a/Scripts/Editor/Core/CollectionCustomEditor.cs +++ b/Scripts/Editor/Core/CollectionCustomEditor.cs @@ -831,9 +831,12 @@ private void CheckGeneratedCodeLocation() } } - public static void SetLastAddedEnum(ScriptableObjectCollectionItem collectionItem) + public static ScriptableObjectCollectionItem AddNewItem(ScriptableObjectCollection collection, Type itemType) { + ScriptableObjectCollectionItem collectionItem = collection.AddNew(itemType); + Selection.objects = new Object[] {collection}; LAST_ADDED_COLLECTION_ITEM = collectionItem; + return collectionItem; } } } diff --git a/Scripts/Editor/Core/CollectionItemDropdown.cs b/Scripts/Editor/Core/CollectionItemDropdown.cs index 55f5429..217a886 100644 --- a/Scripts/Editor/Core/CollectionItemDropdown.cs +++ b/Scripts/Editor/Core/CollectionItemDropdown.cs @@ -86,10 +86,8 @@ protected override void ItemSelected(AdvancedDropdownItem item) if (item.name.Equals(CREATE_NEW_TEXT, StringComparison.OrdinalIgnoreCase)) { ScriptableObjectCollection collection = collections.First(); - ScriptableObjectCollectionItem collectionItem = collection.AddNew(itemType); - callback.Invoke(collectionItem); - Selection.objects = new Object[] {collection}; - CollectionCustomEditor.SetLastAddedEnum(collectionItem); + ScriptableObjectCollectionItem newItem = CollectionCustomEditor.AddNewItem(collection, itemType); + callback.Invoke(newItem); return; } diff --git a/Scripts/Editor/Core/CollectionItemPropertyDrawer.cs b/Scripts/Editor/Core/CollectionItemPropertyDrawer.cs index 5678d96..8521561 100644 --- a/Scripts/Editor/Core/CollectionItemPropertyDrawer.cs +++ b/Scripts/Editor/Core/CollectionItemPropertyDrawer.cs @@ -10,6 +10,8 @@ namespace BrunoMikoski.ScriptableObjectCollections [CustomPropertyDrawer(typeof(ScriptableObjectCollectionItem), true)] public class CollectionItemPropertyDrawer : PropertyDrawer { + public const float BUTTON_WIDTH = 30; + private static readonly CollectionItemEditorOptionsAttribute DefaultAttribute = new CollectionItemEditorOptionsAttribute(); @@ -210,7 +212,7 @@ private void DrawGotoButton(ref Rect popupRect, ScriptableObjectCollectionItem c if (!OptionsAttribute.ShouldDrawGotoButton) return; Rect buttonRect = popupRect; - buttonRect.width = 30; + buttonRect.width = BUTTON_WIDTH; buttonRect.height = 18; popupRect.width -= buttonRect.width; buttonRect.x += popupRect.width; @@ -227,7 +229,7 @@ private void DrawEditFoldoutButton(ref Rect popupRect, ScriptableObjectCollectio return; Rect buttonRect = popupRect; - buttonRect.width = 30; + buttonRect.width = BUTTON_WIDTH; buttonRect.height = 18; popupRect.width -= buttonRect.width; buttonRect.x += popupRect.width; From aa1bb3840f77defb382e8f29f57338d6ba921c6f Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Mon, 12 Dec 2022 18:32:17 +0100 Subject: [PATCH 2/4] Added a Collection Item Picker class for Flags-like picking of values. --- .../CollectionItemPickerPropertyDrawer.cs | 123 ++++++++++++++++++ ...CollectionItemPickerPropertyDrawer.cs.meta | 11 ++ Scripts/Runtime/Core/CollectionItemPicker.cs | 83 ++++++++++++ .../Runtime/Core/CollectionItemPicker.cs.meta | 11 ++ 4 files changed, 228 insertions(+) create mode 100644 Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs create mode 100644 Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs.meta create mode 100644 Scripts/Runtime/Core/CollectionItemPicker.cs create mode 100644 Scripts/Runtime/Core/CollectionItemPicker.cs.meta diff --git a/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs new file mode 100644 index 0000000..27dc13d --- /dev/null +++ b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + /// + /// Lets you pick one or more items from a collection, similar to how an enum field would work if the enum had the + /// [Flags] attribute applied to it. + /// + [CustomPropertyDrawer(typeof(CollectionItemPicker<>))] + public class CollectionItemPickerPropertyDrawer : PropertyDrawer + { + private readonly List + tempMaskItems = new List(); + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + return EditorGUIUtility.singleLineHeight; + } + + private static bool Contains(SerializedProperty itemsProperty, ScriptableObjectCollectionItem item) + { + for (int i = 0; i < itemsProperty.arraySize; i++) + { + if (itemsProperty.GetArrayElementAtIndex(i).objectReferenceValue == item) + return true; + } + + return false; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + // Figure out the collection item type. + Type[] genericArguments = fieldInfo.FieldType.GetGenericArguments(); + Type collectionItemType = genericArguments[0]; + + // Now figure out the collection type. + ScriptableObjectCollection collection; + CollectionsRegistry.Instance.TryGetCollectionFromItemType(collectionItemType, out collection); + + if (collection == null) + { + EditorGUI.LabelField(position, label, new GUIContent("Could not determine collection.")); + return; + } + + // Get the list of items. + SerializedProperty itemsProperty = property.FindPropertyRelative("items"); + + // Reserve space for the buttons. We can't use AdvancedDropdown for Flags kind of behaviour, so we have + // to use a real MaskField to get it done. I still want to support adding a new entry from the inspector + // though. For that reason I am adding an Add button next to the dropdown. + position.width -= CollectionItemPropertyDrawer.BUTTON_WIDTH * 2; + Rect goToButtonRect = new Rect( + position.xMax, position.y, CollectionItemPropertyDrawer.BUTTON_WIDTH, position.height); + Rect addButtonRect = new Rect( + goToButtonRect.xMax, position.y, CollectionItemPropertyDrawer.BUTTON_WIDTH, position.height); + + if (collection.Count == 0) + { + // Calculate the rects for the label and the add button and such. + Rect labelRect = new Rect(position.x, position.y, EditorGUIUtility.labelWidth, position.height); + Rect valueRect = new Rect( + position.x + EditorGUIUtility.labelWidth, position.y, position.width - EditorGUIUtility.labelWidth, + position.height); + + // Draw the label. + EditorGUI.LabelField(labelRect, label); + + // Draw the inactive dropdown. + bool wasGuiEnabled = GUI.enabled; + GUI.enabled = false; + EditorGUI.Popup(valueRect, GUIContent.none, 0, new[] {new GUIContent("")}); + GUI.enabled = wasGuiEnabled; + } + else + { + // Create an array of displayed options. + string[] displayedOptions = new string[collection.Count]; + int mask = 0; + for (int i = 0; i < collection.Count; i++) + { + displayedOptions[i] = collection[i].name; + if (Contains(itemsProperty, collection[i])) + mask |= 1 << i; + } + + int maskNew = EditorGUI.MaskField(position, label, mask, displayedOptions); + if (mask != maskNew) + { + // First convert the newly selected mask to a list of items. + tempMaskItems.Clear(); + for (int i = 0; i < collection.Count; i++) + { + int flag = 1 << i; + if ((maskNew & flag) == flag) + tempMaskItems.Add(collection[i]); + } + + // Now update the property to have the values in that list... + itemsProperty.arraySize = tempMaskItems.Count; + for (int i = 0; i < tempMaskItems.Count; i++) + { + itemsProperty.GetArrayElementAtIndex(i).objectReferenceValue = tempMaskItems[i]; + } + } + } + + // Draw the Go To button. + bool shouldGoToCollection = GUI.Button(goToButtonRect, CollectionEditorGUI.ARROW_RIGHT_CHAR); + if (shouldGoToCollection) + Selection.activeObject = collection; + + // Draw the add button. + bool shouldCreateNewItem = GUI.Button(addButtonRect, "+"); + if (shouldCreateNewItem) + CollectionCustomEditor.AddNewItem(collection, collectionItemType); + } + } +} diff --git a/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs.meta b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs.meta new file mode 100644 index 0000000..4faa4ac --- /dev/null +++ b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 264d8d94cfdf3a346bf47076fd933599 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Runtime/Core/CollectionItemPicker.cs b/Scripts/Runtime/Core/CollectionItemPicker.cs new file mode 100644 index 0000000..0ebfc8e --- /dev/null +++ b/Scripts/Runtime/Core/CollectionItemPicker.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace BrunoMikoski.ScriptableObjectCollections +{ + /// + /// Collection Item Picker lets you pick one or more items from a collection, similar to how an enum field would + /// work if the enum had the [Flags] attribute applied to it. + /// + [Serializable] + public class CollectionItemPicker : IList + where ItemType : ScriptableObjectCollectionItem + { + [SerializeField] private List items = new List(); + public List Items => items; + + // Implement IList and forward its members to items. This way we can conveniently use this thing as a list. + #region IList members implementation + + public IEnumerator GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)items).GetEnumerator(); + } + + public void Add(ItemType item) + { + items.Add(item); + } + + public void Clear() + { + items.Clear(); + } + + public bool Contains(ItemType item) + { + return items.Contains(item); + } + + public void CopyTo(ItemType[] array, int arrayIndex) + { + items.CopyTo(array, arrayIndex); + } + + public bool Remove(ItemType item) + { + return items.Remove(item); + } + + public int Count => items.Count; + + public bool IsReadOnly => false; + + public int IndexOf(ItemType item) + { + return items.IndexOf(item); + } + + public void Insert(int index, ItemType item) + { + items.Insert(index, item); + } + + public void RemoveAt(int index) + { + items.RemoveAt(index); + } + + public ItemType this[int index] + { + get => items[index]; + set => items[index] = value; + } + #endregion + } +} diff --git a/Scripts/Runtime/Core/CollectionItemPicker.cs.meta b/Scripts/Runtime/Core/CollectionItemPicker.cs.meta new file mode 100644 index 0000000..ca93e84 --- /dev/null +++ b/Scripts/Runtime/Core/CollectionItemPicker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4a66a5f731a02d84e8af0aecf1d4aca8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 2337d14056a407f2425699913dd44b53c13d98c2 Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Mon, 12 Dec 2022 18:36:05 +0100 Subject: [PATCH 3/4] Improved a comment. --- Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs index 27dc13d..b917e4f 100644 --- a/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs +++ b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs @@ -59,6 +59,9 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten Rect addButtonRect = new Rect( goToButtonRect.xMax, position.y, CollectionItemPropertyDrawer.BUTTON_WIDTH, position.height); + // If the collection is empty, we cannot use MaskField with an empty array because that throws exceptions. + // Because of that we treat it as a special case where we draw a disabled PopUp field. You can then use + // the Go To and Add buttons to add an item to the collection and then you can begin picking. if (collection.Count == 0) { // Calculate the rects for the label and the add button and such. From 0c1b85aca31e9310316207c9cb1226320cc2dc1f Mon Sep 17 00:00:00 2001 From: Roy Theunissen Date: Mon, 12 Dec 2022 18:48:38 +0100 Subject: [PATCH 4/4] Added a TODO about multiple collections. --- Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs index b917e4f..a407282 100644 --- a/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs +++ b/Scripts/Editor/Core/CollectionItemPickerPropertyDrawer.cs @@ -40,6 +40,9 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten // Now figure out the collection type. ScriptableObjectCollection collection; CollectionsRegistry.Instance.TryGetCollectionFromItemType(collectionItemType, out collection); + + // TODO: Should this support multiple collections? I'm not sure how that use case works. I just saw it + // being used elsewhere. if (collection == null) {