A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
2using System;
3using System.Collections.Generic;
4using System.Collections.ObjectModel;
5using System.Linq;
6using UnityEditor;
7
8namespace UnityEngine.InputSystem.Editor
9{
10 [System.Serializable]
11
12 internal class CutElement
13 {
14 private Guid id;
15 internal Type type;
16
17 public CutElement(Guid id, Type type)
18 {
19 this.id = id;
20 this.type = type;
21 }
22
23 public int GetIndexOfProperty(InputActionsEditorState state)
24 {
25 if (type == typeof(InputActionMap))
26 {
27 var actionMap = state.serializedObject
28 ?.FindProperty(nameof(InputActionAsset.m_ActionMaps))
29 ?.FirstOrDefault(s => InputActionSerializationHelpers.GetId(s).Equals(id));
30 return actionMap.GetIndexOfArrayElement();
31 }
32
33 if (type == typeof(InputAction))
34 {
35 var action = Selectors.GetActionMapAtIndex(state, actionMapIndex(state))?.wrappedProperty.FindPropertyRelative("m_Actions").FirstOrDefault(a => InputActionSerializationHelpers.GetId(a).Equals(id));
36 return action.GetIndexOfArrayElement();
37 }
38
39 if (type == typeof(InputBinding))
40 {
41 var binding = Selectors.GetBindingForId(state, id.ToString(),
42 out _);
43 return binding.GetIndexOfArrayElement();
44 }
45 return -1;
46 }
47
48 public int actionMapIndex(InputActionsEditorState state) => type == typeof(InputActionMap) ? GetIndexOfProperty(state) : GetActionMapIndex(state);
49
50 private int GetActionMapIndex(InputActionsEditorState state)
51 {
52 var actionMaps = state.serializedObject?.FindProperty(nameof(InputActionAsset.m_ActionMaps));
53 var cutActionMapIndex = state.serializedObject
54 ?.FindProperty(nameof(InputActionAsset.m_ActionMaps))
55 ?.FirstOrDefault(s => s.FindPropertyRelative("m_Id").stringValue.Equals(id)).GetIndexOfArrayElement();
56 if (type == typeof(InputBinding))
57 cutActionMapIndex = actionMaps.FirstOrDefault(map => map.FindPropertyRelative("m_Bindings").Select(InputActionSerializationHelpers.GetId).Contains(id)).GetIndexOfArrayElement();
58 else if (type == typeof(InputAction))
59 cutActionMapIndex = actionMaps.FirstOrDefault(map => map.FindPropertyRelative("m_Actions").Select(InputActionSerializationHelpers.GetId).Contains(id)).GetIndexOfArrayElement();
60 return cutActionMapIndex ?? -1;
61 }
62 }
63 internal struct InputActionsEditorState
64 {
65 public int selectedActionMapIndex { get {return m_selectedActionMapIndex; } }
66 public int selectedActionIndex { get {return m_selectedActionIndex; } }
67 public int selectedBindingIndex { get {return m_selectedBindingIndex; } }
68 public SelectionType selectionType { get {return m_selectionType; } }
69 public SerializedObject serializedObject { get; } // Note that state doesn't own this disposable object
70 private readonly List<CutElement> cutElements => m_CutElements;
71
72 // Control schemes
73 public int selectedControlSchemeIndex { get { return m_selectedControlSchemeIndex; } }
74 public int selectedDeviceRequirementIndex { get {return m_selectedDeviceRequirementIndex; } }
75 public InputControlScheme selectedControlScheme => m_ControlScheme; // TODO Bad this either po
76
77 internal InputActionsEditorSessionAnalytic m_Analytics;
78
79 [SerializeField] int m_selectedActionMapIndex;
80 [SerializeField] int m_selectedActionIndex;
81 [SerializeField] int m_selectedBindingIndex;
82 [SerializeField] SelectionType m_selectionType;
83 [SerializeField] int m_selectedControlSchemeIndex;
84 [SerializeField] int m_selectedDeviceRequirementIndex;
85 private List<CutElement> m_CutElements;
86 internal bool hasCutElements => m_CutElements != null && m_CutElements.Count > 0;
87
88 public InputActionsEditorState(
89 InputActionsEditorSessionAnalytic analytics,
90 SerializedObject inputActionAsset,
91 int selectedActionMapIndex = 0,
92 int selectedActionIndex = 0,
93 int selectedBindingIndex = 0,
94 SelectionType selectionType = SelectionType.Action,
95 Dictionary<(string, string), HashSet<int>> expandedBindingIndices = null,
96 InputControlScheme selectedControlScheme = default,
97 int selectedControlSchemeIndex = -1,
98 int selectedDeviceRequirementIndex = -1,
99 List<CutElement> cutElements = null)
100 {
101 Debug.Assert(inputActionAsset != null);
102
103 m_Analytics = analytics;
104
105 serializedObject = inputActionAsset;
106
107 m_selectedActionMapIndex = selectedActionMapIndex;
108 m_selectedActionIndex = selectedActionIndex;
109 m_selectedBindingIndex = selectedBindingIndex;
110 m_selectionType = selectionType;
111 m_ControlScheme = selectedControlScheme;
112 m_selectedControlSchemeIndex = selectedControlSchemeIndex;
113 m_selectedDeviceRequirementIndex = selectedDeviceRequirementIndex;
114
115 m_ExpandedCompositeBindings = expandedBindingIndices == null ?
116 new Dictionary<(string, string), HashSet<int>>() :
117 new Dictionary<(string, string), HashSet<int>>(expandedBindingIndices);
118 m_CutElements = cutElements;
119 }
120
121 public InputActionsEditorState(InputActionsEditorState other, SerializedObject asset)
122 {
123 m_Analytics = other.m_Analytics;
124
125 // Assign serialized object, not that this might be equal to other.serializedObject,
126 // a slight variation of it with any kind of changes or a completely different one.
127 // Hence, we do our best here to keep any selections consistent by remapping objects
128 // based on GUIDs (IDs) and when it fails, attempt to select first object and if that
129 // fails revert to not having a selection. This would even be true for domain reloads
130 // if the asset would be modified during domain reload.
131 serializedObject = asset;
132
133 if (other.Equals(default(InputActionsEditorState)))
134 {
135 // This instance was created by default constructor and thus is missing some appropriate defaults:
136 other.m_selectionType = SelectionType.Action;
137 other.m_selectedControlSchemeIndex = -1;
138 other.m_selectedDeviceRequirementIndex = -1;
139 }
140
141 // Attempt to preserve action map selection by GUID, otherwise select first or last resort none
142 var otherSelectedActionMap = other.GetSelectedActionMap();
143 var actionMapCount = Selectors.GetActionMapCount(asset);
144 m_selectedActionMapIndex = otherSelectedActionMap != null
145 ? Selectors.GetActionMapIndexFromId(asset,
146 InputActionSerializationHelpers.GetId(otherSelectedActionMap))
147 : actionMapCount > 0 ? 0 : -1;
148 var selectedActionMap = m_selectedActionMapIndex >= 0
149 ? Selectors.GetActionMapAtIndex(asset, m_selectedActionMapIndex)?.wrappedProperty : null;
150
151 // Attempt to preserve action selection by GUID, otherwise select first or last resort none
152 var otherSelectedAction = m_selectedActionMapIndex >= 0 ?
153 Selectors.GetSelectedAction(other) : null;
154 m_selectedActionIndex = selectedActionMap != null && otherSelectedAction.HasValue
155 ? Selectors.GetActionIndexFromId(selectedActionMap,
156 InputActionSerializationHelpers.GetId(otherSelectedAction.Value.wrappedProperty))
157 : Selectors.GetActionCount(selectedActionMap) > 0 ? 0 : -1;
158
159 // Attempt to preserve binding selection by GUID, otherwise select first or none
160 m_selectedBindingIndex = -1;
161 if (m_selectedActionMapIndex >= 0)
162 {
163 var otherSelectedBinding = Selectors.GetSelectedBinding(other);
164 if (otherSelectedBinding != null)
165 {
166 var otherSelectedBindingId =
167 InputActionSerializationHelpers.GetId(otherSelectedBinding.Value.wrappedProperty);
168 var binding = Selectors.GetBindingForId(asset, otherSelectedBindingId.ToString(), out _);
169 if (binding != null)
170 m_selectedBindingIndex = binding.GetIndexOfArrayElement();
171 }
172 }
173
174 // Sanity check selection type and override any previous selection if not valid given indices
175 // since we have remapped GUIDs to selection indices for another asset (SerializedObject)
176 if (other.m_selectionType == SelectionType.Binding && m_selectedBindingIndex < 0)
177 m_selectionType = SelectionType.Action;
178 else
179 m_selectionType = other.m_selectionType;
180
181 m_selectedControlSchemeIndex = other.m_selectedControlSchemeIndex;
182 m_selectedDeviceRequirementIndex = other.m_selectedDeviceRequirementIndex;
183
184 // Selected ControlScheme index is serialized but we have to recreated actual object after domain reload.
185 // In case asset is different from from others asset the index might not even be valid range so we need
186 // to reattempt to preserve selection but range adapt.
187 // Note that control schemes and device requirements currently lack any GUID/ID to be uniquely identified.
188 var controlSchemesArrayProperty = serializedObject.FindProperty(nameof(InputActionAsset.m_ControlSchemes));
189 if (m_selectedControlSchemeIndex >= 0 && controlSchemesArrayProperty.arraySize > 0)
190 {
191 if (m_selectedControlSchemeIndex >= controlSchemesArrayProperty.arraySize)
192 m_selectedControlSchemeIndex = 0;
193 m_ControlScheme = new InputControlScheme(
194 controlSchemesArrayProperty.GetArrayElementAtIndex(other.m_selectedControlSchemeIndex));
195 // TODO Preserve device requirement index
196 }
197 else
198 {
199 m_selectedControlSchemeIndex = -1;
200 m_selectedDeviceRequirementIndex = -1;
201 m_ControlScheme = new InputControlScheme();
202 }
203
204 // Editor may leave these as null after domain reloads, so recreate them in that case.
205 // If they exist, we attempt to just preserve the same expanded items based on name for now for simplicity.
206 m_ExpandedCompositeBindings = other.m_ExpandedCompositeBindings == null ?
207 new Dictionary<(string, string), HashSet<int>>() :
208 new Dictionary<(string, string), HashSet<int>>(other.m_ExpandedCompositeBindings);
209
210 m_CutElements = other.cutElements;
211 }
212
213 public InputActionsEditorState With(
214 int? selectedActionMapIndex = null,
215 int? selectedActionIndex = null,
216 int? selectedBindingIndex = null,
217 SelectionType? selectionType = null,
218 InputControlScheme? selectedControlScheme = null,
219 int? selectedControlSchemeIndex = null,
220 int? selectedDeviceRequirementIndex = null,
221 Dictionary<(string, string), HashSet<int>> expandedBindingIndices = null,
222 List<CutElement> cutElements = null)
223 {
224 return new InputActionsEditorState(
225 m_Analytics,
226 serializedObject,
227 selectedActionMapIndex ?? this.selectedActionMapIndex,
228 selectedActionIndex ?? this.selectedActionIndex,
229 selectedBindingIndex ?? this.selectedBindingIndex,
230 selectionType ?? this.selectionType,
231 expandedBindingIndices ?? m_ExpandedCompositeBindings,
232
233 // Control schemes
234 selectedControlScheme ?? this.selectedControlScheme,
235 selectedControlSchemeIndex ?? this.selectedControlSchemeIndex,
236 selectedDeviceRequirementIndex ?? this.selectedDeviceRequirementIndex,
237
238 cutElements ?? m_CutElements
239 );
240 }
241
242 public InputActionsEditorState ClearCutElements()
243 {
244 return new InputActionsEditorState(
245 m_Analytics,
246 serializedObject,
247 selectedActionMapIndex,
248 selectedActionIndex,
249 selectedBindingIndex,
250 selectionType,
251 m_ExpandedCompositeBindings,
252 selectedControlScheme,
253 selectedControlSchemeIndex,
254 selectedDeviceRequirementIndex,
255 cutElements: null);
256 }
257
258 public SerializedProperty GetActionMapByName(string actionMapName)
259 {
260 return serializedObject
261 .FindProperty(nameof(InputActionAsset.m_ActionMaps))
262 .FirstOrDefault(p => p.FindPropertyRelative(nameof(InputActionMap.m_Name)).stringValue == actionMapName);
263 }
264
265 public InputActionsEditorState ExpandCompositeBinding(SerializedInputBinding binding)
266 {
267 var key = GetSelectedActionMapAndActionKey();
268
269 var expandedCompositeBindings = new Dictionary<(string, string), HashSet<int>>(m_ExpandedCompositeBindings);
270 if (!expandedCompositeBindings.TryGetValue(key, out var expandedStates))
271 {
272 expandedStates = new HashSet<int>();
273 expandedCompositeBindings.Add(key, expandedStates);
274 }
275
276 expandedStates.Add(binding.indexOfBinding);
277
278 return With(expandedBindingIndices: expandedCompositeBindings);
279 }
280
281 public InputActionsEditorState CollapseCompositeBinding(SerializedInputBinding binding)
282 {
283 var key = GetSelectedActionMapAndActionKey();
284
285 if (m_ExpandedCompositeBindings.ContainsKey(key) == false)
286 throw new InvalidOperationException("Trying to collapse a composite binding tree that was never expanded.");
287
288 // do the dance of C# immutability
289 var oldExpandedCompositeBindings = m_ExpandedCompositeBindings;
290 var expandedCompositeBindings = oldExpandedCompositeBindings.Keys.Where(dictKey => dictKey != key)
291 .ToDictionary(dictKey => dictKey, dictKey => oldExpandedCompositeBindings[dictKey]);
292 var newHashset = new HashSet<int>(m_ExpandedCompositeBindings[key].Where(index => index != binding.indexOfBinding));
293 expandedCompositeBindings.Add(key, newHashset);
294
295 return With(expandedBindingIndices: expandedCompositeBindings);
296 }
297
298 public InputActionsEditorState SelectAction(string actionName)
299 {
300 var actionMap = GetSelectedActionMap();
301 var actions = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Actions));
302
303 for (var i = 0; i < actions.arraySize; i++)
304 {
305 if (actions.GetArrayElementAtIndex(i)
306 .FindPropertyRelative(nameof(InputAction.m_Name)).stringValue != actionName) continue;
307
308 return With(selectedActionIndex: i, selectionType: SelectionType.Action);
309 }
310
311 // If we cannot find the desired map we should return invalid index
312 return With(selectedActionIndex: -1, selectionType: SelectionType.Action);
313 }
314
315 public InputActionsEditorState SelectAction(SerializedProperty state)
316 {
317 var index = state.GetIndexOfArrayElement();
318 return With(selectedActionIndex: index, selectionType: SelectionType.Action);
319 }
320
321 public InputActionsEditorState SelectActionMap(SerializedProperty actionMap)
322 {
323 var index = actionMap.GetIndexOfArrayElement();
324 return With(selectedBindingIndex: 0, selectedActionMapIndex: index, selectedActionIndex: 0);
325 }
326
327 public InputActionsEditorState SelectActionMap(string actionMapName)
328 {
329 var actionMap = GetActionMapByName(actionMapName);
330 return With(selectedBindingIndex: 0,
331 selectedActionMapIndex: actionMap.GetIndexOfArrayElement(),
332 selectedActionIndex: 0, selectionType: SelectionType.Action);
333 }
334
335 public InputActionsEditorState SelectBinding(int index)
336 {
337 //if no binding selected (due to no bindings in list) set selection type to action
338 if (index == -1)
339 return With(selectedBindingIndex: index, selectionType: SelectionType.Action);
340 return With(selectedBindingIndex: index, selectionType: SelectionType.Binding);
341 }
342
343 public InputActionsEditorState SelectAction(int index)
344 {
345 //if no action selected (no actions available) set selection type to none
346 if (index == -1)
347 return With(selectedActionIndex: index, selectionType: SelectionType.None);
348 return With(selectedActionIndex: index, selectionType: SelectionType.Action);
349 }
350
351 public InputActionsEditorState SelectActionMap(int index)
352 {
353 if (index == -1)
354 return With(selectedActionMapIndex: index, selectionType: SelectionType.None);
355 return With(selectedBindingIndex: 0,
356 selectedActionMapIndex: index,
357 selectedActionIndex: 0, selectionType: SelectionType.Action);
358 }
359
360 public InputActionsEditorState CutActionOrBinding()
361 {
362 m_CutElements = new List<CutElement>();
363 var type = selectionType == SelectionType.Action ? typeof(InputAction) : typeof(InputBinding);
364 var property = selectionType == SelectionType.Action ? Selectors.GetSelectedAction(this)?.wrappedProperty : Selectors.GetSelectedBinding(this)?.wrappedProperty;
365 cutElements.Add(new CutElement(InputActionSerializationHelpers.GetId(property), type));
366 return With(cutElements: cutElements);
367 }
368
369 public InputActionsEditorState CutActionMaps()
370 {
371 m_CutElements = new List<CutElement> { new(InputActionSerializationHelpers.GetId(Selectors.GetSelectedActionMap(this)?.wrappedProperty), typeof(InputActionMap)) };
372 return With(cutElements: cutElements);
373 }
374
375 public IEnumerable<string> GetDisabledActionMaps(List<string> allActionMaps)
376 {
377 if (cutElements == null || cutElements == null)
378 return Enumerable.Empty<string>();
379 var cutActionMaps = cutElements.Where(cut => cut.type == typeof(InputActionMap));
380 var state = this;
381 return allActionMaps.Where(actionMapName =>
382 {
383 return cutActionMaps.Any(am => am.GetIndexOfProperty(state) == allActionMaps.IndexOf(actionMapName));
384 });
385 }
386
387 public readonly bool IsBindingCut(int actionMapIndex, int bindingIndex)
388 {
389 if (cutElements == null)
390 return false;
391
392 var state = this;
393 return cutElements.Any(cutElement => cutElement.actionMapIndex(state) == actionMapIndex &&
394 cutElement.GetIndexOfProperty(state) == bindingIndex &&
395 cutElement.type == typeof(InputBinding));
396 }
397
398 public readonly bool IsActionCut(int actionMapIndex, int actionIndex)
399 {
400 if (cutElements == null)
401 return false;
402
403 var state = this;
404 return cutElements.Any(cutElement => cutElement.actionMapIndex(state) == actionMapIndex &&
405 cutElement.GetIndexOfProperty(state) == actionIndex &&
406 cutElement.type == typeof(InputAction));
407 }
408
409 public readonly bool IsActionMapCut(int actionMapIndex)
410 {
411 if (cutElements == null)
412 return false;
413 var state = this;
414 return cutElements.Any(cutElement => cutElement.GetIndexOfProperty(state) == actionMapIndex && cutElement.type == typeof(InputActionMap));
415 }
416
417 public readonly List<CutElement> GetCutElements()
418 {
419 return m_CutElements;
420 }
421
422 public ReadOnlyCollection<int> GetOrCreateExpandedState()
423 {
424 return new ReadOnlyCollection<int>(GetOrCreateExpandedStateInternal().ToList());
425 }
426
427 private HashSet<int> GetOrCreateExpandedStateInternal()
428 {
429 var key = GetSelectedActionMapAndActionKey();
430
431 if (m_ExpandedCompositeBindings.TryGetValue(key, out var expandedStates))
432 return expandedStates;
433
434 expandedStates = new HashSet<int>();
435 m_ExpandedCompositeBindings.Add(key, expandedStates);
436 return expandedStates;
437 }
438
439 internal (string, string) GetSelectedActionMapAndActionKey()
440 {
441 var selectedActionMap = GetSelectedActionMap();
442
443 var selectedAction = selectedActionMap
444 .FindPropertyRelative(nameof(InputActionMap.m_Actions))
445 .GetArrayElementAtIndex(selectedActionIndex);
446
447 var key = (
448 selectedActionMap.FindPropertyRelative(nameof(InputActionMap.m_Name)).stringValue,
449 selectedAction.FindPropertyRelative(nameof(InputAction.m_Name)).stringValue
450 );
451 return key;
452 }
453
454 private SerializedProperty GetSelectedActionMap()
455 {
456 return Selectors.GetActionMapAtIndex(serializedObject, selectedActionMapIndex)?.wrappedProperty;
457 }
458
459 /// <summary>
460 /// Expanded states for the actions tree view. These are stored per InputActionMap
461 /// </summary>
462 private readonly Dictionary<(string, string), HashSet<int>> m_ExpandedCompositeBindings;
463
464 private readonly InputControlScheme m_ControlScheme;
465 }
466
467 internal enum SelectionType
468 {
469 None,
470 Action,
471 Binding
472 }
473}
474
475#endif