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.Text;
5using UnityEditor;
6
7namespace UnityEngine.InputSystem.Editor
8{
9 // TODO Make buffers an optional argument and only allocate if not already passed, reuse a common buffer
10
11 internal static class CopyPasteHelper
12 {
13 private const string k_CopyPasteMarker = "INPUTASSET ";
14 private const string k_StartOfText = "\u0002";
15 private const string k_EndOfTransmission = "\u0004";
16 private const string k_BindingData = "bindingData";
17 private const string k_EndOfBinding = "+++";
18 private static readonly Dictionary<Type, string> k_TypeMarker = new Dictionary<Type, string>
19 {
20 {typeof(InputActionMap), "InputActionMap"},
21 {typeof(InputAction), "InputAction"},
22 {typeof(InputBinding), "InputBinding"},
23 };
24
25 private static SerializedProperty s_lastAddedElement;
26 private static InputActionsEditorState s_State;
27 private static bool s_lastClipboardActionWasCut = false;
28
29 private static bool IsComposite(SerializedProperty property) => property.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.Composite;
30 private static bool IsPartOfComposite(SerializedProperty property) => property.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.PartOfComposite;
31 private static string PropertyName(SerializedProperty property) => property.FindPropertyRelative("m_Name").stringValue;
32
33 #region Cut
34
35 public static void CutActionMap(InputActionsEditorState state)
36 {
37 CopyActionMap(state);
38 s_lastClipboardActionWasCut = true;
39 }
40
41 public static void Cut(InputActionsEditorState state)
42 {
43 Copy(state);
44 s_lastClipboardActionWasCut = true;
45 }
46
47 #endregion
48
49 #region Copy
50
51 public static void CopyActionMap(InputActionsEditorState state)
52 {
53 var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
54 var selectedObject = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
55 CopySelectedTreeViewItemsToClipboard(new List<SerializedProperty> {selectedObject}, typeof(InputActionMap), actionMap);
56 }
57
58 public static void Copy(InputActionsEditorState state)
59 {
60 var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
61 var selectedObject = Selectors.GetSelectedAction(state)?.wrappedProperty;
62 var type = typeof(InputAction);
63 if (state.selectionType == SelectionType.Binding)
64 {
65 selectedObject = Selectors.GetSelectedBinding(state)?.wrappedProperty;
66 type = typeof(InputBinding);
67 }
68 CopySelectedTreeViewItemsToClipboard(new List<SerializedProperty> {selectedObject}, type, actionMap);
69 }
70
71 private static void CopySelectedTreeViewItemsToClipboard(List<SerializedProperty> items, Type type, SerializedProperty actionMap = null)
72 {
73 var copyBuffer = new StringBuilder();
74 CopyItems(items, copyBuffer, type, actionMap);
75 EditorHelpers.SetSystemCopyBufferContents(copyBuffer.ToString());
76 s_lastClipboardActionWasCut = false;
77 }
78
79 internal static void CopyItems(List<SerializedProperty> items, StringBuilder buffer, Type type, SerializedProperty actionMap)
80 {
81 buffer.Append(k_CopyPasteMarker);
82 buffer.Append(k_TypeMarker[type]);
83 foreach (var item in items)
84 {
85 CopyItemData(item, buffer, type, actionMap);
86 buffer.Append(k_EndOfTransmission);
87 }
88 }
89
90 private static void CopyItemData(SerializedProperty item, StringBuilder buffer, Type type, SerializedProperty actionMap)
91 {
92 if (item == null)
93 return;
94
95 buffer.Append(k_StartOfText);
96 buffer.Append(item.CopyToJson(true));
97 if (type == typeof(InputAction))
98 AppendBindingDataForAction(buffer, actionMap, item);
99 if (type == typeof(InputBinding) && IsComposite(item))
100 AppendBindingDataForComposite(buffer, actionMap, item);
101 }
102
103 private static void AppendBindingDataForAction(StringBuilder buffer, SerializedProperty actionMap, SerializedProperty item)
104 {
105 buffer.Append(k_BindingData);
106 foreach (var binding in GetBindingsForActionInMap(actionMap, item))
107 {
108 buffer.Append(binding.CopyToJson(true));
109 buffer.Append(k_EndOfBinding);
110 }
111 }
112
113 private static void AppendBindingDataForComposite(StringBuilder buffer, SerializedProperty actionMap, SerializedProperty item)
114 {
115 var bindingsArray = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
116 buffer.Append(k_BindingData);
117 foreach (var binding in GetBindingsForComposite(bindingsArray, item.GetIndexOfArrayElement()))
118 {
119 buffer.Append(binding.CopyToJson(true));
120 buffer.Append(k_EndOfBinding);
121 }
122 }
123
124 private static IEnumerable<SerializedProperty> GetBindingsForActionInMap(SerializedProperty actionMap, SerializedProperty action)
125 {
126 var actionName = PropertyName(action);
127 var bindingsArray = actionMap.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
128 var bindings = bindingsArray.Where(binding => binding.FindPropertyRelative("m_Action").stringValue.Equals(actionName));
129 return bindings;
130 }
131
132 #endregion
133
134 #region PasteChecks
135 public static bool HasPastableClipboardData(Type selectedType)
136 {
137 var clipboard = EditorHelpers.GetSystemCopyBufferContents();
138 if (clipboard.Length < k_CopyPasteMarker.Length)
139 return false;
140 var isInputAssetData = clipboard.StartsWith(k_CopyPasteMarker);
141 return isInputAssetData && IsMatchingType(selectedType, GetCopiedClipboardType());
142 }
143
144 private static bool IsMatchingType(Type selectedType, Type copiedType)
145 {
146 if (selectedType == typeof(InputActionMap))
147 return copiedType == typeof(InputActionMap) || copiedType == typeof(InputAction);
148 if (selectedType == typeof(InputAction))
149 return copiedType == typeof(InputAction) || copiedType == typeof(InputBinding);
150 //bindings and composites
151 return copiedType == typeof(InputBinding);
152 }
153
154 public static Type GetCopiedType(string buffer)
155 {
156 if (!buffer.StartsWith(k_CopyPasteMarker))
157 return null;
158 foreach (var typePair in k_TypeMarker)
159 {
160 if (buffer.Substring(k_CopyPasteMarker.Length).StartsWith(typePair.Value))
161 return typePair.Key;
162 }
163 return null;
164 }
165
166 public static Type GetCopiedClipboardType()
167 {
168 return GetCopiedType(EditorHelpers.GetSystemCopyBufferContents());
169 }
170
171 #endregion
172
173 #region Paste
174
175 public static SerializedProperty PasteActionMapsFromClipboard(InputActionsEditorState state)
176 {
177 s_lastAddedElement = null;
178 var typeOfCopiedData = GetCopiedClipboardType();
179 if (typeOfCopiedData != typeof(InputActionMap)) return null;
180 s_State = state;
181 var actionMapArray = state.serializedObject.FindProperty(nameof(InputActionAsset.m_ActionMaps));
182 PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] {state.selectedActionMapIndex}, actionMapArray);
183
184 // Don't want to be able to paste repeatedly after a cut - ISX-1821
185 if (s_lastAddedElement != null && s_lastClipboardActionWasCut)
186 EditorHelpers.SetSystemCopyBufferContents(string.Empty);
187
188 return s_lastAddedElement;
189 }
190
191 public static SerializedProperty PasteActionsOrBindingsFromClipboard(InputActionsEditorState state, bool addLast = false, int mapIndex = -1)
192 {
193 s_lastAddedElement = null;
194 s_State = state;
195 var typeOfCopiedData = GetCopiedClipboardType();
196 if (typeOfCopiedData == typeof(InputAction))
197 PasteActionsFromClipboard(state, addLast, mapIndex);
198 if (typeOfCopiedData == typeof(InputBinding))
199 PasteBindingsFromClipboard(state);
200
201 // Don't want to be able to paste repeatedly after a cut - ISX-1821
202 if (s_lastAddedElement != null && s_lastClipboardActionWasCut)
203 EditorHelpers.SetSystemCopyBufferContents(string.Empty);
204
205 return s_lastAddedElement;
206 }
207
208 private static void PasteActionsFromClipboard(InputActionsEditorState state, bool addLast, int mapIndex)
209 {
210 var actionMap = mapIndex >= 0 ? Selectors.GetActionMapAtIndex(state, mapIndex)?.wrappedProperty
211 : Selectors.GetSelectedActionMap(state)?.wrappedProperty;
212 var actionArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Actions));
213 if (actionArray == null) return;
214 var index = state.selectedActionIndex;
215 if (addLast)
216 index = actionArray.arraySize - 1;
217 PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] {index}, actionArray);
218 }
219
220 private static void PasteBindingsFromClipboard(InputActionsEditorState state)
221 {
222 var actionMap = Selectors.GetSelectedActionMap(state)?.wrappedProperty;
223 var bindingsArray = actionMap?.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
224
225 int newBindingIndex;
226 if (state.selectionType == SelectionType.Action)
227 newBindingIndex = Selectors.GetLastBindingIndexForSelectedAction(state);
228 else
229 newBindingIndex = state.selectedBindingIndex;
230
231 PasteData(EditorHelpers.GetSystemCopyBufferContents(), new[] { newBindingIndex }, bindingsArray);
232 }
233
234 private static void PasteData(string copyBufferString, int[] indicesToInsert, SerializedProperty arrayToInsertInto)
235 {
236 if (!copyBufferString.StartsWith(k_CopyPasteMarker))
237 return;
238 PasteItems(copyBufferString, indicesToInsert, arrayToInsertInto);
239 }
240
241 internal static void PasteItems(string copyBufferString, int[] indicesToInsert, SerializedProperty arrayToInsertInto)
242 {
243 // Split buffer into transmissions and then into transmission blocks
244 var copiedType = GetCopiedType(copyBufferString);
245 int indexOffset = 0;
246 foreach (var transmission in copyBufferString.Substring(k_CopyPasteMarker.Length + k_TypeMarker[copiedType].Length)
247 .Split(new[] {k_EndOfTransmission}, StringSplitOptions.RemoveEmptyEntries))
248 {
249 indexOffset++;
250 foreach (var index in indicesToInsert)
251 PasteBlocks(transmission, index + indexOffset, arrayToInsertInto, copiedType);
252 }
253 }
254
255 private static void PasteBlocks(string transmission, int indexToInsert, SerializedProperty arrayToInsertInto, Type copiedType)
256 {
257 var block = transmission.Substring(transmission.IndexOf(k_StartOfText, StringComparison.Ordinal) + 1);
258 if (copiedType == typeof(InputActionMap))
259 PasteElement(arrayToInsertInto, block, indexToInsert, out _);
260 else if (copiedType == typeof(InputAction))
261 PasteAction(arrayToInsertInto, block, indexToInsert);
262 else
263 {
264 var actionName = Selectors.GetSelectedBinding(s_State)?.wrappedProperty.FindPropertyRelative("m_Action")
265 .stringValue;
266 if (s_State.selectionType == SelectionType.Action)
267 actionName = PropertyName(Selectors.GetSelectedAction(s_State)?.wrappedProperty);
268 PasteBindingOrComposite(arrayToInsertInto, block, indexToInsert, actionName);
269 }
270 }
271
272 private static SerializedProperty PasteElement(SerializedProperty arrayProperty, string json, int index, out string oldId, string name = "newElement", bool changeName = true, bool assignUniqueIDs = true)
273 {
274 var duplicatedProperty = AddElement(arrayProperty, name, index);
275 duplicatedProperty.RestoreFromJson(json);
276 oldId = duplicatedProperty.FindPropertyRelative("m_Id").stringValue;
277 if (changeName)
278 InputActionSerializationHelpers.EnsureUniqueName(duplicatedProperty);
279 if (assignUniqueIDs)
280 InputActionSerializationHelpers.AssignUniqueIDs(duplicatedProperty);
281 s_lastAddedElement = duplicatedProperty;
282 return duplicatedProperty;
283 }
284
285 private static void PasteAction(SerializedProperty arrayProperty, string jsonToInsert, int indexToInsert)
286 {
287 var json = jsonToInsert.Split(k_BindingData, StringSplitOptions.RemoveEmptyEntries);
288 var bindingJsons = new string[] {};
289 if (json.Length > 1)
290 bindingJsons = json[1].Split(k_EndOfBinding, StringSplitOptions.RemoveEmptyEntries);
291 var property = PasteElement(arrayProperty, json[0], indexToInsert, out _, "");
292 var newName = PropertyName(property);
293 var newId = property.FindPropertyRelative("m_Id").stringValue;
294 var actionMapTo = Selectors.GetActionMapForAction(s_State, newId);
295 var bindingArrayToInsertTo = actionMapTo.FindPropertyRelative(nameof(InputActionMap.m_Bindings));
296 var index = Mathf.Clamp(Selectors.GetBindingIndexBeforeAction(arrayProperty, indexToInsert, bindingArrayToInsertTo), 0, bindingArrayToInsertTo.arraySize);
297 foreach (var bindingJson in bindingJsons)
298 {
299 var newIndex = PasteBindingOrComposite(bindingArrayToInsertTo, bindingJson, index, newName, false);
300 index = newIndex;
301 }
302 s_lastAddedElement = property;
303 }
304
305 private static int PasteBindingOrComposite(SerializedProperty arrayProperty, string json, int index, string actionName, bool createCompositeParts = true)
306 {
307 var pastePartOfComposite = IsPartOfComposite(json);
308 bool currentPartOfComposite = false;
309 bool currentIsComposite = false;
310
311 if (arrayProperty.arraySize == 0)
312 index = 0;
313
314 if (index > 0)
315 {
316 var currentProperty = arrayProperty.GetArrayElementAtIndex(index - 1);
317 currentPartOfComposite = IsPartOfComposite(currentProperty);
318 currentIsComposite = IsComposite(currentProperty) || currentPartOfComposite;
319 if (pastePartOfComposite && !currentIsComposite) //prevent pasting part of composite into non-composite
320 return index;
321 }
322
323 // Update the target index for special cases when pasting a Binding
324 if (s_State.selectionType != SelectionType.Action && createCompositeParts)
325 {
326 // - Pasting into a Composite with CompositePart not the target, i.e. Composite "root" selected, paste at the end of the composite
327 // - Pasting a non-CompositePart, i.e. regular Binding, needs to skip all the CompositeParts (if any)
328 if ((pastePartOfComposite && !currentPartOfComposite) || !pastePartOfComposite)
329 index = Selectors.GetSelectedBindingIndexAfterCompositeBindings(s_State) + 1;
330 }
331
332 if (json.Contains(k_BindingData)) //copied data is composite with bindings - only true for directly copied composites, not for composites from copied actions
333 return PasteCompositeFromJson(arrayProperty, json, index, actionName);
334 var property = PasteElement(arrayProperty, json, index, out var oldId, "", false);
335 if (IsComposite(property))
336 return PasteComposite(arrayProperty, property, PropertyName(property), actionName, index, oldId, createCompositeParts); //Paste composites copied with actions
337 property.FindPropertyRelative("m_Action").stringValue = actionName;
338 return index + 1;
339 }
340
341 private static int PasteComposite(SerializedProperty bindingsArray, SerializedProperty duplicatedComposite, string name, string actionName, int index, string oldId, bool createCompositeParts)
342 {
343 duplicatedComposite.FindPropertyRelative("m_Name").stringValue = name;
344 duplicatedComposite.FindPropertyRelative("m_Action").stringValue = actionName;
345 if (createCompositeParts)
346 {
347 var composite = Selectors.GetBindingForId(s_State, oldId, out var bindingsFrom);
348 var bindings = GetBindingsForComposite(bindingsFrom, composite.GetIndexOfArrayElement());
349 PastePartsOfComposite(bindingsArray, bindings, ++index, actionName);
350 }
351 return index + 1;
352 }
353
354 private static int PastePartsOfComposite(SerializedProperty bindingsToInsertTo, List<SerializedProperty> bindingsOfComposite, int index, string actionName)
355 {
356 foreach (var binding in bindingsOfComposite)
357 {
358 var newBinding = DuplicateElement(bindingsToInsertTo, binding, PropertyName(binding), index++, false);
359 newBinding.FindPropertyRelative("m_Action").stringValue = actionName;
360 }
361
362 return index;
363 }
364
365 private static int PasteCompositeFromJson(SerializedProperty arrayProperty, string json, int index, string actionName)
366 {
367 var jsons = json.Split(k_BindingData, StringSplitOptions.RemoveEmptyEntries);
368 var property = PasteElement(arrayProperty, jsons[0], index, out _, "", false);
369 var bindingJsons = jsons[1].Split(k_EndOfBinding, StringSplitOptions.RemoveEmptyEntries);
370 property.FindPropertyRelative("m_Action").stringValue = actionName;
371 foreach (var bindingJson in bindingJsons)
372 PasteBindingOrComposite(arrayProperty, bindingJson, ++index, actionName, false);
373 return index + 1;
374 }
375
376 private static bool IsPartOfComposite(string json)
377 {
378 if (!json.Contains("m_Flags") || json.Contains(k_BindingData))
379 return false;
380 var ob = JsonUtility.FromJson<InputBinding>(json);
381 return ob.m_Flags == InputBinding.Flags.PartOfComposite;
382 }
383
384 private static SerializedProperty AddElement(SerializedProperty arrayProperty, string name, int index = -1)
385 {
386 var uniqueName = InputActionSerializationHelpers.FindUniqueName(arrayProperty, name);
387 if (index < 0)
388 index = arrayProperty.arraySize;
389
390 arrayProperty.InsertArrayElementAtIndex(index);
391 var elementProperty = arrayProperty.GetArrayElementAtIndex(index);
392 elementProperty.ResetValuesToDefault();
393
394 elementProperty.FindPropertyRelative("m_Name").stringValue = uniqueName;
395 elementProperty.FindPropertyRelative("m_Id").stringValue = Guid.NewGuid().ToString();
396
397 return elementProperty;
398 }
399
400 public static int DeleteCutElements(InputActionsEditorState state)
401 {
402 if (!state.hasCutElements)
403 return -1;
404 var cutElements = state.GetCutElements();
405 var index = state.selectedActionMapIndex;
406 if (cutElements[0].type == typeof(InputAction))
407 index = state.selectedActionIndex;
408 else if (cutElements[0].type == typeof(InputBinding))
409 index = state.selectionType == SelectionType.Binding ? state.selectedBindingIndex : state.selectedActionIndex;
410
411 foreach (var cutElement in cutElements)
412 {
413 var cutIndex = cutElement.GetIndexOfProperty(state);
414 var actionMapIndex = cutElement.actionMapIndex(state);
415 var actionMap = Selectors.GetActionMapAtIndex(state, actionMapIndex)?.wrappedProperty;
416 var isInsertBindingIntoAction = cutElement.type == typeof(InputBinding) && state.selectionType == SelectionType.Action;
417 if (cutElement.type == typeof(InputBinding) || cutElement.type == typeof(InputAction))
418 {
419 if (cutElement.type == typeof(InputAction))
420 {
421 var action = Selectors.GetActionForIndex(actionMap, cutIndex);
422 var id = InputActionSerializationHelpers.GetId(action);
423 InputActionSerializationHelpers.DeleteActionAndBindings(actionMap, id);
424 }
425 else
426 {
427 var binding = Selectors.GetCompositeOrBindingInMap(actionMap, cutIndex).wrappedProperty;
428 if (binding.FindPropertyRelative("m_Flags").intValue == (int)InputBinding.Flags.Composite && !isInsertBindingIntoAction)
429 index -= InputActionSerializationHelpers.GetCompositePartCount(Selectors.GetSelectedActionMap(state)?.wrappedProperty.FindPropertyRelative(nameof(InputActionMap.m_Bindings)), cutIndex);
430 InputActionSerializationHelpers.DeleteBinding(binding, actionMap);
431 }
432 if (cutIndex <= index && actionMapIndex == state.selectedActionMapIndex && !isInsertBindingIntoAction)
433 index--;
434 }
435 else if (cutElement.type == typeof(InputActionMap))
436 {
437 InputActionSerializationHelpers.DeleteActionMap(state.serializedObject, InputActionSerializationHelpers.GetId(actionMap));
438 if (cutIndex <= index)
439 index--;
440 }
441 }
442 return index;
443 }
444
445 #endregion
446
447 #region Duplicate
448 public static void DuplicateAction(SerializedProperty arrayProperty, SerializedProperty toDuplicate, SerializedProperty actionMap, InputActionsEditorState state)
449 {
450 s_State = state;
451 var buffer = new StringBuilder();
452 buffer.Append(toDuplicate.CopyToJson(true));
453 AppendBindingDataForAction(buffer, actionMap, toDuplicate);
454 PasteAction(arrayProperty, buffer.ToString(), toDuplicate.GetIndexOfArrayElement() + 1);
455 }
456
457 public static int DuplicateBinding(SerializedProperty arrayProperty, SerializedProperty toDuplicate, string newActionName, int index)
458 {
459 if (IsComposite(toDuplicate))
460 return DuplicateComposite(arrayProperty, toDuplicate, PropertyName(toDuplicate), newActionName, index, out _).GetIndexOfArrayElement();
461 var binding = DuplicateElement(arrayProperty, toDuplicate, newActionName, index, false);
462 binding.FindPropertyRelative("m_Action").stringValue = newActionName;
463 return index;
464 }
465
466 private static SerializedProperty DuplicateComposite(SerializedProperty bindingsArray, SerializedProperty compositeToDuplicate, string name, string actionName, int index, out int newIndex, bool increaseIndex = true)
467 {
468 if (increaseIndex)
469 index += InputActionSerializationHelpers.GetCompositePartCount(bindingsArray, compositeToDuplicate.GetIndexOfArrayElement());
470 var newComposite = DuplicateElement(bindingsArray, compositeToDuplicate, name, index++, false);
471 newComposite.FindPropertyRelative("m_Action").stringValue = actionName;
472 var bindings = GetBindingsForComposite(bindingsArray, compositeToDuplicate.GetIndexOfArrayElement());
473 newIndex = PastePartsOfComposite(bindingsArray, bindings, index, actionName);
474 return newComposite;
475 }
476
477 public static SerializedProperty DuplicateElement(SerializedProperty arrayProperty, SerializedProperty toDuplicate, string name, int index, bool changeName = true)
478 {
479 var json = toDuplicate.CopyToJson(true);
480 return PasteElement(arrayProperty, json, index, out _, name, changeName);
481 }
482
483 #endregion
484
485 internal static List<SerializedProperty> GetBindingsForComposite(SerializedProperty bindingsArray, int indexOfComposite)
486 {
487 var compositeBindings = new List<SerializedProperty>();
488 var compositeStartIndex = InputActionSerializationHelpers.GetCompositeStartIndex(bindingsArray, indexOfComposite);
489 if (compositeStartIndex == -1)
490 return compositeBindings;
491
492 for (var i = compositeStartIndex + 1; i < bindingsArray.arraySize; ++i)
493 {
494 var bindingProperty = bindingsArray.GetArrayElementAtIndex(i);
495 var bindingFlags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
496 if ((bindingFlags & InputBinding.Flags.PartOfComposite) == 0)
497 break;
498 compositeBindings.Add(bindingProperty);
499 }
500 return compositeBindings;
501 }
502 }
503}
504
505#endif