A game about forced loneliness, made by TACStudios
at master 14 kB view raw
1#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS 2using System.Collections.Generic; 3using System.Threading.Tasks; 4using UnityEditor; 5using UnityEditor.ShortcutManagement; 6using UnityEngine.UIElements; 7using UnityEditor.UIElements; 8 9namespace UnityEngine.InputSystem.Editor 10{ 11 internal class InputActionsEditorSettingsProvider : SettingsProvider 12 { 13 private static InputActionsEditorSettingsProvider s_Provider; 14 15 public static string SettingsPath => InputSettingsPath.kSettingsRootPath; 16 17 [SerializeField] InputActionsEditorState m_State; 18 VisualElement m_RootVisualElement; 19 private bool m_HasEditFocus; 20 private bool m_IgnoreActionChangedCallback; 21 private bool m_IsActivated; 22 private static bool m_IMGUIDropdownVisible; 23 StateContainer m_StateContainer; 24 private static InputActionsEditorSettingsProvider m_ActiveSettingsProvider; 25 26 private InputActionsEditorView m_View; 27 28 private InputActionsEditorSessionAnalytic m_ActionEditorAnalytics; 29 30 public InputActionsEditorSettingsProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null) 31 : base(path, scopes, keywords) 32 {} 33 34 public override void OnActivate(string searchContext, VisualElement rootElement) 35 { 36 // There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order. 37 // This flag avoids making assumptions and executing logic twice. 38 if (m_IsActivated) 39 return; 40 41 // Monitor play mode state changes 42 EditorApplication.playModeStateChanged += ModeChanged; 43 44 // Setup root element with focus monitoring 45 m_RootVisualElement = rootElement; 46 m_RootVisualElement.focusable = true; 47 m_RootVisualElement.RegisterCallback<FocusOutEvent>(OnFocusOut); 48 m_RootVisualElement.RegisterCallback<FocusInEvent>(OnFocusIn); 49 50 // Always begin a session when activated (note that OnActivate isn't called when navigating back 51 // to editor from another setting category) 52 m_ActionEditorAnalytics = new InputActionsEditorSessionAnalytic( 53 InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings); 54 m_ActionEditorAnalytics.Begin(); 55 56 CreateUI(); 57 58 // Monitor any changes to InputSystem.actions for as long as this editor is active 59 InputSystem.onActionsChange += BuildUI; 60 61 // Set the asset assigned with the editor which indirectly builds the UI based on setting 62 BuildUI(); 63 64 // Note that focused element will be set if we are navigating back to an existing instance when switching 65 // setting in the left project settings panel since this doesn't recreate the editor. 66 if (m_RootVisualElement?.focusController?.focusedElement != null) 67 OnFocusIn(); 68 69 m_IsActivated = true; 70 } 71 72 public override void OnDeactivate() 73 { 74 // There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order. 75 // This flag avoids making assumptions and executing logic twice. 76 if (!m_IsActivated) 77 return; 78 79 // Stop monitoring play mode state changes 80 EditorApplication.playModeStateChanged -= ModeChanged; 81 82 if (m_RootVisualElement != null) 83 { 84 m_RootVisualElement.UnregisterCallback<FocusInEvent>(OnFocusIn); 85 m_RootVisualElement.UnregisterCallback<FocusOutEvent>(OnFocusOut); 86 } 87 88 // Make sure any remaining changes are actually saved 89 SaveAssetOnFocusLost(); 90 91 // Note that OnDeactivate will also trigger when opening the Project Settings (existing instance). 92 // Hence we guard against duplicate OnDeactivate() calls. 93 if (m_HasEditFocus) 94 { 95 OnFocusOut(); 96 m_HasEditFocus = false; 97 } 98 99 InputSystem.onActionsChange -= BuildUI; 100 101 m_IsActivated = false; 102 103 // Always end a session when deactivated. 104 m_ActionEditorAnalytics?.End(); 105 106 m_View?.DestroyView(); 107 } 108 109 private void OnFocusIn(FocusInEvent @event = null) 110 { 111 if (!m_HasEditFocus) 112 { 113 m_HasEditFocus = true; 114 m_ActionEditorAnalytics.RegisterEditorFocusIn(); 115 m_ActiveSettingsProvider = this; 116 SetIMGUIDropdownVisible(false, false); 117 } 118 } 119 120 void SaveAssetOnFocusLost() 121 { 122#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 123 var asset = GetAsset(); 124 if (asset != null) 125 ValidateAndSaveAsset(asset); 126#endif 127 } 128 129 public static void SetIMGUIDropdownVisible(bool visible, bool optionWasSelected) 130 { 131 if (m_ActiveSettingsProvider == null) 132 return; 133 134 // If we selected an item from the dropdown, we *should* still be focused on this settings window - but 135 // since the IMGUI dropdown is technically a separate window, we have to refocus manually. 136 // 137 // If we didn't select a dropdown option, there's not a simple way to know where the focus has gone, 138 // so assume we lost focus and save if appropriate. ISXB-801 139 if (!visible && m_IMGUIDropdownVisible) 140 { 141 if (optionWasSelected) 142 m_ActiveSettingsProvider.m_RootVisualElement.Focus(); 143 else 144 m_ActiveSettingsProvider.SaveAssetOnFocusLost(); 145 } 146 else if (visible && !m_IMGUIDropdownVisible) 147 { 148 m_ActiveSettingsProvider.m_HasEditFocus = false; 149 } 150 151 m_IMGUIDropdownVisible = visible; 152 } 153 154 private async void DelayFocusLost(bool relatedTargetWasNull) 155 { 156 await Task.Delay(120); 157 158 // We delay this call to ensure that the IMGUI flag has a chance to change first. 159 if (relatedTargetWasNull && m_HasEditFocus && !m_IMGUIDropdownVisible) 160 { 161 m_HasEditFocus = false; 162 SaveAssetOnFocusLost(); 163 } 164 } 165 166 private void OnFocusOut(FocusOutEvent @event = null) 167 { 168 // This can be used to detect focus lost events of container elements, but will not detect window focus. 169 // Note that `event.relatedTarget` contains the element that gains focus, which is null if we select 170 // elements outside of project settings Editor Window. Also note that @event is null when we call this 171 // from OnDeactivate(). 172 var element = (VisualElement)@event?.relatedTarget; 173 174 m_ActionEditorAnalytics.RegisterEditorFocusOut(); 175 176 DelayFocusLost(element == null); 177 } 178 179 private void OnStateChanged(InputActionsEditorState newState) 180 { 181#if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST 182 // No action, auto-saved on edit-focus lost 183#else 184 // Project wide input actions always auto save - don't check the asset auto save status 185 var asset = GetAsset(); 186 if (asset != null) 187 ValidateAndSaveAsset(asset); 188#endif 189 } 190 191 private void ValidateAndSaveAsset(InputActionAsset asset) 192 { 193 ProjectWideActionsAsset.Verify(asset); // Ignore verification result for save 194 EditorHelpers.SaveAsset(AssetDatabase.GetAssetPath(asset), asset.ToJson()); 195 } 196 197 private void CreateUI() 198 { 199 var projectSettingsAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>( 200 InputActionsEditorConstants.PackagePath + 201 InputActionsEditorConstants.ResourcesPath + 202 InputActionsEditorConstants.ProjectSettingsUxml); 203 204 projectSettingsAsset.CloneTree(m_RootVisualElement); 205 206 m_RootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); 207 } 208 209 private void BuildUI() 210 { 211 // Construct from InputSystem.actions asset 212 var asset = InputSystem.actions; 213 var hasAsset = asset != null; 214 m_State = (asset != null) ? new InputActionsEditorState(m_ActionEditorAnalytics, new SerializedObject(asset)) : default; 215 216 // Dynamically show a section indicating that an asset is missing if not currently having an associated asset 217 var missingAssetSection = m_RootVisualElement.Q<VisualElement>("missing-asset-section"); 218 if (missingAssetSection != null) 219 { 220 missingAssetSection.style.visibility = hasAsset ? Visibility.Hidden : Visibility.Visible; 221 missingAssetSection.style.display = hasAsset ? DisplayStyle.None : DisplayStyle.Flex; 222 } 223 224 // Allow the user to select an asset out of the assets available in the project via picker. 225 // Note that we show "None" (null) even if InputSystem.actions is currently a broken/missing reference. 226 var objectField = m_RootVisualElement.Q<ObjectField>("current-asset"); 227 if (objectField != null) 228 { 229 objectField.value = (asset == null) ? null : asset; 230 objectField.RegisterCallback<ChangeEvent<Object>>((evt) => 231 { 232 if (evt.newValue != asset) 233 InputSystem.actions = evt.newValue as InputActionAsset; 234 }); 235 236 // Prevent reassignment in in editor which would result in exception during play-mode 237 objectField.SetEnabled(!EditorApplication.isPlayingOrWillChangePlaymode); 238 } 239 240 // Configure a button to allow the user to create and assign a new project-wide asset based on default template 241 var createAssetButton = m_RootVisualElement.Q<Button>("create-asset"); 242 createAssetButton?.RegisterCallback<ClickEvent>(evt => 243 { 244 var assetPath = ProjectWideActionsAsset.defaultAssetPath; 245 Dialog.Result result = Dialog.Result.Discard; 246 if (AssetDatabase.LoadAssetAtPath<Object>(assetPath) != null) 247 result = Dialog.InputActionAsset.ShowCreateAndOverwriteExistingAsset(assetPath); 248 if (result == Dialog.Result.Discard) 249 InputSystem.actions = ProjectWideActionsAsset.CreateDefaultAssetAtPath(assetPath); 250 }); 251 252 // Remove input action editor if already present 253 { 254 VisualElement element = m_RootVisualElement.Q("action-editor"); 255 if (element != null) 256 m_RootVisualElement.Remove(element); 257 } 258 259 // If the editor is associated with an asset we show input action editor 260 if (hasAsset) 261 { 262 m_StateContainer = new StateContainer(m_State, AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset))); 263 m_StateContainer.StateChanged += OnStateChanged; 264 m_View = new InputActionsEditorView(m_RootVisualElement, m_StateContainer, true, null); 265 m_StateContainer.Initialize(m_RootVisualElement.Q("action-editor")); 266 } 267 } 268 269 private InputActionAsset GetAsset() 270 { 271 return m_State.serializedObject?.targetObject as InputActionAsset; 272 } 273 274 private void SetObjectFieldEnabled(bool enabled) 275 { 276 // Update object picker enabled state based off editor play mode 277 if (m_RootVisualElement != null) 278 UQueryExtensions.Q<ObjectField>(m_RootVisualElement, "current-asset")?.SetEnabled(enabled); 279 } 280 281 private void ModeChanged(PlayModeStateChange change) 282 { 283 switch (change) 284 { 285 case PlayModeStateChange.EnteredEditMode: 286 SetObjectFieldEnabled(true); 287 break; 288 case PlayModeStateChange.ExitingEditMode: 289 // Ensure any changes are saved to the asset; FocusLost isn't always triggered when entering PlayMode. 290 SaveAssetOnFocusLost(); 291 SetObjectFieldEnabled(false); 292 break; 293 case PlayModeStateChange.EnteredPlayMode: 294 case PlayModeStateChange.ExitingPlayMode: 295 default: 296 break; 297 } 298 } 299 300 [SettingsProvider] 301 public static SettingsProvider CreateGlobalInputActionsEditorProvider() 302 { 303 if (s_Provider == null) 304 s_Provider = new InputActionsEditorSettingsProvider(SettingsPath, SettingsScope.Project); 305 306 return s_Provider; 307 } 308 309 #region Shortcuts 310 [Shortcut("Input Action Editor/Project Settings/Add Action Map", null, KeyCode.M, ShortcutModifiers.Alt)] 311 private static void AddActionMapShortcut(ShortcutArguments arguments) 312 { 313 if (m_ActiveSettingsProvider is { m_HasEditFocus : true }) 314 m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddActionMap()); 315 } 316 317 [Shortcut("Input Action Editor/Project Settings/Add Action", null, KeyCode.A, ShortcutModifiers.Alt)] 318 private static void AddActionShortcut(ShortcutArguments arguments) 319 { 320 if (m_ActiveSettingsProvider is { m_HasEditFocus : true }) 321 m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddAction()); 322 } 323 324 [Shortcut("Input Action Editor/Project Settings/Add Binding", null, KeyCode.B, ShortcutModifiers.Alt)] 325 private static void AddBindingShortcut(ShortcutArguments arguments) 326 { 327 if (m_ActiveSettingsProvider is { m_HasEditFocus : true }) 328 m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddBinding()); 329 } 330 331 #endregion 332 } 333} 334 335#endif