A game about forced loneliness, made by TACStudios
1using System.Collections.Generic;
2using System.Text;
3using System.Linq;
4using UnityEngine;
5using UnityEngine.UI;
6using UnityEditor.AnimatedValues;
7
8namespace UnityEditor.UI
9{
10 [CustomEditor(typeof(Selectable), true)]
11 /// <summary>
12 /// Custom Editor for the Selectable Component.
13 /// Extend this class to write a custom editor for a component derived from Selectable.
14 /// </summary>
15 public class SelectableEditor : Editor
16 {
17 SerializedProperty m_Script;
18 SerializedProperty m_InteractableProperty;
19 SerializedProperty m_TargetGraphicProperty;
20 SerializedProperty m_TransitionProperty;
21 SerializedProperty m_ColorBlockProperty;
22 SerializedProperty m_SpriteStateProperty;
23 SerializedProperty m_AnimTriggerProperty;
24 SerializedProperty m_NavigationProperty;
25
26 GUIContent m_VisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements.");
27
28 AnimBool m_ShowColorTint = new AnimBool();
29 AnimBool m_ShowSpriteTrasition = new AnimBool();
30 AnimBool m_ShowAnimTransition = new AnimBool();
31
32 private static List<SelectableEditor> s_Editors = new List<SelectableEditor>();
33 private static bool s_ShowNavigation = false;
34 private static string s_ShowNavigationKey = "SelectableEditor.ShowNavigation";
35
36 // Whenever adding new SerializedProperties to the Selectable and SelectableEditor
37 // Also update this guy in OnEnable. This makes the inherited classes from Selectable not require a CustomEditor.
38 private string[] m_PropertyPathToExcludeForChildClasses;
39
40 protected virtual void OnEnable()
41 {
42 m_Script = serializedObject.FindProperty("m_Script");
43 m_InteractableProperty = serializedObject.FindProperty("m_Interactable");
44 m_TargetGraphicProperty = serializedObject.FindProperty("m_TargetGraphic");
45 m_TransitionProperty = serializedObject.FindProperty("m_Transition");
46 m_ColorBlockProperty = serializedObject.FindProperty("m_Colors");
47 m_SpriteStateProperty = serializedObject.FindProperty("m_SpriteState");
48 m_AnimTriggerProperty = serializedObject.FindProperty("m_AnimationTriggers");
49 m_NavigationProperty = serializedObject.FindProperty("m_Navigation");
50
51 m_PropertyPathToExcludeForChildClasses = new[]
52 {
53 m_Script.propertyPath,
54 m_NavigationProperty.propertyPath,
55 m_TransitionProperty.propertyPath,
56 m_ColorBlockProperty.propertyPath,
57 m_SpriteStateProperty.propertyPath,
58 m_AnimTriggerProperty.propertyPath,
59 m_InteractableProperty.propertyPath,
60 m_TargetGraphicProperty.propertyPath,
61 };
62
63 var trans = GetTransition(m_TransitionProperty);
64 m_ShowColorTint.value = (trans == Selectable.Transition.ColorTint);
65 m_ShowSpriteTrasition.value = (trans == Selectable.Transition.SpriteSwap);
66 m_ShowAnimTransition.value = (trans == Selectable.Transition.Animation);
67
68 m_ShowColorTint.valueChanged.AddListener(Repaint);
69 m_ShowSpriteTrasition.valueChanged.AddListener(Repaint);
70
71 s_Editors.Add(this);
72 RegisterStaticOnSceneGUI();
73
74 s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey);
75 }
76
77 protected virtual void OnDisable()
78 {
79 m_ShowColorTint.valueChanged.RemoveListener(Repaint);
80 m_ShowSpriteTrasition.valueChanged.RemoveListener(Repaint);
81
82 s_Editors.Remove(this);
83 RegisterStaticOnSceneGUI();
84 }
85
86 private void RegisterStaticOnSceneGUI()
87 {
88 SceneView.duringSceneGui -= StaticOnSceneGUI;
89 if (s_Editors.Count > 0)
90 SceneView.duringSceneGui += StaticOnSceneGUI;
91 }
92
93 static Selectable.Transition GetTransition(SerializedProperty transition)
94 {
95 return (Selectable.Transition)transition.enumValueIndex;
96 }
97
98 public override void OnInspectorGUI()
99 {
100 serializedObject.Update();
101
102 EditorGUILayout.PropertyField(m_InteractableProperty);
103
104 var trans = GetTransition(m_TransitionProperty);
105
106 var graphic = m_TargetGraphicProperty.objectReferenceValue as Graphic;
107 if (graphic == null)
108 graphic = (target as Selectable).GetComponent<Graphic>();
109
110 var animator = (target as Selectable).GetComponent<Animator>();
111 m_ShowColorTint.target = (!m_TransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.ColorTint);
112 m_ShowSpriteTrasition.target = (!m_TransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.SpriteSwap);
113 m_ShowAnimTransition.target = (!m_TransitionProperty.hasMultipleDifferentValues && trans == Button.Transition.Animation);
114
115 EditorGUILayout.PropertyField(m_TransitionProperty);
116
117 ++EditorGUI.indentLevel;
118 {
119 if (trans == Selectable.Transition.ColorTint || trans == Selectable.Transition.SpriteSwap)
120 {
121 EditorGUILayout.PropertyField(m_TargetGraphicProperty);
122 }
123
124 switch (trans)
125 {
126 case Selectable.Transition.ColorTint:
127 if (graphic == null)
128 EditorGUILayout.HelpBox("You must have a Graphic target in order to use a color transition.", MessageType.Warning);
129 break;
130
131 case Selectable.Transition.SpriteSwap:
132 if (graphic as Image == null)
133 EditorGUILayout.HelpBox("You must have a Image target in order to use a sprite swap transition.", MessageType.Warning);
134 break;
135 }
136
137 if (EditorGUILayout.BeginFadeGroup(m_ShowColorTint.faded))
138 {
139 EditorGUILayout.PropertyField(m_ColorBlockProperty);
140 }
141 EditorGUILayout.EndFadeGroup();
142
143 if (EditorGUILayout.BeginFadeGroup(m_ShowSpriteTrasition.faded))
144 {
145 EditorGUILayout.PropertyField(m_SpriteStateProperty);
146 }
147 EditorGUILayout.EndFadeGroup();
148
149 if (EditorGUILayout.BeginFadeGroup(m_ShowAnimTransition.faded))
150 {
151 EditorGUILayout.PropertyField(m_AnimTriggerProperty);
152
153 if (animator == null || animator.runtimeAnimatorController == null)
154 {
155 Rect buttonRect = EditorGUILayout.GetControlRect();
156 buttonRect.xMin += EditorGUIUtility.labelWidth;
157 if (GUI.Button(buttonRect, "Auto Generate Animation", EditorStyles.miniButton))
158 {
159 var controller = GenerateSelectableAnimatorContoller((target as Selectable).animationTriggers, target as Selectable);
160 if (controller != null)
161 {
162 if (animator == null)
163 animator = (target as Selectable).gameObject.AddComponent<Animator>();
164
165 Animations.AnimatorController.SetAnimatorController(animator, controller);
166 }
167 }
168 }
169 }
170 EditorGUILayout.EndFadeGroup();
171 }
172 --EditorGUI.indentLevel;
173
174 EditorGUILayout.Space();
175
176 EditorGUILayout.PropertyField(m_NavigationProperty);
177
178 EditorGUI.BeginChangeCheck();
179 Rect toggleRect = EditorGUILayout.GetControlRect();
180 toggleRect.xMin += EditorGUIUtility.labelWidth;
181 s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton);
182 if (EditorGUI.EndChangeCheck())
183 {
184 EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation);
185 SceneView.RepaintAll();
186 }
187
188 // We do this here to avoid requiring the user to also write a Editor for their Selectable-derived classes.
189 // This way if we are on a derived class we dont draw anything else, otherwise draw the remaining properties.
190 ChildClassPropertiesGUI();
191
192 serializedObject.ApplyModifiedProperties();
193 }
194
195 // Draw the extra SerializedProperties of the child class.
196 // We need to make sure that m_PropertyPathToExcludeForChildClasses has all the Selectable properties and in the correct order.
197 // TODO: find a nicer way of doing this. (creating a InheritedEditor class that automagically does this)
198 private void ChildClassPropertiesGUI()
199 {
200 if (IsDerivedSelectableEditor())
201 return;
202
203 DrawPropertiesExcluding(serializedObject, m_PropertyPathToExcludeForChildClasses);
204 }
205
206 private bool IsDerivedSelectableEditor()
207 {
208 return GetType() != typeof(SelectableEditor);
209 }
210
211 private static Animations.AnimatorController GenerateSelectableAnimatorContoller(AnimationTriggers animationTriggers, Selectable target)
212 {
213 if (target == null)
214 return null;
215
216 // Where should we create the controller?
217 var path = GetSaveControllerPath(target);
218 if (string.IsNullOrEmpty(path))
219 return null;
220
221 // figure out clip names
222 var normalName = string.IsNullOrEmpty(animationTriggers.normalTrigger) ? "Normal" : animationTriggers.normalTrigger;
223 var highlightedName = string.IsNullOrEmpty(animationTriggers.highlightedTrigger) ? "Highlighted" : animationTriggers.highlightedTrigger;
224 var pressedName = string.IsNullOrEmpty(animationTriggers.pressedTrigger) ? "Pressed" : animationTriggers.pressedTrigger;
225 var selectedName = string.IsNullOrEmpty(animationTriggers.selectedTrigger) ? "Selected" : animationTriggers.selectedTrigger;
226 var disabledName = string.IsNullOrEmpty(animationTriggers.disabledTrigger) ? "Disabled" : animationTriggers.disabledTrigger;
227
228 // Create controller and hook up transitions.
229 var controller = Animations.AnimatorController.CreateAnimatorControllerAtPath(path);
230 GenerateTriggerableTransition(normalName, controller);
231 GenerateTriggerableTransition(highlightedName, controller);
232 GenerateTriggerableTransition(pressedName, controller);
233 GenerateTriggerableTransition(selectedName, controller);
234 GenerateTriggerableTransition(disabledName, controller);
235
236 AssetDatabase.ImportAsset(path);
237
238 return controller;
239 }
240
241 private static string GetSaveControllerPath(Selectable target)
242 {
243 var defaultName = target.gameObject.name;
244 var message = string.Format("Create a new animator for the game object '{0}':", defaultName);
245 return EditorUtility.SaveFilePanelInProject("New Animation Contoller", defaultName, "controller", message);
246 }
247
248 private static void SetUpCurves(AnimationClip highlightedClip, AnimationClip pressedClip, string animationPath)
249 {
250 string[] channels = { "m_LocalScale.x", "m_LocalScale.y", "m_LocalScale.z" };
251
252 var highlightedKeys = new[] { new Keyframe(0f, 1f), new Keyframe(0.5f, 1.1f), new Keyframe(1f, 1f) };
253 var highlightedCurve = new AnimationCurve(highlightedKeys);
254 foreach (var channel in channels)
255 AnimationUtility.SetEditorCurve(highlightedClip, EditorCurveBinding.FloatCurve(animationPath, typeof(Transform), channel), highlightedCurve);
256
257 var pressedKeys = new[] { new Keyframe(0f, 1.15f) };
258 var pressedCurve = new AnimationCurve(pressedKeys);
259 foreach (var channel in channels)
260 AnimationUtility.SetEditorCurve(pressedClip, EditorCurveBinding.FloatCurve(animationPath, typeof(Transform), channel), pressedCurve);
261 }
262
263 private static string BuildAnimationPath(Selectable target)
264 {
265 // if no target don't hook up any curves.
266 var highlight = target.targetGraphic;
267 if (highlight == null)
268 return string.Empty;
269
270 var startGo = highlight.gameObject;
271 var toFindGo = target.gameObject;
272
273 var pathComponents = new Stack<string>();
274 while (toFindGo != startGo)
275 {
276 pathComponents.Push(startGo.name);
277
278 // didn't exist in hierarchy!
279 if (startGo.transform.parent == null)
280 return string.Empty;
281
282 startGo = startGo.transform.parent.gameObject;
283 }
284
285 // calculate path
286 var animPath = new StringBuilder();
287 if (pathComponents.Count > 0)
288 animPath.Append(pathComponents.Pop());
289
290 while (pathComponents.Count > 0)
291 animPath.Append("/").Append(pathComponents.Pop());
292
293 return animPath.ToString();
294 }
295
296 private static AnimationClip GenerateTriggerableTransition(string name, Animations.AnimatorController controller)
297 {
298 // Create the clip
299 var clip = Animations.AnimatorController.AllocateAnimatorClip(name);
300 AssetDatabase.AddObjectToAsset(clip, controller);
301
302 // Create a state in the animatior controller for this clip
303 var state = controller.AddMotion(clip);
304
305 // Add a transition property
306 controller.AddParameter(name, AnimatorControllerParameterType.Trigger);
307
308 // Add an any state transition
309 var stateMachine = controller.layers[0].stateMachine;
310 var transition = stateMachine.AddAnyStateTransition(state);
311 transition.AddCondition(Animations.AnimatorConditionMode.If, 0, name);
312 return clip;
313 }
314
315 private static void StaticOnSceneGUI(SceneView view)
316 {
317 if (!s_ShowNavigation)
318 return;
319
320 Selectable[] selectables = Selectable.allSelectablesArray;
321
322 for (int i = 0; i < selectables.Length; i++)
323 {
324 Selectable s = selectables[i];
325 if (SceneManagement.StageUtility.IsGameObjectRenderedByCamera(s.gameObject, Camera.current))
326 DrawNavigationForSelectable(s);
327 }
328 }
329
330 private static void DrawNavigationForSelectable(Selectable sel)
331 {
332 if (sel == null)
333 return;
334
335 Transform transform = sel.transform;
336 bool active = Selection.transforms.Any(e => e == transform);
337
338 Handles.color = new Color(1.0f, 0.6f, 0.2f, active ? 1.0f : 0.4f);
339 DrawNavigationArrow(-Vector2.right, sel, sel.FindSelectableOnLeft());
340 DrawNavigationArrow(Vector2.up, sel, sel.FindSelectableOnUp());
341
342 Handles.color = new Color(1.0f, 0.9f, 0.1f, active ? 1.0f : 0.4f);
343 DrawNavigationArrow(Vector2.right, sel, sel.FindSelectableOnRight());
344 DrawNavigationArrow(-Vector2.up, sel, sel.FindSelectableOnDown());
345 }
346
347 const float kArrowThickness = 2.5f;
348 const float kArrowHeadSize = 1.2f;
349
350 private static void DrawNavigationArrow(Vector2 direction, Selectable fromObj, Selectable toObj)
351 {
352 if (fromObj == null || toObj == null)
353 return;
354 Transform fromTransform = fromObj.transform;
355 Transform toTransform = toObj.transform;
356
357 Vector2 sideDir = new Vector2(direction.y, -direction.x);
358 Vector3 fromPoint = fromTransform.TransformPoint(GetPointOnRectEdge(fromTransform as RectTransform, direction));
359 Vector3 toPoint = toTransform.TransformPoint(GetPointOnRectEdge(toTransform as RectTransform, -direction));
360 float fromSize = HandleUtility.GetHandleSize(fromPoint) * 0.05f;
361 float toSize = HandleUtility.GetHandleSize(toPoint) * 0.05f;
362 fromPoint += fromTransform.TransformDirection(sideDir) * fromSize;
363 toPoint += toTransform.TransformDirection(sideDir) * toSize;
364 float length = Vector3.Distance(fromPoint, toPoint);
365 Vector3 fromTangent = fromTransform.rotation * direction * length * 0.3f;
366 Vector3 toTangent = toTransform.rotation * -direction * length * 0.3f;
367
368 Handles.DrawBezier(fromPoint, toPoint, fromPoint + fromTangent, toPoint + toTangent, Handles.color, null, kArrowThickness);
369 Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction - sideDir) * toSize * kArrowHeadSize);
370 Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction + sideDir) * toSize * kArrowHeadSize);
371 }
372
373 private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
374 {
375 if (rect == null)
376 return Vector3.zero;
377 if (dir != Vector2.zero)
378 dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
379 dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f);
380 return dir;
381 }
382 }
383}