A game about forced loneliness, made by TACStudios
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