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