Files
MMORPG/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs
cooney ce83f21c93 UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
2026-01-25 01:31:34 +09:00

320 lines
13 KiB
C#

#if UNITY_EDITOR
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.ShortcutManagement;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace UnityEngine.InputSystem.Editor
{
internal class InputActionsEditorSettingsProvider : SettingsProvider
{
private static InputActionsEditorSettingsProvider s_Provider;
public static string SettingsPath => InputSettingsPath.kSettingsRootPath;
[SerializeField] InputActionsEditorState m_State;
VisualElement m_RootVisualElement;
private bool m_HasEditFocus;
private bool m_IsActivated;
private static bool m_IMGUIDropdownVisible;
StateContainer m_StateContainer;
private static InputActionsEditorSettingsProvider m_ActiveSettingsProvider;
private InputActionsEditorView m_View;
private InputActionsEditorSessionAnalytic m_ActionEditorAnalytics;
public InputActionsEditorSettingsProvider(string path, SettingsScope scopes, IEnumerable<string> keywords = null)
: base(path, scopes, keywords)
{}
public override void OnActivate(string searchContext, VisualElement rootElement)
{
// There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order.
// This flag avoids making assumptions and executing logic twice.
if (m_IsActivated)
return;
// Monitor play mode state changes
EditorApplication.playModeStateChanged += ModeChanged;
// Setup root element with focus monitoring
m_RootVisualElement = rootElement;
m_RootVisualElement.focusable = true;
m_RootVisualElement.RegisterCallback<FocusOutEvent>(OnFocusOut);
m_RootVisualElement.RegisterCallback<FocusInEvent>(OnFocusIn);
// Always begin a session when activated (note that OnActivate isn't called when navigating back
// to editor from another setting category)
m_ActionEditorAnalytics = new InputActionsEditorSessionAnalytic(
InputActionsEditorSessionAnalytic.Data.Kind.EmbeddedInProjectSettings);
m_ActionEditorAnalytics.Begin();
CreateUI();
// Monitor any changes to InputSystem.actions for as long as this editor is active
InputSystem.onActionsChange += BuildUI;
// Set the asset assigned with the editor which indirectly builds the UI based on setting
BuildUI();
// Note that focused element will be set if we are navigating back to an existing instance when switching
// setting in the left project settings panel since this doesn't recreate the editor.
if (m_RootVisualElement?.focusController?.focusedElement != null)
OnFocusIn();
m_IsActivated = true;
}
public override void OnDeactivate()
{
// There is an editor bug UUM-55238 that may cause OnActivate and OnDeactivate to be called in unexpected order.
// This flag avoids making assumptions and executing logic twice.
if (!m_IsActivated)
return;
// Stop monitoring play mode state changes
EditorApplication.playModeStateChanged -= ModeChanged;
if (m_RootVisualElement != null)
{
m_RootVisualElement.UnregisterCallback<FocusInEvent>(OnFocusIn);
m_RootVisualElement.UnregisterCallback<FocusOutEvent>(OnFocusOut);
}
// Make sure any remaining changes are actually saved
SaveAssetOnFocusLost();
// Note that OnDeactivate will also trigger when opening the Project Settings (existing instance).
// Hence we guard against duplicate OnDeactivate() calls.
if (m_HasEditFocus)
{
OnFocusOut();
m_HasEditFocus = false;
}
InputSystem.onActionsChange -= BuildUI;
m_IsActivated = false;
// Always end a session when deactivated.
m_ActionEditorAnalytics?.End();
m_View?.DestroyView();
}
private void OnFocusIn(FocusInEvent @event = null)
{
if (!m_HasEditFocus)
{
m_HasEditFocus = true;
m_ActionEditorAnalytics.RegisterEditorFocusIn();
m_ActiveSettingsProvider = this;
SetIMGUIDropdownVisible(false, false);
}
}
void SaveAssetOnFocusLost()
{
var asset = GetAsset();
if (asset != null)
ValidateAndSaveAsset(asset);
}
public static void SetIMGUIDropdownVisible(bool visible, bool optionWasSelected)
{
if (m_ActiveSettingsProvider == null)
return;
// If we selected an item from the dropdown, we *should* still be focused on this settings window - but
// since the IMGUI dropdown is technically a separate window, we have to refocus manually.
//
// If we didn't select a dropdown option, there's not a simple way to know where the focus has gone,
// so assume we lost focus and save if appropriate. ISXB-801
if (!visible && m_IMGUIDropdownVisible)
{
if (optionWasSelected)
m_ActiveSettingsProvider.m_RootVisualElement.Focus();
else
m_ActiveSettingsProvider.SaveAssetOnFocusLost();
}
else if (visible && !m_IMGUIDropdownVisible)
{
m_ActiveSettingsProvider.m_HasEditFocus = false;
}
m_IMGUIDropdownVisible = visible;
}
private async void DelayFocusLost(bool relatedTargetWasNull)
{
await Task.Delay(120);
// We delay this call to ensure that the IMGUI flag has a chance to change first.
if (relatedTargetWasNull && m_HasEditFocus && !m_IMGUIDropdownVisible)
{
m_HasEditFocus = false;
SaveAssetOnFocusLost();
}
}
private void OnFocusOut(FocusOutEvent @event = null)
{
// This can be used to detect focus lost events of container elements, but will not detect window focus.
// Note that `event.relatedTarget` contains the element that gains focus, which is null if we select
// elements outside of project settings Editor Window. Also note that @event is null when we call this
// from OnDeactivate().
var element = (VisualElement)@event?.relatedTarget;
m_ActionEditorAnalytics.RegisterEditorFocusOut();
DelayFocusLost(element == null);
}
private void ValidateAndSaveAsset(InputActionAsset asset)
{
ProjectWideActionsAsset.Verify(asset); // Ignore verification result for save
EditorHelpers.SaveAsset(AssetDatabase.GetAssetPath(asset), asset.ToJson());
}
private void CreateUI()
{
var projectSettingsAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
InputActionsEditorConstants.PackagePath +
InputActionsEditorConstants.ResourcesPath +
InputActionsEditorConstants.ProjectSettingsUxml);
projectSettingsAsset.CloneTree(m_RootVisualElement);
m_RootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme);
}
private void BuildUI()
{
// Construct from InputSystem.actions asset
var asset = InputSystem.actions;
var hasAsset = asset != null;
m_State = (asset != null) ? new InputActionsEditorState(m_ActionEditorAnalytics, new SerializedObject(asset)) : default;
// Dynamically show a section indicating that an asset is missing if not currently having an associated asset
var missingAssetSection = m_RootVisualElement.Q<VisualElement>("missing-asset-section");
if (missingAssetSection != null)
{
missingAssetSection.style.visibility = hasAsset ? Visibility.Hidden : Visibility.Visible;
missingAssetSection.style.display = hasAsset ? DisplayStyle.None : DisplayStyle.Flex;
}
// Allow the user to select an asset out of the assets available in the project via picker.
// Note that we show "None" (null) even if InputSystem.actions is currently a broken/missing reference.
var objectField = m_RootVisualElement.Q<ObjectField>("current-asset");
if (objectField != null)
{
objectField.value = (asset == null) ? null : asset;
objectField.RegisterCallback<ChangeEvent<Object>>((evt) =>
{
if (evt.newValue != asset)
InputSystem.actions = evt.newValue as InputActionAsset;
});
// Prevent reassignment in in editor which would result in exception during play-mode
objectField.SetEnabled(!EditorApplication.isPlayingOrWillChangePlaymode);
}
// Configure a button to allow the user to create and assign a new project-wide asset based on default template
var createAssetButton = m_RootVisualElement.Q<Button>("create-asset");
createAssetButton?.RegisterCallback<ClickEvent>(evt =>
{
var assetPath = ProjectWideActionsAsset.defaultAssetPath;
Dialog.Result result = Dialog.Result.Discard;
if (AssetDatabase.LoadAssetAtPath<Object>(assetPath) != null)
result = Dialog.InputActionAsset.ShowCreateAndOverwriteExistingAsset(assetPath);
if (result == Dialog.Result.Discard)
InputSystem.actions = ProjectWideActionsAsset.CreateDefaultAssetAtPath(assetPath);
});
// Remove input action editor if already present
{
VisualElement element = m_RootVisualElement.Q("action-editor");
if (element != null)
m_RootVisualElement.Remove(element);
}
// If the editor is associated with an asset we show input action editor
if (hasAsset)
{
m_StateContainer = new StateContainer(m_State, AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset)));
m_View = new InputActionsEditorView(m_RootVisualElement, m_StateContainer, true, null);
m_StateContainer.Initialize(m_RootVisualElement.Q("action-editor"));
}
}
private InputActionAsset GetAsset()
{
return m_State.serializedObject?.targetObject as InputActionAsset;
}
private void SetObjectFieldEnabled(bool enabled)
{
// Update object picker enabled state based off editor play mode
if (m_RootVisualElement != null)
UQueryExtensions.Q<ObjectField>(m_RootVisualElement, "current-asset")?.SetEnabled(enabled);
}
private void ModeChanged(PlayModeStateChange change)
{
switch (change)
{
case PlayModeStateChange.EnteredEditMode:
SetObjectFieldEnabled(true);
break;
case PlayModeStateChange.ExitingEditMode:
// Ensure any changes are saved to the asset; FocusLost isn't always triggered when entering PlayMode.
SaveAssetOnFocusLost();
SetObjectFieldEnabled(false);
break;
case PlayModeStateChange.EnteredPlayMode:
case PlayModeStateChange.ExitingPlayMode:
default:
break;
}
}
[SettingsProvider]
public static SettingsProvider CreateGlobalInputActionsEditorProvider()
{
if (s_Provider == null)
s_Provider = new InputActionsEditorSettingsProvider(SettingsPath, SettingsScope.Project);
return s_Provider;
}
#region Shortcuts
[Shortcut("Input Action Editor/Project Settings/Add Action Map", null, KeyCode.M, ShortcutModifiers.Alt)]
private static void AddActionMapShortcut(ShortcutArguments arguments)
{
if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddActionMap());
}
[Shortcut("Input Action Editor/Project Settings/Add Action", null, KeyCode.A, ShortcutModifiers.Alt)]
private static void AddActionShortcut(ShortcutArguments arguments)
{
if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddAction());
}
[Shortcut("Input Action Editor/Project Settings/Add Binding", null, KeyCode.B, ShortcutModifiers.Alt)]
private static void AddBindingShortcut(ShortcutArguments arguments)
{
if (m_ActiveSettingsProvider is { m_HasEditFocus : true })
m_ActiveSettingsProvider.m_StateContainer.Dispatch(Commands.AddBinding());
}
#endregion
}
}
#endif