From 5be26c85a9b5087874fd87e5a29558f551342777 Mon Sep 17 00:00:00 2001 From: Gregory Labute Date: Wed, 12 Jun 2024 11:10:25 -0400 Subject: [PATCH] CMCL-1587: cleanup UX layout code. fix alignment for U6 (#989) * cleanup UX layout code. fix alignment for U6 * Update InspectorUtility.cs * merge errors * Update CinemachineSplineRollEditor.cs * merge errors * bugfixes * Remove bindItem from BlenderSettingsEditor * Update InspectorUtility.cs * get rid of bindItem in SequencerEditor * get rid of bindItem in StateDrivenCamera editor * fix bindItem in CameraInspectorUtility * add some missing draggers * layout tweaking * Simplify SplineDataInspectorUtility.CreateDataListField * cleanup code for creating draggable properties * remove bindItem from SplineRoll * remove bindItem for SplineLookAtTargets * minor layout tweaks * Minor tweaks, LabeledRow refactor (one container less) * More reduction of excess containers * simplification * Update CinemachineFollowZoom.cs * Update InputAxisPropertyDrawer.cs * fix upgrade message layout * better handling of missing target warnings * layout tweaks * No SaveDuringPlay for PerlinNoise.m_NoiseOffset * Add isDelayed on some fields * Cleanup SaveDuringPlay for input axis controller * Add some missing NoSaveDuringPlay --- .../CinemachineBlenderSettingsEditor.cs | 80 ++- .../Editor/Editors/CinemachineCameraEditor.cs | 7 +- .../CinemachineFreeLookModifierEditor.cs | 13 +- .../Editors/CinemachineOrbitalFollowEditor.cs | 2 +- .../CinemachineSequencerCameraEditor.cs | 76 +-- ...nemachineSplineDollyLookAtTargetsEditor.cs | 210 ++++---- .../Editors/CinemachineSplineRollEditor.cs | 98 ++-- .../CinemachineStateDrivenCameraEditor.cs | 110 ++-- .../Editors/CinemachineStoryboardEditor.cs | 9 +- .../Editors/InputAxisControllerEditor.cs | 8 +- .../CinemachineVirtualCameraBaseEditor.cs | 2 +- .../Obsolete/ObsoleteInspectorUtility.cs | 240 +++++++++ .../Obsolete/ObsoleteInspectorUtility.cs.meta | 11 + .../Obsolete/ObsoletePropertyDrawers.cs | 2 +- .../CameraTargetPropertyDrawer.cs | 2 +- .../DelayedVectorPropertyDrawer.cs | 2 +- .../EmbeddedAssetPropertyDrawer.cs | 2 +- .../FoldoutWithEnabledButtonPropertyDrawer.cs | 4 +- .../InputAxisPropertyDrawer.cs | 32 +- .../LensSettingsPropertyDrawer.cs | 13 +- .../MinMaxRangeSliderPropertyDrawer.cs | 5 +- .../OutputChannelsPropertyDrawer.cs | 2 +- .../SensorSizePropertyDrawer.cs | 3 +- .../SplineAutoDollyPropertyDrawer.cs | 2 +- .../PropertyDrawers/TagFieldPropertyDrawer.cs | 2 +- .../Vector2AsRangePropertyDrawer.cs | 1 - .../Editor/Timeline/CinemachineShotEditor.cs | 2 +- .../Utility/CmCameraInspectorUtility.cs | 88 +-- .../CmPipelineComponentInspectorUtility.cs | 12 +- .../Utility/DelayedFriendlyFieldDragger.cs | 30 +- .../Utility/EmbeddedAssetEditorUtility.cs | 25 +- .../Editor/Utility/InspectorUtility.cs | 508 +++++------------- .../Editor/Utility/ReflectionHelpers.cs | 2 + .../Utility/SerializedPropertyHelper.cs | 50 +- .../Utility/SplineDataInspectorUtility.cs | 106 ++-- .../Editor/Windows/WaveformWindow.cs | 2 +- .../Behaviours/CinemachineClearShot.cs | 4 +- .../Behaviours/CinemachineConfiner2D.cs | 2 +- .../Behaviours/CinemachineDecollider.cs | 1 + .../Behaviours/CinemachineFollowZoom.cs | 6 +- .../Behaviours/CinemachineSequencerCamera.cs | 4 +- .../CinemachineShotQualityEvaluator.cs | 1 + .../Behaviours/CinemachineSplineCart.cs | 6 +- .../CinemachineStateDrivenCamera.cs | 8 +- .../Behaviours/CinemachineTargetGroup.cs | 2 +- .../CinemachineBasicMultiChannelPerlin.cs | 32 +- .../Runtime/Components/CinemachineFollow.cs | 6 +- .../Components/CinemachineSplineDolly.cs | 6 +- .../Core/CinemachinePropertyAttribute.cs | 2 +- .../Core/CinemachineVirtualCameraBase.cs | 2 +- .../Runtime/Core/InputAxisControllerBase.cs | 2 +- .../Impulse/CinemachineImpulseListener.cs | 2 +- .../PostProcessing/CinemachineAutoFocus.cs | 2 +- 53 files changed, 973 insertions(+), 878 deletions(-) create mode 100644 com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs create mode 100644 com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs.meta diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineBlenderSettingsEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineBlenderSettingsEditor.cs index 1f3b460ed..928bfe3a0 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineBlenderSettingsEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineBlenderSettingsEditor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using UnityEngine.UIElements; using UnityEditor.UIElements; +using System; namespace Unity.Cinemachine.Editor { @@ -55,6 +56,7 @@ public override VisualElement CreateInspectorGUI() // Gather the camera candidates var availableCameras = new List(); Dictionary cameraIndexLookup = new(); + Action onCamerasUpdated = null; list.TrackAnyUserActivity(() => { var allCameras = new List(); @@ -65,29 +67,47 @@ public override VisualElement CreateInspectorGUI() for (int i = 0; i < allCameras.Count; ++i) if (allCameras[i] != null && !availableCameras.Contains(allCameras[i].Name)) availableCameras.Add(allCameras[i].Name); - list.RefreshItems(); // rebuild the list + onCamerasUpdated?.Invoke(); }); - list.makeItem = () => new BindableElement { style = { flexDirection = FlexDirection.Row }}; - list.bindItem = (row, index) => + list.makeItem = () => { - // Remove children - items get recycled - for (int i = row.childCount - 1; i >= 0; --i) - row.RemoveAt(i); - var def = new CinemachineBlenderSettings.CustomBlend(); - var element = index < elements.arraySize ? elements.GetArrayElementAtIndex(index) : null; - if (!IsUnityNull(element)) + var row = new BindableElement { style = { flexDirection = FlexDirection.Row }}; + var from = row.AddChild(CreateCameraPopup(SerializedPropertyHelper.PropertyName(() => def.From))); + var to = row.AddChild(CreateCameraPopup(SerializedPropertyHelper.PropertyName(() => def.To))); + var blend = row.AddChild(new PropertyField(null, "") { bindingPath = SerializedPropertyHelper.PropertyName(() => def.Blend)}); + FormatElement(false, from, to, blend); + return row; + + // Local function + VisualElement CreateCameraPopup(string bindingPath) { - var from = row.AddChild(CreateCameraPopup(element.FindPropertyRelative(() => def.From))); - var to = row.AddChild(CreateCameraPopup(element.FindPropertyRelative(() => def.To))); - var blend = row.AddChild(new PropertyField(element.FindPropertyRelative(() => def.Blend), "")); - FormatElement(false, from, to, blend); + var container = new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}; + var textField = container.AddChild(new TextField { bindingPath = bindingPath, isDelayed = true, style = { flexGrow = 1, flexBasis = 20 }}); - ((BindableElement)row).BindProperty(element); // bind must be done at the end + var warning = container.AddChild(InspectorUtility.MiniHelpIcon($"No in-scene camera matches this name")); + textField.RegisterValueChangedCallback((evt) => OnCameraUpdated()); + onCamerasUpdated += OnCameraUpdated; + void OnCameraUpdated() + { + warning.tooltip = $"No in-scene camera matches \"{textField.value}\""; + warning.SetVisible(availableCameras.FindIndex(x => x == textField.value) < 0); + }; + + var popup = container.AddChild(InspectorUtility.MiniDropdownButton( + "Choose from currently-available cameras", new ContextualMenuManipulator((evt) => + { + for (int i = 0; i < availableCameras.Count; ++i) + evt.menu.AppendAction(availableCameras[i], (action) => textField.value = action.name); + }))); + popup.style.marginRight = 5; + return container; } }; + return ux; + // Local function static void FormatElement(bool isHeader, VisualElement e1, VisualElement e2, VisualElement e3) { @@ -101,38 +121,6 @@ static void FormatElement(bool isHeader, VisualElement e1, VisualElement e2, Vis e3.style.flexBasis = 1; e3.style.flexGrow = 2; } - - // Local function - VisualElement CreateCameraPopup(SerializedProperty p) - { - var row = new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}; - var textField = row.AddChild(new TextField { isDelayed = true, style = { flexGrow = 1, flexBasis = 20 }}); - textField.BindProperty(p); - if (availableCameras.FindIndex(x => x == p.stringValue) < 0) - row.AddChild(InspectorUtility.MiniHelpIcon("No in-scene camera matches this name")); - var popup = row.AddChild(InspectorUtility.MiniDropdownButton( - "Choose from currently-available cameras", new ContextualMenuManipulator((evt) => - { - for (int i = 0; i < availableCameras.Count; ++i) - evt.menu.AppendAction(availableCameras[i], - (action) => - { - p.stringValue = action.name; - p.serializedObject.ApplyModifiedProperties(); - }); - }))); - popup.style.marginRight = 5; - return row; - } - - // Local function - static bool IsUnityNull(object obj) - { - // Checks whether an object is null or Unity pseudo-null - // without having to cast to UnityEngine.Object manually - return obj == null || ((obj is UnityEngine.Object) && ((UnityEngine.Object)obj) == null); - } - return ux; } } } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineCameraEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineCameraEditor.cs index df3fdab5e..eadfb8e50 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineCameraEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineCameraEditor.cs @@ -44,9 +44,12 @@ public override VisualElement CreateInspectorGUI() var defaultTargetLabel = new ObjectField(""); defaultTargetLabel.SetEnabled(false); - var defaultTargetRow = ux.AddChild(new InspectorUtility.LabeledRow("Default Target", "", defaultTargetLabel)); - defaultTargetRow.tooltip = "The default target is set in the parent object, and will be used if the Tracking Target is None"; + var defaultTargetRow = ux.AddChild(new InspectorUtility.LabeledRow( + "Default Target", "The default target is set in the parent object, and will be used if the Tracking Target is None", + defaultTargetLabel)); defaultTargetRow.focusable = false; + defaultTargetLabel.style.marginLeft = 5; + defaultTargetLabel.style.marginRight = -2; ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.Target))); ux.AddHeader("Global Settings"); diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineFreeLookModifierEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineFreeLookModifierEditor.cs index b613efe0c..c87782962 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineFreeLookModifierEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineFreeLookModifierEditor.cs @@ -17,9 +17,14 @@ public override VisualElement CreateInspectorGUI() { var ux = new VisualElement(); + ux.Add(new HelpBox("This component is optional and can be removed if you don't need it. " + + "The modifiers you add will override settings for the top and bottom portions " + + "of the camera's vertical orbit.", + HelpBoxMessageType.Info)); + var invalidSrcMsg = ux.AddChild( - new HelpBox("This component will be ignored because no applicable target components are present.\n\n" - + "Applicable target components include: " + new HelpBox("Component will be ignored because no modifiable targets are present.\n\n" + + "Modifiable target components include: " + InspectorUtility.GetAssignableBehaviourNames( typeof(CinemachineFreeLookModifier.IModifierValueSource)), HelpBoxMessageType.Warning)); @@ -110,7 +115,9 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) warningSymbol.SetVisible(showWarning); }); - return new InspectorUtility.FoldoutWithOverlay(foldout, overlay, null); + var ux = new InspectorUtility.FoldoutWithOverlay(foldout, overlay, null); + ux.style.marginLeft = 12; + return ux; } } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineOrbitalFollowEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineOrbitalFollowEditor.cs index edfb4b7a7..0199bf6a9 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineOrbitalFollowEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineOrbitalFollowEditor.cs @@ -16,8 +16,8 @@ public override VisualElement CreateInspectorGUI() { var ux = new VisualElement(); this.AddMissingCmCameraHelpBox(ux); - ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.TargetOffset))); ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.TrackerSettings))); + ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.TargetOffset))); ux.AddSpace(); var orbitModeProp = serializedObject.FindProperty(() => Target.OrbitStyle); diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineSequencerCameraEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineSequencerCameraEditor.cs index d11d06bbe..5a0366e77 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineSequencerCameraEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineSequencerCameraEditor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; @@ -41,7 +42,6 @@ public override VisualElement CreateInspectorGUI() var list = container.AddChild(new ListView() { - name = "InstructionList", reorderable = true, reorderMode = ListViewReorderMode.Animated, showAddRemoveFooter = true, @@ -53,34 +53,28 @@ public override VisualElement CreateInspectorGUI() var instructions = serializedObject.FindProperty(() => Target.Instructions); list.BindProperty(instructions); - list.makeItem = () => new BindableElement { style = { flexDirection = FlexDirection.Row }}; - list.bindItem = (row, index) => + // Available camera candidates + var availableCameras = new List(); + + list.makeItem = () => { - // Remove children - items get recycled - for (int i = row.childCount - 1; i >= 0; --i) - row.RemoveAt(i); + var row = new BindableElement { style = { flexDirection = FlexDirection.Row }}; var def = new CinemachineSequencerCamera.Instruction(); - var element = instructions.GetArrayElementAtIndex(index); - - var vcamSelProp = element.FindPropertyRelative(() => def.Camera); - var vcamSel = row.AddChild(new PopupField { name = $"vcamSelector{index}", choices = new() }); - vcamSel.formatListItemCallback = (obj) => obj == null ? "(null)" : obj.name; - vcamSel.formatSelectedValueCallback = (obj) => obj == null ? "(null)" : obj.name; - vcamSel.TrackPropertyWithInitialCallback(instructions, (p) => UpdateCameraDropdowns()); + var vcamSel = row.AddChild(new PopupField + { + bindingPath = SerializedPropertyHelper.PropertyName(() => def.Camera), + choices = availableCameras, + formatListItemCallback = (obj) => obj == null ? "(null)" : obj.name, + formatSelectedValueCallback = (obj) => obj == null ? "(null)" : obj.name + }); - var blend = row.AddChild(new PropertyField(element.FindPropertyRelative(() => def.Blend), "")); - if (index == 0) - blend.name = "FirstItemBlend"; - var hold = row.AddChild( - new InspectorUtility.CompactPropertyField(element.FindPropertyRelative(() => def.Hold), " ")); - hold.RemoveFromClassList(InspectorUtility.kAlignFieldClass); + var blend = row.AddChild(new PropertyField(null, "") { bindingPath = SerializedPropertyHelper.PropertyName(() => def.Blend), name = "blendSelector" }); + var hold = row.AddChild(InspectorUtility.CreateDraggableField(() => def.Hold, row.AddChild(new Label(" ")), out _)); + hold.SafeSetIsDelayed(); FormatInstructionElement(false, vcamSel, blend, hold); - - // Bind must be last - ((BindableElement)row).BindProperty(element); - vcamSel.BindProperty(vcamSelProp); + return row; }; container.AddSpace(); @@ -88,7 +82,7 @@ public override VisualElement CreateInspectorGUI() container.TrackAnyUserActivity(() => { - if (Target == null) + if (Target == null || list.itemsSource == null) return; // object deleted var isMultiSelect = targets.Length > 1; @@ -96,34 +90,18 @@ public override VisualElement CreateInspectorGUI() container.SetVisible(!isMultiSelect); // Hide the first blend if not looped - list.Q("FirstItemBlend")?.SetEnabled(Target.Loop); + var index = 0; + list.Query().Where((e) => e.name == "blendSelector").ForEach((e) + => e.style.visibility = (index++ == 0 && !Target.Loop) ? Visibility.Hidden : Visibility.Visible); - // Update the list items - UpdateCameraDropdowns(); + // Gather the camera candidates + availableCameras.Clear(); + availableCameras.AddRange(Target.ChildCameras); }); this.AddExtensionsDropdown(ux); return ux; - // Local function - void UpdateCameraDropdowns() - { - var children = Target.ChildCameras; - int index = 0; - var iter = list.itemsSource.GetEnumerator(); - while (iter.MoveNext()) - { - var vcamSel = list.Q>($"vcamSelector{index}"); - if (vcamSel != null) - { - vcamSel.choices.Clear(); - for (int i = 0; i < children.Count; ++i) - vcamSel.choices.Add(children[i]); - } - ++index; - } - } - // Local function static void FormatInstructionElement(bool isHeader, VisualElement e1, VisualElement e2, VisualElement e3) { @@ -133,13 +111,13 @@ static void FormatInstructionElement(bool isHeader, VisualElement e1, VisualElem e1.style.flexBasis = floatFieldWidth + InspectorUtility.SingleLineHeight; e1.style.flexGrow = 1; - e2.style.flexBasis = floatFieldWidth; + e2.style.marginLeft = isHeader ? 4 * InspectorUtility.SingleLineHeight - 3 : 0; + e2.style.flexBasis = floatFieldWidth + InspectorUtility.SingleLineHeight; e2.style.flexGrow = 1; - e3.style.marginRight = 4; + floatFieldWidth += isHeader ? InspectorUtility.SingleLineHeight/2 - 1 : 0; e3.style.flexBasis = floatFieldWidth; e3.style.flexGrow = 0; - e3.style.unityTextAlign = TextAnchor.MiddleRight; } } } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyLookAtTargetsEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyLookAtTargetsEditor.cs index b5b8f8400..d2a097255 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyLookAtTargetsEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyLookAtTargetsEditor.cs @@ -96,14 +96,13 @@ public override VisualElement CreateInspectorGUI() => splineData.GetGetSplineAndDolly(out var spline, out _) ? spline : null)); ux.AddHeader("Data Points"); - var listField = ux.AddChild(SplineDataInspectorUtility.CreateDataListField( + var list = ux.AddChild(SplineDataInspectorUtility.CreateDataListField( splineData.Targets, targetsProp, () => splineData.GetGetSplineAndDolly(out var spline, out _) ? spline : null, () => { // Create a default item for index 0 - var item = new CinemachineSplineDollyLookAtTargets.Item(); - item.LookAt = splineData.VirtualCamera.LookAt; + var item = new CinemachineSplineDollyLookAtTargets.Item { LookAt = splineData.VirtualCamera.LookAt, Easing = 1 }; if (item.LookAt == null) { // No LookAt? Find a point to look at near the spline @@ -115,114 +114,139 @@ public override VisualElement CreateInspectorGUI() })); var arrayProp = targetsProp.FindPropertyRelative("m_DataPoints"); - listField.OnInitialGeometry(() => + list.makeItem = () => { - var list = listField.Q(); - list.makeItem = () => new BindableElement() { style = { marginRight = 4 }}; - list.bindItem = (ux, index) => - { - // Remove children - items get recycled - for (int i = ux.childCount - 1; i >= 0; --i) - ux.RemoveAt(i); - - const string indexTooltip = "The position on the Spline at which this data point will take effect. " - + "The value is interpreted according to the Index Unit setting."; - - var element = index < arrayProp.arraySize ? arrayProp.GetArrayElementAtIndex(index) : null; - CinemachineSplineDollyLookAtTargets.Item def = new (); - var indexProp = element.FindPropertyRelative("m_Index"); - var valueProp = element.FindPropertyRelative("m_Value"); - var lookAtProp = valueProp.FindPropertyRelative(() => def.LookAt); - var offsetProp = valueProp.FindPropertyRelative(() => def.Offset); - - var overlay = new VisualElement () { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}; - var indexField1 = overlay.AddChild(new PropertyField(indexProp, "") { tooltip = indexTooltip, style = { flexGrow = 1, flexBasis = 50 }}); - indexField1.OnInitialGeometry(() => indexField1.SafeSetIsDelayed()); - - var lookAtField1 = overlay.AddChild(new PropertyField(lookAtProp, "") { style = { flexGrow = 4, flexBasis = 50, marginLeft = 3 }}); - var overlayLabel = new Label(indexProp.displayName) { tooltip = indexTooltip, style = { alignSelf = Align.Center }}; - overlayLabel.AddDelayedFriendlyPropertyDragger(indexProp, overlay, OnIndexDraggerCreated); + var itemRootName = "ItemRoot"; + var row = new BindableElement() { name = itemRootName, style = { marginRight = 4 }}; + + var overlay = new VisualElement () { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}; + var overlayLabel = overlay.AddChild(new Label("Index")); + var indexField1 = overlay.AddChild(InspectorUtility.CreateDraggableField( + typeof(float), "m_Index", SplineDataInspectorUtility.ItemIndexTooltip, overlayLabel, out var dragger)); + indexField1.style.flexGrow = 1; + indexField1.style.flexBasis = 50; + indexField1.SafeSetIsDelayed(); + dragger.OnDragValueChangedFloat = (v) => BringCameraToCustomSplinePoint(splineData, v); + dragger.OnStartDrag = (d) => list.selectedIndex = GetIndexInList(list, d.DragElement, itemRootName); + + CinemachineSplineDollyLookAtTargets.Item def = new (); + var lookAtField1 = overlay.AddChild(new ObjectField + { + bindingPath = "m_Value." + SerializedPropertyHelper.PropertyName(() => def.LookAt), + tooltip = SerializedPropertyHelper.PropertyTooltip(() => def.LookAt), + objectType = typeof(Transform), + style = { flexGrow = 4, flexBasis = 50, marginLeft = 6 } + }); - var foldout = new Foldout() { text = $"Target {index}" }; - foldout.BindProperty(element); - var row = foldout.AddChild(new InspectorUtility.LabeledRow(indexProp.displayName, indexTooltip)); - var indexField2 = row.Contents.AddChild(new PropertyField(indexProp, "") { style = { flexGrow = 1 }}); - indexField2.OnInitialGeometry(() => indexField2.SafeSetIsDelayed()); - row.Label.AddDelayedFriendlyPropertyDragger(indexProp, indexField2, OnIndexDraggerCreated); + var foldout = new Foldout() { value = false, text = "Target" }; // do not bind to "m_Value" because it will mess up the binding for index + var indexRow = foldout.AddChild(new InspectorUtility.LabeledRow("Index", SplineDataInspectorUtility.ItemIndexTooltip)); + var indexField2 = indexRow.Contents.AddChild(InspectorUtility.CreateDraggableField( + typeof(float), "m_Index", SplineDataInspectorUtility.ItemIndexTooltip, indexRow.Label, out dragger)); + indexField2.style.flexGrow = 1; + indexField2.style.flexBasis = 50; + indexField2.SafeSetIsDelayed(); + dragger.OnDragValueChangedFloat = (v) => BringCameraToCustomSplinePoint(splineData, v); + dragger.OnStartDrag = (d) => list.selectedIndex = GetIndexInList(list, d.DragElement, itemRootName); - var lookAtField2 = foldout.AddChild(new PropertyField(lookAtProp)); - foldout.Add(new PropertyField(offsetProp)); - foldout.Add(new PropertyField(valueProp.FindPropertyRelative(() => def.Easing))); + var lookAtField2 = foldout.AddChild(new PropertyField { bindingPath = "m_Value." + SerializedPropertyHelper.PropertyName(() => def.LookAt) }); - ux.Add(new InspectorUtility.FoldoutWithOverlay(foldout, overlay, overlayLabel) { style = { marginLeft = 12 }}); + foldout.Add(new PropertyField { bindingPath = "m_Value." + SerializedPropertyHelper.PropertyName(() => def.Offset) }); + foldout.Add(new PropertyField { bindingPath = "m_Value." + SerializedPropertyHelper.PropertyName(() => def.Easing) }); - ux.TrackPropertyValue(lookAtProp, (p) => - { - // Don't mess with the offset if change was a result of undo/redo - if (m_UndoRedoMonitor.IsUndoRedo) - return; + var foldoutWithOverlay = row.AddChild(new InspectorUtility.FoldoutWithOverlay( + foldout, overlay, overlayLabel) { style = { marginLeft = 12 }}); + foldoutWithOverlay.OpenFoldout.name = foldoutWithOverlay.ClosedFoldout.name = "ItemFoldout"; - // if lookAt target was set to null, preserve the worldspace location - if (GetInspectorStateCache(splineData).GetCachedValue(index, out var previous)) - { - var newData = p.objectReferenceValue; - if (newData == null && previous.Value.LookAt != null) - SetOffset(previous.Value.WorldLookAt); - - // if lookAt target was changed, zero the offset - else if (newData != null && newData != previous.Value.LookAt) - SetOffset(Vector3.zero); - - // local function - void SetOffset(Vector3 offset) - { - offsetProp.vector3Value = offset; - p.serializedObject.ApplyModifiedProperties(); - } - } - }); + // When the LookAt is changed, we want to do a little processing to fix up the offset + lookAtField1.RegisterValueChangedCallback((evt) => OnLookAtChanged(GetIndexInList(list, row, itemRootName))); - ((BindableElement)ux).BindProperty(element); // bind must be done at the end + return row; - // local function - void OnIndexDraggerCreated(IDelayedFriendlyDragger dragger) + // Sneaky way to find out which list element we are + static int GetIndexInList(ListView list, VisualElement element, string itemRootName) + { + var container = list.Q("unity-content-container"); + if (container != null) { - dragger.OnStartDrag = () => list.selectedIndex = index; - dragger.OnDragValueChangedFloat = (v) => BringCameraToCustomSplinePoint(splineData, v); + while (element != null && element.name != itemRootName) + element = element.parent; + if (element != null) + return container.IndexOf(element); } - }; + return - 1; + } + + void OnLookAtChanged(int index) + { + // Don't mess with the offset if change was a result of undo/redo + if (m_UndoRedoMonitor.IsUndoRedo || index < 0 || index >= arrayProp.arraySize) + return; - list.TrackPropertyValue(arrayProp, (p) => EditorApplication.delayCall += () => GetInspectorStateCache(splineData).Reset(splineData)); + var offsetProp = arrayProp.GetArrayElementAtIndex(index).FindPropertyRelative("m_Value.Offset"); + var lookAtProp = arrayProp.GetArrayElementAtIndex(index).FindPropertyRelative("m_Value.LookAt"); - // When the list selection changes, cache the index and put the camera at that point on the dolly track - list.selectedIndicesChanged += (indices) => + // if lookAt target was set to null, preserve the worldspace location + if (GetInspectorStateCache(splineData).GetCachedValue(index, out var previous)) + { + var newData = lookAtProp.objectReferenceValue; + if (newData == null && previous.Value.LookAt != null) + SetOffset(previous.Value.WorldLookAt); + + // if lookAt target was changed, zero the offset + else if (newData != null && newData != previous.Value.LookAt) + SetOffset(Vector3.zero); + + // local function + void SetOffset(Vector3 offset) + { + offsetProp.vector3Value = offset; + lookAtProp.serializedObject.ApplyModifiedProperties(); + } + } + } + }; + + list.TrackPropertyWithInitialCallback(arrayProp, (p) => + { + // Fix up the foldout names to reflect the index of the item + int index = 0; + list.Query("ItemFoldout").ForEach((e) => { - var it = indices.GetEnumerator(); - var cache = GetInspectorStateCache(splineData); - cache.CurrentSelection = it.MoveNext() ? it.Current : -1; - BringCameraToSplinePoint(splineData, cache.CurrentSelection); - }; + if (e is Foldout f) + f.text = $"Target {index++ / 2}"; // because there are 2 foldouts for each item + }); + // Reset the state cache after all processing is done + EditorApplication.delayCall += () => GetInspectorStateCache(splineData).Reset(splineData); + }); + + // When the list selection changes, cache the index and put the camera at that point on the dolly track + list.selectedIndicesChanged += (indices) => + { + var it = indices.GetEnumerator(); + var cache = GetInspectorStateCache(splineData); + cache.CurrentSelection = it.MoveNext() ? it.Current : -1; + BringCameraToSplinePoint(splineData, cache.CurrentSelection); + }; - LookAtDataOnSplineTool.s_OnDataLookAtDragged += OnToolDragged; - LookAtDataOnSplineTool.s_OnDataIndexDragged += OnToolDragged; - void OnToolDragged(CinemachineSplineDollyLookAtTargets data, int index) + LookAtDataOnSplineTool.s_OnDataLookAtDragged += OnToolDragged; + LookAtDataOnSplineTool.s_OnDataIndexDragged += OnToolDragged; + void OnToolDragged(CinemachineSplineDollyLookAtTargets data, int index) + { + EditorApplication.delayCall += () => { - EditorApplication.delayCall += () => + // GML This is a hack to avoid spurious exceptions thrown by uitoolkit! + // GML TODO: Remove when they fix it + try { - // GML This is a hack to avoid spurious exceptions thrown by uitoolkit! - // GML TODO: Remove when they fix it - try + if (data == splineData) { - if (data == splineData) - { - list.selectedIndex = index; - BringCameraToSplinePoint(data, index); - } + list.selectedIndex = index; + BringCameraToSplinePoint(data, index); } - catch {} // Ignore exceptions - }; - } - }); + } + catch {} // Ignore exceptions + }; + } return ux; } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs index 7491df73b..523f580d1 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs @@ -35,57 +35,67 @@ public override VisualElement CreateInspectorGUI() ux.Add(SplineDataInspectorUtility.CreatePathUnitField(rollProp, () => splineData == null ? null : splineData.SplineContainer)); ux.AddHeader("Data Points"); - var listField = ux.AddChild(SplineDataInspectorUtility.CreateDataListField( - splineData.Roll, rollProp, () => splineData?.SplineContainer)); + var list = ux.AddChild(SplineDataInspectorUtility.CreateDataListField(splineData.Roll, rollProp, () => splineData?.SplineContainer)); + var arrayProp = rollProp.FindPropertyRelative("m_DataPoints"); - listField.OnInitialGeometry(() => - { - var list = listField.Q(); - list.makeItem = () => new BindableElement() { style = { flexDirection = FlexDirection.Row, marginRight = 4 }}; - list.bindItem = (ux, index) => + list.makeItem = () => + { + var itemRootName = "ItemRoot"; + var row = new BindableElement() { name = itemRootName, style = { flexDirection = FlexDirection.Row, marginRight = 4 }}; + + row.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { flexBasis = 12 }}); // pass-through for selecting row in list + var indexField = row.AddChild(InspectorUtility.CreateDraggableField( + typeof(float), "m_Index", SplineDataInspectorUtility.ItemIndexTooltip, + row.AddChild(new Label("Index")), out var dragger)); + indexField.style.flexGrow = 1; + indexField.style.flexBasis = 50; + indexField.SafeSetIsDelayed(); + dragger.OnStartDrag = (d) => list.selectedIndex = GetIndexInList(list, d.DragElement, itemRootName); + + var def = new CinemachineSplineRoll.RollData(); + var rollTooltip = SerializedPropertyHelper.PropertyTooltip(() => def.Value); + row.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { flexBasis = 12 }}); // pass-through for selecting row in list + var rollField = row.AddChild(InspectorUtility.CreateDraggableField( + typeof(float), "m_Value.Value", rollTooltip, row.AddChild(new Label("Roll")), out dragger)); + rollField.style.flexGrow = 1; + rollField.style.flexBasis = 50; + dragger.OnStartDrag = (d) => list.selectedIndex = GetIndexInList(list, d.DragElement, itemRootName); + + return row; + + // Sneaky way to find out which list element we are + static int GetIndexInList(ListView list, VisualElement element, string itemRootName) { - // Remove children - items get recycled - for (int i = ux.childCount - 1; i >= 0; --i) - ux.RemoveAt(i); - - const string indexTooltip = "The position on the Spline at which this data point will take effect. " - + "The value is interpreted according to the Index Unit setting."; - - var element = index < arrayProp.arraySize ? arrayProp.GetArrayElementAtIndex(index) : null; - var def = new CinemachineSplineRoll.RollData(); - var indexProp = element.FindPropertyRelative("m_Index"); - var valueProp = element.FindPropertyRelative("m_Value"); - - ux.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { width = 12 }}); // pass-through for selecting row in list - var label = ux.AddChild(new Label(indexProp.displayName) { tooltip = indexTooltip, style = { alignSelf = Align.Center }}); - var indexField = ux.AddChild(new PropertyField(indexProp, "") { style = { flexGrow = 1, flexBasis = 20 } }); - indexField.OnInitialGeometry(() => indexField.SafeSetIsDelayed()); - label.AddDelayedFriendlyPropertyDragger(indexProp, indexField, (dragger) => dragger.OnStartDrag = () => list.selectedIndex = index); - - ux.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { width = 12 }}); // pass-through for selecting row in list - ux.Add(new InspectorUtility.CompactPropertyField(valueProp.FindPropertyRelative(() => def.Value), "Roll")); + var container = list.Q("unity-content-container"); + if (container != null) + { + while (element != null && element.name != itemRootName) + element = element.parent; + if (element != null) + return container.IndexOf(element); + } + return - 1; + } - ((BindableElement)ux).BindProperty(element); // bind must be done at the end - }; + }; - SplineRollTool.s_OnDataLookAtDragged += OnToolDragged; - SplineRollTool.s_OnDataIndexDragged += OnToolDragged; - void OnToolDragged(CinemachineSplineRoll data, int index) + SplineRollTool.s_OnDataLookAtDragged += OnToolDragged; + SplineRollTool.s_OnDataIndexDragged += OnToolDragged; + void OnToolDragged(CinemachineSplineRoll data, int index) + { + EditorApplication.delayCall += () => { - EditorApplication.delayCall += () => + // This is a hack to avoid spurious exceptions thrown by uitoolkit! + // GML TODO: Remove when they fix it + try { - // This is a hack to avoid spurious exceptions thrown by uitoolkit! - // GML TODO: Remove when they fix it - try - { - if (data == splineData) - list.selectedIndex = index; - } - catch {} // Ignore exceptions - }; - } - }); + if (data == splineData) + list.selectedIndex = index; + } + catch {} // Ignore exceptions + }; + } ux.TrackPropertyValue(rollProp, (p) => { diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineStateDrivenCameraEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineStateDrivenCameraEditor.cs index dc675f547..53fab70bb 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineStateDrivenCameraEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineStateDrivenCameraEditor.cs @@ -40,7 +40,7 @@ public override VisualElement CreateInspectorGUI() var layerProp = serializedObject.FindProperty(() => Target.LayerIndex); var layerSel = ux.AddChild(new PopupField(layerProp.displayName) { tooltip = layerProp.tooltip }); - layerSel.AddToClassList(InspectorUtility.kAlignFieldClass); + layerSel.AddToClassList(InspectorUtility.AlignFieldClassName); layerSel.RegisterValueChangedCallback((evt) => { layerProp.intValue = Mathf.Max(0, m_LayerNames.FindIndex(v => v == evt.newValue)); @@ -73,7 +73,6 @@ public override VisualElement CreateInspectorGUI() var list = container.AddChild(new ListView() { - name = "InstructionList", reorderable = true, reorderMode = ListViewReorderMode.Animated, showAddRemoveFooter = true, @@ -85,67 +84,73 @@ public override VisualElement CreateInspectorGUI() var instructions = serializedObject.FindProperty(() => Target.Instructions); list.BindProperty(instructions); - list.makeItem = () => new BindableElement { style = { flexDirection = FlexDirection.Row }}; - list.bindItem = (row, index) => + // Available camera candidates + var availableCameras = new List(); + + list.makeItem = () => { - // Remove children - items get recycled - for (int i = row.childCount - 1; i >= 0; --i) - row.RemoveAt(i); + var row = new BindableElement { style = { flexDirection = FlexDirection.Row }}; var def = new CinemachineStateDrivenCamera.Instruction(); - var element = instructions.GetArrayElementAtIndex(index); - var stateSelProp = element.FindPropertyRelative(() => def.FullHash); - int currentState = GetStateHashIndex(stateSelProp.intValue); + // This is the real state field, but it's hiddes + var hashField = row.AddChild(new IntegerField() { bindingPath = SerializedPropertyHelper.PropertyName(() => def.FullHash) }); + hashField.SetVisible(false); + + // Create a state selector popup to drive the state field var stateSel = row.AddChild(new PopupField { - name = $"stateSelector{index}", choices = m_TargetStateNames, - tooltip = "The state that will activate the camera" + tooltip = "The state that will activate the camera" }); - stateSel.RegisterValueChangedCallback((evt) => + + hashField.RegisterValueChangedCallback((evt) => { - if (evt.target == stateSel) + if (evt.target != hashField) + return; + for (int i = 0; i < m_TargetStates.Count; ++i) { - var i = stateSel.index; - if (i >= 0 && i < m_TargetStates.Count) + if (evt.newValue == m_TargetStates[i]) { - stateSelProp.intValue = m_TargetStates[i]; - stateSelProp.serializedObject.ApplyModifiedProperties(); + stateSel.value = m_TargetStateNames[i]; + break; } - evt.StopPropagation(); } + evt.StopPropagation(); }); - stateSel.TrackPropertyWithInitialCallback(stateSelProp, (p) => + + stateSel.RegisterValueChangedCallback((evt) => { - var hash = p.intValue; - for (int i = 0; i < m_TargetStates.Count; ++i) + if (evt.target != stateSel) + return; + + for (int i = 0; i < m_TargetStateNames.Count; ++i) { - if (hash == m_TargetStates[i]) + if (evt.newValue == m_TargetStateNames[i]) { - stateSel.SetValueWithoutNotify(m_TargetStateNames[i]); - return; + hashField.value = m_TargetStates[i]; + break; } } - stateSel.SetValueWithoutNotify(m_TargetStateNames[0]); + evt.StopPropagation(); }); - var vcamSelProp = element.FindPropertyRelative(() => def.Camera); - var vcamSel = row.AddChild(new PopupField { name = $"vcamSelector{index}", choices = new() }); - vcamSel.formatListItemCallback = (obj) => obj == null ? "(null)" : obj.name; - vcamSel.formatSelectedValueCallback = (obj) => obj == null ? "(null)" : obj.name; - vcamSel.TrackPropertyWithInitialCallback(instructions, (p) => UpdateCameraDropdowns()); + var vcamSel = row.AddChild(new PopupField + { + bindingPath = SerializedPropertyHelper.PropertyName(() => def.Camera), + choices = availableCameras, + formatListItemCallback = (obj) => obj == null ? "(null)" : obj.name, + formatSelectedValueCallback = (obj) => obj == null ? "(null)" : obj.name + }); - var wait = row.AddChild( - new InspectorUtility.CompactPropertyField(element.FindPropertyRelative(() => def.ActivateAfter), " ")); - var hold = row.AddChild( - new InspectorUtility.CompactPropertyField(element.FindPropertyRelative(() => def.MinDuration), " ")); - + var wait = row.AddChild(InspectorUtility.CreateDraggableField(() => def.ActivateAfter, row.AddChild(new Label(" ")), out _)); + wait.SafeSetIsDelayed(); + var hold = row.AddChild(InspectorUtility.CreateDraggableField(() => def.MinDuration, row.AddChild(new Label(" ")), out _)); + hold.SafeSetIsDelayed(); + FormatInstructionElement(false, stateSel, vcamSel, wait, hold); - // Bind must be last - ((BindableElement)row).BindProperty(element); - vcamSel.BindProperty(vcamSelProp); + return row; }; container.TrackAnyUserActivity(() => @@ -156,7 +161,10 @@ public override VisualElement CreateInspectorGUI() var isMultiSelect = targets.Length > 1; multiSelectMsg.SetVisible(isMultiSelect); container.SetVisible(!isMultiSelect); - UpdateCameraDropdowns(); + + // Gather the camera candidates + availableCameras.Clear(); + availableCameras.AddRange(Target.ChildCameras); }); container.AddSpace(); this.AddChildCameras(container, null); @@ -165,25 +173,6 @@ public override VisualElement CreateInspectorGUI() return ux; - // Local function - void UpdateCameraDropdowns() - { - var children = Target.ChildCameras; - int index = 0; - var iter = list.itemsSource.GetEnumerator(); - while (iter.MoveNext()) - { - var vcamSel = list.Q>($"vcamSelector{index}"); - if (vcamSel != null) - { - vcamSel.choices.Clear(); - for (int i = 0; i < children.Count; ++i) - vcamSel.choices.Add(children[i]); - } - ++index; - } - } - // Local function static void FormatInstructionElement( bool isHeader, VisualElement e1, VisualElement e2, VisualElement e3, VisualElement e4) @@ -197,13 +186,12 @@ static void FormatInstructionElement( e2.style.flexBasis = floatFieldWidth + InspectorUtility.SingleLineHeight; e2.style.flexGrow = 1; + floatFieldWidth += isHeader ? InspectorUtility.SingleLineHeight/2 - 1 : 0; e3.style.flexBasis = floatFieldWidth; e3.style.flexGrow = 0; - e4.style.marginRight = 4; e4.style.flexBasis = floatFieldWidth; e4.style.flexGrow = 0; - e4.style.unityTextAlign = TextAnchor.MiddleRight; } } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineStoryboardEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineStoryboardEditor.cs index 9ccf10447..6d2830bd9 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineStoryboardEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineStoryboardEditor.cs @@ -24,7 +24,7 @@ public override VisualElement CreateInspectorGUI() tooltip = CinemachineCorePrefs.s_StoryboardGlobalMuteLabel.tooltip, value = CinemachineStoryboard.s_StoryboardGlobalMute }); - muteToggle.AddToClassList(InspectorUtility.kAlignFieldClass); + muteToggle.AddToClassList(InspectorUtility.AlignFieldClassName); muteToggle.RegisterValueChangedCallback((evt) => { CinemachineStoryboard.s_StoryboardGlobalMute = CinemachineCorePrefs.StoryboardGlobalMute.Value = evt.newValue; @@ -35,7 +35,7 @@ public override VisualElement CreateInspectorGUI() var showImageProp = serializedObject.FindProperty(() => Target.ShowImage); var row = ux.AddChild(new InspectorUtility.LabeledRow("Show Image", showImageProp.tooltip)); var imageToggle = row.Contents.AddChild(new Toggle("") - { style = { flexGrow = 0, marginTop = 3, marginLeft = 0, alignSelf = Align.Center }}); + { style = { flexGrow = 0, marginTop = 3, marginLeft = 3, alignSelf = Align.Center }}); imageToggle.BindProperty(showImageProp); var imageField = row.Contents.AddChild(new PropertyField(serializedObject.FindProperty(() => Target.Image), "") { style = { flexGrow = 1, marginLeft = 4 }}); @@ -49,11 +49,10 @@ public override VisualElement CreateInspectorGUI() // Scale var scaleProp = serializedObject.FindProperty(() => Target.Scale); row = ux.AddChild(new InspectorUtility.LabeledRow(scaleProp.displayName, scaleProp.tooltip)); - var scaleField = row.Contents.AddChild(new PropertyField(scaleProp, "") - { style = { flexBasis = 0, flexGrow = 2, marginLeft = -3 }}); + var scaleField = row.Contents.AddChild(new PropertyField(scaleProp, "") { style = { flexBasis = 50, flexGrow = 2 }}); var syncProp = serializedObject.FindProperty(() => Target.SyncScale); var syncField = row.Contents.AddChild(new Toggle("") - { text = " Sync", style = { flexBasis = 0, flexGrow = 1, paddingLeft = 3, paddingTop = 3 }}); + { text = " Sync", style = { flexBasis = 20, flexGrow = 1, paddingLeft = 1, paddingTop = 2 }}); syncField.BindProperty(syncProp); scaleField.TrackPropertyWithInitialCallback(syncProp, (p) => { diff --git a/com.unity.cinemachine/Editor/Editors/InputAxisControllerEditor.cs b/com.unity.cinemachine/Editor/Editors/InputAxisControllerEditor.cs index d20af8142..1306880a4 100644 --- a/com.unity.cinemachine/Editor/Editors/InputAxisControllerEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/InputAxisControllerEditor.cs @@ -26,7 +26,7 @@ static DefaultControlInitializer() { #pragma warning disable CS0219 // Variable is assigned but its value is never used var actionName = ""; -#pragma warning restore CS0219 // Variable is assigned but its value is never used +#pragma warning restore CS0219 var inputName = ""; var invertY = false; bool isMomentary = (axis.DrivenAxis().Restrictions & InputAxis.RestrictionFlags.Momentary) != 0; @@ -115,7 +115,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) #endif #if ENABLE_LEGACY_INPUT_MANAGER overlay.Add(new PropertyField(inputProperty.FindPropertyRelative(() => def.Input.LegacyInput), "") - { style = {flexGrow = 1, flexBasis = 5 * InspectorUtility.SingleLineHeight, marginLeft = 6}} ); + { style = {flexGrow = 1, flexBasis = 5 * InspectorUtility.SingleLineHeight }} ); #endif var foldout = new Foldout() { text = property.displayName, tooltip = property.tooltip }; foldout.BindProperty(property); @@ -128,7 +128,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) foldout.Add(new PropertyField(childProperty)); childProperty.NextVisible(false); } - return new InspectorUtility.FoldoutWithOverlay(foldout, overlay, null) { style = { marginLeft = 12 }}; + return new InspectorUtility.FoldoutWithOverlay(foldout, overlay, null) { style = { marginLeft = 12, marginRight = 3 }}; } } @@ -151,7 +151,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) showBoundCollectionSize = false, showFoldoutHeader = false, virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight, - style = { marginLeft = -12 } + style = { marginLeft = -12, marginRight = -3 } }); list.BindProperty(property); diff --git a/com.unity.cinemachine/Editor/Obsolete/CinemachineVirtualCameraBaseEditor.cs b/com.unity.cinemachine/Editor/Obsolete/CinemachineVirtualCameraBaseEditor.cs index 29a53f909..62bf33c19 100644 --- a/com.unity.cinemachine/Editor/Obsolete/CinemachineVirtualCameraBaseEditor.cs +++ b/com.unity.cinemachine/Editor/Obsolete/CinemachineVirtualCameraBaseEditor.cs @@ -108,7 +108,7 @@ protected void DrawInputProviderButtonInInspector() InspectorUtility.HelpBoxWithButton( "The InputSystem package is installed, but it is not used to control this vcam.", MessageType.Info, - new GUIContent("Add Input\nProvider"), () => + new GUIContent("Add Input Provider"), () => { Undo.SetCurrentGroupName("Add CinemachineInputProvider"); for (int i = 0; i < targets.Length; ++i) diff --git a/com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs b/com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs new file mode 100644 index 000000000..ca42835f3 --- /dev/null +++ b/com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs @@ -0,0 +1,240 @@ +#if !CINEMACHINE_NO_CM2_SUPPORT +using UnityEngine; +using UnityEditor; +using System; +using System.Collections.Generic; + +namespace Unity.Cinemachine.Editor +{ + static partial class InspectorUtility + { + /// Put multiple properties on a single inspector line, with + /// optional label overrides. Passing null as a label (or sublabel) override will + /// cause the property's displayName to be used as a label. For no label at all, + /// pass GUIContent.none. + /// Rect in which to draw + /// Main label + /// Properties to place on the line + /// Sublabels for the properties + public static void MultiPropertyOnLine( + Rect rect, + GUIContent label, + SerializedProperty[] props, GUIContent[] subLabels) + { + if (props == null || props.Length == 0) + return; + + const int hSpace = 2; + int indentLevel = EditorGUI.indentLevel; + float labelWidth = EditorGUIUtility.labelWidth; + + float totalSubLabelWidth = 0; + int numBoolColumns = 0; + List actualLabels = new List(); + for (int i = 0; i < props.Length; ++i) + { + GUIContent sublabel = new GUIContent(props[i].displayName, props[i].tooltip); + if (subLabels != null && subLabels.Length > i && subLabels[i] != null) + sublabel = subLabels[i]; + actualLabels.Add(sublabel); + totalSubLabelWidth += GUI.skin.label.CalcSize(sublabel).x; + if (i > 0) + totalSubLabelWidth += hSpace; + // Special handling for toggles, or it looks stupid + if (props[i].propertyType == SerializedPropertyType.Boolean) + { + totalSubLabelWidth += rect.height + hSpace; + ++numBoolColumns; + } + } + + float subFieldWidth = rect.width - labelWidth - totalSubLabelWidth; + float numCols = props.Length - numBoolColumns; + float colWidth = numCols == 0 ? 0 : subFieldWidth / numCols; + + // Main label. If no first sublabel, then main label must take on that + // role, for mouse dragging value-scrolling support + int subfieldStartIndex = 0; + if (label == null) + label = new GUIContent(props[0].displayName, props[0].tooltip); + if (actualLabels[0] != GUIContent.none) + rect = EditorGUI.PrefixLabel(rect, label); + else + { + rect.width = labelWidth + colWidth; + EditorGUI.PropertyField(rect, props[0], label); + rect.x += rect.width + hSpace; + subfieldStartIndex = 1; + } + + for (int i = subfieldStartIndex; i < props.Length; ++i) + { + EditorGUI.indentLevel = 0; + EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(actualLabels[i]).x; + if (props[i].propertyType == SerializedPropertyType.Boolean) + { + rect.x += hSpace; + rect.width = EditorGUIUtility.labelWidth + rect.height; + EditorGUI.BeginProperty(rect, actualLabels[i], props[i]); + props[i].boolValue = EditorGUI.ToggleLeft(rect, actualLabels[i], props[i].boolValue); + } + else + { + rect.width = EditorGUIUtility.labelWidth + colWidth; + EditorGUI.BeginProperty(rect, actualLabels[i], props[i]); + EditorGUI.PropertyField(rect, props[i], actualLabels[i]); + } + EditorGUI.EndProperty(); + rect.x += rect.width + hSpace; + } + + EditorGUIUtility.labelWidth = labelWidth; + EditorGUI.indentLevel = indentLevel; + } + + public static float PropertyHeightOfChidren(SerializedProperty property) + { + float height = 0; + var childProperty = property.Copy(); + var endProperty = childProperty.GetEndProperty(); + childProperty.NextVisible(true); + while (!SerializedProperty.EqualContents(childProperty, endProperty)) + { + height += EditorGUI.GetPropertyHeight(childProperty) + + EditorGUIUtility.standardVerticalSpacing; + childProperty.NextVisible(false); + } + return height - EditorGUIUtility.standardVerticalSpacing; + } + + public static void DrawChildProperties(Rect position, SerializedProperty property) + { + var childProperty = property.Copy(); + var endProperty = childProperty.GetEndProperty(); + childProperty.NextVisible(true); + while (!SerializedProperty.EqualContents(childProperty, endProperty)) + { + position.height = EditorGUI.GetPropertyHeight(childProperty); + EditorGUI.PropertyField(position, childProperty, true); + position.y += position.height + EditorGUIUtility.standardVerticalSpacing; + childProperty.NextVisible(false); + } + } + + public static void HelpBoxWithButton( + string message, MessageType messageType, + GUIContent buttonContent, Action onClicked) + { + EditorGUILayout.HelpBox(message + "\n\n", messageType, true); + var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(false, 2)); + + float lineHeight = EditorGUIUtility.singleLineHeight; + var buttonSize = GUI.skin.label.CalcSize(buttonContent); + buttonSize.x += lineHeight; + + rect.x += rect.width - buttonSize.x - 6; rect.width = buttonSize.x; + rect.y += rect.height - buttonSize.y - 12; rect.height = buttonSize.y + 3; + if (GUI.Button(rect, buttonContent)) + onClicked(); + } + + public static float EnabledFoldoutHeight(SerializedProperty property, string enabledPropertyName) + { + var enabledProp = property.FindPropertyRelative(enabledPropertyName); + if (enabledProp == null) + return EditorGUI.GetPropertyHeight(property); + if (!enabledProp.boolValue) + return EditorGUIUtility.singleLineHeight; + return PropertyHeightOfChidren(property); + } + + public static bool EnabledFoldout( + Rect rect, SerializedProperty property, string enabledPropertyName, + GUIContent label = null) + { + var enabledProp = property.FindPropertyRelative(enabledPropertyName); + if (enabledProp == null) + { + EditorGUI.PropertyField(rect, property, true); + rect.x += EditorGUIUtility.labelWidth; + EditorGUI.LabelField(rect, new GUIContent($"unknown field `{enabledPropertyName}`")); + return property.isExpanded; + } + rect.height = EditorGUIUtility.singleLineHeight; + label ??= new GUIContent(property.displayName, enabledProp.tooltip); + EditorGUI.PropertyField(rect, enabledProp, label); + if (enabledProp.boolValue) + { + ++EditorGUI.indentLevel; + var childProperty = property.Copy(); + var endProperty = childProperty.GetEndProperty(); + childProperty.NextVisible(true); + while (!SerializedProperty.EqualContents(childProperty, endProperty)) + { + if (!SerializedProperty.EqualContents(childProperty, enabledProp)) + { + rect.y += rect.height + EditorGUIUtility.standardVerticalSpacing; + rect.height = EditorGUI.GetPropertyHeight(childProperty); + EditorGUI.PropertyField(rect, childProperty, true); + } + childProperty.NextVisible(false); + } + --EditorGUI.indentLevel; + } + return enabledProp.boolValue; + } + + public static bool EnabledFoldoutSingleLine( + Rect rect, SerializedProperty property, + string enabledPropertyName, string disabledToggleLabel, + GUIContent label = null) + { + var enabledProp = property.FindPropertyRelative(enabledPropertyName); + if (enabledProp == null) + { + EditorGUI.PropertyField(rect, property, true); + rect.x += EditorGUIUtility.labelWidth; + EditorGUI.LabelField(rect, new GUIContent($"unknown field `{enabledPropertyName}`")); + return property.isExpanded; + } + rect.height = EditorGUIUtility.singleLineHeight; + label ??= new GUIContent(property.displayName, enabledProp.tooltip); + EditorGUI.PropertyField(rect, enabledProp, label); + if (!enabledProp.boolValue) + { + if (!string.IsNullOrEmpty(disabledToggleLabel)) + { + var w = EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight + 3; + var r = rect; r.x += w; r.width -= w; + var oldColor = GUI.color; + GUI.color = new(oldColor.r, oldColor.g, oldColor.g, 0.5f); + EditorGUI.LabelField(r, disabledToggleLabel); + GUI.color = oldColor; + } + } + else + { + rect.width -= EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight; + rect.x += EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight; + + var childProperty = property.Copy(); + var endProperty = childProperty.GetEndProperty(); + childProperty.NextVisible(true); + while (!SerializedProperty.EqualContents(childProperty, endProperty)) + { + if (!SerializedProperty.EqualContents(childProperty, enabledProp)) + { + var oldWidth = EditorGUIUtility.labelWidth; + EditorGUIUtility.labelWidth = 6; // for dragging + EditorGUI.PropertyField(rect, childProperty, new GUIContent(" ")); + EditorGUIUtility.labelWidth = oldWidth; + break; // Draw only the first property + } + childProperty.NextVisible(false); + } + } + return enabledProp.boolValue; + } + } +} +#endif diff --git a/com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs.meta b/com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs.meta new file mode 100644 index 000000000..8fcdcebe6 --- /dev/null +++ b/com.unity.cinemachine/Editor/Obsolete/ObsoleteInspectorUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e431aac1b3a84b64e93b38e833c448be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.cinemachine/Editor/Obsolete/ObsoletePropertyDrawers.cs b/com.unity.cinemachine/Editor/Obsolete/ObsoletePropertyDrawers.cs index da6b405fa..64063cabf 100644 --- a/com.unity.cinemachine/Editor/Obsolete/ObsoletePropertyDrawers.cs +++ b/com.unity.cinemachine/Editor/Obsolete/ObsoletePropertyDrawers.cs @@ -158,7 +158,7 @@ public override void OnGUI(Rect rect, SerializedProperty property, GUIContent la } } - partial class InputAxisWithNamePropertyDrawer : PropertyDrawer + partial class InputAxisPropertyDrawer : PropertyDrawer { public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) { diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/CameraTargetPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/CameraTargetPropertyDrawer.cs index f6156e9dd..630d032fb 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/CameraTargetPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/CameraTargetPropertyDrawer.cs @@ -23,7 +23,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) VisualElement CreateTargetProperty(SerializedProperty property, SerializedProperty customProp) { var row = InspectorUtility.PropertyRow(property, out _); - row.Add(InspectorUtility.MiniPopupButton(null, new ContextualMenuManipulator((evt) => + row.Contents.Add(InspectorUtility.MiniPopupButton(null, new ContextualMenuManipulator((evt) => { evt.menu.AppendAction("Convert to TargetGroup", (action) => diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/DelayedVectorPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/DelayedVectorPropertyDrawer.cs index 4bf7e8662..62d723a9a 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/DelayedVectorPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/DelayedVectorPropertyDrawer.cs @@ -21,7 +21,7 @@ VisualElement ChildField(SerializedProperty p) if (p == null) return new VisualElement { style = { flexBasis = 30, flexGrow = 1, marginLeft = 6 } }; var f = new InspectorUtility.CompactPropertyField(p) - { style = { flexBasis = 30, flexGrow = 1, marginLeft = 6 } }; + { style = { flexBasis = 30, flexGrow = 1, marginLeft = 9 } }; f.Label.style.minWidth = 15; f.OnInitialGeometry(() => { diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/EmbeddedAssetPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/EmbeddedAssetPropertyDrawer.cs index 5b85afa50..1b4b802a9 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/EmbeddedAssetPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/EmbeddedAssetPropertyDrawer.cs @@ -7,7 +7,7 @@ namespace Unity.Cinemachine.Editor { - // GML todo: remove this class, replace with EmbeddedAssetEditorUtility.AddAssetSelectorWithPresets + // GML todo: remove this class, replace with EmbeddedAssetEditorUtility.AssetSelectorWithPresets // Currently only used by CinemachineImpulseDefinition editor [CustomPropertyDrawer(typeof(CinemachineEmbeddedAssetPropertyAttribute))] diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs index 133ffca0a..5fb0a462b 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs @@ -75,7 +75,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var row = new InspectorUtility.LabeledRow(preferredLabel, enabledProp.tooltip); var toggle = row.Contents.AddChild(new Toggle("") - { style = { flexGrow = 0, marginTop = 2, marginLeft = 0, alignSelf = Align.Center }}); + { style = { flexGrow = 0, marginTop = 2, marginLeft = 3, alignSelf = Align.Center }}); toggle.BindProperty(enabledProp); Label disabledText = null; @@ -88,7 +88,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var childField = row.Contents.AddChild(new PropertyField(childProperty, "") { style = { flexGrow = 1, marginTop = -1, marginLeft = 5, marginBottom = -1 }}); childLabel.AddDelayedFriendlyPropertyDragger(childProperty, childField, (d) => d.CancelDelayedWhenDragging = true); - childField.RemoveFromClassList(InspectorUtility.kAlignFieldClass); + childField.RemoveFromClassList(InspectorUtility.AlignFieldClassName); row.TrackPropertyWithInitialCallback(enabledProp, (p) => { diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs index 1fbac8a59..2be7ffb4e 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs @@ -5,47 +5,33 @@ namespace Unity.Cinemachine.Editor { [CustomPropertyDrawer(typeof(InputAxis))] - partial class InputAxisWithNamePropertyDrawer : PropertyDrawer + partial class InputAxisPropertyDrawer : PropertyDrawer { InputAxis def = new (); // to access name strings public override VisualElement CreatePropertyGUI(SerializedProperty property) { // When foldout is closed, we display the axis value on the same line, for convenience - var foldout = new Foldout { text = property.displayName, tooltip = property.tooltip, value = property.isExpanded }; - foldout.RegisterValueChangedCallback((evt) => - { - if (evt.target == foldout) - { - property.isExpanded = evt.newValue; - property.serializedObject.ApplyModifiedProperties(); - evt.StopPropagation(); - } - }); + var foldout = new Foldout { text = property.displayName, tooltip = property.tooltip }; + foldout.BindProperty(property); + var valueProp = property.FindPropertyRelative(() => def.Value); - var valueLabel = new Label(" ") { style = { minWidth = InspectorUtility.SingleLineHeight * 2}}; - var valueField = new InspectorUtility.CompactPropertyField(valueProp, "") { style = { flexGrow = 1}}; + var valueLabel = new Label(" ") { style = { minWidth = InspectorUtility.SingleLineHeight * 2 }}; + var valueField = new PropertyField(valueProp, "") { style = { flexGrow = 1, marginLeft = 2 }}; valueField.OnInitialGeometry(() => valueField.SafeSetIsDelayed()); valueLabel.AddDelayedFriendlyPropertyDragger(valueProp, valueField, (d) => d.CancelDelayedWhenDragging = true); var ux = new InspectorUtility.FoldoutWithOverlay(foldout, valueField, valueLabel); - // We want dynamic dragging on the value, even if isDelayed is set - var valueFieldRow = foldout.AddChild(new InspectorUtility.LabeledRow( - valueProp.displayName, valueProp.tooltip, new PropertyField(valueProp, ""))); - valueFieldRow.Contents.OnInitialGeometry(() => - { - valueFieldRow.Contents.SafeSetIsDelayed(); - valueFieldRow.Contents.Q().style.marginLeft = 0; - }); - valueFieldRow.Label.AddDelayedFriendlyPropertyDragger(valueProp, valueFieldRow.Contents, (d) => d.CancelDelayedWhenDragging = true); + // We want dynamic dragging on the value, even if isDelayed is set. PropertyRow puts a delayed-friendly dragger. + foldout.AddChild(InspectorUtility.PropertyRow(valueProp, out _)); var centerField = foldout.AddChild(new PropertyField(property.FindPropertyRelative(() => def.Center))); var rangeContainer = foldout.AddChild(new VisualElement() { style = { flexDirection = FlexDirection.Row }}); rangeContainer.Add(new PropertyField(property.FindPropertyRelative(() => def.Range)) { style = { flexGrow = 1 }}); var wrapProp = property.FindPropertyRelative(() => def.Wrap); var wrap = rangeContainer.AddChild(new PropertyField(wrapProp, "") - { style = { alignSelf = Align.Center, marginLeft = 5, marginRight = 5 }}); + { style = { alignSelf = Align.Center, marginLeft = 5, marginRight = 5, marginTop = 2 }}); var wrapLabel = rangeContainer.AddChild(new Label(wrapProp.displayName) { tooltip = wrapProp.tooltip, style = { alignSelf = Align.Center }}); var recentering = foldout.AddChild(new PropertyField(property.FindPropertyRelative(() => def.Recentering))); diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/LensSettingsPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/LensSettingsPropertyDrawer.cs index 98cfff934..b4d9214ef 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/LensSettingsPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/LensSettingsPropertyDrawer.cs @@ -86,15 +86,15 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var ux = new VisualElement(); // When foldout is closed, we display the FOV on the same line, for convenience - var foldout = new Foldout { text = property.displayName, tooltip = property.tooltip, value = property.isExpanded }; + var foldout = new Foldout { text = property.displayName, tooltip = property.tooltip }; foldout.BindProperty(property); - var outerFovControl = new FovPropertyControl(property, true) { style = { flexGrow = 1 }}; + var outerFovControl = new FovPropertyControl(property, true); ux.Add(new InspectorUtility.FoldoutWithOverlay( foldout, outerFovControl, outerFovControl.ShortLabel) { style = { flexGrow = 1 }}); // Populate the foldout - var innerFovControl = foldout.AddChild(new FovPropertyControl(property, false) { style = { flexGrow = 1 }}); + var innerFovControl = foldout.AddChild(new FovPropertyControl(property, false)); var nearClip = property.FindPropertyRelative(() => s_Def.NearClipPlane); foldout.AddChild(new PropertyField(nearClip)).RegisterValueChangeCallback((evt) => @@ -179,13 +179,12 @@ Modes GetLensMode() public FovPropertyControl(SerializedProperty property, bool hideLabel) : base(hideLabel ? "" : "(fov)") { - style.flexDirection = FlexDirection.Row; - m_LensProperty = property; var physicalProp = property.FindPropertyRelative(() => s_Def.PhysicalProperties); m_SensorSizeProperty = physicalProp.FindPropertyRelative(() => s_Def.PhysicalProperties.SensorSize); - m_Control = Contents.AddChild(new FloatField("") { style = {flexBasis = 20, flexGrow = 2, marginLeft = 0}}); + m_Control = Contents.AddChild(new FloatField("") + { style = { flexBasis = 20, flexGrow = 2, marginLeft = hideLabel ? 0 : 3 }}); m_Control.RegisterValueChangedCallback(OnControlValueChanged); Label.SetVisible(!hideLabel); Label.AddToClassList("unity-base-field__label--with-dragger"); @@ -193,7 +192,7 @@ public FovPropertyControl(SerializedProperty property, bool hideLabel) : base(hi m_Control.OnInitialGeometry(() => m_Control.SafeSetIsDelayed()); m_Presets = Contents.AddChild(new PopupField - { tooltip = "Customizable Lens Palette", style = {flexBasis = 20, flexGrow = 1}}); + { tooltip = "Customizable Lens Palette", style = { flexBasis = 20, flexGrow = 1 }}); m_Presets.RegisterValueChangedCallback(OnPresetValueChanged); ShortLabel = new Label("X") { style = { alignSelf = Align.Center, opacity = 0.5f }}; diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/MinMaxRangeSliderPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/MinMaxRangeSliderPropertyDrawer.cs index c252f9489..dd748645b 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/MinMaxRangeSliderPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/MinMaxRangeSliderPropertyDrawer.cs @@ -14,7 +14,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var minField = new FloatField { value = property.vector2Value.x, isDelayed = true, style = { flexGrow = 1, flexBasis = 0 }}; - minField.AddToClassList(InspectorUtility.kAlignFieldClass); + minField.AddToClassList(InspectorUtility.AlignFieldClassName); minField.TrackPropertyValue(property, (evt) => minField.value = evt.vector2Value.x); minField.RegisterValueChangedCallback((evt) => { @@ -26,6 +26,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var slider = new MinMaxSlider() { + focusable = false, lowLimit = a.Min, highLimit = a.Max, style = { flexGrow = 3, flexBasis = 0, paddingLeft = 5, paddingRight = 5 } }; @@ -42,7 +43,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) property.serializedObject.ApplyModifiedProperties(); }); - var row = new InspectorUtility.LeftRightRow { style = { flexGrow = 1 }}; + var row = new InspectorUtility.LeftRightRow(); row.Left.Add(new Label { text = property.displayName, tooltip = property.tooltip, style = { alignSelf = Align.Center }}); row.Right.Add(minField); row.Right.Add(slider); diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/OutputChannelsPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/OutputChannelsPropertyDrawer.cs index 9c31773ef..70c0d7af5 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/OutputChannelsPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/OutputChannelsPropertyDrawer.cs @@ -20,7 +20,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var row = new InspectorUtility.LabeledRow(preferredLabel, property.tooltip); var selector = row.Contents.AddChild(new MaskField(choices, 1) - { tooltip = property.tooltip, style = { flexBasis = 100, flexGrow = 1, marginLeft = 0 }}); + { tooltip = property.tooltip, style = { flexBasis = 100, flexGrow = 1, marginLeft = 3 }}); selector.BindProperty(property); row.Contents.Add(InspectorUtility.MiniPopupButton(null, new ContextualMenuManipulator((evt) => diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/SensorSizePropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/SensorSizePropertyDrawer.cs index bf2f4fd4b..245e63d4b 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/SensorSizePropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/SensorSizePropertyDrawer.cs @@ -75,8 +75,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) property.serializedObject.ApplyModifiedProperties(); // Find matching preset (if any) - var index = s_PresetSizes.FindIndex( - e => Mathf.Approximately(v.x, e.x) && Mathf.Approximately(v.y, e.y)); + var index = s_PresetSizes.FindIndex(e => Mathf.Approximately(v.x, e.x) && Mathf.Approximately(v.y, e.y)); presets.SetValueWithoutNotify(index >= 0 ? s_PresetNames[index] : "Custom"); }); diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/SplineAutoDollyPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/SplineAutoDollyPropertyDrawer.cs index 5f868676c..f9226f62d 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/SplineAutoDollyPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/SplineAutoDollyPropertyDrawer.cs @@ -27,7 +27,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) choices = AutoDollyMenuItems.s_ItemNames, style = { flexGrow = 1 } }); - dropdown.AddToClassList(InspectorUtility.kAlignFieldClass); + dropdown.AddToClassList(InspectorUtility.AlignFieldClassName); dropdown.RegisterValueChangedCallback((evt) => { var index = AutoDollyMenuItems.GetTypeIndex(evt.newValue); diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/TagFieldPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/TagFieldPropertyDrawer.cs index 5fc82bac6..d11ded050 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/TagFieldPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/TagFieldPropertyDrawer.cs @@ -11,7 +11,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) { var row = new InspectorUtility.LabeledRow(property.displayName, property.tooltip); - var enabled = row.Contents.AddChild(new Toggle() { style = { marginTop = 2, marginLeft = 0 }}); + var enabled = row.Contents.AddChild(new Toggle() { style = { marginTop = 2, marginLeft = 3 }}); enabled.RegisterValueChangedCallback((evt) => { property.stringValue = evt.newValue ? "Untagged" : string.Empty; diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs index 49f73cda5..817fd5238 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs @@ -23,7 +23,6 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) { minField.SafeSetIsDelayed(); maxField.SafeSetIsDelayed(); - minField.Q().style.marginLeft = 2; }); label.AddDelayedFriendlyPropertyDragger(xProp, minField, (d) => d.CancelDelayedWhenDragging = true); diff --git a/com.unity.cinemachine/Editor/Timeline/CinemachineShotEditor.cs b/com.unity.cinemachine/Editor/Timeline/CinemachineShotEditor.cs index e79b57610..adb6057a2 100644 --- a/com.unity.cinemachine/Editor/Timeline/CinemachineShotEditor.cs +++ b/com.unity.cinemachine/Editor/Timeline/CinemachineShotEditor.cs @@ -105,7 +105,7 @@ public override VisualElement CreateInspectorGUI() tooltip = CinemachineTimelinePrefs.s_AutoCreateLabel.tooltip, value = CinemachineTimelinePrefs.AutoCreateShotFromSceneView.Value }); - toggle.AddToClassList(InspectorUtility.kAlignFieldClass); + toggle.AddToClassList(InspectorUtility.AlignFieldClassName); toggle.RegisterValueChangedCallback((evt) => CinemachineTimelinePrefs.AutoCreateShotFromSceneView.Value = evt.newValue); // Cached scrubbing diff --git a/com.unity.cinemachine/Editor/Utility/CmCameraInspectorUtility.cs b/com.unity.cinemachine/Editor/Utility/CmCameraInspectorUtility.cs index 8c7b19c73..ae2d9ffc5 100644 --- a/com.unity.cinemachine/Editor/Utility/CmCameraInspectorUtility.cs +++ b/com.unity.cinemachine/Editor/Utility/CmCameraInspectorUtility.cs @@ -57,7 +57,7 @@ public static void AddCameraStatus(this UnityEditor.Editor editor, VisualElement { text = "Solo", style = { flexGrow = 1, paddingLeft = 0, paddingRight = 0, - marginLeft = 0, marginRight = 0, borderLeftWidth = 1, borderRightWidth = 1 } + marginLeft = 3, marginRight = 0, borderLeftWidth = 1, borderRightWidth = 1 } }); var updateMode = row.Contents.AddChild(new Label("(Update Mode)") { style = { flexGrow = 0, alignSelf = Align.Center }}); updateMode.SetEnabled(false); @@ -290,7 +290,7 @@ public static void AddExtensionsDropdown(this UnityEditor.Editor editor, VisualE text = "(select)", style = { - flexGrow = 1, marginRight = 0, marginLeft = 3, + flexGrow = 1, marginRight = -2, marginLeft = 3, paddingTop = 0, paddingBottom = 0, paddingLeft = 1, height = InspectorUtility.SingleLineHeight + 2, unityTextAlign = TextAnchor.MiddleLeft @@ -392,7 +392,7 @@ public static void AddGlobalControls(this UnityEditor.Editor editor, VisualEleme tooltip = CinemachineCorePrefs.s_SaveDuringPlayLabel.tooltip, value = SaveDuringPlay.Enabled }); - toggle.AddToClassList(InspectorUtility.kAlignFieldClass); + toggle.AddToClassList(InspectorUtility.AlignFieldClassName); toggle.RegisterValueChangedCallback((evt) => { SaveDuringPlay.Enabled = evt.newValue; @@ -409,7 +409,7 @@ public static void AddGlobalControls(this UnityEditor.Editor editor, VisualEleme index = index, style = { flexGrow = 1 } }); - dropdown.AddToClassList(InspectorUtility.kAlignFieldClass); + dropdown.AddToClassList(InspectorUtility.AlignFieldClassName); dropdown.RegisterValueChangedCallback((evt) => { CinemachineCorePrefs.ShowInGameGuides.Value = evt.newValue != choices[0]; @@ -497,7 +497,7 @@ public static void AddChildCameras( if (vcam == null) return; - var floatFieldWidth = EditorGUIUtility.singleLineHeight * 2.5f; + var floatFieldWidth = EditorGUIUtility.singleLineHeight * 3f; var helpBox = ux.AddChild(new HelpBox( "Child Cameras cannot be displayed when multiple objects are selected.", @@ -508,8 +508,7 @@ public static void AddChildCameras( var header = container.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = -2 } }); header.AddToClassList("unity-collection-view--with-border"); header.AddChild(new Label("Child Cameras") { style = { marginLeft = 3, flexGrow = 1, flexBasis = 10 }}); - header.AddChild(new Label("Priority") - { style = { marginRight = 4, flexGrow = 1, flexBasis = floatFieldWidth, unityTextAlign = TextAnchor.MiddleRight }}); + header.AddChild(new Label("Priority") { style = { flexGrow = 0, flexBasis = floatFieldWidth + 4}}); var list = container.AddChild(new ListView() { @@ -522,50 +521,63 @@ public static void AddChildCameras( }); list.itemsSource = vcam.ChildCameras; - list.makeItem = () => new VisualElement { style = { flexDirection = FlexDirection.Row }}; - list.bindItem = (row, index) => + list.makeItem = () => { - // Remove children - items seem to get recycled - for (int i = row.childCount - 1; i >= 0; --i) - row.RemoveAt(i); + var row = new VisualElement { style = { flexDirection = FlexDirection.Row }}; + + var warningIcon = row.AddChild(InspectorUtility.MiniHelpIcon("Item is null")); + warningIcon.name = "warningIcon"; - var element = list.itemsSource[index] as CinemachineVirtualCameraBase; row.AddChild(new ObjectField { - value = element, + name = "vcamSelector", objectType = typeof(CinemachineVirtualCameraBase), style = { flexBasis = 20, flexGrow = 1 } }).SetEnabled(false); - if (element == null) - return; - var warningIcon = row.AddChild(InspectorUtility.MiniHelpIcon("Item is null")); + var priorityField = row.AddChild(InspectorUtility.CreateDraggableField( + typeof(int), "", "The child camera's Priority", row.AddChild(new Label(" ")), out _)); + priorityField.name = "priorityField"; + priorityField.style.flexBasis = floatFieldWidth; + priorityField.style.flexGrow = 0; + priorityField.style.marginRight = 4; + priorityField.SafeSetIsDelayed(); + + return row; + }; + + list.bindItem = (row, index) => + { + var element = list.itemsSource[index] as CinemachineVirtualCameraBase; + row.Q("vcamSelector").value = element; + + var warningIcon = row.Q("warningIcon"); var warningText = getChildWarning == null ? string.Empty : getChildWarning(element); warningIcon.tooltip = warningText; warningIcon.SetVisible(!string.IsNullOrEmpty(warningText)); - var dragger = row.AddChild(new Label(" ")); - dragger.AddToClassList("unity-base-field__label--with-dragger"); - - var so = new SerializedObject(element); - var prop = so.FindProperty("Priority"); - var enabledProp = prop.FindPropertyRelative("Enabled"); - var priorityProp = prop.FindPropertyRelative("m_Value"); - var priorityField = row.AddChild(new IntegerField - { - value = enabledProp.boolValue ? priorityProp.intValue : 0, - style = { flexBasis = floatFieldWidth, flexGrow = 0, marginRight = 4 } - }); - new DelayedFriendlyFieldDragger(priorityField) { CancelDelayedWhenDragging = true }.SetDragZone(dragger); - priorityField.RegisterValueChangedCallback((evt) => + if (element != null) { - if (evt.newValue != 0) - enabledProp.boolValue = true; - priorityProp.intValue = evt.newValue; - so.ApplyModifiedProperties(); - }); - priorityField.TrackPropertyValue(priorityProp, (p) => priorityField.value = p.intValue); - priorityField.TrackPropertyValue(enabledProp, (p) => priorityField.value = p.boolValue ? priorityProp.intValue : 0); + var so = new SerializedObject(element); + var prop = so.FindProperty("Priority"); + var enabledProp = prop.FindPropertyRelative("Enabled"); + var priorityProp = prop.FindPropertyRelative("m_Value"); + var priorityField = row.Q("priorityField"); + + priorityField.Unbind(); + priorityField.TrackPropertyWithInitialCallback(priorityProp, (p) => priorityField.value = p.intValue); + priorityField.TrackPropertyWithInitialCallback(enabledProp, (p) => priorityField.value = p.boolValue ? priorityProp.intValue : 0); + + priorityField.UnregisterValueChangedCallback(OnValueChanged); + priorityField.RegisterValueChangedCallback(OnValueChanged); + void OnValueChanged(ChangeEvent evt) + { + if (evt.newValue != 0) + enabledProp.boolValue = true; + priorityProp.intValue = evt.newValue; + so.ApplyModifiedProperties(); + } + } }; list.itemsAdded += (added) => diff --git a/com.unity.cinemachine/Editor/Utility/CmPipelineComponentInspectorUtility.cs b/com.unity.cinemachine/Editor/Utility/CmPipelineComponentInspectorUtility.cs index ee49ad063..0d7fc70cd 100644 --- a/com.unity.cinemachine/Editor/Utility/CmPipelineComponentInspectorUtility.cs +++ b/com.unity.cinemachine/Editor/Utility/CmPipelineComponentInspectorUtility.cs @@ -27,12 +27,14 @@ static class CmPipelineComponentInspectorUtility /// public static void AddMissingCmCameraHelpBox(this UnityEditor.Editor editor, VisualElement ux) { - // Look for a RequiredTargetAttribute + // Look for a RequiredTargetAttribute, but only if component is on a CinemachineCamera var requiredTargets = RequiredTargetAttribute.RequiredTargets.None; - var a = editor.target.GetType().GetCustomAttribute(); - if (a != null) - requiredTargets = a.RequiredTarget; - + if (editor.target is Component c && c.TryGetComponent(out _)) + { + var a = editor.target.GetType().GetCustomAttribute(); + if (a != null) + requiredTargets = a.RequiredTarget; + } var targets = editor.targets; var noCameraHelp = ux.AddChild(InspectorUtility.HelpBoxWithButton( k_NeedCamera, HelpBoxMessageType.Warning, diff --git a/com.unity.cinemachine/Editor/Utility/DelayedFriendlyFieldDragger.cs b/com.unity.cinemachine/Editor/Utility/DelayedFriendlyFieldDragger.cs index 48538f46b..43b0dff88 100644 --- a/com.unity.cinemachine/Editor/Utility/DelayedFriendlyFieldDragger.cs +++ b/com.unity.cinemachine/Editor/Utility/DelayedFriendlyFieldDragger.cs @@ -12,16 +12,19 @@ interface IDelayedFriendlyDragger public bool CancelDelayedWhenDragging { get; set; } /// Called when dragging starts. - public Action OnStartDrag { get; set; } + public Action OnStartDrag { get; set; } /// Called when dragging stops. - public Action OnStopDrag { get; set; } + public Action OnStopDrag { get; set; } /// Called when the value changes during dragging. public Action OnDragValueChangedInt { get; set; } /// Called when the value changes during dragging. public Action OnDragValueChangedFloat { get; set; } + + /// Get the VisualElement being dragged + public VisualElement DragElement { get; } } /// @@ -48,24 +51,27 @@ public DelayedFriendlyFieldDragger(IValueField drivenField) /// Is dragging. public bool dragging { get; set; } - /// Start value before drag. + /// public T startValue { get; set; } - /// If true, temporarily disable isDelayed when draggin + /// public bool CancelDelayedWhenDragging { get; set; } - /// Called when dragging starts. - public Action OnStartDrag { get; set; } + /// + public Action OnStartDrag { get; set; } - /// Called when dragging stops. - public Action OnStopDrag { get; set; } + /// + public Action OnStopDrag { get; set; } - /// Called when the value changes during dragging. + /// public Action OnDragValueChangedInt { get; set; } - /// Called when the value changes during dragging. + /// public Action OnDragValueChangedFloat { get; set; } + /// + public VisualElement DragElement => m_DragElement; + /// public sealed override void SetDragZone(VisualElement dragElement, Rect hotZone) { @@ -136,7 +142,7 @@ private void ProcessDownEvent(EventBase evt) m_DrivenField.StartDragging(); EditorGUIUtility.SetWantsMouseJumping(1); - OnStartDrag?.Invoke(); + OnStartDrag?.Invoke(this); } private void UpdateValueOnPointerMove(PointerMoveEvent evt) @@ -175,7 +181,7 @@ private void ProcessUpEvent(EventBase evt, int pointerId) { if (dragging) { - OnStopDrag?.Invoke(); + OnStopDrag?.Invoke(this); dragging = false; m_DragElement.UnregisterCallback(UpdateValueOnPointerMove); m_DragElement.ReleasePointer(pointerId); diff --git a/com.unity.cinemachine/Editor/Utility/EmbeddedAssetEditorUtility.cs b/com.unity.cinemachine/Editor/Utility/EmbeddedAssetEditorUtility.cs index e006aa77d..a6463db1e 100644 --- a/com.unity.cinemachine/Editor/Utility/EmbeddedAssetEditorUtility.cs +++ b/com.unity.cinemachine/Editor/Utility/EmbeddedAssetEditorUtility.cs @@ -100,8 +100,7 @@ void OnAssetChanged(SerializedProperty sProp, EmbeddedEditorContext eContext) if (embeddedInspectorParent != null) eContext.Inspector = embeddedInspectorParent.AddChild(new InspectorElement(eContext.Editor)); } - if (unassignedUx != null) - unassignedUx.SetVisible(target == null); + unassignedUx?.SetVisible(target == null); if (assignedUx != null) assignedUx.SetVisible(target != null); } @@ -124,8 +123,21 @@ public static VisualElement AssetSelectorWithPresets( SerializedProperty property, string label = null, string presetsPath = null, string warningTextIfNull = null) where T : ScriptableObject { - var row = InspectorUtility.PropertyRow(property, out var selector, label); - var contents = row.Contents; + VisualElement ux; + VisualElement contents; + if (label == string.Empty) + { + contents = new VisualElement { style = { flexGrow = 1, flexDirection = FlexDirection.Row }}; + ux = contents; + } + else + { + var row = new InspectorUtility.LabeledRow(label ?? property.displayName, property.tooltip); + contents = row.Contents; + ux = row; + } + + var selector = contents.AddChild(new PropertyField(property, "") { style = { flexGrow = 1 }}); Label warningIcon = null; if (!string.IsNullOrEmpty(warningTextIfNull)) @@ -208,7 +220,7 @@ public static VisualElement AssetSelectorWithPresets( } }))); - row.TrackPropertyWithInitialCallback(property, (p) => + ux.TrackPropertyWithInitialCallback(property, (p) => { if (p.serializedObject == null) return; // object deleted @@ -227,12 +239,11 @@ public static VisualElement AssetSelectorWithPresets( selector.SetVisible(presetIndex < 0); }); - return row; + return ux; // Local function static List GetAssetTypes(Type baseType) { - // GML todo: optimize with TypeCache var allTypes = ReflectionHelpers.GetTypesDerivedFrom(baseType, (t) => !t.IsAbstract && t.GetCustomAttribute() == null); var list = new List(); diff --git a/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs b/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs index c08392277..ca2d7271e 100644 --- a/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs +++ b/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs @@ -6,6 +6,7 @@ using System.Reflection; using UnityEngine.UIElements; using UnityEditor.UIElements; +using System.Linq.Expressions; namespace Unity.Cinemachine.Editor { @@ -13,7 +14,7 @@ namespace Unity.Cinemachine.Editor /// Collection of tools and helpers for drawing inspectors /// [InitializeOnLoad] - static class InspectorUtility + static partial class InspectorUtility { /// /// Callback that happens whenever something undoable happens, either with @@ -31,244 +32,7 @@ static InspectorUtility() static void OnUserDidSomething() => UserDidSomething?.Invoke(); static void OnUserDidSomethingStream(ref ObjectChangeEventStream stream) => UserDidSomething?.Invoke(); } - -#if !CINEMACHINE_NO_CM2_SUPPORT - /// Put multiple properties on a single inspector line, with - /// optional label overrides. Passing null as a label (or sublabel) override will - /// cause the property's displayName to be used as a label. For no label at all, - /// pass GUIContent.none. - /// Rect in which to draw - /// Main label - /// Properties to place on the line - /// Sublabels for the properties - public static void MultiPropertyOnLine( - Rect rect, - GUIContent label, - SerializedProperty[] props, GUIContent[] subLabels) - { - if (props == null || props.Length == 0) - return; - - const int hSpace = 2; - int indentLevel = EditorGUI.indentLevel; - float labelWidth = EditorGUIUtility.labelWidth; - - float totalSubLabelWidth = 0; - int numBoolColumns = 0; - List actualLabels = new List(); - for (int i = 0; i < props.Length; ++i) - { - GUIContent sublabel = new GUIContent(props[i].displayName, props[i].tooltip); - if (subLabels != null && subLabels.Length > i && subLabels[i] != null) - sublabel = subLabels[i]; - actualLabels.Add(sublabel); - totalSubLabelWidth += GUI.skin.label.CalcSize(sublabel).x; - if (i > 0) - totalSubLabelWidth += hSpace; - // Special handling for toggles, or it looks stupid - if (props[i].propertyType == SerializedPropertyType.Boolean) - { - totalSubLabelWidth += rect.height + hSpace; - ++numBoolColumns; - } - } - - float subFieldWidth = rect.width - labelWidth - totalSubLabelWidth; - float numCols = props.Length - numBoolColumns; - float colWidth = numCols == 0 ? 0 : subFieldWidth / numCols; - - // Main label. If no first sublabel, then main label must take on that - // role, for mouse dragging value-scrolling support - int subfieldStartIndex = 0; - if (label == null) - label = new GUIContent(props[0].displayName, props[0].tooltip); - if (actualLabels[0] != GUIContent.none) - rect = EditorGUI.PrefixLabel(rect, label); - else - { - rect.width = labelWidth + colWidth; - EditorGUI.PropertyField(rect, props[0], label); - rect.x += rect.width + hSpace; - subfieldStartIndex = 1; - } - - for (int i = subfieldStartIndex; i < props.Length; ++i) - { - EditorGUI.indentLevel = 0; - EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(actualLabels[i]).x; - if (props[i].propertyType == SerializedPropertyType.Boolean) - { - rect.x += hSpace; - rect.width = EditorGUIUtility.labelWidth + rect.height; - EditorGUI.BeginProperty(rect, actualLabels[i], props[i]); - props[i].boolValue = EditorGUI.ToggleLeft(rect, actualLabels[i], props[i].boolValue); - } - else - { - rect.width = EditorGUIUtility.labelWidth + colWidth; - EditorGUI.BeginProperty(rect, actualLabels[i], props[i]); - EditorGUI.PropertyField(rect, props[i], actualLabels[i]); - } - EditorGUI.EndProperty(); - rect.x += rect.width + hSpace; - } - - EditorGUIUtility.labelWidth = labelWidth; - EditorGUI.indentLevel = indentLevel; - } - - public static float PropertyHeightOfChidren(SerializedProperty property) - { - float height = 0; - var childProperty = property.Copy(); - var endProperty = childProperty.GetEndProperty(); - childProperty.NextVisible(true); - while (!SerializedProperty.EqualContents(childProperty, endProperty)) - { - height += EditorGUI.GetPropertyHeight(childProperty) - + EditorGUIUtility.standardVerticalSpacing; - childProperty.NextVisible(false); - } - return height - EditorGUIUtility.standardVerticalSpacing; - } - - public static void DrawChildProperties(Rect position, SerializedProperty property) - { - var childProperty = property.Copy(); - var endProperty = childProperty.GetEndProperty(); - childProperty.NextVisible(true); - while (!SerializedProperty.EqualContents(childProperty, endProperty)) - { - position.height = EditorGUI.GetPropertyHeight(childProperty); - EditorGUI.PropertyField(position, childProperty, true); - position.y += position.height + EditorGUIUtility.standardVerticalSpacing; - childProperty.NextVisible(false); - } - } - - public static void HelpBoxWithButton( - string message, MessageType messageType, - GUIContent buttonContent, Action onClicked) - { - float lineHeight = EditorGUIUtility.singleLineHeight; - var buttonSize = GUI.skin.label.CalcSize(buttonContent); - buttonSize.x += lineHeight; - - var rect = EditorGUI.IndentedRect(EditorGUILayout.GetControlRect(false, 2)); - - var boxContent = new GUIContent(message + "\n"); // to make room for the button - var boxWidth = rect.width; - var boxHeight = GUI.skin.GetStyle("helpbox").CalcHeight(boxContent, rect.width - 3 * lineHeight) + buttonSize.y; - - rect = EditorGUILayout.GetControlRect(false, boxHeight); - rect = EditorGUI.IndentedRect(rect); - rect.width = boxWidth; rect.height = boxHeight; - EditorGUI.HelpBox(rect, boxContent.text, messageType); - - rect.x += rect.width - buttonSize.x - 6; rect.width = buttonSize.x; - rect.y += rect.height - buttonSize.y - 6; rect.height = buttonSize.y; - if (GUI.Button(rect, buttonContent, EditorStyles.miniButton)) - onClicked(); - } - - public static float EnabledFoldoutHeight(SerializedProperty property, string enabledPropertyName) - { - var enabledProp = property.FindPropertyRelative(enabledPropertyName); - if (enabledProp == null) - return EditorGUI.GetPropertyHeight(property); - if (!enabledProp.boolValue) - return EditorGUIUtility.singleLineHeight; - return PropertyHeightOfChidren(property); - } - - public static bool EnabledFoldout( - Rect rect, SerializedProperty property, string enabledPropertyName, - GUIContent label = null) - { - var enabledProp = property.FindPropertyRelative(enabledPropertyName); - if (enabledProp == null) - { - EditorGUI.PropertyField(rect, property, true); - rect.x += EditorGUIUtility.labelWidth; - EditorGUI.LabelField(rect, new GUIContent($"unknown field `{enabledPropertyName}`")); - return property.isExpanded; - } - rect.height = EditorGUIUtility.singleLineHeight; - label ??= new GUIContent(property.displayName, enabledProp.tooltip); - EditorGUI.PropertyField(rect, enabledProp, label); - if (enabledProp.boolValue) - { - ++EditorGUI.indentLevel; - var childProperty = property.Copy(); - var endProperty = childProperty.GetEndProperty(); - childProperty.NextVisible(true); - while (!SerializedProperty.EqualContents(childProperty, endProperty)) - { - if (!SerializedProperty.EqualContents(childProperty, enabledProp)) - { - rect.y += rect.height + EditorGUIUtility.standardVerticalSpacing; - rect.height = EditorGUI.GetPropertyHeight(childProperty); - EditorGUI.PropertyField(rect, childProperty, true); - } - childProperty.NextVisible(false); - } - --EditorGUI.indentLevel; - } - return enabledProp.boolValue; - } - public static bool EnabledFoldoutSingleLine( - Rect rect, SerializedProperty property, - string enabledPropertyName, string disabledToggleLabel, - GUIContent label = null) - { - var enabledProp = property.FindPropertyRelative(enabledPropertyName); - if (enabledProp == null) - { - EditorGUI.PropertyField(rect, property, true); - rect.x += EditorGUIUtility.labelWidth; - EditorGUI.LabelField(rect, new GUIContent($"unknown field `{enabledPropertyName}`")); - return property.isExpanded; - } - rect.height = EditorGUIUtility.singleLineHeight; - label ??= new GUIContent(property.displayName, enabledProp.tooltip); - EditorGUI.PropertyField(rect, enabledProp, label); - if (!enabledProp.boolValue) - { - if (!string.IsNullOrEmpty(disabledToggleLabel)) - { - var w = EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight + 3; - var r = rect; r.x += w; r.width -= w; - var oldColor = GUI.color; - GUI.color = new (oldColor.r, oldColor.g, oldColor.g, 0.5f); - EditorGUI.LabelField(r, disabledToggleLabel); - GUI.color = oldColor; - } - } - else - { - rect.width -= EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight; - rect.x += EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight; - - var childProperty = property.Copy(); - var endProperty = childProperty.GetEndProperty(); - childProperty.NextVisible(true); - while (!SerializedProperty.EqualContents(childProperty, endProperty)) - { - if (!SerializedProperty.EqualContents(childProperty, enabledProp)) - { - var oldWidth = EditorGUIUtility.labelWidth; - EditorGUIUtility.labelWidth = 6; // for dragging - EditorGUI.PropertyField(rect, childProperty, new GUIContent(" ")); - EditorGUIUtility.labelWidth = oldWidth; - break; // Draw only the first property - } - childProperty.NextVisible(false); - } - } - return enabledProp.boolValue; - } -#endif /// /// Add to a list all assets of a given type found in a given location /// @@ -319,7 +83,6 @@ public static string NicifyClassName(string name) { if (name.StartsWith("Cinemachine")) name = name.Substring(11); // Trim the prefix - return ObjectNames.NicifyVariableName(name); } @@ -331,30 +94,11 @@ public static string NicifyClassName(string name) /// The nicified name public static string NicifyClassName(Type type) { - var name = type.Name; - if (name.StartsWith("Cinemachine")) - name = name.Substring(11); // Trim the prefix - - name = ObjectNames.NicifyVariableName(name); - + var name = NicifyClassName(type.Name); if (type.GetCustomAttribute() != null) name += " (Deprecated)"; - return name; } - - // Temporarily here - /// - /// Creates a new GameObject. - /// - /// Name to give the object. - /// Optional components to add. - /// The GameObject that was created. - [Obsolete("Use ObjectFactory.CreateGameObject(string name, params Type[] types) instead.")] - public static GameObject CreateGameObject(string name, params Type[] types) - { - return ObjectFactory.CreateGameObject(name, types); - } private static int m_lastRepaintFrame; @@ -372,7 +116,7 @@ public static void RepaintGameView(UnityEngine.Object unused = null) UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } - static Dictionary s_AssignableTypes = new Dictionary(); + static Dictionary s_AssignableTypes = new (); public const string s_NoneString = "(none)"; public static string GetAssignableBehaviourNames(Type inputType) @@ -398,14 +142,8 @@ public static string GetAssignableBehaviourNames(Type inputType) return s_AssignableTypes[inputType]; } - ///============================================================================================== - ///============================================================================================== - /// UI Elements utilities - ///============================================================================================== - ///============================================================================================== - /// Aligns fields created by UI toolkit the unity inspector standard way. - public static string kAlignFieldClass => BaseField.alignedFieldUssClassName; + public static string AlignFieldClassName => BaseField.alignedFieldUssClassName; // this is a hack to get around some vertical alignment issues in UITK public static float SingleLineHeight => EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing; @@ -515,7 +253,7 @@ public static void AddHeader(this VisualElement ux, string text, string tooltip row.focusable = false; row.Label.style.flexGrow = 1; row.Label.style.paddingTop = verticalPad; - row.Label.style.paddingBottom = EditorGUIUtility.standardVerticalSpacing; + row.Label.style.paddingBottom = EditorGUIUtility.standardVerticalSpacing - 2; } /// @@ -554,6 +292,40 @@ public static void AddDelayedFriendlyPropertyDragger( }); } } + + public static VisualElement CreateDraggableField(Expression> exp, Label label, out IDelayedFriendlyDragger dragger) + { + var bindingPath = SerializedPropertyHelper.PropertyName(exp); + var tooltip = SerializedPropertyHelper.PropertyTooltip(exp); + return CreateDraggableField(SerializedPropertyHelper.PropertyType(exp), bindingPath, tooltip, label, out dragger); + } + + public static VisualElement CreateDraggableField(Type type, string bindingPath, string tooltip, Label label, out IDelayedFriendlyDragger dragger) + { + VisualElement field; + label.AddToClassList("unity-base-field__label--with-dragger"); + label.tooltip = tooltip; + label.style.alignSelf = Align.Center; + if (type == typeof(float)) + { + field = new FloatField { bindingPath = bindingPath, tooltip = tooltip }; + dragger = new DelayedFriendlyFieldDragger((FloatField)field); + } + else if (type == typeof(int)) + { + field = new IntegerField { bindingPath = bindingPath, tooltip = tooltip }; + dragger = new DelayedFriendlyFieldDragger((IntegerField)field); + } + else + { + field = new PropertyField(null, "") { bindingPath = bindingPath, tooltip = tooltip }; + dragger = null; + } + var d = dragger as BaseFieldMouseDragger; + d?.SetDragZone(label); + return field; + } + /// A small warning sybmol, suitable for embedding in an inspector row /// The tooltip text @@ -592,7 +364,7 @@ public static Button MiniPopupButton(string tooltip = null, ContextualMenuManipu backgroundImage = (StyleBackground)EditorGUIUtility.IconContent("_Popup").image, width = SingleLineHeight, height = SingleLineHeight, alignSelf = Align.Center, - paddingRight = 0, borderRightWidth = 0, marginRight = 0 + paddingRight = 0, marginRight = 0 }}; if (contextMenu != null) { @@ -615,7 +387,7 @@ public static Button MiniDropdownButton(string tooltip = null, ContextualMenuMan backgroundImage = (StyleBackground)EditorGUIUtility.IconContent("dropdown").image, width = SingleLineHeight, height = SingleLineHeight, alignSelf = Align.Center, - paddingRight = 0, borderRightWidth = 0, marginRight = 0 + paddingRight = 0, marginRight = 0 }}; if (contextMenu != null) { @@ -626,34 +398,6 @@ public static Button MiniDropdownButton(string tooltip = null, ContextualMenuMan return button; } - /// - /// This is a hack to get proper layout within th inspector. - /// There seems to be no sanctioned way to get the current inspector label width. - /// This creates a row with a properly-sized label in front of it. - /// - public class LabeledRow : BaseField // bool is just a dummy because it has to be something - { - public Label Label => labelElement; - public VisualElement Contents { get; } - - public LabeledRow(string label, string tooltip = "") - : this (label, tooltip, new VisualElement()) - { - style.flexDirection = FlexDirection.Row; - style.flexGrow = 1; - Contents.style.flexDirection = FlexDirection.Row; - Contents.style.flexGrow = 1; - } - - public LabeledRow(string label, string tooltip, VisualElement contents) : base(label, contents) - { - Contents = contents; - AddToClassList(alignedFieldUssClassName); - this.tooltip = tooltip; - Label.tooltip = tooltip; - } - } - /// /// This is an inspector container with 2 side-by-side rows. The Left row's width is /// locked to the inspector field label size, for proper alignment. @@ -673,25 +417,106 @@ public class LeftRightRow : VisualElement /// public bool KillLeftMargin; - public LeftRightRow() + public LeftRightRow(VisualElement left = null, VisualElement right = null) { // This is to peek at the resolved label width - var hack = AddChild(this, new LabeledRow(" ") { style = { height = 1, marginTop = -2 }}); - - var row = AddChild(this, new VisualElement - { style = { flexDirection = FlexDirection.Row }}); - Left = row.AddChild(new VisualElement - { style = { flexDirection = FlexDirection.Row, flexGrow = 0 }}); - Right = row.AddChild(new VisualElement - { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}); - - hack.Label.RegisterCallback((_) => + Add(new AlignFieldSizer { OnLabelWidthChanged = (w) => { if (KillLeftMargin) - hack.style.marginLeft = 0; - Left.style.width = hack.Label.resolvedStyle.width + DivisionOffset; - row.style.marginLeft = hack.resolvedStyle.marginLeft; - }); + style.marginLeft = 0; + Left.style.width = w + DivisionOffset; + }}); + + // Actual contents will live in this row + var row = AddChild(this, new VisualElement { style = { flexDirection = FlexDirection.Row }}); + style.marginLeft = 3; + + left ??= new VisualElement(); + Left = row.AddChild(left); + Left.style.flexDirection = FlexDirection.Row; + Left.style.flexGrow = 0; + + right ??= new VisualElement(); + Right = row.AddChild(right); + Right.style.flexDirection = FlexDirection.Row; + Right.style.flexGrow = 1; + Right.style.marginLeft = 2; + } + + // This is a hacky thing to create custom inspector rows with labels that are the correct size + class AlignFieldSizer : BaseField // bool is just a dummy because it has to be something + { + public Action OnLabelWidthChanged; + public AlignFieldSizer() : base (" ", new VisualElement()) + { + focusable = false; + style.flexDirection = FlexDirection.Row; + style.flexGrow = 1; + style.height = 1; + style.marginTop = -2; + AddToClassList(AlignFieldClassName); + labelElement.RegisterCallback((_) + => OnLabelWidthChanged?.Invoke(labelElement.resolvedStyle.width)); + } + } + } + + /// + /// This creates a row with a properly-sized label in front of it. + /// The label's width is locked to the inspector field label size, for proper alignment. + /// + public class LabeledRow : LeftRightRow + { + public Label Label { get; private set; } + public VisualElement Contents { get; private set; } + + public LabeledRow(string label, string tooltip = "", VisualElement contents = null) + : base(new Label(label) { tooltip = tooltip, style = { alignSelf = Align.Center, flexGrow = 1 }}, contents) + { + Label = Left as Label; + Contents = Right; + style.marginRight = 0; + style.flexGrow = 1; + Contents.tooltip = tooltip; + Contents.style.marginRight = 0; + Contents.style.flexGrow = 1; + } + } + + /// + /// A row containing a property field. Suitable for adding widgets next to the property field. + /// + public static LabeledRow PropertyRow( + SerializedProperty property, out PropertyField propertyField, string label = null) + { + var row = new LabeledRow(label ?? property.displayName, property.tooltip); + propertyField = row.Contents.AddChild(new PropertyField(property, "") + { style = { flexGrow = 1, flexBasis = SingleLineHeight * 5 }}); + AddDelayedFriendlyPropertyDragger(row.Label, property, propertyField, (d) => d.CancelDelayedWhenDragging = true); + return row; + } + + /// + /// A property field with a minimally-sized label that does not respect inspector sizing. + /// Suitable for embedding in a row within the right-hand side of the inspector. + /// + public class CompactPropertyField : VisualElement + { + public Label Label; + public PropertyField Field; + + public CompactPropertyField(SerializedProperty property) : this(property, property.displayName) {} + + public CompactPropertyField(SerializedProperty property, string label, float minLabelWidth = 0) + { + style.flexDirection = FlexDirection.Row; + if (!string.IsNullOrEmpty(label)) + Label = AddChild(this, new Label(label) + { tooltip = property?.tooltip, style = { alignSelf = Align.Center, minWidth = minLabelWidth }}); + Field = AddChild(this, new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = 50 } }); + Field.style.marginLeft = Field.style.marginLeft.value.value - 1; + if (Label != null && property != null) + AddDelayedFriendlyPropertyDragger(Label, property, Field, (d) => d.CancelDelayedWhenDragging = true); } } @@ -740,7 +565,7 @@ public FoldoutWithOverlay(Foldout foldout, VisualElement overlay, Label overlayL foldout.SetVisible(true); foldout.value = true; closedFoldout.SetValueWithoutNotify(false); - //foldout.Focus(); // GML why doesn't this work? + foldout.Q().Focus(); } evt.StopPropagation(); } @@ -757,7 +582,7 @@ public FoldoutWithOverlay(Foldout foldout, VisualElement overlay, Label overlayL foldout.SetVisible(false); closedFoldout.SetValueWithoutNotify(false); foldout.value = false; - //closedFoldout.Focus(); // GML why doesn't this work? + closedFoldout.Q().Focus(); } evt.StopPropagation(); } @@ -765,71 +590,26 @@ public FoldoutWithOverlay(Foldout foldout, VisualElement overlay, Label overlayL } } - /// - /// A property field with a minimally-sized label that does not respect inspector sizing. - /// Suitable for embedding in a row within the right-hand side of the inspector. - /// - public class CompactPropertyField : VisualElement - { - public Label Label; - public PropertyField Field; - - public CompactPropertyField(SerializedProperty property) : this(property, property.displayName) {} - - public CompactPropertyField(SerializedProperty property, string label, float minLabelWidth = 0) - { - style.flexDirection = FlexDirection.Row; - style.flexGrow = 1; - if (!string.IsNullOrEmpty(label)) - Label = AddChild(this, new Label(label) - { tooltip = property?.tooltip, style = { alignSelf = Align.Center, minWidth = minLabelWidth }}); - Field = AddChild(this, new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = 20 } }); - if (Label != null) - AddDelayedFriendlyPropertyDragger(Label, property, Field, (d) => d.CancelDelayedWhenDragging = true); - } - } - - /// - /// A row containing a property field. Suitable for adding widgets nest to the property field. - /// - public static LabeledRow PropertyRow( - SerializedProperty property, out PropertyField propertyField, string label = null) - { - var row = new LabeledRow(label ?? property.displayName, property.tooltip); - var field = propertyField = row.Contents.AddChild(new PropertyField(property, "") - { style = { flexGrow = 1, flexBasis = SingleLineHeight * 5 }}); - AddDelayedFriendlyPropertyDragger(row.Label, property, propertyField, (d) => d.CancelDelayedWhenDragging = true); - - // Kill any left margin that gets inserted into the property field - field.OnInitialGeometry(() => - { - var children = field.Children().GetEnumerator(); - if (children.MoveNext()) - children.Current.style.marginLeft = 0; - children.Dispose(); - }); - return row; - } - public static VisualElement HelpBoxWithButton( string message, HelpBoxMessageType messageType, string buttonText, Action onClicked, ContextualMenuManipulator contextMenu = null) { var box = new VisualElement { style = { - flexDirection = FlexDirection.Column, + flexDirection = FlexDirection.Row, paddingTop = 8, paddingBottom = 8, paddingLeft = 8, paddingRight = 8 }}; box.AddToClassList("unity-help-box"); + var innerBox = box.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Column, flexGrow = 1 }}); - var row = box.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}); + var row = innerBox.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}); var icon = row.AddChild(MiniHelpIcon("", messageType)); icon.style.alignSelf = Align.Auto; icon.style.marginRight = 6; var text = row.AddChild(new Label(message) { style = { flexGrow = 1, flexBasis = 100, alignSelf = Align.Center, whiteSpace = WhiteSpace.Normal }}); - var buttons = box.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1, marginTop = 6 }}); + var buttons = innerBox.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1, marginTop = 6 }}); buttons.Add(new VisualElement { style = { flexGrow = 1 }}); var button = buttons.AddChild(new Button(onClicked) { text = buttonText }); if (contextMenu != null) diff --git a/com.unity.cinemachine/Editor/Utility/ReflectionHelpers.cs b/com.unity.cinemachine/Editor/Utility/ReflectionHelpers.cs index 29747e7e4..50721e7e3 100644 --- a/com.unity.cinemachine/Editor/Utility/ReflectionHelpers.cs +++ b/com.unity.cinemachine/Editor/Utility/ReflectionHelpers.cs @@ -39,6 +39,8 @@ public static void CopyFields( public static IEnumerable GetTypesDerivedFrom(Type type, Predicate predicate) { var list = new List(); + if (predicate(type)) + list.Add(type); var iter = TypeCache.GetTypesDerivedFrom(type).GetEnumerator(); while (iter.MoveNext()) { diff --git a/com.unity.cinemachine/Editor/Utility/SerializedPropertyHelper.cs b/com.unity.cinemachine/Editor/Utility/SerializedPropertyHelper.cs index 70796879c..937739791 100644 --- a/com.unity.cinemachine/Editor/Utility/SerializedPropertyHelper.cs +++ b/com.unity.cinemachine/Editor/Utility/SerializedPropertyHelper.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using System.Reflection; using UnityEditor; namespace Unity.Cinemachine.Editor @@ -23,17 +24,60 @@ static class SerializedPropertyHelper /// /// /// Magic expression that resolves to a field: () => myClass.m_MyField - /// - public static string PropertyName(Expression> exp) + /// string name of field + public static MemberInfo MemberInfo(Expression> exp) { if (exp.Body is not MemberExpression body) { var ubody = (UnaryExpression)exp.Body; body = ubody.Operand as MemberExpression; } - return body.Member.Name; + return body.Member; } + /// + /// This is a way to get a field name string in such a manner that the compiler will + /// generate errors for invalid fields. Much better than directly using strings. + /// Usage: instead of + /// + /// "m_MyField"; + /// + /// do this: + /// + /// MyClass myclass = null; + /// SerializedPropertyHelper.PropertyName( () => myClass.m_MyField); + /// + /// + /// Magic expression that resolves to a field: () => myClass.m_MyField + /// string name of field + public static string PropertyName(Expression> exp) => MemberInfo(exp).Name; + + /// + /// This is a way to get a field tooltip string in such a manner that the compiler will + /// generate errors for invalid fields. Much better than directly using strings. + /// + /// Magic expression that resolves to a field: () => myClass.m_MyField + /// Tooltip text + public static string PropertyTooltip(Expression> exp) + { + var attrs = MemberInfo(exp).GetCustomAttributes(typeof(UnityEngine.TooltipAttribute), false); + return attrs.Length > 0 ? ((UnityEngine.TooltipAttribute)attrs[0]).tooltip : string.Empty; + } + + /// + /// This is a way to get a field name string in such a manner that the compiler will + /// generate errors for invalid fields. Much better than directly using strings. + /// + /// Magic expression that resolves to a field: () => myClass.m_MyField + /// Type of field + public static Type PropertyType(Expression> exp) + { + var member = MemberInfo(exp); + if (member.MemberType == MemberTypes.Field) + return ((FieldInfo)member).FieldType; + throw new ArgumentException ( "Input MemberInfo must be of type FieldInfo" ); + } + /// /// A compiler-assisted (non-string-based) way to call SerializedProperty.FindProperty /// diff --git a/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs b/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs index 1086ac41b..bb9719add 100644 --- a/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs +++ b/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs @@ -13,6 +13,9 @@ static class SplineDataInspectorUtility public delegate ISplineContainer GetSplineDelegate(); public delegate T GetDefaultValueDelegate(); + public const string ItemIndexTooltip = "The position on the Spline at which this data point will take effect. " + + "The value is interpreted according to the Index Unit setting."; + public static VisualElement CreatePathUnitField(SerializedProperty splineDataProp, GetSplineDelegate getSpline) { var indexUnitProp = splineDataProp.FindPropertyRelative("m_IndexUnit"); @@ -59,7 +62,7 @@ static void ConvertPathUnit( pathUnitProp.enumValueIndex = (int)newIndexUnit; } - public static PropertyField CreateDataListField( + public static ListView CreateDataListField( SplineData splineData, SerializedProperty splineDataProp, GetSplineDelegate getSpline, @@ -71,67 +74,68 @@ public static PropertyField CreateDataListField( var pathUnitProp = splineDataProp.FindPropertyRelative("m_IndexUnit"); var arrayProp = splineDataProp.FindPropertyRelative("m_DataPoints"); - var list = new PropertyField(arrayProp); - list.OnInitialGeometry(() => + var list = new ListView + { + reorderable = false, + showBorder = true, + showFoldoutHeader = false, + showBoundCollectionSize = false, + showAddRemoveFooter = true, + virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight, + showAlternatingRowBackgrounds = AlternatingRowBackground.None + }; + list.BindProperty(arrayProp); + + // When we add the first item, make sure to use the default value + var button = list.Q