A game about forced loneliness, made by TACStudios
1// UITK TreeView is not supported in earlier versions 2// Therefore the UITK version of the InputActionAsset Editor is not available on earlier Editor versions either. 3#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 4using System; 5using System.Linq; 6using UnityEditor; 7using UnityEditor.Callbacks; 8using UnityEditor.PackageManager.UI; 9using UnityEditor.ShortcutManagement; 10using UnityEngine.UIElements; 11using UnityEditor.UIElements; 12 13namespace UnityEngine.InputSystem.Editor 14{ 15 // TODO: Remove when UIToolkit editor is complete and set as the default editor 16 [InitializeOnLoad] 17 internal static class EnableUITKEditor 18 { 19 static EnableUITKEditor() 20 { 21 } 22 } 23 24 internal class InputActionsEditorWindow : EditorWindow, IInputActionAssetEditor 25 { 26 // Register editor type via static constructor to enable asset monitoring 27 static InputActionsEditorWindow() 28 { 29 InputActionAssetEditor.RegisterType<InputActionsEditorWindow>(); 30 } 31 32 static readonly Vector2 k_MinWindowSize = new Vector2(650, 450); 33 // For UI testing purpose 34 internal InputActionAsset currentAssetInEditor => m_AssetObjectForEditing; 35 [SerializeField] private InputActionAsset m_AssetObjectForEditing; 36 [SerializeField] private InputActionsEditorState m_State; 37 [SerializeField] private string m_AssetGUID; 38 39 private string m_AssetJson; 40 private bool m_IsDirty; 41 42 private StateContainer m_StateContainer; 43 private InputActionsEditorView m_View; 44 45 private InputActionsEditorSessionAnalytic m_Analytics; 46 47 private InputActionsEditorSessionAnalytic analytics => 48 m_Analytics ??= new InputActionsEditorSessionAnalytic( 49 InputActionsEditorSessionAnalytic.Data.Kind.EditorWindow); 50 51 [OnOpenAsset] 52 public static bool OpenAsset(int instanceId, int line) 53 { 54 if (InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets)) 55 return false; 56 if (!InputActionImporter.IsInputActionAssetPath(AssetDatabase.GetAssetPath(instanceId))) 57 return false; 58 59 // Grab InputActionAsset. 60 // NOTE: We defer checking out an asset until we save it. This allows a user to open an .inputactions asset and look at it 61 // without forcing a checkout. 62 var obj = EditorUtility.InstanceIDToObject(instanceId); 63 var asset = obj as InputActionAsset; 64 65 string actionMapToSelect = null; 66 string actionToSelect = null; 67 68 // Means we're dealing with an InputActionReference, e.g. when expanding the an .input action asset 69 // on the Asset window and selecting an Action. 70 if (asset == null) 71 { 72 var actionReference = obj as InputActionReference; 73 if (actionReference != null && actionReference.asset != null) 74 { 75 asset = actionReference.asset; 76 actionMapToSelect = actionReference.action.actionMap?.name; 77 actionToSelect = actionReference.action?.name; 78 } 79 else 80 { 81 return false; 82 } 83 } 84 85 OpenWindow(asset, actionMapToSelect, actionToSelect); 86 return true; 87 } 88 89 private static InputActionsEditorWindow OpenWindow(InputActionAsset asset, string actionMapToSelect = null, string actionToSelect = null) 90 { 91 ////REVIEW: It'd be great if the window got docked by default but the public EditorWindow API doesn't allow that 92 //// to be done for windows that aren't singletons (GetWindow<T>() will only create one window and it's the 93 //// only way to get programmatic docking with the current API). 94 // See if we have an existing editor window that has the asset open. 95 var existingWindow = InputActionAssetEditor.FindOpenEditor<InputActionsEditorWindow>(AssetDatabase.GetAssetPath(asset)); 96 if (existingWindow != null) 97 { 98 existingWindow.Focus(); 99 return existingWindow; 100 } 101 102 var window = GetWindow<InputActionsEditorWindow>(); 103 window.m_IsDirty = false; 104 window.minSize = k_MinWindowSize; 105 window.SetAsset(asset, actionToSelect, actionMapToSelect); 106 window.Show(); 107 108 return window; 109 } 110 111 /// <summary> 112 /// Open the specified <paramref name="asset"/> in an editor window. Used when someone hits the "Edit Asset" button in the 113 /// importer inspector. 114 /// </summary> 115 /// <param name="asset">The InputActionAsset to open.</param> 116 /// <returns>The editor window.</returns> 117 public static InputActionsEditorWindow OpenEditor(InputActionAsset asset) 118 { 119 return OpenWindow(asset, null, null); 120 } 121 122 private static GUIContent GetEditorTitle(InputActionAsset asset, bool isDirty) 123 { 124 var text = asset.name + " (Input Actions Editor)"; 125 if (isDirty) 126 text = "(*) " + text; 127 return new GUIContent(text); 128 } 129 130 private void SetAsset(InputActionAsset asset, string actionToSelect = null, string actionMapToSelect = null) 131 { 132 var existingWorkingCopy = m_AssetObjectForEditing; 133 134 try 135 { 136 // Obtain and persist GUID for the associated asset 137 Debug.Assert(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out m_AssetGUID, out long _), 138 $"Failed to get asset {asset.name} GUID"); 139 140 // Attempt to update editor and internals based on associated asset 141 if (!TryUpdateFromAsset()) 142 return; 143 144 // Select the action that was selected on the Asset window. 145 if (actionMapToSelect != null && actionToSelect != null) 146 { 147 m_State = m_State.SelectActionMap(actionMapToSelect); 148 m_State = m_State.SelectAction(actionToSelect); 149 } 150 151 BuildUI(); 152 } 153 catch (Exception e) 154 { 155 Debug.LogException(e); 156 } 157 finally 158 { 159 if (existingWorkingCopy != null) 160 DestroyImmediate(existingWorkingCopy); 161 } 162 } 163 164 private void CreateGUI() // Only domain reload 165 { 166 // When opening the window for the first time there will be no state or asset yet. 167 // In that case, we don't do anything as SetAsset() will be called later and at that point the UI can be created. 168 // Here we only recreate the UI e.g. after a domain reload. 169 if (string.IsNullOrEmpty(m_AssetGUID)) 170 return; 171 172 // After domain reloads the state will be in a invalid state as some of the fields 173 // cannot be serialized and will become null. 174 // Therefore we recreate the state here using the fields which were saved. 175 if (m_State.serializedObject == null) 176 { 177 InputActionAsset workingCopy = null; 178 try 179 { 180 var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID); 181 var asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath); 182 183 if (asset == null) 184 throw new Exception($"Failed to load asset \"{assetPath}\". The file may have been deleted or moved."); 185 186 m_AssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(asset); 187 188 if (m_AssetObjectForEditing == null) 189 { 190 workingCopy = InputActionAssetManager.CreateWorkingCopy(asset); 191 if (m_State.m_Analytics == null) 192 m_State.m_Analytics = analytics; 193 m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy)); 194 m_AssetObjectForEditing = workingCopy; 195 } 196 else 197 m_State = new InputActionsEditorState(m_State, new SerializedObject(m_AssetObjectForEditing)); 198 m_IsDirty = HasContentChanged(); 199 } 200 catch (Exception e) 201 { 202 Debug.LogException(e); 203 if (workingCopy != null) 204 DestroyImmediate(workingCopy); 205 Close(); 206 return; 207 } 208 } 209 210 BuildUI(); 211 } 212 213 private void CleanupStateContainer() 214 { 215 if (m_StateContainer != null) 216 { 217 m_StateContainer.StateChanged -= OnStateChanged; 218 m_StateContainer = null; 219 } 220 } 221 222 private void BuildUI() 223 { 224 CleanupStateContainer(); 225 226 if (m_State.m_Analytics == null) 227 m_State.m_Analytics = m_Analytics; 228 229 m_StateContainer = new StateContainer(m_State, m_AssetGUID); 230 m_StateContainer.StateChanged += OnStateChanged; 231 232 rootVisualElement.Clear(); 233 if (!rootVisualElement.styleSheets.Contains(InputActionsEditorWindowUtils.theme)) 234 rootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); 235 m_View = new InputActionsEditorView(rootVisualElement, m_StateContainer, false, () => Save(isAutoSave: false)); 236 237 m_StateContainer.Initialize(rootVisualElement.Q("action-editor")); 238 } 239 240 private void OnStateChanged(InputActionsEditorState newState) 241 { 242 DirtyInputActionsEditorWindow(newState); 243 m_State = newState; 244 245 #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 246 // No action taken apart from setting dirty flag, auto-save triggered as part of having a dirty asset 247 // and editor loosing focus instead. 248 #else 249 if (InputEditorUserSettings.autoSaveInputActionAssets) 250 Save(isAutoSave: false); 251 #endif 252 } 253 254 private void UpdateWindowTitle() 255 { 256 titleContent = GetEditorTitle(GetEditedAsset(), m_IsDirty); 257 } 258 259 private InputActionAsset GetEditedAsset() 260 { 261 return m_State.serializedObject.targetObject as InputActionAsset; 262 } 263 264 private void Save(bool isAutoSave) 265 { 266 var path = AssetDatabase.GUIDToAssetPath(m_AssetGUID); 267 #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 268 var projectWideActions = InputSystem.actions; 269 if (projectWideActions != null && path == AssetDatabase.GetAssetPath(projectWideActions)) 270 ProjectWideActionsAsset.Verify(GetEditedAsset()); 271 #endif 272 if (InputActionAssetManager.SaveAsset(path, GetEditedAsset().ToJson())) 273 TryUpdateFromAsset(); 274 275 if (isAutoSave) 276 analytics.RegisterAutoSave(); 277 else 278 analytics.RegisterExplicitSave(); 279 } 280 281 private bool HasContentChanged() 282 { 283 var editedAsset = GetEditedAsset(); 284 var editedAssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(editedAsset); 285 return editedAssetJson != m_AssetJson; 286 } 287 288 private void DirtyInputActionsEditorWindow(InputActionsEditorState newState) 289 { 290 #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 291 // Window is dirty is equivalent to if asset has changed 292 var isWindowDirty = HasContentChanged(); 293 #else 294 // Window is dirty is never true since every change is auto-saved 295 var isWindowDirty = !InputEditorUserSettings.autoSaveInputActionAssets && HasContentChanged(); 296 #endif 297 298 if (m_IsDirty == isWindowDirty) 299 return; 300 301 m_IsDirty = isWindowDirty; 302 UpdateWindowTitle(); 303 } 304 305 private void OnEnable() 306 { 307 analytics.Begin(); 308 } 309 310 private void OnDisable() 311 { 312 analytics.End(); 313 } 314 315 private void OnFocus() 316 { 317 analytics.RegisterEditorFocusIn(); 318 } 319 320 private void OnLostFocus() 321 { 322 // Auto-save triggers on focus-lost instead of on every change 323 #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 324 if (InputEditorUserSettings.autoSaveInputActionAssets && m_IsDirty) 325 Save(isAutoSave: true); 326 #endif 327 328 analytics.RegisterEditorFocusOut(); 329 } 330 331 private void HandleOnDestroy() 332 { 333 // Do we have unsaved changes that we need to ask the user to save or discard? 334 if (!m_IsDirty) 335 return; 336 337 // Get target asset path from GUID, if this fails file no longer exists and we need to abort. 338 var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID); 339 if (string.IsNullOrEmpty(assetPath)) 340 return; 341 342 // Prompt user with a dialog 343 var result = Dialog.InputActionAsset.ShowSaveChanges(assetPath); 344 switch (result) 345 { 346 case Dialog.Result.Save: 347 Save(isAutoSave: false); 348 break; 349 case Dialog.Result.Cancel: 350 // Cancel editor quit. (open new editor window with the edited asset) 351 ReshowEditorWindowWithUnsavedChanges(); 352 break; 353 case Dialog.Result.Discard: 354 // Don't save, quit - reload the old asset from the json to prevent the asset from being dirtied 355 break; 356 default: 357 throw new ArgumentOutOfRangeException(nameof(result)); 358 } 359 } 360 361 private void OnDestroy() 362 { 363 HandleOnDestroy(); 364 365 // Clean-up 366 CleanupStateContainer(); 367 if (m_AssetObjectForEditing != null) 368 DestroyImmediate(m_AssetObjectForEditing); 369 370 m_View?.DestroyView(); 371 } 372 373 private void ReshowEditorWindowWithUnsavedChanges() 374 { 375 var window = CreateWindow<InputActionsEditorWindow>(); 376 377 // Move/transfer ownership of m_AssetObjectForEditing to new window 378 window.m_AssetObjectForEditing = m_AssetObjectForEditing; 379 m_AssetObjectForEditing = null; 380 381 // Move/transfer ownership of m_State to new window (struct) 382 window.m_State = m_State; 383 m_State = new InputActionsEditorState(); 384 385 // Just copy trivial arguments 386 window.m_AssetGUID = m_AssetGUID; 387 window.m_AssetJson = m_AssetJson; 388 window.m_IsDirty = m_IsDirty; 389 390 // Note that view and state container will get destroyed with this window instance 391 // and recreated for this window below 392 window.BuildUI(); 393 window.Show(); 394 395 // Make sure window title is up to date 396 window.UpdateWindowTitle(); 397 } 398 399 private bool TryUpdateFromAsset() 400 { 401 Debug.Assert(!string.IsNullOrEmpty(m_AssetGUID), "Asset GUID is empty"); 402 var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID); 403 if (assetPath == null) 404 { 405 Debug.LogWarning( 406 $"Failed to open InputActionAsset with GUID {m_AssetGUID}. The asset might have been deleted."); 407 return false; 408 } 409 410 InputActionAsset workingCopy = null; 411 try 412 { 413 var asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath); 414 workingCopy = InputActionAssetManager.CreateWorkingCopy(asset); 415 m_AssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(asset); 416 m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy)); 417 m_IsDirty = false; 418 } 419 catch (Exception e) 420 { 421 if (workingCopy != null) 422 DestroyImmediate(workingCopy); 423 Debug.LogException(e); 424 Close(); 425 return false; 426 } 427 428 m_AssetObjectForEditing = workingCopy; 429 UpdateWindowTitle(); 430 431 return true; 432 } 433 434 #region IInputActionEditorWindow 435 436 public string assetGUID => m_AssetGUID; 437 public bool isDirty => m_IsDirty; 438 439 public void OnAssetMoved() 440 { 441 // When an asset is moved, we only need to update window title since content is unchanged 442 UpdateWindowTitle(); 443 } 444 445 public void OnAssetDeleted() 446 { 447 // When associated asset is deleted on disk, just close the editor, but also mark the editor 448 // as not being dirty to avoid prompting the user to save changes. 449 m_IsDirty = false; 450 Close(); 451 } 452 453 public void OnAssetImported() 454 { 455 // If the editor has pending changes done by the user and the contents changes on disc, there 456 // is not much we can do about it but to ignore loading the changes. If the editors asset is 457 // unmodified, we can refresh the editor with the latest content from disc. 458 if (m_IsDirty) 459 return; 460 461 // If our asset has disappeared from disk, just close the window. 462 var assetPath = AssetDatabase.GUIDToAssetPath(assetGUID); 463 if (string.IsNullOrEmpty(assetPath)) 464 { 465 m_IsDirty = false; // Avoid checks 466 Close(); 467 return; 468 } 469 470 SetAsset(AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath)); 471 } 472 473 #endregion 474 475 #region Shortcuts 476 [Shortcut("Input Action Editor/Save", typeof(InputActionsEditorWindow), KeyCode.S, ShortcutModifiers.Action)] 477 private static void SaveShortcut(ShortcutArguments arguments) 478 { 479 var window = (InputActionsEditorWindow)arguments.context; 480 window.Save(isAutoSave: false); 481 } 482 483 [Shortcut("Input Action Editor/Add Action Map", typeof(InputActionsEditorWindow), KeyCode.M, ShortcutModifiers.Alt)] 484 private static void AddActionMapShortcut(ShortcutArguments arguments) 485 { 486 var window = (InputActionsEditorWindow)arguments.context; 487 window.m_StateContainer.Dispatch(Commands.AddActionMap()); 488 } 489 490 [Shortcut("Input Action Editor/Add Action", typeof(InputActionsEditorWindow), KeyCode.A, ShortcutModifiers.Alt)] 491 private static void AddActionShortcut(ShortcutArguments arguments) 492 { 493 var window = (InputActionsEditorWindow)arguments.context; 494 window.m_StateContainer.Dispatch(Commands.AddAction()); 495 } 496 497 [Shortcut("Input Action Editor/Add Binding", typeof(InputActionsEditorWindow), KeyCode.B, ShortcutModifiers.Alt)] 498 private static void AddBindingShortcut(ShortcutArguments arguments) 499 { 500 var window = (InputActionsEditorWindow)arguments.context; 501 window.m_StateContainer.Dispatch(Commands.AddBinding()); 502 } 503 504 #endregion 505 } 506} 507 508#endif