update
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
#if !UNITY_2022_1_OR_NEWER
|
||||
using UnityEditor.UIElements; // Required for Unity 2021
|
||||
#endif
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Rive.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// We don't directly use the property drawer for the Alignment class, but we keep it as a way to create the dropdown field in the inspector for RiveBaseEditor.
|
||||
/// We do this because the RiveBaseEditor class supports a bunch of the custom attributes we've created and having multiple drawers for the same class can cause conflicts so we do it all in the RiveBaseEditor class.
|
||||
/// </summary>
|
||||
//[CustomPropertyDrawer(typeof(Alignment))]
|
||||
internal class AlignmentPropertyDrawer : PropertyDrawer
|
||||
{
|
||||
private static readonly (string display, Alignment value)[] OPTIONS = new[]
|
||||
{
|
||||
("Top Left", Alignment.TopLeft),
|
||||
("Top Center", Alignment.TopCenter),
|
||||
("Top Right", Alignment.TopRight),
|
||||
("Center Left", Alignment.CenterLeft),
|
||||
("Center", Alignment.Center),
|
||||
("Center Right", Alignment.CenterRight),
|
||||
("Bottom Left", Alignment.BottomLeft),
|
||||
("Bottom Center", Alignment.BottomCenter),
|
||||
("Bottom Right", Alignment.BottomRight)
|
||||
};
|
||||
|
||||
|
||||
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
var container = new VisualElement();
|
||||
|
||||
var xProp = property.FindPropertyRelative(Alignment.BindingPath_Xfield);
|
||||
var yProp = property.FindPropertyRelative(Alignment.BindingPath_Yfield);
|
||||
|
||||
// Default to Center if we can't get the values
|
||||
var centerIndex = Array.FindIndex(OPTIONS, o => o.value.Equals(Alignment.Center));
|
||||
var currentIndex = centerIndex;
|
||||
|
||||
if (xProp != null && yProp != null)
|
||||
{
|
||||
var currentAlignment = new Alignment(xProp.floatValue, yProp.floatValue);
|
||||
currentIndex = Array.FindIndex(OPTIONS, o => o.value.Equals(currentAlignment));
|
||||
if (currentIndex < 0) currentIndex = centerIndex;
|
||||
}
|
||||
|
||||
var choices = OPTIONS.Select(o => o.display).ToList();
|
||||
|
||||
var dropdown = new PopupField<string>(
|
||||
property.displayName,
|
||||
choices,
|
||||
currentIndex
|
||||
);
|
||||
|
||||
dropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var index = choices.IndexOf(evt.newValue);
|
||||
if (index >= 0 && xProp != null && yProp != null)
|
||||
{
|
||||
var selectedAlignment = OPTIONS[index].value;
|
||||
xProp.floatValue = selectedAlignment.X;
|
||||
yProp.floatValue = selectedAlignment.Y;
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.AddToClassList(StyleHelper.CLASS_FIELD);
|
||||
|
||||
// This ensures that the dropdown is aligned with other fields in the inspector
|
||||
dropdown.AddToClassList(BaseField<UnityEditor.UIElements.PropertyField>.alignedFieldUssClassName);
|
||||
container.Add(dropdown);
|
||||
return container;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 114c8e3b6a625400a8497c76cf6af315
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 350858
|
||||
packageName: Rive
|
||||
packageVersion: 0.4.2
|
||||
assetPath: Packages/app.rive.rive-unity/Editor/Components/CustomElements/AlignmentPropertyDrawer.cs
|
||||
uploadId: 896810
|
||||
@@ -0,0 +1,144 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine.UIElements;
|
||||
using Rive.Utils;
|
||||
|
||||
namespace Rive.EditorTools
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(DropdownAttribute))]
|
||||
internal class DropdownDrawer : PropertyDrawer
|
||||
{
|
||||
private PopupOrTextField dropdown;
|
||||
private SerializedProperty property;
|
||||
private object target;
|
||||
private DropdownAttribute dropdownAttr;
|
||||
private MemberInfo optionsMember;
|
||||
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
this.property = property;
|
||||
dropdownAttr = attribute as DropdownAttribute;
|
||||
target = property.serializedObject.targetObject;
|
||||
var targetType = target.GetType();
|
||||
|
||||
// Try to find member (field, property, or method)
|
||||
optionsMember = targetType.GetField(dropdownAttr.OptionsMemberName,
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
|
||||
if (optionsMember == null)
|
||||
{
|
||||
optionsMember = targetType.GetProperty(dropdownAttr.OptionsMemberName,
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
}
|
||||
|
||||
if (optionsMember == null)
|
||||
{
|
||||
optionsMember = targetType.GetMethod(dropdownAttr.OptionsMemberName,
|
||||
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
}
|
||||
|
||||
if (optionsMember == null)
|
||||
{
|
||||
var errorContainer = new VisualElement();
|
||||
errorContainer.Add(new HelpBox($"Member {dropdownAttr.OptionsMemberName} not found", HelpBoxMessageType.Error));
|
||||
errorContainer.Add(new PropertyField(property));
|
||||
return errorContainer;
|
||||
}
|
||||
|
||||
dropdown = CreateDropdown();
|
||||
|
||||
// Only register for updates if TrackChanges is enabled
|
||||
if (dropdownAttr.TrackChanges)
|
||||
{
|
||||
EditorApplication.update += UpdateDropdownOptions;
|
||||
|
||||
dropdown.RegisterCallback<DetachFromPanelEvent>(evt =>
|
||||
{
|
||||
EditorApplication.update -= UpdateDropdownOptions;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-tracked dropdowns, we still want to update when the panel is attached
|
||||
dropdown.RegisterCallback<AttachToPanelEvent>(evt =>
|
||||
{
|
||||
UpdateDropdownOptions();
|
||||
});
|
||||
}
|
||||
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
private PopupOrTextField CreateDropdown()
|
||||
{
|
||||
var options = GetCurrentOptions();
|
||||
var currentValue = property.stringValue;
|
||||
|
||||
var label = ReflectionUtils.GetPropertyLabel(property);
|
||||
var dropdown = new PopupOrTextField(options, currentValue,
|
||||
label);
|
||||
|
||||
dropdown.BindProperty(property);
|
||||
|
||||
var inspectorFieldAttr = fieldInfo.GetCustomAttribute<InspectorFieldAttribute>();
|
||||
if (inspectorFieldAttr != null)
|
||||
{
|
||||
dropdown.AddToClassList(StyleHelper.CLASS_FIELD);
|
||||
}
|
||||
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
private List<string> GetCurrentOptions()
|
||||
{
|
||||
object options = null;
|
||||
|
||||
switch (optionsMember)
|
||||
{
|
||||
case FieldInfo field:
|
||||
options = field.GetValue(target);
|
||||
break;
|
||||
case PropertyInfo prop:
|
||||
options = prop.GetValue(target);
|
||||
break;
|
||||
case MethodInfo method:
|
||||
options = method.Invoke(target, null);
|
||||
break;
|
||||
}
|
||||
|
||||
if (options is IEnumerable<string> enumerable)
|
||||
{
|
||||
return enumerable.ToList();
|
||||
}
|
||||
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
private void UpdateDropdownOptions()
|
||||
{
|
||||
if (dropdown?.panel == null) return;
|
||||
|
||||
var newOptions = GetCurrentOptions();
|
||||
if (!AreOptionsEqual(dropdown.Choices, newOptions))
|
||||
{
|
||||
dropdown.Choices = newOptions;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private bool AreOptionsEqual(List<string> a, List<string> b)
|
||||
{
|
||||
if (a.Count != b.Count) return false;
|
||||
for (int i = 0; i < a.Count; i++)
|
||||
{
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55ad3672c80b042f288de391c21e3bf9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 350858
|
||||
packageName: Rive
|
||||
packageVersion: 0.4.2
|
||||
assetPath: Packages/app.rive.rive-unity/Editor/Components/CustomElements/DropdownDrawer.cs
|
||||
uploadId: 896810
|
||||
@@ -0,0 +1,237 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Rive.Utils;
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
using MaterialShaderPropertyType = UnityEngine.Rendering.ShaderPropertyType;
|
||||
#else
|
||||
using MaterialShaderPropertyType = UnityEditor.ShaderUtil.ShaderPropertyType;
|
||||
#endif
|
||||
|
||||
namespace Rive.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Draws a list of properties from a material on a component as a dropdown. This is useful if you want to display a list of properties from a material on a component in the inspector.
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(MaterialPropertiesAttribute))]
|
||||
internal class MaterialPropertiesDrawer : PropertyDrawer
|
||||
{
|
||||
private VisualElement m_root;
|
||||
private List<string> m_availablePropertyNames = new List<string>();
|
||||
private SerializedObject m_serializedObject;
|
||||
|
||||
private Material[] GetMaterialsFromSource(object target, string sourceName)
|
||||
{
|
||||
// Try to get materials directly
|
||||
if (ReflectionUtils.TryGetValue<Material[]>(target, sourceName, out var materials))
|
||||
{
|
||||
return materials;
|
||||
}
|
||||
|
||||
// If we got a renderer instead, get its materials
|
||||
if (ReflectionUtils.TryGetValue<UnityEngine.Renderer>(target, sourceName, out var renderer))
|
||||
{
|
||||
return renderer?.sharedMaterials;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
var attr = attribute as MaterialPropertiesAttribute;
|
||||
m_root = new VisualElement();
|
||||
m_serializedObject = property.serializedObject;
|
||||
|
||||
var target = property.serializedObject.targetObject;
|
||||
var materials = GetMaterialsFromSource(target, attr.MaterialsSourceName);
|
||||
|
||||
if (materials == null)
|
||||
{
|
||||
m_root.Add(new HelpBox($"Could not find materials source: {attr.MaterialsSourceName}", HelpBoxMessageType.Error));
|
||||
return m_root;
|
||||
}
|
||||
|
||||
UpdateUI(property, materials, attr.PropertyType);
|
||||
return m_root;
|
||||
}
|
||||
|
||||
private void UpdateUI(SerializedProperty property, Material[] materials, MaterialShaderPropertyType propertyType)
|
||||
{
|
||||
m_root.Clear();
|
||||
UpdateAvailablePropertyNames(materials, propertyType);
|
||||
|
||||
var keysProperty = property.FindPropertyRelative(SerializedDictionary<int, Components.RiveTextureRenderer.PropertyNameListHolder>.BindingPath_Keys);
|
||||
var valuesProperty = property.FindPropertyRelative(SerializedDictionary<int, Components.RiveTextureRenderer.PropertyNameListHolder>.BindingPath_Values);
|
||||
|
||||
// Pre-create property holders for all materials
|
||||
EnsurePropertyHoldersExist(keysProperty, valuesProperty, materials.Length);
|
||||
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
var material = materials[i];
|
||||
if (material == null) continue;
|
||||
|
||||
var materialFoldout = new Foldout { text = $"Material {i}: {material.name}" };
|
||||
m_root.Add(materialFoldout);
|
||||
|
||||
var propertyListHolder = FindPropertyListHolder(keysProperty, valuesProperty, i);
|
||||
if (propertyListHolder != null)
|
||||
{
|
||||
var propertyList = propertyListHolder.FindPropertyRelative(Components.RiveTextureRenderer.PropertyNameListHolder.BindingPath_PropertyNames);
|
||||
if (propertyList != null && propertyList.serializedObject != null)
|
||||
{
|
||||
var listView = CreateListView(propertyList);
|
||||
materialFoldout.Add(listView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply any changes made during setup
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private void EnsurePropertyHoldersExist(SerializedProperty keysProperty, SerializedProperty valuesProperty, int materialCount)
|
||||
{
|
||||
// First, create a list of existing material indices
|
||||
var existingIndices = new HashSet<int>();
|
||||
for (int i = 0; i < keysProperty.arraySize; i++)
|
||||
{
|
||||
existingIndices.Add(keysProperty.GetArrayElementAtIndex(i).intValue);
|
||||
}
|
||||
|
||||
// Create missing property holders
|
||||
for (int i = 0; i < materialCount; i++)
|
||||
{
|
||||
if (!existingIndices.Contains(i))
|
||||
{
|
||||
keysProperty.InsertArrayElementAtIndex(keysProperty.arraySize);
|
||||
keysProperty.GetArrayElementAtIndex(keysProperty.arraySize - 1).intValue = i;
|
||||
|
||||
valuesProperty.InsertArrayElementAtIndex(valuesProperty.arraySize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SerializedProperty FindPropertyListHolder(SerializedProperty keysProperty, SerializedProperty valuesProperty, int materialIndex)
|
||||
{
|
||||
for (int i = 0; i < keysProperty.arraySize; i++)
|
||||
{
|
||||
if (keysProperty.GetArrayElementAtIndex(i).intValue == materialIndex)
|
||||
{
|
||||
return valuesProperty.GetArrayElementAtIndex(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ListView CreateListView(SerializedProperty propertyList)
|
||||
{
|
||||
var listView = new ListView()
|
||||
{
|
||||
reorderable = true,
|
||||
showAddRemoveFooter = true,
|
||||
showBorder = true,
|
||||
showFoldoutHeader = false,
|
||||
showBoundCollectionSize = false,
|
||||
virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight
|
||||
};
|
||||
|
||||
// Delay binding until the next frame to ensure proper initialization
|
||||
m_root.schedule.Execute(() =>
|
||||
{
|
||||
listView.bindingPath = propertyList.propertyPath;
|
||||
listView.BindProperty(propertyList.serializedObject);
|
||||
});
|
||||
|
||||
listView.makeItem = () => new PopupOrTextField(m_availablePropertyNames, "");
|
||||
listView.bindItem = (element, index) =>
|
||||
{
|
||||
var popupOrTextField = element as PopupOrTextField;
|
||||
popupOrTextField.Choices = m_availablePropertyNames;
|
||||
|
||||
if (propertyList != null && propertyList.serializedObject != null)
|
||||
{
|
||||
var itemProperty = propertyList.GetArrayElementAtIndex(index);
|
||||
popupOrTextField.BindProperty(itemProperty);
|
||||
}
|
||||
};
|
||||
|
||||
listView.itemsAdded += (indexes) =>
|
||||
{
|
||||
if (propertyList != null && propertyList.serializedObject != null)
|
||||
{
|
||||
foreach (int index in indexes)
|
||||
{
|
||||
var itemProperty = propertyList.GetArrayElementAtIndex(index);
|
||||
if (string.IsNullOrEmpty(itemProperty.stringValue))
|
||||
{
|
||||
itemProperty.stringValue = m_availablePropertyNames.FirstOrDefault() ?? "";
|
||||
propertyList.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
listView.Rebuild();
|
||||
};
|
||||
|
||||
return listView;
|
||||
}
|
||||
|
||||
private static int GetPropertyCount(Shader shader)
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
return shader.GetPropertyCount();
|
||||
#else
|
||||
return ShaderUtil.GetPropertyCount(shader);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static MaterialShaderPropertyType GetPropertyType(Shader shader, int index)
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
return shader.GetPropertyType(index);
|
||||
#else
|
||||
return ShaderUtil.GetPropertyType(shader, index);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static string GetPropertyName(Shader shader, int index)
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
return shader.GetPropertyName(index);
|
||||
#else
|
||||
return ShaderUtil.GetPropertyName(shader, index);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UpdateAvailablePropertyNames(Material[] materials, MaterialShaderPropertyType propertyType)
|
||||
{
|
||||
m_availablePropertyNames.Clear();
|
||||
|
||||
foreach (var material in materials)
|
||||
{
|
||||
if (material != null)
|
||||
{
|
||||
var shader = material.shader;
|
||||
for (int i = 0; i < GetPropertyCount(shader); i++)
|
||||
{
|
||||
if (GetPropertyType(shader, i) == propertyType)
|
||||
{
|
||||
string propertyName = GetPropertyName(shader, i);
|
||||
if (!m_availablePropertyNames.Contains(propertyName))
|
||||
{
|
||||
m_availablePropertyNames.Add(propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13ea997b79c3b4e1b82a9adeabd70082
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 350858
|
||||
packageName: Rive
|
||||
packageVersion: 0.4.2
|
||||
assetPath: Packages/app.rive.rive-unity/Editor/Components/CustomElements/MaterialPropertiesDrawer.cs
|
||||
uploadId: 896810
|
||||
@@ -0,0 +1,445 @@
|
||||
using UnityEngine.UIElements;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEditor.UIElements;
|
||||
using Rive.Utils;
|
||||
|
||||
namespace Rive.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// A visual element that allows the user to select from a list of choices or enter a custom value.
|
||||
/// </summary>
|
||||
internal class PopupOrTextField : VisualElement, INotifyValueChanged<string>
|
||||
{
|
||||
private PopupField<string> popupField;
|
||||
private TextField textField;
|
||||
private Button switchModeButton;
|
||||
private bool isCustomValue;
|
||||
private bool isUserEditing;
|
||||
private bool isProgrammaticChange;
|
||||
private SerializedProperty boundProperty;
|
||||
private SerializedObject serializedObject;
|
||||
private UnityEngine.Object targetObject;
|
||||
|
||||
private string m_Value;
|
||||
public string value
|
||||
{
|
||||
get => m_Value;
|
||||
set
|
||||
{
|
||||
if (m_Value != value)
|
||||
{
|
||||
using (var changeEvent = ChangeEvent<string>.GetPooled(m_Value, value))
|
||||
{
|
||||
changeEvent.target = this;
|
||||
SetValueWithoutNotify(value);
|
||||
SendEvent(changeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> Choices
|
||||
{
|
||||
get => popupField.choices;
|
||||
set
|
||||
{
|
||||
popupField.choices = value;
|
||||
bool valueIsInChoices = value.Contains(m_Value);
|
||||
|
||||
// Handle transition from Popup to Custom
|
||||
if (!isCustomValue && !valueIsInChoices)
|
||||
{
|
||||
isCustomValue = true;
|
||||
isProgrammaticChange = true;
|
||||
SetValueWithoutNotify(m_Value);
|
||||
isProgrammaticChange = false;
|
||||
}
|
||||
// Handle transition from Custom to Popup
|
||||
else if (valueIsInChoices && isCustomValue)
|
||||
{
|
||||
isCustomValue = false;
|
||||
isProgrammaticChange = true;
|
||||
SetValueWithoutNotify(m_Value);
|
||||
isProgrammaticChange = false;
|
||||
}
|
||||
|
||||
UpdateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
public string Label
|
||||
{
|
||||
get => popupField.label;
|
||||
set
|
||||
{
|
||||
popupField.label = value;
|
||||
textField.label = value;
|
||||
}
|
||||
}
|
||||
|
||||
public PopupOrTextField() : this(new List<string>(), "") { }
|
||||
|
||||
public PopupOrTextField(List<string> choices, string currentValue, string labelText = null)
|
||||
{
|
||||
popupField = new PopupField<string>(choices, 0);
|
||||
textField = new TextField();
|
||||
switchModeButton = new Button(ToggleMode)
|
||||
{
|
||||
text = "✎",
|
||||
tooltip = "Switch to text input"
|
||||
};
|
||||
|
||||
SetupUI();
|
||||
SetupCallbacks();
|
||||
SetInitialState(currentValue);
|
||||
|
||||
if (labelText != null)
|
||||
{
|
||||
Label = labelText;
|
||||
}
|
||||
|
||||
RegisterCallback<SerializedPropertyChangeEvent>(OnSerializedPropertyChange);
|
||||
}
|
||||
|
||||
private void SetupUI()
|
||||
{
|
||||
var container = new VisualElement();
|
||||
container.style.flexDirection = FlexDirection.Row;
|
||||
container.style.width = new StyleLength(Length.Percent(100));
|
||||
container.Add(popupField);
|
||||
container.Add(textField);
|
||||
container.Add(switchModeButton);
|
||||
|
||||
SetupFieldStyles(popupField);
|
||||
SetupFieldStyles(textField);
|
||||
SetupButtonStyles(switchModeButton);
|
||||
|
||||
textField.style.display = DisplayStyle.None;
|
||||
textField.visible = false;
|
||||
popupField.style.display = DisplayStyle.Flex;
|
||||
popupField.visible = true;
|
||||
|
||||
Add(container);
|
||||
}
|
||||
|
||||
private void SetupFieldStyles(VisualElement field)
|
||||
{
|
||||
// This keeps inspector positioned around the same point as other unity fields. Otherwise the popup fills the whole row, when it should stop in the middle.
|
||||
field.AddToClassList(BaseField<PropertyField>.alignedFieldUssClassName); // Same as using "unity-base-field__aligned" in UXML
|
||||
|
||||
|
||||
field.style.flexGrow = 1;
|
||||
field.style.marginRight = 20;
|
||||
field.style.paddingBottom = 0;
|
||||
field.style.paddingTop = 0;
|
||||
field.style.paddingLeft = 0;
|
||||
field.style.marginBottom = 0;
|
||||
field.style.marginTop = 0;
|
||||
field.style.marginLeft = 0;
|
||||
}
|
||||
|
||||
private void SetupButtonStyles(Button button)
|
||||
{
|
||||
button.style.position = Position.Absolute;
|
||||
button.style.right = 0;
|
||||
button.style.width = 20;
|
||||
button.style.height = popupField.style.height;
|
||||
button.style.marginRight = 0;
|
||||
button.style.marginLeft = 0;
|
||||
button.style.marginTop = 0;
|
||||
button.style.marginBottom = 0;
|
||||
button.style.paddingBottom = 0;
|
||||
button.style.paddingTop = 0;
|
||||
button.style.paddingLeft = 0;
|
||||
button.style.paddingRight = 0;
|
||||
}
|
||||
|
||||
|
||||
private void SetupCallbacks()
|
||||
{
|
||||
popupField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (isProgrammaticChange)
|
||||
return; // Ignore programmatic changes to prevent recursive calls
|
||||
|
||||
value = evt.newValue;
|
||||
});
|
||||
|
||||
textField.RegisterCallback<FocusInEvent>(evt => isUserEditing = true);
|
||||
textField.RegisterCallback<FocusOutEvent>(evt =>
|
||||
{
|
||||
isUserEditing = false;
|
||||
UpdateVisualState();
|
||||
});
|
||||
|
||||
textField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (isProgrammaticChange)
|
||||
return;
|
||||
|
||||
value = evt.newValue;
|
||||
});
|
||||
}
|
||||
|
||||
private void SetInitialState(string initialValue)
|
||||
{
|
||||
m_Value = initialValue;
|
||||
isCustomValue = !Choices.Contains(initialValue);
|
||||
|
||||
if (!isCustomValue)
|
||||
{
|
||||
popupField.SetValueWithoutNotify(initialValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
textField.SetValueWithoutNotify(initialValue);
|
||||
}
|
||||
|
||||
UpdateVisibility();
|
||||
}
|
||||
|
||||
private void ToggleMode()
|
||||
{
|
||||
if (targetObject != null)
|
||||
{
|
||||
Undo.RecordObject(targetObject, "Toggle PopupOrTextField Mode");
|
||||
}
|
||||
|
||||
var initialValue = m_Value;
|
||||
isCustomValue = !isCustomValue;
|
||||
|
||||
if (isCustomValue)
|
||||
{
|
||||
textField.SetValueWithoutNotify(m_Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Choices.Contains(m_Value))
|
||||
{
|
||||
popupField.SetValueWithoutNotify(m_Value);
|
||||
}
|
||||
else if (Choices.Count > 0)
|
||||
{
|
||||
SetValueWithoutNotify(Choices[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
popupField.SetValueWithoutNotify(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateVisibility();
|
||||
|
||||
if (targetObject != null)
|
||||
{
|
||||
EditorUtility.SetDirty(targetObject);
|
||||
}
|
||||
|
||||
if (initialValue != m_Value)
|
||||
{
|
||||
using (var changeEvent = ChangeEvent<string>.GetPooled(initialValue, m_Value))
|
||||
{
|
||||
changeEvent.target = this;
|
||||
SendEvent(changeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisualState()
|
||||
{
|
||||
bool valueInChoices = Choices.Contains(m_Value);
|
||||
|
||||
if (valueInChoices)
|
||||
{
|
||||
if (!isCustomValue)
|
||||
{
|
||||
// We make sure the popupField reflects the current value
|
||||
popupField.SetValueWithoutNotify(m_Value);
|
||||
}
|
||||
|
||||
if (isCustomValue && !isUserEditing)
|
||||
{
|
||||
isCustomValue = false;
|
||||
UpdateVisibility();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
textField.SetValueWithoutNotify(m_Value);
|
||||
if (!isCustomValue)
|
||||
{
|
||||
isCustomValue = true;
|
||||
UpdateVisibility();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisibility()
|
||||
{
|
||||
if (isCustomValue)
|
||||
{
|
||||
popupField.style.display = DisplayStyle.None;
|
||||
popupField.visible = false;
|
||||
|
||||
textField.style.display = DisplayStyle.Flex;
|
||||
textField.visible = true;
|
||||
|
||||
switchModeButton.text = "▼";
|
||||
switchModeButton.tooltip = "Switch to dropdown";
|
||||
}
|
||||
else
|
||||
{
|
||||
textField.style.display = DisplayStyle.None;
|
||||
textField.visible = false;
|
||||
|
||||
popupField.style.display = DisplayStyle.Flex;
|
||||
popupField.visible = true;
|
||||
|
||||
switchModeButton.text = "✎";
|
||||
switchModeButton.tooltip = "Switch to text input";
|
||||
}
|
||||
|
||||
this.MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
public void SetValueWithoutNotify(string newValue)
|
||||
{
|
||||
if (serializedObject != null)
|
||||
{
|
||||
serializedObject.Update();
|
||||
}
|
||||
|
||||
m_Value = newValue;
|
||||
|
||||
// Force check if value is in choices and update mode accordingly
|
||||
bool valueInChoices = Choices.Contains(newValue);
|
||||
|
||||
if (valueInChoices && (!isUserEditing || isProgrammaticChange))
|
||||
{
|
||||
isCustomValue = false;
|
||||
isProgrammaticChange = true;
|
||||
popupField.SetValueWithoutNotify(newValue);
|
||||
isProgrammaticChange = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!valueInChoices)
|
||||
{
|
||||
isCustomValue = true;
|
||||
}
|
||||
isProgrammaticChange = true;
|
||||
textField.SetValueWithoutNotify(newValue);
|
||||
isProgrammaticChange = false;
|
||||
}
|
||||
|
||||
UpdateVisibility();
|
||||
|
||||
if (boundProperty != null)
|
||||
{
|
||||
boundProperty.stringValue = newValue;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSerializedPropertyChange(SerializedPropertyChangeEvent evt)
|
||||
{
|
||||
if (evt.changedProperty == boundProperty)
|
||||
{
|
||||
isProgrammaticChange = true;
|
||||
SetValueWithoutNotify(boundProperty.stringValue);
|
||||
isProgrammaticChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void BindProperty(SerializedProperty property)
|
||||
{
|
||||
UnbindProperty();
|
||||
|
||||
if (property != null && property.propertyType == SerializedPropertyType.String)
|
||||
{
|
||||
boundProperty = property;
|
||||
serializedObject = property.serializedObject;
|
||||
targetObject = serializedObject.targetObject;
|
||||
SetInitialState(property.stringValue);
|
||||
EditorApplication.update += UpdateFromSerializedProperty;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugLogger.Instance.LogError("PopupOrTextField: Attempted to bind to a null or non-string property.");
|
||||
}
|
||||
|
||||
this.RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
|
||||
this.RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
|
||||
}
|
||||
|
||||
private void OnAttachToPanel(AttachToPanelEvent evt)
|
||||
{
|
||||
if (serializedObject != null && boundProperty != null)
|
||||
{
|
||||
serializedObject.Update();
|
||||
SetInitialState(boundProperty.stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDetachFromPanel(DetachFromPanelEvent evt)
|
||||
{
|
||||
UnbindProperty();
|
||||
}
|
||||
|
||||
private void UpdateFromSerializedProperty()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (serializedObject == null || boundProperty == null)
|
||||
{
|
||||
UnbindProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the serializedObject is still valid
|
||||
if (serializedObject.targetObject == null)
|
||||
{
|
||||
UnbindProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
// Double-check everything is still valid after the update
|
||||
if (boundProperty == null || boundProperty.serializedObject == null || boundProperty.serializedObject.targetObject == null)
|
||||
{
|
||||
UnbindProperty();
|
||||
return;
|
||||
}
|
||||
|
||||
if (boundProperty.propertyType == SerializedPropertyType.String)
|
||||
{
|
||||
string newValue = boundProperty.stringValue;
|
||||
if (m_Value != newValue && !isUserEditing)
|
||||
{
|
||||
SetValueWithoutNotify(newValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugLogger.Instance.LogWarning($"PopupOrTextField: Bound property is not a string. Property path: {boundProperty.propertyPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
UnbindProperty();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnbindProperty()
|
||||
{
|
||||
boundProperty = null;
|
||||
serializedObject = null;
|
||||
targetObject = null;
|
||||
EditorApplication.update -= UpdateFromSerializedProperty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 226b35944e5bc4908b226f5fb91d99d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 350858
|
||||
packageName: Rive
|
||||
packageVersion: 0.4.2
|
||||
assetPath: Packages/app.rive.rive-unity/Editor/Components/CustomElements/PopupOrTextField.cs
|
||||
uploadId: 896810
|
||||
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
using Rive.Utils;
|
||||
|
||||
namespace Rive.EditorTools
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(WidthHeightDimensionsAttribute))]
|
||||
internal class WidthHeightDimensionsDrawer : PropertyDrawer
|
||||
{
|
||||
public override VisualElement CreatePropertyGUI(SerializedProperty property)
|
||||
{
|
||||
var attr = attribute as WidthHeightDimensionsAttribute;
|
||||
var label = ReflectionUtils.GetPropertyLabel(property) ?? attr.Label;
|
||||
|
||||
// Get tooltip from TooltipAttribute if present
|
||||
string tooltip = null;
|
||||
var tooltipAttribute = fieldInfo.GetCustomAttributes(typeof(TooltipAttribute), true);
|
||||
if (tooltipAttribute.Length > 0)
|
||||
{
|
||||
tooltip = (tooltipAttribute[0] as TooltipAttribute).tooltip;
|
||||
}
|
||||
|
||||
var field = new WidthHeightDimensionsField(
|
||||
label,
|
||||
attr.WidthLabel,
|
||||
attr.HeightLabel,
|
||||
tooltip
|
||||
);
|
||||
|
||||
field.BindProperty(property);
|
||||
return field;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d32c71bc31ac443d5a36065d1d672d12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 350858
|
||||
packageName: Rive
|
||||
packageVersion: 0.4.2
|
||||
assetPath: Packages/app.rive.rive-unity/Editor/Components/CustomElements/WidthHeightDimensionsDrawer.cs
|
||||
uploadId: 896810
|
||||
@@ -0,0 +1,50 @@
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Rive.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// A field for editing a Vector2Int representing width and height.
|
||||
/// </summary>
|
||||
internal class WidthHeightDimensionsField : VisualElement
|
||||
{
|
||||
|
||||
public IntegerField WidthField { get; private set; }
|
||||
public IntegerField HeightField { get; private set; }
|
||||
|
||||
public WidthHeightDimensionsField(string label, string widthLabel = "Width", string heightLabel = "Height", string tooltip = null)
|
||||
{
|
||||
var foldout = new Foldout
|
||||
{
|
||||
text = label,
|
||||
tooltip = tooltip,
|
||||
value = true // Start expanded
|
||||
};
|
||||
Add(foldout);
|
||||
|
||||
var container = new VisualElement();
|
||||
foldout.Add(container);
|
||||
|
||||
WidthField = new IntegerField(widthLabel)
|
||||
{
|
||||
style = { marginTop = 4 }
|
||||
};
|
||||
WidthField.AddToClassList(BaseField<int>.alignedFieldUssClassName);
|
||||
container.Add(WidthField);
|
||||
|
||||
HeightField = new IntegerField(heightLabel)
|
||||
{
|
||||
style = { marginTop = 4 }
|
||||
};
|
||||
HeightField.AddToClassList(BaseField<int>.alignedFieldUssClassName);
|
||||
container.Add(HeightField);
|
||||
}
|
||||
|
||||
public void BindProperty(SerializedProperty property)
|
||||
{
|
||||
WidthField.BindProperty(property.FindPropertyRelative("x"));
|
||||
HeightField.BindProperty(property.FindPropertyRelative("y"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18c92b4c6ab3f42e6a1e827ed99c91fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 350858
|
||||
packageName: Rive
|
||||
packageVersion: 0.4.2
|
||||
assetPath: Packages/app.rive.rive-unity/Editor/Components/CustomElements/WidthHeightDimensionsField.cs
|
||||
uploadId: 896810
|
||||
Reference in New Issue
Block a user