UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7fcb545eb7743c8bd27f341b51151dc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,328 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Analytics record for tracking engagement with Input Action Asset editor(s).
|
||||
/// </summary>
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class InputActionsEditorSessionAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_actionasset_editor_closed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new <c>InputActionsEditorSession</c> record of the given <para>type</para>.
|
||||
/// </summary>
|
||||
/// <param name="kind">The editor type for which this record is valid.</param>
|
||||
public InputActionsEditorSessionAnalytic(Data.Kind kind)
|
||||
{
|
||||
if (kind == Data.Kind.Invalid)
|
||||
throw new ArgumentException(nameof(kind));
|
||||
|
||||
Initialize(kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register that an action map edit has occurred.
|
||||
/// </summary>
|
||||
public void RegisterActionMapEdit()
|
||||
{
|
||||
if (ImplicitFocus())
|
||||
++m_Data.action_map_modification_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register that an action edit has occurred.
|
||||
/// </summary>
|
||||
public void RegisterActionEdit()
|
||||
{
|
||||
if (ImplicitFocus() && ComputeDuration() > 0.5) // Avoid logging actions triggered via UI initialization
|
||||
++m_Data.action_modification_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register than a binding edit has occurred.
|
||||
/// </summary>
|
||||
public void RegisterBindingEdit()
|
||||
{
|
||||
if (ImplicitFocus())
|
||||
++m_Data.binding_modification_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register that a control scheme edit has occurred.
|
||||
/// </summary>
|
||||
public void RegisterControlSchemeEdit()
|
||||
{
|
||||
if (ImplicitFocus())
|
||||
++m_Data.control_scheme_modification_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register that the editor has received focus which is expected to reflect that the user
|
||||
/// is currently exploring or editing it.
|
||||
/// </summary>
|
||||
public void RegisterEditorFocusIn()
|
||||
{
|
||||
if (!hasSession || hasFocus)
|
||||
return;
|
||||
|
||||
m_FocusStart = currentTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register that the editor has lost focus which is expected to reflect that the user currently
|
||||
/// has the attention elsewhere.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Calling this method without having an ongoing session and having focus will not have any effect.
|
||||
/// </remarks>
|
||||
public void RegisterEditorFocusOut()
|
||||
{
|
||||
if (!hasSession || !hasFocus)
|
||||
return;
|
||||
|
||||
var duration = currentTime - m_FocusStart;
|
||||
m_FocusStart = float.NaN;
|
||||
m_Data.session_focus_duration_seconds += (float)duration;
|
||||
++m_Data.session_focus_switch_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a user-event related to explicitly saving in the editor, e.g.
|
||||
/// using a button, menu or short-cut to trigger the save command.
|
||||
/// </summary>
|
||||
public void RegisterExplicitSave()
|
||||
{
|
||||
if (!hasSession)
|
||||
return; // No pending session
|
||||
|
||||
++m_Data.explicit_save_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a user-event related to implicitly saving in the editor, e.g.
|
||||
/// by having auto-save enabled and indirectly saving the associated asset.
|
||||
/// </summary>
|
||||
public void RegisterAutoSave()
|
||||
{
|
||||
if (!hasSession)
|
||||
return; // No pending session
|
||||
|
||||
++m_Data.auto_save_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a user-event related to resetting the editor action configuration to defaults.
|
||||
/// </summary>
|
||||
public void RegisterReset()
|
||||
{
|
||||
if (!hasSession)
|
||||
return; // No pending session
|
||||
|
||||
++m_Data.reset_count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a new session if the session has not already been started.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the session has already been started due to a previous call to <see cref="Begin()"/> without
|
||||
/// a call to <see cref="End()"/> this method has no effect.
|
||||
/// </remarks>
|
||||
public void Begin()
|
||||
{
|
||||
if (hasSession)
|
||||
return; // Session already started.
|
||||
|
||||
m_SessionStart = currentTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the current session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the session has not previously been started via a call to <see cref="Begin()"/> calling this
|
||||
/// method has no effect.
|
||||
/// </remarks>
|
||||
public void End()
|
||||
{
|
||||
if (!hasSession)
|
||||
return; // No pending session
|
||||
|
||||
// Make sure we register focus out if failed to capture or not invoked
|
||||
if (hasFocus)
|
||||
RegisterEditorFocusOut();
|
||||
|
||||
// Compute and record total session duration
|
||||
var duration = ComputeDuration();
|
||||
m_Data.session_duration_seconds += duration;
|
||||
|
||||
// Sanity check data, if less than a second its likely a glitch so avoid sending incorrect data
|
||||
// Send analytics event
|
||||
if (duration >= 1.0)
|
||||
runtime.SendAnalytic(this);
|
||||
|
||||
// Reset to allow instance to be reused
|
||||
Initialize(m_Data.kind);
|
||||
}
|
||||
|
||||
#region IInputAnalytic Interface
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
if (!isValid)
|
||||
{
|
||||
data = null;
|
||||
error = new Exception("Unable to gather data without a valid session");
|
||||
return false;
|
||||
}
|
||||
|
||||
data = this.m_Data;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info => new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
|
||||
#endregion
|
||||
|
||||
private double ComputeDuration() => hasSession ? currentTime - m_SessionStart : 0.0;
|
||||
|
||||
private void Initialize(Data.Kind kind)
|
||||
{
|
||||
m_FocusStart = float.NaN;
|
||||
m_SessionStart = float.NaN;
|
||||
|
||||
m_Data = new Data(kind);
|
||||
}
|
||||
|
||||
private bool ImplicitFocus()
|
||||
{
|
||||
if (!hasSession)
|
||||
return false;
|
||||
if (!hasFocus)
|
||||
RegisterEditorFocusIn();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Data m_Data;
|
||||
private double m_FocusStart;
|
||||
private double m_SessionStart;
|
||||
|
||||
private static IInputRuntime runtime => InputSystem.s_Manager.m_Runtime;
|
||||
private bool hasFocus => !double.IsNaN(m_FocusStart);
|
||||
private bool hasSession => !double.IsNaN(m_SessionStart);
|
||||
// Returns current time since startup. Note that IInputRuntime explicitly defines in interface that
|
||||
// IInputRuntime.currentTime corresponds to EditorApplication.timeSinceStartup in editor.
|
||||
private double currentTime => runtime.currentTime;
|
||||
private bool isValid => m_Data.session_duration_seconds >= 0;
|
||||
|
||||
[Serializable]
|
||||
public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an editor type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This may be added to in the future but items may never be removed.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public enum Kind
|
||||
{
|
||||
Invalid = 0,
|
||||
EditorWindow = 1,
|
||||
EmbeddedInProjectSettings = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <c>InputActionsEditorSessionData</c>.
|
||||
/// </summary>
|
||||
/// <param name="kind">Specifies the kind of editor metrics is being collected for.</param>
|
||||
public Data(Kind kind)
|
||||
{
|
||||
this.kind = kind;
|
||||
session_duration_seconds = 0;
|
||||
session_focus_duration_seconds = 0;
|
||||
session_focus_switch_count = 0;
|
||||
action_map_modification_count = 0;
|
||||
action_modification_count = 0;
|
||||
binding_modification_count = 0;
|
||||
explicit_save_count = 0;
|
||||
auto_save_count = 0;
|
||||
reset_count = 0;
|
||||
control_scheme_modification_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies what kind of Input Actions editor this event represents.
|
||||
/// </summary>
|
||||
public Kind kind;
|
||||
|
||||
/// <summary>
|
||||
/// The total duration for the session, i.e. the duration during which the editor window was open.
|
||||
/// </summary>
|
||||
public double session_duration_seconds;
|
||||
|
||||
/// <summary>
|
||||
/// The total duration for which the editor window was open and had focus.
|
||||
/// </summary>
|
||||
public double session_focus_duration_seconds;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the number of times the window has transitioned from not having focus to having focus in a single session.
|
||||
/// </summary>
|
||||
public int session_focus_switch_count;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of action map modifications during the session.
|
||||
/// </summary>
|
||||
public int action_map_modification_count;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of action modifications during the session.
|
||||
/// </summary>
|
||||
public int action_modification_count;
|
||||
|
||||
/// The total number of binding modifications during the session.
|
||||
/// </summary>
|
||||
public int binding_modification_count;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of controls scheme modifications during the session.
|
||||
/// </summary>
|
||||
public int control_scheme_modification_count;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of explicit saves during the session, i.e. as in user-initiated save.
|
||||
/// </summary>
|
||||
public int explicit_save_count;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of automatic saves during the session, i.e. as in auto-save on close or focus-lost.
|
||||
/// </summary>
|
||||
public int auto_save_count;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of user-initiated resets during the session, i.e. as in using Reset option in menu.
|
||||
/// </summary>
|
||||
public int reset_count;
|
||||
|
||||
public bool isValid => kind != Kind.Invalid && session_duration_seconds >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d771fd88f0934b4dbe724b2690a9f330
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,384 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Content;
|
||||
using UnityEditor.Build.Reporting;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Analytics for tracking Player Input component user engagement in the editor.
|
||||
/// </summary>
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class InputBuildAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_build_completed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
private readonly BuildReport m_BuildReport;
|
||||
|
||||
public InputBuildAnalytic(BuildReport buildReport)
|
||||
{
|
||||
m_BuildReport = buildReport;
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info =>
|
||||
new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
InputSettings defaultSettings = null;
|
||||
try
|
||||
{
|
||||
defaultSettings = ScriptableObject.CreateInstance<InputSettings>();
|
||||
data = new InputBuildAnalyticData(m_BuildReport, InputSystem.settings, defaultSettings);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data = null;
|
||||
error = e;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (defaultSettings != null)
|
||||
Object.DestroyImmediate(defaultSettings);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input system build analytics data structure.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct InputBuildAnalyticData : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
#region InputSettings
|
||||
|
||||
[Serializable]
|
||||
public enum UpdateMode
|
||||
{
|
||||
ProcessEventsInBothFixedAndDynamicUpdate = 0, // Note: Deprecated
|
||||
ProcessEventsInDynamicUpdate = 1,
|
||||
ProcessEventsInFixedUpdate = 2,
|
||||
ProcessEventsManually = 3,
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public enum BackgroundBehavior
|
||||
{
|
||||
ResetAndDisableNonBackgroundDevices = 0,
|
||||
ResetAndDisableAllDevices = 1,
|
||||
IgnoreFocus = 2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public enum EditorInputBehaviorInPlayMode
|
||||
{
|
||||
PointersAndKeyboardsRespectGameViewFocus = 0,
|
||||
AllDevicesRespectGameViewFocus = 1,
|
||||
AllDeviceInputAlwaysGoesToGameView = 2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public enum InputActionPropertyDrawerMode
|
||||
{
|
||||
Compact = 0,
|
||||
MultilineEffective = 1,
|
||||
MultilineBoth = 2
|
||||
}
|
||||
|
||||
public InputBuildAnalyticData(BuildReport report, InputSettings settings, InputSettings defaultSettings)
|
||||
{
|
||||
switch (settings.updateMode)
|
||||
{
|
||||
case 0: // ProcessEventsInBothFixedAndDynamicUpdate (deprecated/removed)
|
||||
update_mode = UpdateMode.ProcessEventsInBothFixedAndDynamicUpdate;
|
||||
break;
|
||||
case InputSettings.UpdateMode.ProcessEventsManually:
|
||||
update_mode = UpdateMode.ProcessEventsManually;
|
||||
break;
|
||||
case InputSettings.UpdateMode.ProcessEventsInDynamicUpdate:
|
||||
update_mode = UpdateMode.ProcessEventsInDynamicUpdate;
|
||||
break;
|
||||
case InputSettings.UpdateMode.ProcessEventsInFixedUpdate:
|
||||
update_mode = UpdateMode.ProcessEventsInFixedUpdate;
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported updateMode");
|
||||
}
|
||||
|
||||
switch (settings.backgroundBehavior)
|
||||
{
|
||||
case InputSettings.BackgroundBehavior.IgnoreFocus:
|
||||
background_behavior = BackgroundBehavior.IgnoreFocus;
|
||||
break;
|
||||
case InputSettings.BackgroundBehavior.ResetAndDisableAllDevices:
|
||||
background_behavior = BackgroundBehavior.ResetAndDisableAllDevices;
|
||||
break;
|
||||
case InputSettings.BackgroundBehavior.ResetAndDisableNonBackgroundDevices:
|
||||
background_behavior = BackgroundBehavior.ResetAndDisableNonBackgroundDevices;
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported background behavior");
|
||||
}
|
||||
|
||||
switch (settings.editorInputBehaviorInPlayMode)
|
||||
{
|
||||
case InputSettings.EditorInputBehaviorInPlayMode.PointersAndKeyboardsRespectGameViewFocus:
|
||||
editor_input_behavior_in_playmode = EditorInputBehaviorInPlayMode
|
||||
.PointersAndKeyboardsRespectGameViewFocus;
|
||||
break;
|
||||
case InputSettings.EditorInputBehaviorInPlayMode.AllDevicesRespectGameViewFocus:
|
||||
editor_input_behavior_in_playmode = EditorInputBehaviorInPlayMode
|
||||
.AllDevicesRespectGameViewFocus;
|
||||
break;
|
||||
case InputSettings.EditorInputBehaviorInPlayMode.AllDeviceInputAlwaysGoesToGameView:
|
||||
editor_input_behavior_in_playmode = EditorInputBehaviorInPlayMode
|
||||
.AllDeviceInputAlwaysGoesToGameView;
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported editor background behavior");
|
||||
}
|
||||
|
||||
switch (settings.inputActionPropertyDrawerMode)
|
||||
{
|
||||
case InputSettings.InputActionPropertyDrawerMode.Compact:
|
||||
input_action_property_drawer_mode = InputActionPropertyDrawerMode.Compact;
|
||||
break;
|
||||
case InputSettings.InputActionPropertyDrawerMode.MultilineBoth:
|
||||
input_action_property_drawer_mode = InputActionPropertyDrawerMode.MultilineBoth;
|
||||
break;
|
||||
case InputSettings.InputActionPropertyDrawerMode.MultilineEffective:
|
||||
input_action_property_drawer_mode = InputActionPropertyDrawerMode.MultilineEffective;
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported editor property drawer mode");
|
||||
}
|
||||
|
||||
var inputSystemActions = InputSystem.actions;
|
||||
var actionsPath = inputSystemActions == null ? null : AssetDatabase.GetAssetPath(inputSystemActions);
|
||||
has_projectwide_input_action_asset = !string.IsNullOrEmpty(actionsPath);
|
||||
|
||||
var settingsPath = settings == null ? null : AssetDatabase.GetAssetPath(settings);
|
||||
has_settings_asset = !string.IsNullOrEmpty(settingsPath);
|
||||
|
||||
compensate_for_screen_orientation = settings.compensateForScreenOrientation;
|
||||
default_deadzone_min = settings.defaultDeadzoneMin;
|
||||
default_deadzone_max = settings.defaultDeadzoneMax;
|
||||
default_button_press_point = settings.defaultButtonPressPoint;
|
||||
button_release_threshold = settings.buttonReleaseThreshold;
|
||||
default_tap_time = settings.defaultTapTime;
|
||||
default_slow_tap_time = settings.defaultSlowTapTime;
|
||||
default_hold_time = settings.defaultHoldTime;
|
||||
tap_radius = settings.tapRadius;
|
||||
multi_tap_delay_time = settings.multiTapDelayTime;
|
||||
max_event_bytes_per_update = settings.maxEventBytesPerUpdate;
|
||||
max_queued_events_per_update = settings.maxQueuedEventsPerUpdate;
|
||||
supported_devices = settings.supportedDevices.ToArray();
|
||||
disable_redundant_events_merging = settings.disableRedundantEventsMerging;
|
||||
shortcut_keys_consume_input = settings.shortcutKeysConsumeInput;
|
||||
|
||||
feature_optimized_controls_enabled = settings.IsFeatureEnabled(InputFeatureNames.kUseOptimizedControls);
|
||||
feature_read_value_caching_enabled = settings.IsFeatureEnabled(InputFeatureNames.kUseReadValueCaching);
|
||||
feature_paranoid_read_value_caching_checks_enabled =
|
||||
settings.IsFeatureEnabled(InputFeatureNames.kParanoidReadValueCachingChecks);
|
||||
|
||||
feature_disable_unity_remote_support =
|
||||
settings.IsFeatureEnabled(InputFeatureNames.kDisableUnityRemoteSupport);
|
||||
feature_run_player_updates_in_editmode =
|
||||
settings.IsFeatureEnabled(InputFeatureNames.kRunPlayerUpdatesInEditMode);
|
||||
|
||||
has_default_settings = InputSettings.AreEqual(settings, defaultSettings);
|
||||
|
||||
build_guid = report != null ? report.summary.guid.ToString() : string.Empty; // Allows testing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.updateMode"/> and indicates how the project handles updates.
|
||||
/// </summary>
|
||||
public UpdateMode update_mode;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.compensateForScreenOrientation"/> and if true automatically
|
||||
/// adjust rotations when the screen orientation changes.
|
||||
/// </summary>
|
||||
public bool compensate_for_screen_orientation;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.backgroundBehavior"/> which determines what happens when application
|
||||
/// focus changes and how the system handle input while running in the background.
|
||||
/// </summary>
|
||||
public BackgroundBehavior background_behavior;
|
||||
|
||||
// Note: InputSettings.filterNoiseOnCurrent not present since already deprecated when these analytics
|
||||
// where added.
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.defaultDeadzoneMin"/>
|
||||
/// </summary>
|
||||
public float default_deadzone_min;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.defaultDeadzoneMax"/>
|
||||
/// </summary>
|
||||
public float default_deadzone_max;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.defaultButtonPressPoint"/>
|
||||
/// </summary>
|
||||
public float default_button_press_point;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.buttonReleaseThreshold"/>
|
||||
/// </summary>
|
||||
public float button_release_threshold;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.defaultSlowTapTime"/>
|
||||
/// </summary>
|
||||
public float default_tap_time;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.defaultSlowTapTime"/>
|
||||
/// </summary>
|
||||
public float default_slow_tap_time;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.defaultHoldTime"/>
|
||||
/// </summary>
|
||||
public float default_hold_time;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.tapRadius"/>
|
||||
/// </summary>
|
||||
public float tap_radius;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.multiTapDelayTime"/>
|
||||
/// </summary>
|
||||
public float multi_tap_delay_time;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.editorInputBehaviorInPlayMode"/>
|
||||
/// </summary>
|
||||
public EditorInputBehaviorInPlayMode editor_input_behavior_in_playmode;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.inputActionPropertyDrawerMode"/>
|
||||
/// </summary>
|
||||
public InputActionPropertyDrawerMode input_action_property_drawer_mode;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.maxEventBytesPerUpdate"/>
|
||||
/// </summary>
|
||||
public int max_event_bytes_per_update;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.maxQueuedEventsPerUpdate"/>
|
||||
/// </summary>
|
||||
public int max_queued_events_per_update;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.supportedDevices"/>
|
||||
/// </summary>
|
||||
public string[] supported_devices;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.disableRedundantEventsMerging"/>
|
||||
/// </summary>
|
||||
public bool disable_redundant_events_merging;
|
||||
|
||||
/// <summary>
|
||||
/// Represents <see cref="InputSettings.shortcutKeysConsumeInput"/>
|
||||
/// </summary>
|
||||
public bool shortcut_keys_consume_input;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Feature flag settings
|
||||
|
||||
/// <summary>
|
||||
/// Represents internal feature flag <see cref="InputFeatureNames.kUseOptimizedControls"/> as defined
|
||||
/// in Input System 1.8.x.
|
||||
/// </summary>
|
||||
public bool feature_optimized_controls_enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Represents internal feature flag <see cref="InputFeatureNames.kUseReadValueCaching" /> as defined
|
||||
/// in Input System 1.8.x.
|
||||
/// </summary>
|
||||
public bool feature_read_value_caching_enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Represents internal feature flag <see cref="InputFeatureNames.kParanoidReadValueCachingChecks" />
|
||||
/// as defined in InputSystem 1.8.x.
|
||||
/// </summary>
|
||||
public bool feature_paranoid_read_value_caching_checks_enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Represents internal feature flag <see cref="InputFeatureNames.kDisableUnityRemoteSupport" />
|
||||
/// as defined in InputSystem 1.8.x.
|
||||
/// </summary>
|
||||
public bool feature_disable_unity_remote_support;
|
||||
|
||||
/// <summary>
|
||||
/// Represents internal feature flag <see cref="InputFeatureNames.kRunPlayerUpdatesInEditMode" />
|
||||
/// as defined in InputSystem 1.8.x.
|
||||
/// </summary>
|
||||
public bool feature_run_player_updates_in_editmode;
|
||||
|
||||
#endregion
|
||||
|
||||
#region
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the project is using a project-wide input actions asset or not.
|
||||
/// </summary>
|
||||
public bool has_projectwide_input_action_asset;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the project is using a user-provided settings asset or not.
|
||||
/// </summary>
|
||||
public bool has_settings_asset;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the settings asset (if present) of the built project is equal to default settings
|
||||
/// or not. In case of no settings asset this is also true since implicitly using default settings.
|
||||
/// </summary>
|
||||
public bool has_default_settings;
|
||||
|
||||
/// <summary>
|
||||
/// A unique GUID identifying the build.
|
||||
/// </summary>
|
||||
public string build_guid;
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input System build analytics.
|
||||
/// </summary>
|
||||
internal class ReportProcessor : IPostprocessBuildWithReport
|
||||
{
|
||||
public int callbackOrder => int.MaxValue;
|
||||
|
||||
public void OnPostprocessBuild(BuildReport report)
|
||||
{
|
||||
InputSystem.s_Manager?.m_Runtime?.SendAnalytic(new InputBuildAnalytic(report));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f760afcbd6744a0e8c9d0b7039dda306
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration type identifying a Input System MonoBehavior component.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal enum InputSystemComponent
|
||||
{
|
||||
// Feature components
|
||||
PlayerInput = 1,
|
||||
PlayerInputManager = 2,
|
||||
OnScreenStick = 3,
|
||||
OnScreenButton = 4,
|
||||
VirtualMouseInput = 5,
|
||||
|
||||
// Debug components
|
||||
TouchSimulation = 1000,
|
||||
|
||||
// Integration components
|
||||
StandaloneInputModule = 2000,
|
||||
InputSystemUIInputModule = 2001,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analytics record for tracking engagement with Input Component editor(s).
|
||||
/// </summary>
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class InputComponentEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_component_editor_closed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
/// <summary>
|
||||
/// The associated component type.
|
||||
/// </summary>
|
||||
private readonly InputSystemComponent m_Component;
|
||||
|
||||
/// <summary>
|
||||
/// Represents component inspector editor data.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ideally this struct should be readonly but then Unity cannot serialize/deserialize it.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <c>ComponentEditorData</c> instance.
|
||||
/// </summary>
|
||||
/// <param name="component">The associated component.</param>
|
||||
public Data(InputSystemComponent component)
|
||||
{
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the associated component.
|
||||
/// </summary>
|
||||
public InputSystemComponent component;
|
||||
}
|
||||
|
||||
public InputComponentEditorAnalytic(InputSystemComponent component)
|
||||
{
|
||||
info = new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
m_Component = component;
|
||||
}
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
data = new Data(m_Component);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info { get; }
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b36e69515ff4a45be02062b5584e1a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal static class InputEditorAnalytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents notification behavior setting associated with <see cref="PlayerInput"/> and
|
||||
/// <see cref="PlayerInputManager"/>.
|
||||
/// </summary>
|
||||
internal enum PlayerNotificationBehavior
|
||||
{
|
||||
SendMessages = 0,
|
||||
BroadcastMessages = 1,
|
||||
UnityEvents = 2,
|
||||
CSharpEvents = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts from current <see cref="PlayerNotifications"/> type to analytics counterpart.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to be converted.</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">If there is no available remapping.</exception>
|
||||
internal static PlayerNotificationBehavior ToNotificationBehavior(PlayerNotifications value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case PlayerNotifications.SendMessages:
|
||||
return PlayerNotificationBehavior.SendMessages;
|
||||
case PlayerNotifications.BroadcastMessages:
|
||||
return PlayerNotificationBehavior.BroadcastMessages;
|
||||
case PlayerNotifications.InvokeUnityEvents:
|
||||
return PlayerNotificationBehavior.UnityEvents;
|
||||
case PlayerNotifications.InvokeCSharpEvents:
|
||||
return PlayerNotificationBehavior.CSharpEvents;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af8ecd25eda841f98ce3f6555888e43b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,165 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class InputExitPlayModeAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_exit_playmode";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
/// <summary>
|
||||
/// Enumeration type for code authoring APIs mapping to <see cref="InputActionSetupExtensions"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This enumeration type may be added to, but NEVER changed, since it would break older data.
|
||||
/// </remarks>
|
||||
public enum Api
|
||||
{
|
||||
AddBinding = 0,
|
||||
AddCompositeBinding = 1,
|
||||
ChangeBinding = 2,
|
||||
ChangeCompositeBinding = 3,
|
||||
Rename = 4,
|
||||
AddControlScheme = 5,
|
||||
RemoveControlScheme = 6,
|
||||
ControlSchemeWithBindingGroup = 7,
|
||||
ControlSchemeWithDevice = 8,
|
||||
ControlSchemeWithRequiredDevice = 9,
|
||||
ControlSchemeWithOptionalDevice = 10,
|
||||
ControlSchemeOrWithRequiredDevice = 11,
|
||||
ControlSchemeOrWithOptionalDevice = 12
|
||||
}
|
||||
|
||||
private static readonly int[] m_Counters = new int[Enum.GetNames(typeof(Api)).Length];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a call to the associated API.
|
||||
/// </summary>
|
||||
/// <param name="api">Enumeration identifying the API.</param>
|
||||
public static void Register(Api api)
|
||||
{
|
||||
if (suppress)
|
||||
return;
|
||||
|
||||
// Note: Currently discards detailed information and only sets a boolean (aggregated) value.
|
||||
++m_Counters[(int)api];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suppresses the registration of analytics.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// May be used to temporarily suppress analytics to avoid false positives from internal usage.
|
||||
/// </remarks>
|
||||
public static bool suppress
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
// Cache delegate
|
||||
private static readonly Action<PlayModeStateChange> PlayModeChanged = OnPlayModeStateChange;
|
||||
|
||||
// Note: Internal visibility to simplify unit testing
|
||||
internal static void OnPlayModeStateChange(PlayModeStateChange change)
|
||||
{
|
||||
if (change == PlayModeStateChange.ExitingEditMode)
|
||||
{
|
||||
// Reset all counters when exiting edit mode
|
||||
Array.Clear(m_Counters, 0, m_Counters.Length);
|
||||
|
||||
// Make sure not suppressed
|
||||
suppress = false;
|
||||
}
|
||||
if (change == PlayModeStateChange.ExitingPlayMode)
|
||||
{
|
||||
// Send analytics and unhook delegate when exiting play-mode
|
||||
EditorApplication.playModeStateChanged -= PlayModeChanged;
|
||||
new InputExitPlayModeAnalytic().Send();
|
||||
|
||||
// No reason to not suppress
|
||||
suppress = true;
|
||||
}
|
||||
}
|
||||
|
||||
[InitializeOnEnterPlayMode]
|
||||
private static void Hook()
|
||||
{
|
||||
// Make sure only a single play-mode change delegate is registered
|
||||
EditorApplication.playModeStateChanged -= PlayModeChanged;
|
||||
EditorApplication.playModeStateChanged += PlayModeChanged;
|
||||
}
|
||||
|
||||
private InputExitPlayModeAnalytic()
|
||||
{
|
||||
info = new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents data collected when exiting play-mode..
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Ideally this struct should be readonly but then Unity cannot serialize/deserialize it.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <c>Data</c> instance.
|
||||
/// </summary>
|
||||
/// <param name="usesCodeAuthoring">Specifies whether code authoring has been used during play-mode.</param>
|
||||
public Data(bool usesCodeAuthoring)
|
||||
{
|
||||
uses_code_authoring = usesCodeAuthoring;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether code-authoring (Input Action setup via extensions) was used at least once during play-mode.
|
||||
/// </summary>
|
||||
public bool uses_code_authoring;
|
||||
}
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
// Determine aggregated perspective, i.e. was "any" code-authoring API used
|
||||
var usedCodeAuthoringDuringPlayMode = false;
|
||||
for (var i = 0; i < m_Counters.Length; ++i)
|
||||
{
|
||||
if (m_Counters[i] > 0)
|
||||
{
|
||||
usedCodeAuthoringDuringPlayMode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
data = new Data(usedCodeAuthoringDuringPlayMode);
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data = null;
|
||||
error = e;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info { get; }
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07c4c48bac384a9e91c29c2f04328c0b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,92 @@
|
||||
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_ENABLE_UI
|
||||
using System;
|
||||
using UnityEngine.InputSystem.OnScreen;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Analytics record for tracking engagement with Input Action Asset editor(s).
|
||||
/// </summary>
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey, version: 2)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class OnScreenStickEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_onscreenstick_editor_destroyed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
/// <summary>
|
||||
/// Represents select configuration data of interest related to an <see cref="OnScreenStick"/> component.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
public enum OnScreenStickBehaviour
|
||||
{
|
||||
RelativePositionWithStaticOrigin = 0,
|
||||
ExactPositionWithStaticOrigin = 1,
|
||||
ExactPositionWithDynamicOrigin = 2,
|
||||
}
|
||||
|
||||
private static OnScreenStickBehaviour ToBehaviour(OnScreenStick.Behaviour value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case OnScreenStick.Behaviour.RelativePositionWithStaticOrigin:
|
||||
return OnScreenStickBehaviour.RelativePositionWithStaticOrigin;
|
||||
case OnScreenStick.Behaviour.ExactPositionWithDynamicOrigin:
|
||||
return OnScreenStickBehaviour.ExactPositionWithDynamicOrigin;
|
||||
case OnScreenStick.Behaviour.ExactPositionWithStaticOrigin:
|
||||
return OnScreenStickBehaviour.ExactPositionWithStaticOrigin;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
public Data(OnScreenStick value)
|
||||
{
|
||||
behavior = ToBehaviour(value.behaviour);
|
||||
movement_range = value.movementRange;
|
||||
dynamic_origin_range = value.dynamicOriginRange;
|
||||
use_isolated_input_actions = value.useIsolatedInputActions;
|
||||
}
|
||||
|
||||
public OnScreenStickBehaviour behavior;
|
||||
public float movement_range;
|
||||
public float dynamic_origin_range;
|
||||
public bool use_isolated_input_actions;
|
||||
}
|
||||
|
||||
private readonly UnityEditor.Editor m_Editor;
|
||||
|
||||
public OnScreenStickEditorAnalytic(UnityEditor.Editor editor)
|
||||
{
|
||||
m_Editor = editor;
|
||||
}
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
data = new Data(m_Editor.target as OnScreenStick);
|
||||
error = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data = null;
|
||||
error = e;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info =>
|
||||
new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2167295510884d5eb4722df2ba677996
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Analytics for tracking Player Input component user engagement in the editor.
|
||||
/// </summary>
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class PlayerInputEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_playerinput_editor_destroyed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
private readonly UnityEditor.Editor m_Editor;
|
||||
|
||||
public PlayerInputEditorAnalytic(UnityEditor.Editor editor)
|
||||
{
|
||||
m_Editor = editor;
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info =>
|
||||
new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
data = new Data(m_Editor.target as PlayerInput);
|
||||
error = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data = null;
|
||||
error = e;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
public InputEditorAnalytics.PlayerNotificationBehavior behavior;
|
||||
public bool has_actions;
|
||||
public bool has_default_map;
|
||||
public bool has_ui_input_module;
|
||||
public bool has_camera;
|
||||
|
||||
public Data(PlayerInput playerInput)
|
||||
{
|
||||
behavior = InputEditorAnalytics.ToNotificationBehavior(playerInput.notificationBehavior);
|
||||
has_actions = playerInput.actions != null;
|
||||
has_default_map = playerInput.defaultActionMap != null;
|
||||
#if UNITY_INPUT_SYSTEM_ENABLE_UI
|
||||
has_ui_input_module = playerInput.uiInputModule != null;
|
||||
#else
|
||||
has_ui_input_module = false;
|
||||
#endif
|
||||
has_camera = playerInput.camera != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56bc028967c346c2bd33842d7b252123
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class PlayerInputManagerEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_playerinputmanager_editor_destroyed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info =>
|
||||
new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
|
||||
private readonly UnityEditor.Editor m_Editor;
|
||||
|
||||
public PlayerInputManagerEditorAnalytic(UnityEditor.Editor editor)
|
||||
{
|
||||
m_Editor = editor;
|
||||
}
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
data = new Data(m_Editor.target as PlayerInputManager);
|
||||
error = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data = null;
|
||||
error = e;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
public enum PlayerJoinBehavior
|
||||
{
|
||||
JoinPlayersWhenButtonIsPressed = 0, // default
|
||||
JoinPlayersWhenJoinActionIsTriggered = 1,
|
||||
JoinPlayersManually = 2
|
||||
}
|
||||
|
||||
public InputEditorAnalytics.PlayerNotificationBehavior behavior;
|
||||
public PlayerJoinBehavior join_behavior;
|
||||
public bool joining_enabled_by_default;
|
||||
public int max_player_count;
|
||||
|
||||
public Data(PlayerInputManager value)
|
||||
{
|
||||
behavior = InputEditorAnalytics.ToNotificationBehavior(value.notificationBehavior);
|
||||
join_behavior = ToPlayerJoinBehavior(value.joinBehavior);
|
||||
joining_enabled_by_default = value.joiningEnabled;
|
||||
max_player_count = value.maxPlayerCount;
|
||||
}
|
||||
|
||||
private static PlayerJoinBehavior ToPlayerJoinBehavior(UnityEngine.InputSystem.PlayerJoinBehavior value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case UnityEngine.InputSystem.PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
|
||||
return PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed;
|
||||
case UnityEngine.InputSystem.PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
|
||||
return PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered;
|
||||
case UnityEngine.InputSystem.PlayerJoinBehavior.JoinPlayersManually:
|
||||
return PlayerJoinBehavior.JoinPlayersManually;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 199b31cbe22b4c269aa78f8139347afd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,95 @@
|
||||
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_ENABLE_UI
|
||||
using System;
|
||||
using UnityEngine.InputSystem.UI;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Analytics record for tracking engagement with Input Action Asset editor(s).
|
||||
/// </summary>
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
[UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour,
|
||||
maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)]
|
||||
#endif // UNITY_2023_2_OR_NEWER
|
||||
internal class VirtualMouseInputEditorAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic
|
||||
{
|
||||
public const string kEventName = "input_virtualmouseinput_editor_destroyed";
|
||||
public const int kMaxEventsPerHour = 100; // default: 1000
|
||||
public const int kMaxNumberOfElements = 100; // default: 1000
|
||||
|
||||
[Serializable]
|
||||
internal struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps to <see cref="VirtualMouseInput.cursorMode"/>. Determines which cursor representation to use.
|
||||
/// </summary>
|
||||
public CursorMode cursor_mode;
|
||||
|
||||
/// <summary>
|
||||
/// Maps to <see cref="VirtualMouseInput.cursorSpeed" />. Speed in pixels per second with which to move the cursor.
|
||||
/// </summary>
|
||||
public float cursor_speed;
|
||||
|
||||
/// <summary>
|
||||
/// Maps to <see cref="VirtualMouseInput.cursorMode"/>. Multiplier for values received from <see cref="VirtualMouseInput.scrollWheelAction"/>.
|
||||
/// </summary>
|
||||
public float scroll_speed;
|
||||
|
||||
public enum CursorMode
|
||||
{
|
||||
SoftwareCursor = 0,
|
||||
HardwareCursorIfAvailable = 1
|
||||
}
|
||||
|
||||
private static CursorMode ToCursorMode(VirtualMouseInput.CursorMode value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case VirtualMouseInput.CursorMode.SoftwareCursor:
|
||||
return CursorMode.SoftwareCursor;
|
||||
case VirtualMouseInput.CursorMode.HardwareCursorIfAvailable:
|
||||
return CursorMode.HardwareCursorIfAvailable;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
public Data(VirtualMouseInput value)
|
||||
{
|
||||
cursor_mode = ToCursorMode(value.cursorMode);
|
||||
cursor_speed = value.cursorSpeed;
|
||||
scroll_speed = value.scrollSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
public InputAnalytics.InputAnalyticInfo info =>
|
||||
new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements);
|
||||
|
||||
private readonly UnityEditor.Editor m_Editor;
|
||||
|
||||
public VirtualMouseInputEditorAnalytic(UnityEditor.Editor editor)
|
||||
{
|
||||
m_Editor = editor;
|
||||
}
|
||||
|
||||
#if UNITY_2023_2_OR_NEWER
|
||||
public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error)
|
||||
#else
|
||||
public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
data = new Data(m_Editor.target as VirtualMouseInput);
|
||||
error = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
data = null;
|
||||
error = e;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 183f7887e5104e1593ce980b9d0159e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5c84a68c16874bd5ba1a9d15a5e5044
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,249 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
|
||||
////TODO: ensure that GUIDs in the asset are unique
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps a reference to the asset being edited and maintains a copy of the asset object
|
||||
/// around for editing.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class InputActionAssetManager : IDisposable
|
||||
{
|
||||
[SerializeField] private InputActionAsset m_AssetObjectForEditing;
|
||||
[SerializeField] private InputActionAsset m_ImportedAssetObject;
|
||||
[SerializeField] private string m_AssetGUID;
|
||||
[SerializeField] private string m_ImportedAssetJson;
|
||||
[SerializeField] private bool m_IsDirty;
|
||||
|
||||
private SerializedObject m_SerializedObject;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Asset GUID uniquely identifying the associated imported asset.
|
||||
/// </summary>
|
||||
public string guid => m_AssetGUID;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current Asset Path for the associated imported asset.
|
||||
/// If the asset have been deleted this will be <c>null</c>.
|
||||
/// </summary>
|
||||
public string path
|
||||
{
|
||||
get
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(m_AssetGUID), "Asset GUID is empty");
|
||||
return AssetDatabase.GUIDToAssetPath(m_AssetGUID);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name of the associated imported asset.
|
||||
/// </summary>
|
||||
public string name
|
||||
{
|
||||
get
|
||||
{
|
||||
var asset = importedAsset;
|
||||
if (asset != null)
|
||||
return asset.name;
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
return Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private InputActionAsset importedAsset
|
||||
{
|
||||
get
|
||||
{
|
||||
// Note that this may be null after deserialization from domain reload
|
||||
if (m_ImportedAssetObject == null)
|
||||
LoadImportedObjectFromGuid();
|
||||
|
||||
return m_ImportedAssetObject;
|
||||
}
|
||||
}
|
||||
|
||||
public InputActionAsset editedAsset => m_AssetObjectForEditing; // TODO Remove if redundant
|
||||
|
||||
public Action<bool> onDirtyChanged { get; set; }
|
||||
|
||||
public InputActionAssetManager(InputActionAsset inputActionAsset)
|
||||
{
|
||||
if (inputActionAsset == null)
|
||||
throw new NullReferenceException(nameof(inputActionAsset));
|
||||
m_AssetGUID = EditorHelpers.GetAssetGUID(inputActionAsset);
|
||||
if (m_AssetGUID == null)
|
||||
throw new Exception($"Failed to get asset {inputActionAsset.name} GUID");
|
||||
|
||||
m_ImportedAssetObject = inputActionAsset;
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public SerializedObject serializedObject => m_SerializedObject;
|
||||
|
||||
public bool dirty => m_IsDirty;
|
||||
|
||||
public bool Initialize()
|
||||
{
|
||||
if (m_AssetObjectForEditing == null)
|
||||
{
|
||||
if (importedAsset == null)
|
||||
{
|
||||
// The asset we want to edit no longer exists.
|
||||
return false;
|
||||
}
|
||||
|
||||
CreateWorkingCopyAsset();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_SerializedObject = new SerializedObject(m_AssetObjectForEditing);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_SerializedObject == null)
|
||||
return;
|
||||
m_SerializedObject?.Dispose();
|
||||
m_SerializedObject = null;
|
||||
}
|
||||
|
||||
public bool ReInitializeIfAssetHasChanged()
|
||||
{
|
||||
var json = importedAsset.ToJson();
|
||||
if (m_ImportedAssetJson == json)
|
||||
return false;
|
||||
|
||||
CreateWorkingCopyAsset();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static InputActionAsset CreateWorkingCopy(InputActionAsset source)
|
||||
{
|
||||
var copy = Object.Instantiate(source);
|
||||
copy.hideFlags = HideFlags.HideAndDontSave;
|
||||
copy.name = source.name;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static void CreateWorkingCopyAsset(ref InputActionAsset copy, InputActionAsset source)
|
||||
{
|
||||
if (copy != null)
|
||||
Cleanup(ref copy);
|
||||
|
||||
copy = CreateWorkingCopy(source);
|
||||
}
|
||||
|
||||
private void CreateWorkingCopyAsset() // TODO Can likely be removed if combined with Initialize
|
||||
{
|
||||
if (m_AssetObjectForEditing != null)
|
||||
Cleanup();
|
||||
|
||||
// Duplicate the asset along 1:1. Unlike calling Clone(), this will also preserve GUIDs.
|
||||
var asset = importedAsset;
|
||||
m_AssetObjectForEditing = CreateWorkingCopy(asset);
|
||||
m_ImportedAssetJson = asset.ToJson();
|
||||
m_SerializedObject = new SerializedObject(m_AssetObjectForEditing);
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
Cleanup(ref m_AssetObjectForEditing);
|
||||
}
|
||||
|
||||
public static void Cleanup(ref InputActionAsset asset)
|
||||
{
|
||||
if (asset == null)
|
||||
return;
|
||||
|
||||
Object.DestroyImmediate(asset);
|
||||
asset = null;
|
||||
}
|
||||
|
||||
private void LoadImportedObjectFromGuid()
|
||||
{
|
||||
// https://fogbugz.unity3d.com/f/cases/1313185/
|
||||
// InputActionEditorWindow being an EditorWindow, it will be saved as part of the editor's
|
||||
// window layout. When a project is opened that has no Library/ folder, the layout from the
|
||||
// most recently opened project is used. Which means that when opening an .inputactions
|
||||
// asset in project A, then closing it, and then opening project B, restoring the window layout
|
||||
// also tries to restore the InputActionEditorWindow having that very same asset open -- which
|
||||
// will lead nowhere except there happens to be an InputActionAsset with the very same GUID in
|
||||
// the project.
|
||||
var assetPath = path;
|
||||
if (!string.IsNullOrEmpty(assetPath))
|
||||
m_ImportedAssetObject = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath);
|
||||
}
|
||||
|
||||
public void ApplyChanges()
|
||||
{
|
||||
m_SerializedObject.ApplyModifiedProperties();
|
||||
m_SerializedObject.Update();
|
||||
}
|
||||
|
||||
internal void SaveChangesToAsset()
|
||||
{
|
||||
// If this is invoked after a domain reload, importAsset will resolve itself.
|
||||
// However, if the asset do not exist importedAsset will be null and we cannot complete the operation.
|
||||
if (importedAsset == null)
|
||||
throw new Exception("Unable to save changes. Associated asset does not exist.");
|
||||
|
||||
SaveAsset(path, m_AssetObjectForEditing.ToJson());
|
||||
SetDirty(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves an asset to the given <c>assetPath</c> with file content corresponding to <c>assetJson</c>
|
||||
/// if the current content of the asset given by <c>assetPath</c> is different or the asset do not exist.
|
||||
/// </summary>
|
||||
/// <param name="assetPath">Destination asset path.</param>
|
||||
/// <param name="assetJson">The JSON file content to be written to the asset.</param>
|
||||
/// <returns><c>true</c> if the asset was successfully modified or created, else <c>false</c>.</returns>
|
||||
internal static bool SaveAsset(string assetPath, string assetJson)
|
||||
{
|
||||
var existingJson = File.Exists(assetPath) ? File.ReadAllText(assetPath) : string.Empty;
|
||||
|
||||
// Return immediately if file content has not changed, i.e. touching the file would not yield a difference.
|
||||
if (assetJson == existingJson)
|
||||
return false;
|
||||
|
||||
// Attempt to write asset to disc (including checkout the file) and inform the user if this fails.
|
||||
if (EditorHelpers.WriteAsset(assetPath, assetJson))
|
||||
return true;
|
||||
|
||||
Debug.LogError($"Unable save asset to \"{assetPath}\" since the asset-path could not be checked-out as editable in the underlying version-control system.");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void MarkDirty()
|
||||
{
|
||||
SetDirty(true);
|
||||
}
|
||||
|
||||
public void UpdateAssetDirtyState()
|
||||
{
|
||||
m_SerializedObject.Update();
|
||||
SetDirty(m_AssetObjectForEditing.ToJson() != importedAsset.ToJson()); // TODO Why not using cached version?
|
||||
}
|
||||
|
||||
private void SetDirty(bool newValue)
|
||||
{
|
||||
m_IsDirty = newValue;
|
||||
if (onDirtyChanged != null)
|
||||
onDirtyChanged(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5b4f9c3390c14106a5006fd5d3d3cbe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,130 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Controls;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// UI that edits the properties of an <see cref="InputAction"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Right-most pane in <see cref="InputActionEditorWindow"/> when an action is selected.
|
||||
/// </remarks>
|
||||
internal class InputActionPropertiesView : PropertiesViewBase
|
||||
{
|
||||
public static FourCC k_PropertiesChanged => new FourCC("PROP");
|
||||
|
||||
public InputActionPropertiesView(SerializedProperty actionProperty, Action<FourCC> onChange = null)
|
||||
: base("Action", actionProperty, onChange, actionProperty.FindPropertyRelative("m_ExpectedControlType").stringValue)
|
||||
{
|
||||
m_ExpectedControlTypeProperty = actionProperty.FindPropertyRelative(nameof(InputAction.m_ExpectedControlType));
|
||||
m_ActionTypeProperty = actionProperty.FindPropertyRelative(nameof(InputAction.m_Type));
|
||||
m_ActionFlagsProperty = actionProperty.FindPropertyRelative(nameof(InputAction.m_Flags));
|
||||
|
||||
m_SelectedActionType = (InputActionType)m_ActionTypeProperty.intValue;
|
||||
m_WantsInitialStateCheck = (m_ActionFlagsProperty.intValue & (int)InputAction.ActionFlags.WantsInitialStateCheck) != 0;
|
||||
|
||||
BuildControlTypeList();
|
||||
m_SelectedControlType = Array.IndexOf(m_ControlTypeList, m_ExpectedControlTypeProperty.stringValue);
|
||||
if (m_SelectedControlType == -1)
|
||||
m_SelectedControlType = 0;
|
||||
|
||||
if (s_ControlTypeLabel == null)
|
||||
s_ControlTypeLabel = EditorGUIUtility.TrTextContent("Control Type", m_ExpectedControlTypeProperty.GetTooltip());
|
||||
if (s_ActionTypeLabel == null)
|
||||
s_ActionTypeLabel = EditorGUIUtility.TrTextContent("Action Type", m_ActionTypeProperty.GetTooltip());
|
||||
if (s_WantsInitialStateCheckLabel == null)
|
||||
s_WantsInitialStateCheckLabel = EditorGUIUtility.TrTextContent("Initial State Check",
|
||||
"Whether in the next input update after the action was enabled, the action should "
|
||||
+ "immediately trigger if any of its bound controls are currently in a non-default state. "
|
||||
+ "This check happens implicitly for Value actions but can be explicitly enabled for Button and Pass-Through actions.");
|
||||
}
|
||||
|
||||
protected override void DrawGeneralProperties()
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
m_SelectedActionType = EditorGUILayout.EnumPopup(s_ActionTypeLabel, m_SelectedActionType);
|
||||
if ((InputActionType)m_SelectedActionType != InputActionType.Button)
|
||||
m_SelectedControlType = EditorGUILayout.Popup(s_ControlTypeLabel, m_SelectedControlType, m_ControlTypeOptions);
|
||||
|
||||
if ((InputActionType)m_SelectedActionType != InputActionType.Value)
|
||||
m_WantsInitialStateCheck = EditorGUILayout.Toggle(s_WantsInitialStateCheckLabel, m_WantsInitialStateCheck);
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if ((InputActionType)m_SelectedActionType == InputActionType.Button)
|
||||
m_ExpectedControlTypeProperty.stringValue = "Button";
|
||||
else if (m_SelectedControlType == 0)
|
||||
m_ExpectedControlTypeProperty.stringValue = string.Empty;
|
||||
else
|
||||
m_ExpectedControlTypeProperty.stringValue = m_ControlTypeList[m_SelectedControlType];
|
||||
|
||||
m_ActionTypeProperty.intValue = (int)(InputActionType)m_SelectedActionType;
|
||||
|
||||
if (m_WantsInitialStateCheck)
|
||||
m_ActionFlagsProperty.intValue |= (int)InputAction.ActionFlags.WantsInitialStateCheck;
|
||||
else
|
||||
m_ActionFlagsProperty.intValue &= ~(int)InputAction.ActionFlags.WantsInitialStateCheck;
|
||||
|
||||
m_ActionTypeProperty.serializedObject.ApplyModifiedProperties();
|
||||
UpdateProcessors(m_ExpectedControlTypeProperty.stringValue);
|
||||
|
||||
onChange(k_PropertiesChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildControlTypeList()
|
||||
{
|
||||
var types = new List<string>();
|
||||
var allLayouts = InputSystem.s_Manager.m_Layouts;
|
||||
foreach (var layoutName in allLayouts.layoutTypes.Keys)
|
||||
{
|
||||
if (EditorInputControlLayoutCache.TryGetLayout(layoutName).hideInUI)
|
||||
continue;
|
||||
|
||||
// If the action type is InputActionType.Value, skip button controls.
|
||||
var type = allLayouts.layoutTypes[layoutName];
|
||||
if ((InputActionType)m_SelectedActionType == InputActionType.Value &&
|
||||
typeof(ButtonControl).IsAssignableFrom(type))
|
||||
continue;
|
||||
|
||||
////TODO: skip aliases
|
||||
|
||||
if (typeof(InputControl).IsAssignableFrom(type) &&
|
||||
!typeof(InputDevice).IsAssignableFrom(type))
|
||||
{
|
||||
types.Add(layoutName);
|
||||
}
|
||||
}
|
||||
// Sort alphabetically.
|
||||
types.Sort((a, b) => string.Compare(a, b, StringComparison.OrdinalIgnoreCase));
|
||||
// Make sure "Any" is always topmost entry.
|
||||
types.Insert(0, "Any");
|
||||
|
||||
m_ControlTypeList = types.ToArray();
|
||||
m_ControlTypeOptions = m_ControlTypeList.Select(x => new GUIContent(ObjectNames.NicifyVariableName(x)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private readonly SerializedProperty m_ExpectedControlTypeProperty;
|
||||
private readonly SerializedProperty m_ActionTypeProperty;
|
||||
private readonly SerializedProperty m_ActionFlagsProperty;
|
||||
|
||||
private string m_ExpectedControlLayout;
|
||||
private string[] m_ControlTypeList;
|
||||
private GUIContent[] m_ControlTypeOptions;
|
||||
private int m_SelectedControlType;
|
||||
private Enum m_SelectedActionType;
|
||||
private bool m_WantsInitialStateCheck;
|
||||
|
||||
private static GUIContent s_ActionTypeLabel;
|
||||
private static GUIContent s_ControlTypeLabel;
|
||||
private static GUIContent s_WantsInitialStateCheckLabel;
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d295e36fd3cd747f4a2123088241bd6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 713b035f669b942adbd1cc05c4fb6e1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,569 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
#if UNITY_6000_2_OR_NEWER
|
||||
using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem<int>;
|
||||
#endif
|
||||
|
||||
////TODO: sync expanded state of SerializedProperties to expanded state of tree (will help preserving expansion in inspector)
|
||||
|
||||
////REVIEW: would be great to align all "[device]" parts of binding strings neatly in a column
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal abstract class ActionTreeItemBase : TreeViewItem
|
||||
{
|
||||
public SerializedProperty property { get; }
|
||||
public virtual string expectedControlLayout => string.Empty;
|
||||
public virtual bool canRename => true;
|
||||
public virtual bool serializedDataIncludesChildren => false;
|
||||
public abstract GUIStyle colorTagStyle { get; }
|
||||
public string name { get; }
|
||||
public Guid guid { get; }
|
||||
public virtual bool showWarningIcon => false;
|
||||
|
||||
// For some operations (like copy-paste), we want to include information that we have filtered out.
|
||||
internal List<ActionTreeItemBase> m_HiddenChildren;
|
||||
public bool hasChildrenIncludingHidden => hasChildren || (m_HiddenChildren != null && m_HiddenChildren.Count > 0);
|
||||
public IEnumerable<ActionTreeItemBase> hiddenChildren => m_HiddenChildren ?? Enumerable.Empty<ActionTreeItemBase>();
|
||||
public IEnumerable<ActionTreeItemBase> childrenIncludingHidden
|
||||
{
|
||||
get
|
||||
{
|
||||
if (hasChildren)
|
||||
foreach (var child in children)
|
||||
if (child is ActionTreeItemBase item)
|
||||
yield return item;
|
||||
if (m_HiddenChildren != null)
|
||||
foreach (var child in m_HiddenChildren)
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
|
||||
// Action data is generally stored in arrays. Action maps are stored in m_ActionMaps arrays in assets,
|
||||
// actions are stored in m_Actions arrays on maps and bindings are stored in m_Bindings arrays on maps.
|
||||
public SerializedProperty arrayProperty => property.GetArrayPropertyFromElement();
|
||||
|
||||
// Dynamically look up the array index instead of just taking it from `property`.
|
||||
// This makes sure whatever insertions or deletions we perform on the serialized data,
|
||||
// we get the right array index from an item.
|
||||
public int arrayIndex => InputActionSerializationHelpers.GetIndex(arrayProperty, guid);
|
||||
|
||||
protected ActionTreeItemBase(SerializedProperty property)
|
||||
{
|
||||
this.property = property;
|
||||
|
||||
// Look up name.
|
||||
var nameProperty = property.FindPropertyRelative("m_Name");
|
||||
Debug.Assert(nameProperty != null, $"Cannot find m_Name property on {property.propertyPath}");
|
||||
name = nameProperty.stringValue;
|
||||
|
||||
// Look up ID.
|
||||
var idProperty = property.FindPropertyRelative("m_Id");
|
||||
Debug.Assert(idProperty != null, $"Cannot find m_Id property on {property.propertyPath}");
|
||||
var idPropertyString = idProperty.stringValue;
|
||||
if (string.IsNullOrEmpty(idPropertyString))
|
||||
{
|
||||
// This is somewhat questionable but we can't operate if we don't have IDs on the data used in the tree.
|
||||
// Rather than requiring users of the tree to set this up consistently, we assign IDs
|
||||
// on the fly, if necessary.
|
||||
guid = Guid.NewGuid();
|
||||
idPropertyString = guid.ToString();
|
||||
idProperty.stringValue = idPropertyString;
|
||||
idProperty.serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
else
|
||||
{
|
||||
guid = new Guid(idPropertyString);
|
||||
}
|
||||
|
||||
// All our elements (maps, actions, bindings) carry unique IDs. We use their hash
|
||||
// codes as item IDs in the tree. This should result in stable item IDs that keep
|
||||
// identifying the right item across all reloads and tree mutations.
|
||||
id = guid.GetHashCode();
|
||||
}
|
||||
|
||||
public virtual void Rename(string newName)
|
||||
{
|
||||
Debug.Assert(!canRename, "Item is marked as allowing renames yet does not implement Rename()");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete serialized data for the tree item and its children.
|
||||
/// </summary>
|
||||
public abstract void DeleteData();
|
||||
|
||||
public abstract bool AcceptsDrop(ActionTreeItemBase item);
|
||||
|
||||
/// <summary>
|
||||
/// Get information about where to drop an item of the given type and (optionally) the given index.
|
||||
/// </summary>
|
||||
public abstract bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex);
|
||||
|
||||
protected static class Styles
|
||||
{
|
||||
private static GUIStyle StyleWithBackground(string fileName)
|
||||
{
|
||||
return new GUIStyle("Label").WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>($"{InputActionTreeView.SharedResourcesPath}{fileName}.png"));
|
||||
}
|
||||
|
||||
public static readonly GUIStyle yellowRect = StyleWithBackground("yellow");
|
||||
public static readonly GUIStyle greenRect = StyleWithBackground("green");
|
||||
public static readonly GUIStyle blueRect = StyleWithBackground("blue");
|
||||
public static readonly GUIStyle pinkRect = StyleWithBackground("pink");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tree view item for an action map.
|
||||
/// </summary>
|
||||
/// <seealso cref="InputActionMap"/>
|
||||
internal class ActionMapTreeItem : ActionTreeItemBase
|
||||
{
|
||||
public ActionMapTreeItem(SerializedProperty actionMapProperty)
|
||||
: base(actionMapProperty)
|
||||
{
|
||||
}
|
||||
|
||||
public override GUIStyle colorTagStyle => Styles.yellowRect;
|
||||
public SerializedProperty bindingsArrayProperty => property.FindPropertyRelative("m_Bindings");
|
||||
public SerializedProperty actionsArrayProperty => property.FindPropertyRelative("m_Actions");
|
||||
public override bool serializedDataIncludesChildren => true;
|
||||
|
||||
public override void Rename(string newName)
|
||||
{
|
||||
InputActionSerializationHelpers.RenameActionMap(property, newName);
|
||||
}
|
||||
|
||||
public override void DeleteData()
|
||||
{
|
||||
var assetObject = property.serializedObject;
|
||||
if (!(assetObject.targetObject is InputActionAsset))
|
||||
throw new InvalidOperationException(
|
||||
$"Action map must be part of InputActionAsset but is in {assetObject.targetObject} instead");
|
||||
|
||||
InputActionSerializationHelpers.DeleteActionMap(assetObject, guid);
|
||||
}
|
||||
|
||||
public override bool AcceptsDrop(ActionTreeItemBase item)
|
||||
{
|
||||
return item is ActionTreeItem;
|
||||
}
|
||||
|
||||
public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
|
||||
{
|
||||
// Drop actions into action array.
|
||||
if (itemType == typeof(ActionTreeItem))
|
||||
{
|
||||
array = actionsArrayProperty;
|
||||
arrayIndex = childIndex ?? -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// For action maps in assets, drop other action maps next to them.
|
||||
if (itemType == typeof(ActionMapTreeItem) && property.serializedObject.targetObject is InputActionAsset)
|
||||
{
|
||||
array = property.GetArrayPropertyFromElement();
|
||||
arrayIndex = this.arrayIndex + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
////REVIEW: would be nice to be able to replace the entire contents of a map in the inspector by dropping in another map
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ActionMapTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty)
|
||||
{
|
||||
var item = new ActionMapTreeItem(actionMapProperty);
|
||||
|
||||
item.depth = parent.depth + 1;
|
||||
item.displayName = item.name;
|
||||
parent.AddChild(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public void AddActionsTo(TreeViewItem parent)
|
||||
{
|
||||
AddActionsTo(parent, addBindings: false);
|
||||
}
|
||||
|
||||
public void AddActionsAndBindingsTo(TreeViewItem parent)
|
||||
{
|
||||
AddActionsTo(parent, addBindings: true);
|
||||
}
|
||||
|
||||
private void AddActionsTo(TreeViewItem parent, bool addBindings)
|
||||
{
|
||||
var actionsArrayProperty = this.actionsArrayProperty;
|
||||
Debug.Assert(actionsArrayProperty != null, $"Cannot find m_Actions in {property}");
|
||||
|
||||
for (var i = 0; i < actionsArrayProperty.arraySize; i++)
|
||||
{
|
||||
var actionProperty = actionsArrayProperty.GetArrayElementAtIndex(i);
|
||||
var actionItem = ActionTreeItem.AddTo(parent, property, actionProperty);
|
||||
|
||||
if (addBindings)
|
||||
actionItem.AddBindingsTo(actionItem);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddActionMapsFromAssetTo(TreeViewItem parent, SerializedObject assetObject)
|
||||
{
|
||||
var actionMapsArrayProperty = assetObject.FindProperty("m_ActionMaps");
|
||||
Debug.Assert(actionMapsArrayProperty != null, $"Cannot find m_ActionMaps in {assetObject}");
|
||||
Debug.Assert(actionMapsArrayProperty.isArray, $"m_ActionMaps in {assetObject} is not an array");
|
||||
|
||||
var mapCount = actionMapsArrayProperty.arraySize;
|
||||
for (var i = 0; i < mapCount; ++i)
|
||||
{
|
||||
var mapProperty = actionMapsArrayProperty.GetArrayElementAtIndex(i);
|
||||
AddTo(parent, mapProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tree view item for an action.
|
||||
/// </summary>
|
||||
/// <see cref="InputAction"/>
|
||||
internal class ActionTreeItem : ActionTreeItemBase
|
||||
{
|
||||
public ActionTreeItem(SerializedProperty actionMapProperty, SerializedProperty actionProperty)
|
||||
: base(actionProperty)
|
||||
{
|
||||
this.actionMapProperty = actionMapProperty;
|
||||
}
|
||||
|
||||
public SerializedProperty actionMapProperty { get; }
|
||||
public override GUIStyle colorTagStyle => Styles.greenRect;
|
||||
public bool isSingletonAction => actionMapProperty == null;
|
||||
|
||||
public override string expectedControlLayout
|
||||
{
|
||||
get
|
||||
{
|
||||
var expectedControlType = property.FindPropertyRelative("m_ExpectedControlType").stringValue;
|
||||
if (!string.IsNullOrEmpty(expectedControlType))
|
||||
return expectedControlType;
|
||||
|
||||
var type = property.FindPropertyRelative("m_Type").intValue;
|
||||
if (type == (int)InputActionType.Button)
|
||||
return "Button";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public SerializedProperty bindingsArrayProperty => isSingletonAction
|
||||
? property.FindPropertyRelative("m_SingletonActionBindings")
|
||||
: actionMapProperty.FindPropertyRelative("m_Bindings");
|
||||
|
||||
// If we're a singleton action (no associated action map property), we include all our bindings in the
|
||||
// serialized data.
|
||||
public override bool serializedDataIncludesChildren => actionMapProperty == null;
|
||||
|
||||
public override void Rename(string newName)
|
||||
{
|
||||
InputActionSerializationHelpers.RenameAction(property, actionMapProperty, newName);
|
||||
}
|
||||
|
||||
public override void DeleteData()
|
||||
{
|
||||
InputActionSerializationHelpers.DeleteActionAndBindings(actionMapProperty, guid);
|
||||
}
|
||||
|
||||
public override bool AcceptsDrop(ActionTreeItemBase item)
|
||||
{
|
||||
return item is BindingTreeItem && !(item is PartOfCompositeBindingTreeItem);
|
||||
}
|
||||
|
||||
public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
|
||||
{
|
||||
// Drop bindings into binding array.
|
||||
if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
|
||||
{
|
||||
array = bindingsArrayProperty;
|
||||
|
||||
// Indexing by tree items is relative to each action but indexing in
|
||||
// binding array is global for all actions in a map. Adjust index accordingly.
|
||||
// NOTE: Bindings for any one action need not be stored contiguously in the binding array
|
||||
// so we can't just add something to the index of the first binding to the action.
|
||||
arrayIndex =
|
||||
InputActionSerializationHelpers.ConvertBindingIndexOnActionToBindingIndexInArray(
|
||||
array, name, childIndex ?? -1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drop other actions next to us.
|
||||
if (itemType == typeof(ActionTreeItem))
|
||||
{
|
||||
array = arrayProperty;
|
||||
arrayIndex = this.arrayIndex + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ActionTreeItem AddTo(TreeViewItem parent, SerializedProperty actionMapProperty, SerializedProperty actionProperty)
|
||||
{
|
||||
var item = new ActionTreeItem(actionMapProperty, actionProperty);
|
||||
|
||||
item.depth = parent.depth + 1;
|
||||
item.displayName = item.name;
|
||||
parent.AddChild(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add items for the bindings of just this action to the given parent tree item.
|
||||
/// </summary>
|
||||
public void AddBindingsTo(TreeViewItem parent)
|
||||
{
|
||||
var isSingleton = actionMapProperty == null;
|
||||
var bindingsArrayProperty = isSingleton
|
||||
? property.FindPropertyRelative("m_SingletonActionBindings")
|
||||
: actionMapProperty.FindPropertyRelative("m_Bindings");
|
||||
|
||||
var bindingsCountInMap = bindingsArrayProperty.arraySize;
|
||||
var currentComposite = (CompositeBindingTreeItem)null;
|
||||
for (var i = 0; i < bindingsCountInMap; ++i)
|
||||
{
|
||||
var bindingProperty = bindingsArrayProperty.GetArrayElementAtIndex(i);
|
||||
|
||||
// Skip if binding is not for action.
|
||||
var actionProperty = bindingProperty.FindPropertyRelative("m_Action");
|
||||
Debug.Assert(actionProperty != null, $"Could not find m_Action in {bindingProperty}");
|
||||
if (!actionProperty.stringValue.Equals(name, StringComparison.InvariantCultureIgnoreCase))
|
||||
continue;
|
||||
|
||||
// See what kind of binding we have.
|
||||
var flagsProperty = bindingProperty.FindPropertyRelative("m_Flags");
|
||||
Debug.Assert(actionProperty != null, $"Could not find m_Flags in {bindingProperty}");
|
||||
var flags = (InputBinding.Flags)flagsProperty.intValue;
|
||||
if ((flags & InputBinding.Flags.PartOfComposite) != 0 && currentComposite != null)
|
||||
{
|
||||
// Composite part binding.
|
||||
PartOfCompositeBindingTreeItem.AddTo(currentComposite, bindingProperty);
|
||||
}
|
||||
else if ((flags & InputBinding.Flags.Composite) != 0)
|
||||
{
|
||||
// Composite binding.
|
||||
currentComposite = CompositeBindingTreeItem.AddTo(parent, bindingProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
// "Normal" binding.
|
||||
BindingTreeItem.AddTo(parent, bindingProperty);
|
||||
currentComposite = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tree view item for a binding.
|
||||
/// </summary>
|
||||
/// <seealso cref="InputBinding"/>
|
||||
internal class BindingTreeItem : ActionTreeItemBase
|
||||
{
|
||||
public BindingTreeItem(SerializedProperty bindingProperty)
|
||||
: base(bindingProperty)
|
||||
{
|
||||
path = property.FindPropertyRelative("m_Path").stringValue;
|
||||
groups = property.FindPropertyRelative("m_Groups").stringValue;
|
||||
action = property.FindPropertyRelative("m_Action").stringValue;
|
||||
}
|
||||
|
||||
public string path { get; }
|
||||
public string groups { get; }
|
||||
public string action { get; }
|
||||
public override bool showWarningIcon => InputSystem.ShouldDrawWarningIconForBinding(path);
|
||||
|
||||
public override bool canRename => false;
|
||||
public override GUIStyle colorTagStyle => Styles.blueRect;
|
||||
|
||||
public string displayPath =>
|
||||
!string.IsNullOrEmpty(path) ? InputControlPath.ToHumanReadableString(path) : "<No Binding>";
|
||||
|
||||
private ActionTreeItem actionItem
|
||||
{
|
||||
get
|
||||
{
|
||||
// Find the action we're under.
|
||||
for (var node = parent; node != null; node = node.parent)
|
||||
if (node is ActionTreeItem item)
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override string expectedControlLayout
|
||||
{
|
||||
get
|
||||
{
|
||||
var currentActionItem = actionItem;
|
||||
return currentActionItem != null ? currentActionItem.expectedControlLayout : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override void DeleteData()
|
||||
{
|
||||
var currentActionItem = actionItem;
|
||||
Debug.Assert(currentActionItem != null, "BindingTreeItem should always have a parent action");
|
||||
var bindingsArrayProperty = currentActionItem.bindingsArrayProperty;
|
||||
InputActionSerializationHelpers.DeleteBinding(bindingsArrayProperty, guid);
|
||||
}
|
||||
|
||||
public override bool AcceptsDrop(ActionTreeItemBase item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
|
||||
{
|
||||
// Drop bindings next to us.
|
||||
if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
|
||||
{
|
||||
array = arrayProperty;
|
||||
arrayIndex = this.arrayIndex + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static BindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
|
||||
{
|
||||
var item = new BindingTreeItem(bindingProperty);
|
||||
|
||||
item.depth = parent.depth + 1;
|
||||
item.displayName = item.displayPath;
|
||||
parent.AddChild(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tree view item for a composite binding.
|
||||
/// </summary>
|
||||
/// <seealso cref="InputBinding.isComposite"/>
|
||||
internal class CompositeBindingTreeItem : BindingTreeItem
|
||||
{
|
||||
public CompositeBindingTreeItem(SerializedProperty bindingProperty)
|
||||
: base(bindingProperty)
|
||||
{
|
||||
}
|
||||
|
||||
public override GUIStyle colorTagStyle => Styles.blueRect;
|
||||
public override bool canRename => true;
|
||||
|
||||
public string compositeName => NameAndParameters.ParseName(path);
|
||||
|
||||
public override void Rename(string newName)
|
||||
{
|
||||
InputActionSerializationHelpers.RenameComposite(property, newName);
|
||||
}
|
||||
|
||||
public override bool AcceptsDrop(ActionTreeItemBase item)
|
||||
{
|
||||
return item is PartOfCompositeBindingTreeItem;
|
||||
}
|
||||
|
||||
public override bool GetDropLocation(Type itemType, int? childIndex, ref SerializedProperty array, ref int arrayIndex)
|
||||
{
|
||||
// Drop part binding into composite.
|
||||
if (itemType == typeof(PartOfCompositeBindingTreeItem))
|
||||
{
|
||||
array = arrayProperty;
|
||||
|
||||
// Adjust child index by index of composite item itself.
|
||||
arrayIndex = childIndex != null
|
||||
? this.arrayIndex + 1 + childIndex.Value // Dropping at #0 should put as our index plus one.
|
||||
: this.arrayIndex + 1 + InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drop other bindings next to us.
|
||||
if (typeof(BindingTreeItem).IsAssignableFrom(itemType))
|
||||
{
|
||||
array = arrayProperty;
|
||||
arrayIndex = this.arrayIndex + 1 +
|
||||
InputActionSerializationHelpers.GetCompositePartCount(array, this.arrayIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public new static CompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
|
||||
{
|
||||
var item = new CompositeBindingTreeItem(bindingProperty);
|
||||
|
||||
item.depth = parent.depth + 1;
|
||||
item.displayName = !string.IsNullOrEmpty(item.name)
|
||||
? item.name
|
||||
: ObjectNames.NicifyVariableName(NameAndParameters.ParseName(item.path));
|
||||
|
||||
parent.AddChild(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tree view item for bindings that are parts of composites.
|
||||
/// </summary>
|
||||
/// <see cref="InputBinding.isPartOfComposite"/>
|
||||
internal class PartOfCompositeBindingTreeItem : BindingTreeItem
|
||||
{
|
||||
public PartOfCompositeBindingTreeItem(SerializedProperty bindingProperty)
|
||||
: base(bindingProperty)
|
||||
{
|
||||
}
|
||||
|
||||
public override GUIStyle colorTagStyle => Styles.pinkRect;
|
||||
public override bool canRename => false;
|
||||
|
||||
public override string expectedControlLayout
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_ExpectedControlLayout == null)
|
||||
{
|
||||
var partName = name;
|
||||
var compositeName = ((CompositeBindingTreeItem)parent).compositeName;
|
||||
var layoutName = InputBindingComposite.GetExpectedControlLayoutName(compositeName, partName);
|
||||
m_ExpectedControlLayout = layoutName ?? "";
|
||||
}
|
||||
|
||||
return m_ExpectedControlLayout;
|
||||
}
|
||||
}
|
||||
|
||||
private string m_ExpectedControlLayout;
|
||||
|
||||
public new static PartOfCompositeBindingTreeItem AddTo(TreeViewItem parent, SerializedProperty bindingProperty)
|
||||
{
|
||||
var item = new PartOfCompositeBindingTreeItem(bindingProperty);
|
||||
|
||||
item.depth = parent.depth + 1;
|
||||
item.displayName = $"{ObjectNames.NicifyVariableName(item.name)}: {item.displayPath}";
|
||||
parent.AddChild(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76d591911f4ae4f62953b13f6ff50429
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,397 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Editor.Lists;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////REVIEW: when we start with a blank tree view state, we should initialize the control picker to select the control currently
|
||||
//// selected by the path property
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// UI for editing properties of an <see cref="InputBinding"/>. Right-most pane in action editor when
|
||||
/// binding is selected in middle pane.
|
||||
/// </summary>
|
||||
internal class InputBindingPropertiesView : PropertiesViewBase, IDisposable
|
||||
{
|
||||
public static FourCC k_GroupsChanged => new FourCC("GRPS");
|
||||
public static FourCC k_PathChanged => new FourCC("PATH");
|
||||
public static FourCC k_CompositeTypeChanged => new FourCC("COMP");
|
||||
public static FourCC k_CompositePartAssignmentChanged => new FourCC("PART");
|
||||
|
||||
public InputBindingPropertiesView(
|
||||
SerializedProperty bindingProperty,
|
||||
Action<FourCC> onChange = null,
|
||||
InputControlPickerState controlPickerState = null,
|
||||
string expectedControlLayout = null,
|
||||
ReadOnlyArray<InputControlScheme> controlSchemes = new ReadOnlyArray<InputControlScheme>(),
|
||||
IEnumerable<string> controlPathsToMatch = null)
|
||||
: base(InputActionSerializationHelpers.IsCompositeBinding(bindingProperty) ? "Composite" : "Binding",
|
||||
bindingProperty, onChange, expectedControlLayout)
|
||||
{
|
||||
m_BindingProperty = bindingProperty;
|
||||
m_GroupsProperty = bindingProperty.FindPropertyRelative("m_Groups");
|
||||
m_PathProperty = bindingProperty.FindPropertyRelative("m_Path");
|
||||
m_BindingGroups = m_GroupsProperty.stringValue
|
||||
.Split(new[] {InputBinding.Separator}, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
m_ExpectedControlLayout = expectedControlLayout;
|
||||
m_ControlSchemes = controlSchemes;
|
||||
|
||||
var flags = (InputBinding.Flags)bindingProperty.FindPropertyRelative("m_Flags").intValue;
|
||||
m_IsPartOfComposite = (flags & InputBinding.Flags.PartOfComposite) != 0;
|
||||
m_IsComposite = (flags & InputBinding.Flags.Composite) != 0;
|
||||
|
||||
// Set up control picker for m_Path. Not needed if the binding is a composite.
|
||||
if (!m_IsComposite)
|
||||
{
|
||||
m_ControlPickerState = controlPickerState ?? new InputControlPickerState();
|
||||
m_ControlPathEditor = new InputControlPathEditor(m_PathProperty, m_ControlPickerState, OnPathChanged);
|
||||
m_ControlPathEditor.SetExpectedControlLayout(m_ExpectedControlLayout);
|
||||
if (controlPathsToMatch != null)
|
||||
m_ControlPathEditor.SetControlPathsToMatch(controlPathsToMatch);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_ControlPathEditor?.Dispose();
|
||||
}
|
||||
|
||||
protected override void DrawGeneralProperties()
|
||||
{
|
||||
var currentPath = m_PathProperty.stringValue;
|
||||
InputSystem.OnDrawCustomWarningForBindingPath(currentPath);
|
||||
|
||||
if (m_IsComposite)
|
||||
{
|
||||
if (m_CompositeParameters == null)
|
||||
InitializeCompositeProperties();
|
||||
|
||||
// Composite type dropdown.
|
||||
var selectedCompositeType = EditorGUILayout.Popup(s_CompositeTypeLabel, m_SelectedCompositeType, m_CompositeTypeOptions);
|
||||
if (selectedCompositeType != m_SelectedCompositeType)
|
||||
{
|
||||
m_SelectedCompositeType = selectedCompositeType;
|
||||
OnCompositeTypeChanged();
|
||||
}
|
||||
|
||||
// Composite parameters.
|
||||
m_CompositeParameters.OnGUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Path.
|
||||
m_ControlPathEditor.OnGUI();
|
||||
|
||||
// Composite part.
|
||||
if (m_IsPartOfComposite)
|
||||
{
|
||||
if (m_CompositeParts == null)
|
||||
InitializeCompositePartProperties();
|
||||
|
||||
// If m_CompositeParts still null after InitializeCompositePartProperties something went wrong and we can't select
|
||||
if (m_CompositeParts != null)
|
||||
{
|
||||
var selectedPart = EditorGUILayout.Popup(s_CompositePartAssignmentLabel, m_SelectedCompositePart,
|
||||
m_CompositePartOptions);
|
||||
if (selectedPart != m_SelectedCompositePart)
|
||||
{
|
||||
m_SelectedCompositePart = selectedPart;
|
||||
OnCompositePartAssignmentChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show the specific controls which match the current path
|
||||
DrawMatchingControlPaths();
|
||||
|
||||
// Control scheme matrix.
|
||||
DrawUseInControlSchemes();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to keep track of which foldouts are expanded.
|
||||
/// </summary>
|
||||
private static bool showMatchingLayouts = false;
|
||||
private static Dictionary<string, bool> showMatchingChildLayouts = new Dictionary<string, bool>();
|
||||
|
||||
private static void DrawMatchingControlPaths(List<MatchingControlPath> matchingControlPaths)
|
||||
{
|
||||
foreach (var matchingControlPath in matchingControlPaths)
|
||||
{
|
||||
bool showLayout = false;
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
var text = $"{matchingControlPath.deviceName} > {matchingControlPath.controlName}";
|
||||
if (matchingControlPath.children.Count() > 0 && !matchingControlPath.isRoot)
|
||||
{
|
||||
showMatchingChildLayouts.TryGetValue(matchingControlPath.deviceName, out showLayout);
|
||||
showMatchingChildLayouts[matchingControlPath.deviceName] = EditorGUILayout.Foldout(showLayout, text);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.LabelField(text);
|
||||
}
|
||||
|
||||
showLayout |= matchingControlPath.isRoot;
|
||||
if (showLayout)
|
||||
DrawMatchingControlPaths(matchingControlPath.children);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all registered control paths implemented by concrete classes which match the current binding path and renders it.
|
||||
/// </summary>
|
||||
private void DrawMatchingControlPaths()
|
||||
{
|
||||
bool controlPathUsagePresent = false;
|
||||
List<MatchingControlPath> matchingControlPaths = MatchingControlPath.CollectMatchingControlPaths(m_ControlPathEditor.pathProperty.stringValue, showMatchingLayouts, ref controlPathUsagePresent);
|
||||
if (matchingControlPaths == null || matchingControlPaths.Count != 0)
|
||||
{
|
||||
EditorGUILayout.BeginVertical();
|
||||
showMatchingLayouts = EditorGUILayout.Foldout(showMatchingLayouts, "Derived Bindings");
|
||||
|
||||
if (showMatchingLayouts)
|
||||
{
|
||||
if (matchingControlPaths == null)
|
||||
{
|
||||
if (controlPathUsagePresent)
|
||||
EditorGUILayout.HelpBox("No registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
|
||||
else
|
||||
EditorGUILayout.HelpBox("No other registered controls match this current binding. Some controls are only registered at runtime.", MessageType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawMatchingControlPaths(matchingControlPaths);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw control scheme matrix that allows selecting which control schemes a particular
|
||||
/// binding appears in.
|
||||
/// </summary>
|
||||
private void DrawUseInControlSchemes()
|
||||
{
|
||||
if (m_ControlSchemes.Count <= 0)
|
||||
return;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField(s_UseInControlSchemesLAbel, EditorStyles.boldLabel);
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
foreach (var scheme in m_ControlSchemes)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var result = EditorGUILayout.Toggle(scheme.name, m_BindingGroups.Contains(scheme.bindingGroup));
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (result)
|
||||
{
|
||||
m_BindingGroups.Add(scheme.bindingGroup);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_BindingGroups.Remove(scheme.bindingGroup);
|
||||
}
|
||||
OnBindingGroupsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private void InitializeCompositeProperties()
|
||||
{
|
||||
// Find name of current composite.
|
||||
var path = m_PathProperty.stringValue;
|
||||
var compositeNameAndParameters = NameAndParameters.Parse(path);
|
||||
var compositeName = compositeNameAndParameters.name;
|
||||
var compositeType = InputBindingComposite.s_Composites.LookupTypeRegistration(compositeName);
|
||||
|
||||
// Collect all possible composite types.
|
||||
var selectedCompositeIndex = -1;
|
||||
var compositeTypeOptionsList = new List<GUIContent>();
|
||||
var compositeTypeList = new List<string>();
|
||||
var currentIndex = 0;
|
||||
foreach (var composite in InputBindingComposite.s_Composites.internedNames.Where(x =>
|
||||
!InputBindingComposite.s_Composites.aliases.Contains(x)).OrderBy(x => x))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(m_ExpectedControlLayout))
|
||||
{
|
||||
var valueType = InputBindingComposite.GetValueType(composite);
|
||||
if (valueType != null &&
|
||||
!InputControlLayout.s_Layouts.ValueTypeIsAssignableFrom(
|
||||
new InternedString(m_ExpectedControlLayout), valueType))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (InputBindingComposite.s_Composites.LookupTypeRegistration(composite) == compositeType)
|
||||
selectedCompositeIndex = currentIndex;
|
||||
var name = ObjectNames.NicifyVariableName(composite);
|
||||
compositeTypeOptionsList.Add(new GUIContent(name));
|
||||
compositeTypeList.Add(composite);
|
||||
++currentIndex;
|
||||
}
|
||||
|
||||
// If the current composite type isn't a registered type, add it to the list as
|
||||
// an extra option.
|
||||
if (selectedCompositeIndex == -1)
|
||||
{
|
||||
selectedCompositeIndex = compositeTypeList.Count;
|
||||
compositeTypeOptionsList.Add(new GUIContent(ObjectNames.NicifyVariableName(compositeName)));
|
||||
compositeTypeList.Add(compositeName);
|
||||
}
|
||||
|
||||
m_CompositeTypes = compositeTypeList.ToArray();
|
||||
m_CompositeTypeOptions = compositeTypeOptionsList.ToArray();
|
||||
m_SelectedCompositeType = selectedCompositeIndex;
|
||||
|
||||
// Initialize parameters.
|
||||
m_CompositeParameters = new ParameterListView
|
||||
{
|
||||
onChange = OnCompositeParametersModified
|
||||
};
|
||||
if (compositeType != null)
|
||||
m_CompositeParameters.Initialize(compositeType, compositeNameAndParameters.parameters);
|
||||
}
|
||||
|
||||
private void InitializeCompositePartProperties()
|
||||
{
|
||||
var currentCompositePart = m_BindingProperty.FindPropertyRelative("m_Name").stringValue;
|
||||
|
||||
////REVIEW: this makes a lot of assumptions about the serialized data based on the one property we've been given in the ctor
|
||||
// Determine the name of the current composite type that the part belongs to.
|
||||
var bindingArrayProperty = m_BindingProperty.GetArrayPropertyFromElement();
|
||||
var partBindingIndex = InputActionSerializationHelpers.GetIndex(bindingArrayProperty, m_BindingProperty);
|
||||
var compositeBindingIndex =
|
||||
InputActionSerializationHelpers.GetCompositeStartIndex(bindingArrayProperty, partBindingIndex);
|
||||
if (compositeBindingIndex == -1)
|
||||
return;
|
||||
var compositeBindingProperty = bindingArrayProperty.GetArrayElementAtIndex(compositeBindingIndex);
|
||||
var compositePath = compositeBindingProperty.FindPropertyRelative("m_Path").stringValue;
|
||||
var compositeNameAndParameters = NameAndParameters.Parse(compositePath);
|
||||
|
||||
// Initialize option list from all parts available for the composite.
|
||||
var optionList = new List<GUIContent>();
|
||||
var nameList = new List<string>();
|
||||
var currentIndex = 0;
|
||||
var selectedPartNameIndex = -1;
|
||||
foreach (var partName in InputBindingComposite.GetPartNames(compositeNameAndParameters.name))
|
||||
{
|
||||
if (partName.Equals(currentCompositePart, StringComparison.InvariantCultureIgnoreCase))
|
||||
selectedPartNameIndex = currentIndex;
|
||||
var niceName = ObjectNames.NicifyVariableName(partName);
|
||||
optionList.Add(new GUIContent(niceName));
|
||||
nameList.Add(partName);
|
||||
++currentIndex;
|
||||
}
|
||||
|
||||
// If currently selected part is not in list, add it as an option.
|
||||
if (selectedPartNameIndex == -1)
|
||||
{
|
||||
selectedPartNameIndex = nameList.Count;
|
||||
optionList.Add(new GUIContent(ObjectNames.NicifyVariableName(currentCompositePart)));
|
||||
nameList.Add(currentCompositePart);
|
||||
}
|
||||
|
||||
m_CompositeParts = nameList.ToArray();
|
||||
m_CompositePartOptions = optionList.ToArray();
|
||||
m_SelectedCompositePart = selectedPartNameIndex;
|
||||
}
|
||||
|
||||
private void OnCompositeParametersModified()
|
||||
{
|
||||
Debug.Assert(m_CompositeParameters != null);
|
||||
|
||||
var path = m_PathProperty.stringValue;
|
||||
var nameAndParameters = NameAndParameters.Parse(path);
|
||||
nameAndParameters.parameters = m_CompositeParameters.GetParameters();
|
||||
|
||||
m_PathProperty.stringValue = nameAndParameters.ToString();
|
||||
m_PathProperty.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
OnPathChanged();
|
||||
}
|
||||
|
||||
private void OnBindingGroupsChanged()
|
||||
{
|
||||
m_GroupsProperty.stringValue = string.Join(InputBinding.kSeparatorString, m_BindingGroups.ToArray());
|
||||
m_GroupsProperty.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
onChange?.Invoke(k_GroupsChanged);
|
||||
}
|
||||
|
||||
private void OnPathChanged()
|
||||
{
|
||||
m_BindingProperty.serializedObject.ApplyModifiedProperties();
|
||||
onChange?.Invoke(k_PathChanged);
|
||||
}
|
||||
|
||||
private void OnCompositeTypeChanged()
|
||||
{
|
||||
var nameAndParameters = new NameAndParameters
|
||||
{
|
||||
name = m_CompositeTypes[m_SelectedCompositeType],
|
||||
parameters = m_CompositeParameters.GetParameters()
|
||||
};
|
||||
|
||||
InputActionSerializationHelpers.ChangeCompositeBindingType(m_BindingProperty, nameAndParameters);
|
||||
m_PathProperty.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
onChange?.Invoke(k_CompositeTypeChanged);
|
||||
}
|
||||
|
||||
private void OnCompositePartAssignmentChanged()
|
||||
{
|
||||
m_BindingProperty.FindPropertyRelative("m_Name").stringValue = m_CompositeParts[m_SelectedCompositePart];
|
||||
m_BindingProperty.serializedObject.ApplyModifiedProperties();
|
||||
|
||||
onChange?.Invoke(k_CompositePartAssignmentChanged);
|
||||
}
|
||||
|
||||
private readonly bool m_IsComposite;
|
||||
private ParameterListView m_CompositeParameters;
|
||||
private int m_SelectedCompositeType;
|
||||
private GUIContent[] m_CompositeTypeOptions;
|
||||
private string[] m_CompositeTypes;
|
||||
|
||||
private int m_SelectedCompositePart;
|
||||
private GUIContent[] m_CompositePartOptions;
|
||||
private string[] m_CompositeParts;
|
||||
|
||||
private readonly SerializedProperty m_GroupsProperty;
|
||||
private readonly SerializedProperty m_BindingProperty;
|
||||
private readonly SerializedProperty m_PathProperty;
|
||||
|
||||
private readonly InputControlPickerState m_ControlPickerState;
|
||||
private readonly InputControlPathEditor m_ControlPathEditor;
|
||||
|
||||
private static readonly GUIContent s_CompositeTypeLabel = EditorGUIUtility.TrTextContent("Composite Type",
|
||||
"Type of composite. Allows changing the composite type retroactively. Doing so will modify the bindings that are part of the composite.");
|
||||
private static readonly GUIContent s_UseInControlSchemesLAbel = EditorGUIUtility.TrTextContent("Use in control scheme",
|
||||
"In which control schemes the binding is active. A binding can be used by arbitrary many control schemes. If a binding is not "
|
||||
+ "assigned to a specific control schemes, it is active in all of them.");
|
||||
private static readonly GUIContent s_CompositePartAssignmentLabel = EditorGUIUtility.TrTextContent(
|
||||
"Composite Part",
|
||||
"The named part of the composite that the binding is assigned to. Multiple bindings may be assigned the same part. All controls from "
|
||||
+ "all bindings that are assigned the same part will collectively feed values into that part of the composite.");
|
||||
|
||||
private ReadOnlyArray<InputControlScheme> m_ControlSchemes;
|
||||
private readonly List<string> m_BindingGroups;
|
||||
private readonly string m_ExpectedControlLayout;
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8afcd24f0b647f4f9f8c539d613ee6d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,229 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ReorderableList"/> to manage a set of name-and-parameter pairs and a <see cref="ParameterListView"/>
|
||||
/// to edit the parameters of the currently selected pair.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Produces output that can be consumed by <see cref="NameAndParameters.ParseMultiple"/>.
|
||||
/// </remarks>
|
||||
internal abstract class NameAndParameterListView
|
||||
{
|
||||
protected NameAndParameterListView(SerializedProperty property, Action applyAction, string expectedControlLayout, TypeTable listOptions, Func<Type, Type> getValueType, string itemName)
|
||||
{
|
||||
m_ItemName = itemName;
|
||||
m_GetValueType = getValueType;
|
||||
m_DeleteButton = EditorGUIUtility.TrIconContent("Toolbar Minus", $"Delete {itemName}");
|
||||
m_UpButton = EditorGUIUtility.TrIconContent(GUIHelpers.LoadIcon("ChevronUp"), $"Move {itemName} up");
|
||||
m_DownButton = EditorGUIUtility.TrIconContent(GUIHelpers.LoadIcon("ChevronDown"), $"Move {itemName} down");
|
||||
|
||||
m_Property = property;
|
||||
m_Apply = applyAction;
|
||||
m_ListOptions = listOptions;
|
||||
|
||||
m_ExpectedControlLayout = expectedControlLayout;
|
||||
if (!string.IsNullOrEmpty(m_ExpectedControlLayout))
|
||||
m_ExpectedValueType = EditorInputControlLayoutCache.GetValueType(m_ExpectedControlLayout);
|
||||
|
||||
m_ParametersForEachListItem = NameAndParameters.ParseMultiple(m_Property.stringValue).ToArray();
|
||||
m_EditableParametersForEachListItem = new ParameterListView[m_ParametersForEachListItem.Length];
|
||||
|
||||
for (var i = 0; i < m_ParametersForEachListItem.Length; i++)
|
||||
{
|
||||
m_EditableParametersForEachListItem[i] = new ParameterListView { onChange = OnParametersChanged };
|
||||
var typeName = m_ParametersForEachListItem[i].name;
|
||||
var rowType = m_ListOptions.LookupTypeRegistration(typeName);
|
||||
m_EditableParametersForEachListItem[i].Initialize(rowType, m_ParametersForEachListItem[i].parameters);
|
||||
|
||||
var name = ObjectNames.NicifyVariableName(typeName);
|
||||
|
||||
////REVIEW: finding this kind of stuff should probably have better support globally on the asset; e.g. some
|
||||
//// notification that pops up and allows fixing all occurrences in one click
|
||||
// Find out if we still support this option and indicate it in the list, if we don't.
|
||||
if (rowType == null)
|
||||
name += " (Obsolete)";
|
||||
else if (m_ExpectedValueType != null)
|
||||
{
|
||||
var valueType = getValueType(rowType);
|
||||
if (valueType != null && !m_ExpectedValueType.IsAssignableFrom(valueType))
|
||||
name += " (Incompatible Value Type)";
|
||||
}
|
||||
m_EditableParametersForEachListItem[i].name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAddDropdown(Rect r)
|
||||
{
|
||||
// Add only original names to the menu and not aliases.
|
||||
var menu = new GenericMenu();
|
||||
foreach (var name in m_ListOptions.internedNames.Where(x => !m_ListOptions.ShouldHideInUI(x)).OrderBy(x => x.ToString()))
|
||||
{
|
||||
// Skip if not compatible with value type.
|
||||
if (m_ExpectedValueType != null)
|
||||
{
|
||||
var type = m_ListOptions.LookupTypeRegistration(name);
|
||||
var valueType = m_GetValueType(type);
|
||||
if (valueType != null && !m_ExpectedValueType.IsAssignableFrom(valueType))
|
||||
continue;
|
||||
}
|
||||
|
||||
var niceName = ObjectNames.NicifyVariableName(name);
|
||||
menu.AddItem(new GUIContent(niceName), false, OnAddElement, name.ToString());
|
||||
}
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
private void OnAddElement(object data)
|
||||
{
|
||||
var name = (string)data;
|
||||
|
||||
ArrayHelpers.Append(ref m_ParametersForEachListItem,
|
||||
new NameAndParameters {name = name});
|
||||
ArrayHelpers.Append(ref m_EditableParametersForEachListItem,
|
||||
new ParameterListView { onChange = OnParametersChanged });
|
||||
|
||||
var index = m_EditableParametersForEachListItem.Length - 1;
|
||||
var typeName = m_ParametersForEachListItem[index].name;
|
||||
var rowType = m_ListOptions.LookupTypeRegistration(typeName);
|
||||
m_EditableParametersForEachListItem[index].Initialize(rowType, m_ParametersForEachListItem[index].parameters);
|
||||
m_EditableParametersForEachListItem[index].name = ObjectNames.NicifyVariableName(name);
|
||||
|
||||
m_Apply();
|
||||
}
|
||||
|
||||
private void OnParametersChanged()
|
||||
{
|
||||
for (var i = 0; i < m_ParametersForEachListItem.Length; i++)
|
||||
{
|
||||
m_ParametersForEachListItem[i] = new NameAndParameters
|
||||
{
|
||||
name = m_ParametersForEachListItem[i].name,
|
||||
parameters = m_EditableParametersForEachListItem[i].GetParameters(),
|
||||
};
|
||||
}
|
||||
|
||||
m_Apply();
|
||||
}
|
||||
|
||||
private static class Styles
|
||||
{
|
||||
public static readonly GUIStyle s_FoldoutStyle = new GUIStyle("foldout").WithFontStyle(FontStyle.Bold);
|
||||
public static readonly GUIStyle s_UpDownButtonStyle = new GUIStyle("label").WithFixedWidth(12).WithFixedHeight(12).WithPadding(new RectOffset());
|
||||
}
|
||||
|
||||
private void SwapEntry(int oldIndex, int newIndex)
|
||||
{
|
||||
MemoryHelpers.Swap(ref m_ParametersForEachListItem[oldIndex], ref m_ParametersForEachListItem[newIndex]);
|
||||
MemoryHelpers.Swap(ref m_EditableParametersForEachListItem[oldIndex], ref m_EditableParametersForEachListItem[newIndex]);
|
||||
m_Apply();
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
if (m_EditableParametersForEachListItem == null || m_EditableParametersForEachListItem.Length == 0)
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.LabelField($"No {m_ItemName}s have been added.");
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
else
|
||||
for (var i = 0; i < m_EditableParametersForEachListItem.Length; i++)
|
||||
{
|
||||
var editableParams = m_EditableParametersForEachListItem[i];
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (editableParams.hasUIToShow)
|
||||
editableParams.visible = EditorGUILayout.Foldout(editableParams.visible, editableParams.name, true, Styles.s_FoldoutStyle);
|
||||
else
|
||||
{
|
||||
GUILayout.Space(16);
|
||||
EditorGUILayout.LabelField(editableParams.name, EditorStyles.boldLabel);
|
||||
}
|
||||
GUILayout.FlexibleSpace();
|
||||
using (new EditorGUI.DisabledScope(i == 0))
|
||||
{
|
||||
if (GUILayout.Button(m_UpButton, Styles.s_UpDownButtonStyle))
|
||||
SwapEntry(i, i - 1);
|
||||
}
|
||||
using (new EditorGUI.DisabledScope(i == m_EditableParametersForEachListItem.Length - 1))
|
||||
{
|
||||
if (GUILayout.Button(m_DownButton, Styles.s_UpDownButtonStyle))
|
||||
SwapEntry(i, i + 1);
|
||||
}
|
||||
if (GUILayout.Button(m_DeleteButton, EditorStyles.label))
|
||||
{
|
||||
// Unfocus controls, because otherwise, the editor can get confused and have text from a text field
|
||||
// on the deleted item leak to a different field.
|
||||
GUI.FocusControl(null);
|
||||
ArrayHelpers.EraseAt(ref m_ParametersForEachListItem, i);
|
||||
ArrayHelpers.EraseAt(ref m_EditableParametersForEachListItem, i);
|
||||
m_Apply();
|
||||
GUIUtility.ExitGUI();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
if (editableParams.visible)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
editableParams.OnGUI();
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
GUIHelpers.DrawLineSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
public string ToSerializableString()
|
||||
{
|
||||
if (m_ParametersForEachListItem == null)
|
||||
return string.Empty;
|
||||
|
||||
return string.Join(NamedValue.Separator,
|
||||
m_ParametersForEachListItem.Select(x => x.ToString()).ToArray());
|
||||
}
|
||||
|
||||
private Func<Type, Type> m_GetValueType;
|
||||
private SerializedProperty m_Property;
|
||||
private readonly TypeTable m_ListOptions;
|
||||
private readonly string m_ExpectedControlLayout;
|
||||
private readonly Type m_ExpectedValueType;
|
||||
private readonly GUIContent m_DeleteButton;
|
||||
private readonly GUIContent m_UpButton;
|
||||
private readonly GUIContent m_DownButton;
|
||||
private NameAndParameters[] m_ParametersForEachListItem;
|
||||
private ParameterListView[] m_EditableParametersForEachListItem;
|
||||
private readonly Action m_Apply;
|
||||
private string m_ItemName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of processors and their parameters.
|
||||
/// </summary>
|
||||
internal class ProcessorsListView : NameAndParameterListView
|
||||
{
|
||||
public ProcessorsListView(SerializedProperty property, Action applyAction, string expectedControlLayout)
|
||||
: base(property, applyAction, expectedControlLayout, InputProcessor.s_Processors, InputProcessor.GetValueTypeFromType, "Processor")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list view of interactions and their parameters.
|
||||
/// </summary>
|
||||
internal class InteractionsListView : NameAndParameterListView
|
||||
{
|
||||
public InteractionsListView(SerializedProperty property, Action applyAction, string expectedControlLayout)
|
||||
: base(property, applyAction, expectedControlLayout, InputInteraction.s_Interactions, InputInteraction.GetValueType, "Interaction")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: da649da79e1b9455791f5ddd31355edc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad61ded360bb0784b9301188fc603d6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,114 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33f32142c2c2d4662be5f5274db27158
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,114 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ffeba93637e654de6a5906b48520dc94
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95391b99439094c00a105cd26ba61b1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,127 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce5f11cd993284c4f9c54f854bdc8c9c
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 32
|
||||
resizeAlgorithm: 1
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 1
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 9cfdf4253b3de4d389dff51ab21111ad
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,114 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf632ed80bf1f46c980de6b1b8b903ef
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,127 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ab69704aa4984b49bc426da72fabbed
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 32
|
||||
resizeAlgorithm: 1
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 1
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,127 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa4c2360f910d46f497f75b4b6677358
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 32
|
||||
resizeAlgorithm: 1
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 1
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,114 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7b144323984d4298924c65ed071dea1
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be2b3c041afc2477ea1f172b6d14c3dc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,127 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4b66e31395c54087a2fbc13ae7e0f3c
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 32
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 1
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,114 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 943deaaee9c9347e0a455aa208526c42
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,127 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0996bc290ff74bf4834f4a8b7dc8c3e
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 32
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 1
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,127 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b02e3f0c0798a4f1789a89a7697660be
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 0
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 32
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 1
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,114 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47cf506b4219343519083fe1c0b8fc9c
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 0
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 2
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 1
|
||||
swizzle: 50462976
|
||||
cookieLightType: 1
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 0
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,401 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
////TODO: show description of interaction or processor when selected
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor.Lists
|
||||
{
|
||||
/// <summary>
|
||||
/// Inspector-like functionality for editing parameter lists as used in <see cref="InputControlLayout"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This can be used for parameters on interactions, processors, and composites.
|
||||
///
|
||||
/// Call <see cref="Initialize"/> to set up (can be done repeatedly on the same instance). Call
|
||||
/// <see cref="OnGUI"/> to render.
|
||||
///
|
||||
/// Custom parameter GUIs can be defined by deriving from <see cref="InputParameterEditor{TObject}"/>.
|
||||
/// This class will automatically incorporate custom GUIs and fall back to default GUIs where no custom
|
||||
/// ones are defined.
|
||||
/// </remarks>
|
||||
internal class ParameterListView
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked whenever a parameter is changed.
|
||||
/// </summary>
|
||||
public Action onChange { get; set; }
|
||||
|
||||
public bool hasUIToShow => (m_Parameters != null && m_Parameters.Length > 0) || m_ParameterEditor != null;
|
||||
public bool visible { get; set; }
|
||||
public string name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the current parameter values according to the editor state.
|
||||
/// </summary>
|
||||
/// <returns>An array of parameter values.</returns>
|
||||
public NamedValue[] GetParameters()
|
||||
{
|
||||
if (m_Parameters == null)
|
||||
return null;
|
||||
|
||||
// See if we have parameters that aren't at their default value.
|
||||
var countOfParametersNotAtDefaultValue = 0;
|
||||
for (var i = 0; i < m_Parameters.Length; ++i)
|
||||
{
|
||||
if (!m_Parameters[i].isAtDefault)
|
||||
++countOfParametersNotAtDefaultValue;
|
||||
}
|
||||
|
||||
// If not, we return null.
|
||||
if (countOfParametersNotAtDefaultValue == 0)
|
||||
return null;
|
||||
|
||||
// Collect non-default parameter values.
|
||||
var result = new NamedValue[countOfParametersNotAtDefaultValue];
|
||||
var index = 0;
|
||||
for (var i = 0; i < m_Parameters.Length; ++i)
|
||||
{
|
||||
var parameter = m_Parameters[i];
|
||||
if (parameter.isAtDefault)
|
||||
continue;
|
||||
|
||||
result[index++] = parameter.value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the parameter list view based on the given registered type that has parameters to edit. This can be
|
||||
/// things such as interactions, processors, or composites.
|
||||
/// </summary>
|
||||
/// <param name="registeredType">Type of object that the parameters will be passed to at runtime.
|
||||
/// We need this to be able to determine the possible set of parameters and their possible values. This
|
||||
/// can be a class implementing <see cref="IInputInteraction"/>, for example.</param>
|
||||
/// <param name="existingParameters">List of existing parameters. Can be empty.</param>
|
||||
public void Initialize(Type registeredType, ReadOnlyArray<NamedValue> existingParameters)
|
||||
{
|
||||
if (registeredType == null)
|
||||
{
|
||||
// No registered type. This usually happens when data references a registration that has
|
||||
// been removed in the meantime (e.g. an interaction that is no longer supported). We want
|
||||
// to accept this case and simply pretend that the given type has no parameters.
|
||||
|
||||
Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
visible = true;
|
||||
|
||||
// Try to instantiate object so that we can determine defaults.
|
||||
object instance = null;
|
||||
try
|
||||
{
|
||||
instance = Activator.CreateInstance(registeredType);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Swallow. If we can't create an instance, we simply assume no defaults.
|
||||
}
|
||||
|
||||
var parameters = new List<EditableParameterValue>();
|
||||
|
||||
////REVIEW: support properties here?
|
||||
// Go through public instance fields and add every parameter found on the registered
|
||||
// type.
|
||||
var fields = registeredType.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
// Skip all fields that have an [InputControl] attribute. This is relevant
|
||||
// only for composites, but we just always do it here.
|
||||
if (field.GetCustomAttribute<InputControlAttribute>(false) != null)
|
||||
continue;
|
||||
|
||||
// Determine parameter name from field.
|
||||
var parameter = new EditableParameterValue {field = field};
|
||||
var name = field.Name;
|
||||
parameter.value.name = name;
|
||||
|
||||
// Determine parameter type from field.
|
||||
var fieldType = field.FieldType;
|
||||
if (fieldType.IsEnum)
|
||||
{
|
||||
// For enums, we want the underlying integer type.
|
||||
var underlyingType = fieldType.GetEnumUnderlyingType();
|
||||
var underlyingTypeCode = Type.GetTypeCode(underlyingType);
|
||||
|
||||
parameter.value = parameter.value.ConvertTo(underlyingTypeCode);
|
||||
|
||||
// Read enum names and values.
|
||||
parameter.enumNames = Enum.GetNames(fieldType);
|
||||
////REVIEW: this probably falls apart if multiple members have the same value
|
||||
var list = new List<int>();
|
||||
foreach (var value in Enum.GetValues(fieldType))
|
||||
list.Add((int)value);
|
||||
parameter.enumValues = list.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
var typeCode = Type.GetTypeCode(fieldType);
|
||||
parameter.value = parameter.value.ConvertTo(typeCode);
|
||||
}
|
||||
|
||||
// Determine default value.
|
||||
if (instance != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = field.GetValue(instance);
|
||||
parameter.defaultValue = new NamedValue
|
||||
{
|
||||
name = name,
|
||||
value = PrimitiveValue.FromObject(value)
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the getter throws, ignore. All we lose is the actual default value from
|
||||
// the field.
|
||||
}
|
||||
}
|
||||
|
||||
// If the parameter already exists in the given list, maintain its value.
|
||||
var existingParameterIndex = existingParameters.IndexOf(x => x.name == field.Name);
|
||||
if (existingParameterIndex >= 0)
|
||||
{
|
||||
// Make sure we're preserving the right type.
|
||||
parameter.value = existingParameters[existingParameterIndex].ConvertTo(parameter.value.type);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not assigned. Set to default.
|
||||
if (parameter.defaultValue != null)
|
||||
parameter.value = parameter.defaultValue.Value;
|
||||
}
|
||||
|
||||
// Add.
|
||||
parameters.Add(parameter);
|
||||
}
|
||||
|
||||
m_Parameters = parameters.ToArray();
|
||||
|
||||
// See if we have a dedicated parameter editor.
|
||||
var parameterEditorType = InputParameterEditor.LookupEditorForType(registeredType);
|
||||
if (parameterEditorType != null)
|
||||
{
|
||||
// Create an editor instance and hand it the instance we created. Unlike our default
|
||||
// editing logic, on this path we will be operating on an object instance that contains
|
||||
// the parameter values. So on this path, we actually need to update the object to reflect
|
||||
// the current parameter values.
|
||||
|
||||
NamedValue.ApplyAllToObject(instance, m_Parameters.Select(x => x.value));
|
||||
|
||||
m_ParameterEditor = (InputParameterEditor)Activator.CreateInstance(parameterEditorType);
|
||||
|
||||
// We have to jump through some hoops here to create instances of any CustomOrDefaultSetting fields on the
|
||||
// parameter editor. This is because those types changed from structs to classes when UIToolkit was
|
||||
// introduced, and we don't want to force users to have to create those instances manually on any of their
|
||||
// own editors.
|
||||
var genericArgumentType = TypeHelpers.GetGenericTypeArgumentFromHierarchy(parameterEditorType,
|
||||
typeof(InputParameterEditor<>), 0);
|
||||
if (genericArgumentType != null)
|
||||
{
|
||||
var fieldInfos = parameterEditorType
|
||||
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var customOrDefaultGenericType = typeof(InputParameterEditor<>.CustomOrDefaultSetting);
|
||||
var customOrDefaultType = customOrDefaultGenericType.MakeGenericType(genericArgumentType);
|
||||
foreach (var customOrDefaultEditorField in fieldInfos.Where(f => f.FieldType == customOrDefaultType))
|
||||
{
|
||||
customOrDefaultEditorField.SetValue(m_ParameterEditor, Activator.CreateInstance(customOrDefaultEditorField.FieldType));
|
||||
}
|
||||
}
|
||||
m_ParameterEditor.SetTarget(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ParameterEditor = null;
|
||||
|
||||
// Create parameter labels.
|
||||
m_ParameterLabels = new(string text, string tooltip)[m_Parameters.Length];
|
||||
for (var i = 0; i < m_Parameters.Length; ++i)
|
||||
{
|
||||
// Look up tooltip from field.
|
||||
var tooltip = string.Empty;
|
||||
var field = m_Parameters[i].field;
|
||||
var tooltipAttribute = field.GetCustomAttribute<TooltipAttribute>();
|
||||
if (tooltipAttribute != null)
|
||||
tooltip = tooltipAttribute.tooltip;
|
||||
|
||||
// Create label.
|
||||
var niceName = ObjectNames.NicifyVariableName(m_Parameters[i].value.name);
|
||||
m_ParameterLabels[i] = (niceName, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
m_Parameters = null;
|
||||
m_ParameterEditor = null;
|
||||
}
|
||||
|
||||
public void OnDrawVisualElements(VisualElement root)
|
||||
{
|
||||
if (m_ParameterEditor != null)
|
||||
{
|
||||
m_ParameterEditor.OnDrawVisualElements(root, OnValuesChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_Parameters == null)
|
||||
return;
|
||||
|
||||
void OnValueChanged(ref EditableParameterValue parameter, object result, int i)
|
||||
{
|
||||
parameter.value.value = PrimitiveValue.FromObject(result).ConvertTo(parameter.value.type);
|
||||
m_Parameters[i] = parameter;
|
||||
}
|
||||
|
||||
void OnEditEnd()
|
||||
{
|
||||
onChange?.Invoke();
|
||||
}
|
||||
|
||||
for (var i = 0; i < m_Parameters.Length; i++)
|
||||
{
|
||||
var parameter = m_Parameters[i];
|
||||
var label = m_ParameterLabels[i];
|
||||
var closedIndex = i;
|
||||
|
||||
if (parameter.isEnum)
|
||||
{
|
||||
var names = parameter.enumNames.ToList();
|
||||
var rawValue = parameter.value.value.ToInt32();
|
||||
var selectedIndex = parameter.enumValues.IndexOf(rawValue);
|
||||
if (selectedIndex < 0 || selectedIndex >= names.Count)
|
||||
selectedIndex = 0;
|
||||
|
||||
var field = new DropdownField(label.text, names, selectedIndex)
|
||||
{
|
||||
tooltip = label.tooltip
|
||||
};
|
||||
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var newBackingValue = parameter.enumValues[field.index];
|
||||
parameter.value.value = PrimitiveValue.FromObject(newBackingValue).ConvertTo(parameter.value.type);
|
||||
m_Parameters[closedIndex] = parameter;
|
||||
onChange?.Invoke();
|
||||
});
|
||||
|
||||
field.RegisterCallback<BlurEvent>(_ => onChange?.Invoke());
|
||||
root.Add(field);
|
||||
}
|
||||
else if (parameter.value.type == TypeCode.Int64 || parameter.value.type == TypeCode.UInt64)
|
||||
{
|
||||
var longValue = parameter.value.value.ToInt64();
|
||||
var field = new LongField(label.text) { value = longValue, tooltip = label.tooltip };
|
||||
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
|
||||
field.RegisterCallback<BlurEvent>(_ => OnEditEnd());
|
||||
root.Add(field);
|
||||
}
|
||||
else if (parameter.value.type.IsInt())
|
||||
{
|
||||
var intValue = parameter.value.value.ToInt32();
|
||||
var field = new IntegerField(label.text) { value = intValue, tooltip = label.tooltip };
|
||||
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
|
||||
field.RegisterCallback<BlurEvent>(_ => OnEditEnd());
|
||||
root.Add(field);
|
||||
}
|
||||
else if (parameter.value.type == TypeCode.Single)
|
||||
{
|
||||
var floatValue = parameter.value.value.ToSingle();
|
||||
var field = new FloatField(label.text) { value = floatValue, tooltip = label.tooltip };
|
||||
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
|
||||
field.RegisterCallback<BlurEvent>(_ => OnEditEnd());
|
||||
root.Add(field);
|
||||
}
|
||||
else if (parameter.value.type == TypeCode.Double)
|
||||
{
|
||||
var floatValue = parameter.value.value.ToDouble();
|
||||
var field = new DoubleField(label.text) { value = floatValue, tooltip = label.tooltip };
|
||||
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
|
||||
field.RegisterCallback<BlurEvent>(_ => OnEditEnd());
|
||||
root.Add(field);
|
||||
}
|
||||
else if (parameter.value.type == TypeCode.Boolean)
|
||||
{
|
||||
var boolValue = parameter.value.value.ToBoolean();
|
||||
var field = new Toggle(label.text) { value = boolValue, tooltip = label.tooltip };
|
||||
field.RegisterValueChangedCallback(evt => OnValueChanged(ref parameter, evt.newValue, closedIndex));
|
||||
field.RegisterValueChangedCallback(_ => OnEditEnd());
|
||||
root.Add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValuesChanged()
|
||||
{
|
||||
ReadParameterValuesFrom(m_ParameterEditor.target);
|
||||
onChange?.Invoke();
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
}
|
||||
|
||||
////REVIEW: check whether parameters have *actually* changed?
|
||||
/// <summary>
|
||||
/// Refresh <see cref="m_Parameters"/> from the current parameter values in <paramref name="target"/>.
|
||||
/// </summary>
|
||||
/// <param name="target">An instance of the current type we are editing parameters on.</param>
|
||||
private void ReadParameterValuesFrom(object target)
|
||||
{
|
||||
if (m_Parameters == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < m_Parameters.Length; ++i)
|
||||
{
|
||||
var parameter = m_Parameters[i];
|
||||
|
||||
object value = null;
|
||||
try
|
||||
{
|
||||
value = parameter.field.GetValue(target);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions from getters.
|
||||
}
|
||||
|
||||
m_Parameters[i].value.value = PrimitiveValue.FromObject(value).ConvertTo(parameter.value.type);
|
||||
}
|
||||
}
|
||||
|
||||
private InputParameterEditor m_ParameterEditor;
|
||||
private EditableParameterValue[] m_Parameters;
|
||||
|
||||
private (string text, string tooltip)[] m_ParameterLabels;
|
||||
|
||||
private struct EditableParameterValue
|
||||
{
|
||||
public NamedValue value;
|
||||
public NamedValue? defaultValue;
|
||||
public int[] enumValues;
|
||||
public string[] enumNames;
|
||||
public FieldInfo field;
|
||||
|
||||
public bool isEnum => enumValues != null;
|
||||
public bool isAtDefault => defaultValue != null && value == defaultValue.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb0e54f1ead384cd191254077544faf0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,146 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Editor.Lists;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////TODO: show parameters for selected interaction or processor inline in list rather than separately underneath list
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for views that show the properties of actions or bindings.
|
||||
/// </summary>
|
||||
internal abstract class PropertiesViewBase
|
||||
{
|
||||
protected PropertiesViewBase(string label, SerializedProperty bindingOrAction, Action<FourCC> onChange, string expectedControlLayout = null)
|
||||
{
|
||||
if (bindingOrAction == null)
|
||||
throw new ArgumentNullException(nameof(bindingOrAction));
|
||||
|
||||
m_InteractionsProperty = bindingOrAction.FindPropertyRelative("m_Interactions");
|
||||
m_ProcessorsProperty = bindingOrAction.FindPropertyRelative("m_Processors");
|
||||
|
||||
m_InteractionsList = new InteractionsListView(m_InteractionsProperty, OnInteractionsModified, expectedControlLayout);
|
||||
UpdateProcessors(expectedControlLayout);
|
||||
|
||||
m_OnChange = onChange;
|
||||
m_GeneralFoldoutLabel = EditorGUIUtility.TrTextContent(label);
|
||||
}
|
||||
|
||||
protected void UpdateProcessors(string expectedControlLayout)
|
||||
{
|
||||
m_ProcessorsList = new ProcessorsListView(m_ProcessorsProperty, OnProcessorsModified, expectedControlLayout);
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
EditorGUILayout.BeginVertical();
|
||||
DrawGeneralGroup();
|
||||
if (!m_IsPartOfComposite)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
DrawInteractionsGroup();
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
DrawProcessorsGroup();
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
protected abstract void DrawGeneralProperties();
|
||||
|
||||
private void DrawGeneralGroup()
|
||||
{
|
||||
m_GeneralFoldout = DrawFoldout(m_GeneralFoldoutLabel, m_GeneralFoldout);
|
||||
if (m_GeneralFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
DrawGeneralProperties();
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawProcessorsGroup()
|
||||
{
|
||||
m_ProcessorsFoldout = DrawFoldout(s_ProcessorsFoldoutLabel, m_ProcessorsFoldout, s_ProcessorsAddButton, m_ProcessorsList.OnAddDropdown);
|
||||
if (m_ProcessorsFoldout)
|
||||
m_ProcessorsList.OnGUI();
|
||||
}
|
||||
|
||||
private void DrawInteractionsGroup()
|
||||
{
|
||||
m_InteractionsFoldout = DrawFoldout(s_InteractionsFoldoutLabel, m_InteractionsFoldout, s_InteractionsAddButton, m_InteractionsList.OnAddDropdown);
|
||||
if (m_InteractionsFoldout)
|
||||
m_InteractionsList.OnGUI();
|
||||
}
|
||||
|
||||
private static bool DrawFoldout(GUIContent content, bool folded, GUIContent addButton = null, Action<Rect> addDropDown = null)
|
||||
{
|
||||
const int k_PopupSize = 20;
|
||||
var bgRect = GUILayoutUtility.GetRect(content, Styles.s_FoldoutBackgroundStyle);
|
||||
EditorGUI.LabelField(bgRect, GUIContent.none, Styles.s_FoldoutBackgroundStyle);
|
||||
var foldoutRect = bgRect;
|
||||
foldoutRect.xMax -= k_PopupSize;
|
||||
var retval = EditorGUI.Foldout(foldoutRect, folded, content, true, Styles.s_FoldoutStyle);
|
||||
if (addButton != null)
|
||||
{
|
||||
var popupRect = bgRect;
|
||||
popupRect.xMin = popupRect.xMax - k_PopupSize;
|
||||
if (GUI.Button(popupRect, addButton, EditorStyles.label))
|
||||
addDropDown(popupRect);
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
|
||||
private void OnProcessorsModified()
|
||||
{
|
||||
m_ProcessorsProperty.stringValue = m_ProcessorsList.ToSerializableString();
|
||||
m_ProcessorsProperty.serializedObject.ApplyModifiedProperties();
|
||||
m_OnChange(k_ProcessorsChanged);
|
||||
}
|
||||
|
||||
private void OnInteractionsModified()
|
||||
{
|
||||
m_InteractionsProperty.stringValue = m_InteractionsList.ToSerializableString();
|
||||
m_InteractionsProperty.serializedObject.ApplyModifiedProperties();
|
||||
m_OnChange(k_InteractionsChanged);
|
||||
}
|
||||
|
||||
public Action<FourCC> onChange => m_OnChange;
|
||||
|
||||
private bool m_GeneralFoldout = true;
|
||||
private bool m_InteractionsFoldout = true;
|
||||
private bool m_ProcessorsFoldout = true;
|
||||
protected bool m_IsPartOfComposite;
|
||||
|
||||
private readonly Action<FourCC> m_OnChange;
|
||||
|
||||
private readonly InteractionsListView m_InteractionsList;
|
||||
private ProcessorsListView m_ProcessorsList;
|
||||
|
||||
private readonly SerializedProperty m_InteractionsProperty;
|
||||
private readonly SerializedProperty m_ProcessorsProperty;
|
||||
|
||||
private readonly GUIContent m_GeneralFoldoutLabel;
|
||||
|
||||
////TODO: tooltips
|
||||
private static readonly GUIContent s_ProcessorsFoldoutLabel = EditorGUIUtility.TrTextContent("Processors");
|
||||
public static readonly GUIContent s_ProcessorsAddButton = EditorGUIUtility.TrIconContent("Toolbar Plus More", "Add Processor");
|
||||
private static readonly GUIContent s_InteractionsFoldoutLabel = EditorGUIUtility.TrTextContent("Interactions");
|
||||
public static readonly GUIContent s_InteractionsAddButton = EditorGUIUtility.TrIconContent("Toolbar Plus More", "Add Interaction");
|
||||
|
||||
public static FourCC k_InteractionsChanged => new FourCC("IACT");
|
||||
public static FourCC k_ProcessorsChanged => new FourCC("PROC");
|
||||
|
||||
private static class Styles
|
||||
{
|
||||
public static readonly GUIStyle s_FoldoutBackgroundStyle = new GUIStyle("Label")
|
||||
.WithNormalBackground(AssetDatabase.LoadAssetAtPath<Texture2D>(InputActionTreeView.ResourcesPath + "foldoutBackground.png"))
|
||||
.WithBorder(new RectOffset(3, 3, 3, 3))
|
||||
.WithMargin(new RectOffset(1, 1, 3, 3));
|
||||
public static readonly GUIStyle s_FoldoutStyle = new GUIStyle("foldout");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 060ae4265f580481f9bc2f2e7ab367ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d7486ba41233414a9ed80716a544026
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// For clarity, the tables below indicate the callback sequences of the asset modification processor and
|
||||
// asset post-processor for various user operations done on assets.
|
||||
//
|
||||
// User operation: Callback sequence:
|
||||
// ----------------------------------------------------------------------------------------
|
||||
// Save Imported(s)
|
||||
// Delete OnWillDelete(s), Deleted(s)
|
||||
// Copy Imported(s)
|
||||
// Rename OnWillMove(s,d), Imported(d), Moved(s,d)
|
||||
// Move (drag) / Cut+Paste OnWillMove(s,d), Moved(s,d)
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
//
|
||||
// User operation: Callback/call sequence:
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
// Save Imported(s)
|
||||
// Delete OnWillDelete(s), Deleted(s)
|
||||
// Copy Imported(s), Fix(s), Imported(s)
|
||||
// Rename OnWillMove(s,d), Imported(d), Fix(d), Moved(s,d), Imported(d)
|
||||
// Move(drag) / Cut+Paste OnWillMove(s,d), Moved(s,d)
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
// Note that as stated in table above, JSON name changes (called "Fix" above) will only be executed when either
|
||||
// Copying, Renaming within the editor. For all other operations the name and file name would not differ.
|
||||
//
|
||||
// External user operation: Callback/call sequence:
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
// Save Imported(s)
|
||||
// Delete Deleted(s)
|
||||
// Copy Imported(s)
|
||||
// Rename Imported(d), Deleted(s)
|
||||
// Move(drag) / Cut+Paste Imported(d), Deleted(s)
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Callback interface for monitoring changes to assets.
|
||||
/// </summary>
|
||||
internal interface IAssetObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback triggered when the associated asset is imported.
|
||||
/// </summary>
|
||||
void OnAssetImported();
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when the associated asset is moved.
|
||||
/// </summary>
|
||||
void OnAssetMoved();
|
||||
|
||||
/// <summary>
|
||||
/// Callback triggered when the associated asset is deleted.
|
||||
/// </summary>
|
||||
void OnAssetDeleted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface representing an editor capable of editing <c>InputActionAsset</c> instances associated
|
||||
/// with an asset file in the Asset Database (ADB).
|
||||
/// </summary>
|
||||
internal interface IInputActionAssetEditor : IAssetObserver
|
||||
{
|
||||
/// <summary>
|
||||
/// A read-only string representation of the asset GUID associated with the asset being edited.
|
||||
/// </summary>
|
||||
string assetGUID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the editor has unsaved changes compared to the associated imported source asset.
|
||||
/// </summary>
|
||||
bool isDirty { get; }
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2603947746e342918fcad117e9ce6db6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,262 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// We want an empty editor in the inspector. Editing happens in a dedicated window.
|
||||
[CustomEditor(typeof(InputActionAsset))]
|
||||
internal class InputActionAssetEditor : UnityEditor.Editor
|
||||
{
|
||||
protected override void OnHeaderGUI()
|
||||
{
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
}
|
||||
|
||||
#region Support abstract editor registration
|
||||
|
||||
private static readonly List<Type> s_EditorTypes = new List<Type>();
|
||||
|
||||
// Registers an asset editor type for receiving asset modification callbacks.
|
||||
public static void RegisterType<T>() where T : IInputActionAssetEditor
|
||||
{
|
||||
if (!s_EditorTypes.Contains(typeof(T)))
|
||||
s_EditorTypes.Add(typeof(T));
|
||||
}
|
||||
|
||||
// Unregisters an asset editor type from receiving asset modification callbacks.
|
||||
public static void UnregisterType<T>() where T : IInputActionAssetEditor
|
||||
{
|
||||
s_EditorTypes.Remove(typeof(T));
|
||||
}
|
||||
|
||||
public static T FindOpenEditor<T>(string path) where T : EditorWindow
|
||||
{
|
||||
var openEditors = FindAllEditorsForPath(path);
|
||||
foreach (var openEditor in openEditors)
|
||||
{
|
||||
if (openEditor.GetType() == typeof(T))
|
||||
return (T)openEditor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Finds all asset editors associated with the asset given by path.
|
||||
public static IInputActionAssetEditor[] FindAllEditorsForPath(string path)
|
||||
{
|
||||
var guid = AssetDatabase.AssetPathToGUID(path);
|
||||
return guid != null ? FindAllEditors((editor) => editor.assetGUID == guid) :
|
||||
Array.Empty<IInputActionAssetEditor>();
|
||||
}
|
||||
|
||||
// Finds all asset editors fulfilling the given predicate.
|
||||
public static IInputActionAssetEditor[] FindAllEditors(Predicate<IInputActionAssetEditor> predicate = null)
|
||||
{
|
||||
List<IInputActionAssetEditor> editors = null;
|
||||
foreach (var type in s_EditorTypes)
|
||||
editors = FindAllEditors(type, predicate, editors);
|
||||
return editors != null ? editors.ToArray() : Array.Empty<IInputActionAssetEditor>();
|
||||
}
|
||||
|
||||
private static List<IInputActionAssetEditor> FindAllEditors(Type type,
|
||||
Predicate<IInputActionAssetEditor> predicate = null,
|
||||
List<IInputActionAssetEditor> result = null)
|
||||
{
|
||||
if (result == null)
|
||||
result = new List<IInputActionAssetEditor>();
|
||||
var editors = Resources.FindObjectsOfTypeAll(type);
|
||||
foreach (var editor in editors)
|
||||
{
|
||||
if (editor is IInputActionAssetEditor actionsAssetEditor && (predicate == null || predicate(actionsAssetEditor)))
|
||||
result.Add(actionsAssetEditor);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Asset modification processor to intercept Unity editor move or delete operations
|
||||
// Asset modification processor designed to handle the following scenarios:
|
||||
// - When an asset is about to get deleted, evaluate if there is a pending unsaved edited copy of the asset
|
||||
// open in any associated editor and in this case, prompt the user that there are unsaved changes and allow
|
||||
// the user to cancel the operation and allow to save the pending changes or confirm to delete the asset and
|
||||
// discard the pending unsaved changes (via OnAssetDeleted() notification).
|
||||
// - If the asset being deleted is not open in any editors or any open copies are not modified, no dialog
|
||||
// prompt is displayed and the asset is deleted.
|
||||
// - When an asset is about to get moved, notify any editors having the asset open about the move.
|
||||
//
|
||||
// See comments further down in this class for expected callback sequences.
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Intantiated through reflection by Unity")]
|
||||
private class InputActionAssetModificationProcessor : UnityEditor.AssetModificationProcessor
|
||||
{
|
||||
public static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options)
|
||||
{
|
||||
if (InputActionImporter.IsInputActionAssetPath(path))
|
||||
{
|
||||
// Find any open editors associated to the asset and if any of them holds unsaved changes
|
||||
// allow the user to discard unsaved changes or cancel deletion.
|
||||
var editorWithAssetOpen = InputActionAssetEditor.FindAllEditorsForPath(path);
|
||||
foreach (var editor in editorWithAssetOpen)
|
||||
{
|
||||
if (editor.isDirty)
|
||||
{
|
||||
var result = Dialog.InputActionAsset.ShowDiscardUnsavedChanges(path);
|
||||
if (result == Dialog.Result.Cancel)
|
||||
return AssetDeleteResult.FailedDelete;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Notify all associated editors that asset will be deleted
|
||||
foreach (var editor in editorWithAssetOpen)
|
||||
editor.OnAssetDeleted();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public static AssetMoveResult OnWillMoveAsset(string sourcePath, string destinationPath)
|
||||
{
|
||||
if (InputActionImporter.IsInputActionAssetPath(sourcePath))
|
||||
{
|
||||
var editorWithAssetOpen = InputActionAssetEditor.FindAllEditorsForPath(sourcePath);
|
||||
foreach (var editor in editorWithAssetOpen)
|
||||
editor.OnAssetMoved();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Asset post processor to react to internal or external asset import, move, delete events.
|
||||
// Processor detecting any Unity editor internal or external (file system) changes to an asset and notifies
|
||||
// any associated asset editors about those changes via callbacks.
|
||||
//
|
||||
// Note that any editor classes interested in receiving notifications need to be registered.
|
||||
//
|
||||
// For clarity, the tables below indicate the callback sequences of the asset modification processor and
|
||||
// asset post-processor for various user operations done on assets.
|
||||
//
|
||||
// s = source file
|
||||
// d = destination file
|
||||
// * = operation may be aborted by user
|
||||
//
|
||||
// User operation: Callback sequence:
|
||||
// ----------------------------------------------------------------------------------------
|
||||
// Write (Save) Imported(s)
|
||||
// Delete OnWillDelete(s), Deleted(s)*
|
||||
// Copy Imported(s)
|
||||
// Rename OnWillMove(s,d), Imported(d), Moved(s,d)
|
||||
// Move (drag) / Cut+Paste OnWillMove(s,d), Moved(s,d)
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
//
|
||||
// External user operation: Callback/call sequence:
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
// Save Imported(s)
|
||||
// Delete Deleted(s)
|
||||
// Copy Imported(s)
|
||||
// Rename Imported(d), Deleted(s)
|
||||
// Move(drag) / Cut+Paste Imported(d), Deleted(s)
|
||||
// ------------------------------------------------------------------------------------------------------------
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Intantiated through reflection by Unity")]
|
||||
private class InputActionAssetPostprocessor : AssetPostprocessor
|
||||
{
|
||||
private static bool s_DoNotifyEditorsScheduled;
|
||||
private static List<string> s_Imported = new List<string>();
|
||||
private static List<string> s_Deleted = new List<string>();
|
||||
private static List<string> s_Moved = new List<string>();
|
||||
|
||||
private static void Notify(IReadOnlyCollection<string> assets,
|
||||
IReadOnlyCollection<IInputActionAssetEditor> editors, Action<IInputActionAssetEditor> callback)
|
||||
{
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
var assetGuid = AssetDatabase.AssetPathToGUID(asset);
|
||||
foreach (var editor in editors)
|
||||
{
|
||||
if (editor.assetGUID != assetGuid)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
callback(editor);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyEditors()
|
||||
{
|
||||
try
|
||||
{
|
||||
// When the asset is modified outside of the editor and the importer settings are
|
||||
// visible in the Inspector the asset references in the importer inspector need to be
|
||||
// force rebuild (otherwise we gets lots of exceptions).
|
||||
ActiveEditorTracker.sharedTracker.ForceRebuild();
|
||||
|
||||
// Unconditionally find all existing editors regardless of associated asset
|
||||
var editors = InputActionAssetEditor.FindAllEditors();
|
||||
|
||||
// Abort if there are no available candidate editors
|
||||
if (editors == null || editors.Length == 0)
|
||||
return;
|
||||
|
||||
// Notify editors about asset changes
|
||||
Notify(s_Imported, editors, (editor) => editor.OnAssetImported());
|
||||
Notify(s_Deleted, editors, (editor) => editor.OnAssetDeleted());
|
||||
Notify(s_Moved, editors, (editor) => editor.OnAssetMoved());
|
||||
}
|
||||
finally
|
||||
{
|
||||
s_Imported.Clear();
|
||||
s_Deleted.Clear();
|
||||
s_Moved.Clear();
|
||||
|
||||
s_DoNotifyEditorsScheduled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Process(string[] assets, ICollection<string> target)
|
||||
{
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
// Ignore any assets with non matching extensions
|
||||
if (!InputActionImporter.IsInputActionAssetPath(asset))
|
||||
continue;
|
||||
|
||||
// Register asset in target collection for delay invocation
|
||||
target.Add(asset);
|
||||
|
||||
// If a notification execution has already been scheduled do nothing apart from registration.
|
||||
// We do this with delayed execution to avoid excessive updates interfering with ADB.
|
||||
if (!s_DoNotifyEditorsScheduled)
|
||||
{
|
||||
EditorApplication.delayCall += NotifyEditors;
|
||||
s_DoNotifyEditorsScheduled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets,
|
||||
string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload)
|
||||
{
|
||||
Process(importedAssets, s_Imported);
|
||||
Process(deletedAssets, s_Deleted);
|
||||
Process(movedAssets, s_Moved);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57f4c22a7e71744499d25ed776ef6cf2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
// Note that non-existing caching here is intentional since icon selected might be theme dependent.
|
||||
// There is no reason to cache icons unless there is a significant performance impact on the editor.
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to icons associated with <see cref="InputActionAsset"/> and <see cref="InputActionReference"/>.
|
||||
/// </summary>
|
||||
internal static class InputActionAssetIconLoader
|
||||
{
|
||||
private const string kActionIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputAction.png";
|
||||
private const string kAssetIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputActionAsset.png";
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load the icon associated with an <see cref="InputActionAsset"/>.
|
||||
/// </summary>
|
||||
/// <returns>Icon resource reference or <code>null</code> if the resource could not be loaded.</returns>
|
||||
internal static Texture2D LoadAssetIcon()
|
||||
{
|
||||
return (Texture2D)EditorGUIUtility.Load(kAssetIcon);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load the icon associated with an <see cref="InputActionReference"/> sub-asset of an
|
||||
/// <see cref="InputActionAsset"/>.
|
||||
/// </summary>
|
||||
/// <returns>Icon resource reference or <code>null</code> if the resource could not be loaded.</returns>
|
||||
internal static Texture2D LoadActionIcon()
|
||||
{
|
||||
return (Texture2D)EditorGUIUtility.Load(kActionIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // #if UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 015a929bdae646a99b91b756b998233f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,599 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using UnityEditor;
|
||||
|
||||
////TODO: option to allow referencing the original asset rather than embedding it
|
||||
|
||||
////TODO: emit indexer directly at toplevel so you can more easily look up actions dynamically
|
||||
|
||||
////TODO: put the generated code behind #if that depends on input system
|
||||
|
||||
////TODO: suffix map properties with Map or Actions (e.g. "PlayerMap" instead of "Player")
|
||||
|
||||
////TODO: unify the generated events so that performed, canceled, and started all go into a single event
|
||||
|
||||
////TODO: look up actions and maps by ID rather than by name
|
||||
|
||||
////TODO: only generate @something if @ is really needed
|
||||
|
||||
////TODO: allow having an unnamed or default-named action set which spills actions directly into the toplevel wrapper
|
||||
|
||||
////TODO: add cleanup for ActionEvents
|
||||
|
||||
////TODO: protect generated wrapper against modifications made to asset
|
||||
|
||||
////TODO: make capitalization consistent in the generated code
|
||||
|
||||
////TODO: instead of loading from JSON, generate the structure in code
|
||||
|
||||
////REVIEW: allow putting *all* of the data from the inputactions asset into the generated class?
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility to generate code that makes it easier to work with action sets.
|
||||
/// </summary>
|
||||
public static class InputActionCodeGenerator
|
||||
{
|
||||
private const int kSpacesPerIndentLevel = 4;
|
||||
|
||||
private const string kClassExample = @"using namespace UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
// Example of using an InputActionMap named ""Player"" from a UnityEngine.MonoBehaviour implementing callback interface.
|
||||
public class Example : MonoBehaviour, MyActions.IPlayerActions
|
||||
{
|
||||
private MyActions_Actions m_Actions; // Source code representation of asset.
|
||||
private MyActions_Actions.PlayerActions m_Player; // Source code representation of action map.
|
||||
|
||||
void Awake()
|
||||
{
|
||||
m_Actions = new MyActions_Actions(); // Create asset object.
|
||||
m_Player = m_Actions.Player; // Extract action map object.
|
||||
m_Player.AddCallbacks(this); // Register callback interface IPlayerActions.
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
m_Actions.Dispose(); // Destroy asset object.
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
m_Player.Enable(); // Enable all actions within map.
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
m_Player.Disable(); // Disable all actions within map.
|
||||
}
|
||||
|
||||
#region Interface implementation of MyActions.IPlayerActions
|
||||
|
||||
// Invoked when ""Move"" action is either started, performed or canceled.
|
||||
public void OnMove(InputAction.CallbackContext context)
|
||||
{
|
||||
Debug.Log($""OnMove: {context.ReadValue<Vector2>()}"");
|
||||
}
|
||||
|
||||
// Invoked when ""Attack"" action is either started, performed or canceled.
|
||||
public void OnAttack(InputAction.CallbackContext context)
|
||||
{
|
||||
Debug.Log($""OnAttack: {context.ReadValue<float>()}"");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}";
|
||||
|
||||
public struct Options
|
||||
{
|
||||
public string className { get; set; }
|
||||
public string namespaceName { get; set; }
|
||||
public string sourceAssetPath { get; set; }
|
||||
}
|
||||
|
||||
public static string GenerateWrapperCode(InputActionAsset asset, Options options = default)
|
||||
{
|
||||
if (asset == null)
|
||||
throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
if (string.IsNullOrEmpty(options.sourceAssetPath))
|
||||
options.sourceAssetPath = AssetDatabase.GetAssetPath(asset);
|
||||
if (string.IsNullOrEmpty(options.className) && !string.IsNullOrEmpty(asset.name))
|
||||
options.className =
|
||||
CSharpCodeHelpers.MakeTypeName(asset.name);
|
||||
|
||||
if (string.IsNullOrEmpty(options.className))
|
||||
{
|
||||
if (string.IsNullOrEmpty(options.sourceAssetPath))
|
||||
throw new ArgumentException("options.sourceAssetPath");
|
||||
options.className =
|
||||
CSharpCodeHelpers.MakeTypeName(Path.GetFileNameWithoutExtension(options.sourceAssetPath));
|
||||
}
|
||||
|
||||
var writer = new Writer
|
||||
{
|
||||
buffer = new StringBuilder()
|
||||
};
|
||||
|
||||
// Header.
|
||||
writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputActionCodeGenerator",
|
||||
InputSystem.version.ToString(),
|
||||
options.sourceAssetPath));
|
||||
|
||||
// Usings.
|
||||
writer.WriteLine("using System;");
|
||||
writer.WriteLine("using System.Collections;");
|
||||
writer.WriteLine("using System.Collections.Generic;");
|
||||
writer.WriteLine("using UnityEngine.InputSystem;");
|
||||
writer.WriteLine("using UnityEngine.InputSystem.Utilities;");
|
||||
writer.WriteLine("");
|
||||
|
||||
// Begin namespace.
|
||||
var haveNamespace = !string.IsNullOrEmpty(options.namespaceName);
|
||||
if (haveNamespace)
|
||||
{
|
||||
writer.WriteLine($"namespace {options.namespaceName}");
|
||||
writer.BeginBlock();
|
||||
}
|
||||
|
||||
// Begin class.
|
||||
writer.DocSummary($"Provides programmatic access to <see cref=\"InputActionAsset\" />, " +
|
||||
"<see cref=\"InputActionMap\" />, <see cref=\"InputAction\" /> and " +
|
||||
"<see cref=\"InputControlScheme\" /> instances defined " +
|
||||
$"in asset \"{options.sourceAssetPath}\".");
|
||||
writer.DocRemarks("This class is source generated and any manual edits will be discarded if the associated asset is reimported or modified.");
|
||||
writer.DocExample(kClassExample);
|
||||
|
||||
writer.WriteLine($"public partial class @{options.className}: IInputActionCollection2, IDisposable");
|
||||
writer.BeginBlock();
|
||||
|
||||
writer.DocSummary("Provides access to the underlying asset instance.");
|
||||
writer.WriteLine($"public InputActionAsset asset {{ get; }}");
|
||||
writer.WriteLine();
|
||||
|
||||
// Default constructor.
|
||||
writer.DocSummary("Constructs a new instance.");
|
||||
writer.WriteLine($"public @{options.className}()");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine($"asset = InputActionAsset.FromJson(@\"{asset.ToJson().Replace("\"", "\"\"")}\");");
|
||||
|
||||
var maps = asset.actionMaps;
|
||||
var schemes = asset.controlSchemes;
|
||||
foreach (var map in maps)
|
||||
{
|
||||
var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
|
||||
writer.WriteLine($"// {map.name}");
|
||||
writer.WriteLine($"m_{mapName} = asset.FindActionMap(\"{map.name}\", throwIfNotFound: true);");
|
||||
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
|
||||
writer.WriteLine($"m_{mapName}_{actionName} = m_{mapName}.FindAction(\"{action.name}\", throwIfNotFound: true);");
|
||||
}
|
||||
}
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine($"~@{options.className}()");
|
||||
writer.BeginBlock();
|
||||
foreach (var map in maps)
|
||||
{
|
||||
var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
|
||||
writer.WriteLine($"UnityEngine.Debug.Assert(!m_{mapName}.enabled, \"This will cause a leak and performance issues, {options.className}.{mapName}.Disable() has not been called.\");");
|
||||
}
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocSummary("Destroys this asset and all associated <see cref=\"InputAction\"/> instances.");
|
||||
writer.WriteLine("public void Dispose()");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("UnityEngine.Object.Destroy(asset);");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
var classNamePrefix = typeof(InputActionAsset).Namespace + "." + nameof(InputActionAsset) + ".";
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.bindingMask));
|
||||
writer.WriteLine("public InputBinding? bindingMask");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("get => asset.bindingMask;");
|
||||
writer.WriteLine("set => asset.bindingMask = value;");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.devices));
|
||||
writer.WriteLine("public ReadOnlyArray<InputDevice>? devices");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("get => asset.devices;");
|
||||
writer.WriteLine("set => asset.devices = value;");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.controlSchemes));
|
||||
writer.WriteLine("public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;");
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Contains) + "(InputAction)");
|
||||
writer.WriteLine("public bool Contains(InputAction action)");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("return asset.Contains(action);");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.GetEnumerator) + "()");
|
||||
writer.WriteLine("public IEnumerator<InputAction> GetEnumerator()");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("return asset.GetEnumerator();");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(nameof(IEnumerable) + "." + nameof(IEnumerable.GetEnumerator) + "()");
|
||||
writer.WriteLine("IEnumerator IEnumerable.GetEnumerator()");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("return GetEnumerator();");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Enable) + "()");
|
||||
writer.WriteLine("public void Enable()");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("asset.Enable();");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Disable) + "()");
|
||||
writer.WriteLine("public void Disable()");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("asset.Disable();");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.bindings));
|
||||
writer.WriteLine("public IEnumerable<InputBinding> bindings => asset.bindings;");
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.FindAction) + "(string, bool)");
|
||||
writer.WriteLine("public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("return asset.FindAction(actionNameOrId, throwIfNotFound);");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
writer.DocInherit(classNamePrefix + nameof(InputActionAsset.FindBinding) + "(InputBinding, out InputAction)");
|
||||
writer.WriteLine("public int FindBinding(InputBinding bindingMask, out InputAction action)");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("return asset.FindBinding(bindingMask, out action);");
|
||||
writer.EndBlock();
|
||||
|
||||
// Action map accessors.
|
||||
var inputActionMapClassPrefix = typeof(InputActionMap).Namespace + "." + nameof(InputActionMap) + ".";
|
||||
foreach (var map in maps)
|
||||
{
|
||||
writer.WriteLine();
|
||||
writer.WriteLine($"// {map.name}");
|
||||
|
||||
var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
|
||||
var mapTypeName = CSharpCodeHelpers.MakeTypeName(mapName, "Actions");
|
||||
|
||||
// Caching field for action map.
|
||||
writer.WriteLine($"private readonly InputActionMap m_{mapName};");
|
||||
writer.WriteLine(string.Format("private List<I{0}> m_{0}CallbackInterfaces = new List<I{0}>();", mapTypeName));
|
||||
|
||||
// Caching fields for all actions.
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
|
||||
writer.WriteLine($"private readonly InputAction m_{mapName}_{actionName};");
|
||||
}
|
||||
|
||||
// Struct wrapping access to action set.
|
||||
writer.DocSummary($"Provides access to input actions defined in input action map \"{map.name}\".");
|
||||
writer.WriteLine($"public struct {mapTypeName}");
|
||||
writer.BeginBlock();
|
||||
|
||||
writer.WriteLine($"private @{options.className} m_Wrapper;");
|
||||
writer.WriteLine();
|
||||
|
||||
// Constructor.
|
||||
writer.DocSummary("Construct a new instance of the input action map wrapper class.");
|
||||
writer.WriteLine($"public {mapTypeName}(@{options.className} wrapper) {{ m_Wrapper = wrapper; }}");
|
||||
|
||||
// Getter for each action.
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
|
||||
writer.DocSummary($"Provides access to the underlying input action \"{mapName}/{actionName}\".");
|
||||
writer.WriteLine(
|
||||
$"public InputAction @{actionName} => m_Wrapper.m_{mapName}_{actionName};");
|
||||
}
|
||||
|
||||
// Action map getter.
|
||||
writer.DocSummary("Provides access to the underlying input action map instance.");
|
||||
writer.WriteLine($"public InputActionMap Get() {{ return m_Wrapper.m_{mapName}; }}");
|
||||
|
||||
// Enable/disable methods.
|
||||
writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.Enable) + "()");
|
||||
writer.WriteLine("public void Enable() { Get().Enable(); }");
|
||||
writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.Disable) + "()");
|
||||
writer.WriteLine("public void Disable() { Get().Disable(); }");
|
||||
writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.enabled));
|
||||
writer.WriteLine("public bool enabled => Get().enabled;");
|
||||
|
||||
// Implicit conversion operator.
|
||||
writer.DocSummary($"Implicitly converts an <see ref=\"{mapTypeName}\" /> to an <see ref=\"InputActionMap\" /> instance.");
|
||||
writer.WriteLine(
|
||||
$"public static implicit operator InputActionMap({mapTypeName} set) {{ return set.Get(); }}");
|
||||
|
||||
// AddCallbacks method.
|
||||
writer.DocSummary("Adds <see cref=\"InputAction.started\"/>, <see cref=\"InputAction.performed\"/> and <see cref=\"InputAction.canceled\"/> callbacks provided via <param cref=\"instance\" /> on all input actions contained in this map.");
|
||||
writer.DocParam("instance", "Callback instance.");
|
||||
writer.DocRemarks("If <paramref name=\"instance\" /> is <c>null</c> or <paramref name=\"instance\"/> have already been added this method does nothing.");
|
||||
writer.DocSeeAlso(mapTypeName);
|
||||
writer.WriteLine($"public void AddCallbacks(I{mapTypeName} instance)");
|
||||
writer.BeginBlock();
|
||||
|
||||
// Initialize new interface.
|
||||
writer.WriteLine($"if (instance == null || m_Wrapper.m_{mapTypeName}CallbackInterfaces.Contains(instance)) return;");
|
||||
writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Add(instance);");
|
||||
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
|
||||
var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name);
|
||||
|
||||
writer.WriteLine($"@{actionName}.started += instance.On{actionTypeName};");
|
||||
writer.WriteLine($"@{actionName}.performed += instance.On{actionTypeName};");
|
||||
writer.WriteLine($"@{actionName}.canceled += instance.On{actionTypeName};");
|
||||
}
|
||||
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
// UnregisterCallbacks method.
|
||||
writer.DocSummary("Removes <see cref=\"InputAction.started\"/>, <see cref=\"InputAction.performed\"/> and <see cref=\"InputAction.canceled\"/> callbacks provided via <param cref=\"instance\" /> on all input actions contained in this map.");
|
||||
writer.DocRemarks("Calling this method when <paramref name=\"instance\" /> have not previously been registered has no side-effects.");
|
||||
writer.DocSeeAlso(mapTypeName);
|
||||
writer.WriteLine($"private void UnregisterCallbacks(I{mapTypeName} instance)");
|
||||
writer.BeginBlock();
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
|
||||
var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name);
|
||||
|
||||
writer.WriteLine($"@{actionName}.started -= instance.On{actionTypeName};");
|
||||
writer.WriteLine($"@{actionName}.performed -= instance.On{actionTypeName};");
|
||||
writer.WriteLine($"@{actionName}.canceled -= instance.On{actionTypeName};");
|
||||
}
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
// RemoveCallbacks method.
|
||||
writer.DocSummary($"Unregisters <param cref=\"instance\" /> and unregisters all input action callbacks via <see cref=\"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})\" />.");
|
||||
writer.DocSeeAlso($"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})");
|
||||
writer.WriteLine($"public void RemoveCallbacks(I{mapTypeName} instance)");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine($"if (m_Wrapper.m_{mapTypeName}CallbackInterfaces.Remove(instance))");
|
||||
writer.WriteLine($" UnregisterCallbacks(instance);");
|
||||
writer.EndBlock();
|
||||
writer.WriteLine();
|
||||
|
||||
// SetCallbacks method.
|
||||
writer.DocSummary($"Replaces all existing callback instances and previously registered input action callbacks associated with them with callbacks provided via <param cref=\"instance\" />.");
|
||||
writer.DocRemarks($"If <paramref name=\"instance\" /> is <c>null</c>, calling this method will only unregister all existing callbacks but not register any new callbacks.");
|
||||
writer.DocSeeAlso($"{mapTypeName}.AddCallbacks(I{mapTypeName})");
|
||||
writer.DocSeeAlso($"{mapTypeName}.RemoveCallbacks(I{mapTypeName})");
|
||||
writer.DocSeeAlso($"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})");
|
||||
writer.WriteLine($"public void SetCallbacks(I{mapTypeName} instance)");
|
||||
writer.BeginBlock();
|
||||
////REVIEW: this would benefit from having a single callback on InputActions rather than three different endpoints
|
||||
|
||||
writer.WriteLine($"foreach (var item in m_Wrapper.m_{mapTypeName}CallbackInterfaces)");
|
||||
writer.WriteLine($" UnregisterCallbacks(item);");
|
||||
writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Clear();");
|
||||
|
||||
// Initialize new interface.
|
||||
writer.WriteLine("AddCallbacks(instance);");
|
||||
writer.EndBlock();
|
||||
writer.EndBlock();
|
||||
|
||||
// Getter for instance of struct.
|
||||
writer.DocSummary($"Provides a new <see cref=\"{mapTypeName}\" /> instance referencing this action map.");
|
||||
writer.WriteLine($"public {mapTypeName} @{mapName} => new {mapTypeName}(this);");
|
||||
}
|
||||
|
||||
// Control scheme accessors.
|
||||
foreach (var scheme in schemes)
|
||||
{
|
||||
var identifier = CSharpCodeHelpers.MakeIdentifier(scheme.name);
|
||||
|
||||
writer.WriteLine($"private int m_{identifier}SchemeIndex = -1;");
|
||||
writer.DocSummary("Provides access to the input control scheme.");
|
||||
writer.DocSeeAlso(typeof(InputControlScheme).Namespace + "." + nameof(InputControlScheme));
|
||||
writer.WriteLine($"public InputControlScheme {identifier}Scheme");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine("get");
|
||||
writer.BeginBlock();
|
||||
writer.WriteLine($"if (m_{identifier}SchemeIndex == -1) m_{identifier}SchemeIndex = asset.FindControlSchemeIndex(\"{scheme.name}\");");
|
||||
writer.WriteLine($"return asset.controlSchemes[m_{identifier}SchemeIndex];");
|
||||
writer.EndBlock();
|
||||
writer.EndBlock();
|
||||
}
|
||||
|
||||
// Generate interfaces.
|
||||
var inputActionClassReference = typeof(InputAction).Namespace + "." + nameof(InputAction) + ".";
|
||||
foreach (var map in maps)
|
||||
{
|
||||
var typeName = CSharpCodeHelpers.MakeTypeName(map.name);
|
||||
writer.DocSummary($"Interface to implement callback methods for all input action callbacks associated with input actions defined by \"{map.name}\" which allows adding and removing callbacks.");
|
||||
writer.DocSeeAlso($"{typeName}Actions.AddCallbacks(I{typeName}Actions)");
|
||||
writer.DocSeeAlso($"{typeName}Actions.RemoveCallbacks(I{typeName}Actions)");
|
||||
writer.WriteLine($"public interface I{typeName}Actions");
|
||||
writer.BeginBlock();
|
||||
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var methodName = CSharpCodeHelpers.MakeTypeName(action.name);
|
||||
writer.DocSummary($"Method invoked when associated input action \"{action.name}\" is either <see cref=\"UnityEngine.InputSystem.InputAction.started\" />, <see cref=\"UnityEngine.InputSystem.InputAction.performed\" /> or <see cref=\"UnityEngine.InputSystem.InputAction.canceled\" />.");
|
||||
writer.DocSeeAlso(string.Concat(inputActionClassReference, "started"));
|
||||
writer.DocSeeAlso(string.Concat(inputActionClassReference, "performed"));
|
||||
writer.DocSeeAlso(string.Concat(inputActionClassReference, "canceled"));
|
||||
writer.WriteLine($"void On{methodName}(InputAction.CallbackContext context);");
|
||||
}
|
||||
|
||||
writer.EndBlock();
|
||||
}
|
||||
|
||||
// End class.
|
||||
writer.EndBlock();
|
||||
|
||||
// End namespace.
|
||||
if (haveNamespace)
|
||||
writer.EndBlock();
|
||||
|
||||
return writer.buffer.ToString();
|
||||
}
|
||||
|
||||
////TODO: move this to a shared place
|
||||
internal struct Writer
|
||||
{
|
||||
public StringBuilder buffer;
|
||||
public int indentLevel;
|
||||
|
||||
public void BeginBlock()
|
||||
{
|
||||
WriteIndent();
|
||||
buffer.Append("{\n");
|
||||
++indentLevel;
|
||||
}
|
||||
|
||||
public void EndBlock()
|
||||
{
|
||||
--indentLevel;
|
||||
WriteIndent();
|
||||
buffer.Append("}\n");
|
||||
}
|
||||
|
||||
public void WriteLine()
|
||||
{
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
if (!text.All(char.IsWhiteSpace))
|
||||
{
|
||||
WriteIndent();
|
||||
buffer.Append(text);
|
||||
}
|
||||
buffer.Append('\n');
|
||||
}
|
||||
|
||||
public void Write(string text)
|
||||
{
|
||||
buffer.Append(text);
|
||||
}
|
||||
|
||||
public void WriteIndent()
|
||||
{
|
||||
for (var i = 0; i < indentLevel; ++i)
|
||||
{
|
||||
for (var n = 0; n < kSpacesPerIndentLevel; ++n)
|
||||
buffer.Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
public void DocSummary(string text)
|
||||
{
|
||||
DocElement("summary", text);
|
||||
}
|
||||
|
||||
public void DocParam(string paramName, string text)
|
||||
{
|
||||
WriteLine($"/// <param name=\"{paramName}\">{text}</param>");
|
||||
}
|
||||
|
||||
public void DocRemarks(string text)
|
||||
{
|
||||
DocElement("remarks", text);
|
||||
}
|
||||
|
||||
public void DocInherit(string cref)
|
||||
{
|
||||
DocReference("inheritdoc", cref);
|
||||
}
|
||||
|
||||
public void DocSeeAlso(string cref)
|
||||
{
|
||||
DocReference("seealso", cref: cref);
|
||||
}
|
||||
|
||||
public void DocExample(string code)
|
||||
{
|
||||
DocComment("<example>");
|
||||
DocComment("<code>");
|
||||
|
||||
foreach (var line in code.Split('\n'))
|
||||
DocComment(line.Replace("<", "<").Replace(">", ">"));
|
||||
|
||||
DocComment("</code>");
|
||||
DocComment("</example>");
|
||||
}
|
||||
|
||||
private void DocComment(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
WriteLine("///");
|
||||
else
|
||||
WriteLine(string.Concat("/// ", text));
|
||||
}
|
||||
|
||||
private void DocElement(string tag, string text)
|
||||
{
|
||||
DocComment($"<{tag}>");
|
||||
DocComment(text);
|
||||
DocComment($"</{tag}>");
|
||||
}
|
||||
|
||||
private void DocReference(string tag, string cref)
|
||||
{
|
||||
DocInlineElement(tag, "cref", cref);
|
||||
}
|
||||
|
||||
private void DocInlineElement(string tag, string property, string value)
|
||||
{
|
||||
DocComment($"<{tag} {property}=\"{value}\" />");
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the given file with wrapper code generated for the given action sets.
|
||||
// If the generated code is unchanged, does not touch the file.
|
||||
// Returns true if the file was touched, false otherwise.
|
||||
public static bool GenerateWrapperCode(string filePath, InputActionAsset asset, Options options)
|
||||
{
|
||||
if (!Path.HasExtension(filePath))
|
||||
filePath += ".cs";
|
||||
|
||||
// Generate code.
|
||||
var code = GenerateWrapperCode(asset, options);
|
||||
|
||||
// Check if the code changed. Don't write if it hasn't.
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var existingCode = File.ReadAllText(filePath);
|
||||
if (existingCode == code || existingCode.WithAllWhitespaceStripped() == code.WithAllWhitespaceStripped())
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write.
|
||||
EditorHelpers.CheckOut(filePath);
|
||||
File.WriteAllText(filePath, code);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a85130d4e1e5b49878f8f51b2d4e0ded
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,460 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AssetImporters;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////FIXME: The importer accesses icons through the asset db (which EditorGUIUtility.LoadIcon falls back on) which will
|
||||
//// not yet have been imported when the project is imported from scratch; this results in errors in the log and in generic
|
||||
//// icons showing up for the assets
|
||||
|
||||
#pragma warning disable 0649
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Imports an <see cref="InputActionAsset"/> from JSON.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can generate code wrappers for the contained action sets as a convenience.
|
||||
/// Will not overwrite existing wrappers except if the generated code actually differs.
|
||||
/// </remarks>
|
||||
[ScriptedImporter(kVersion, InputActionAsset.Extension)]
|
||||
internal class InputActionImporter : ScriptedImporter
|
||||
{
|
||||
private const int kVersion = 14;
|
||||
|
||||
[SerializeField] private bool m_GenerateWrapperCode;
|
||||
[SerializeField] private string m_WrapperCodePath;
|
||||
[SerializeField] private string m_WrapperClassName;
|
||||
[SerializeField] private string m_WrapperCodeNamespace;
|
||||
|
||||
private static InlinedArray<Action> s_OnImportCallbacks;
|
||||
|
||||
public static event Action onImport
|
||||
{
|
||||
add => s_OnImportCallbacks.Append(value);
|
||||
remove => s_OnImportCallbacks.Remove(value);
|
||||
}
|
||||
|
||||
private static InputActionAsset CreateFromJson(AssetImportContext context)
|
||||
{
|
||||
////REVIEW: need to check with version control here?
|
||||
// Read JSON file.
|
||||
string content;
|
||||
try
|
||||
{
|
||||
content = File.ReadAllText(EditorHelpers.GetPhysicalPath(context.assetPath));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
context.LogImportError($"Could not read file '{context.assetPath}' ({exception})");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create asset.
|
||||
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
|
||||
|
||||
// Parse JSON and configure asset.
|
||||
try
|
||||
{
|
||||
// Attempt to parse JSON
|
||||
asset.LoadFromJson(content);
|
||||
// Make sure action map names are unique within JSON file
|
||||
var names = new HashSet<string>();
|
||||
foreach (var map in asset.actionMaps)
|
||||
{
|
||||
if (!names.Add(map.name))
|
||||
{
|
||||
throw new Exception(
|
||||
"Unable to parse {context.assetPath} due to duplicate Action Map name: '{map.name}'. Make sure Action Map names are unique within the asset and reattempt import.");
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure action names are unique within each action map in JSON file
|
||||
names.Clear();
|
||||
foreach (var map in asset.actionMaps)
|
||||
{
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
if (!names.Add(action.name))
|
||||
{
|
||||
throw new Exception(
|
||||
$"Unable to parse {{context.assetPath}} due to duplicate Action name: '{action.name}' within Action Map '{map.name}'. Make sure Action Map names are unique within the asset and reattempt import.");
|
||||
}
|
||||
}
|
||||
|
||||
names.Clear();
|
||||
}
|
||||
|
||||
// Force name of asset to be that on the file on disk instead of what may be serialized
|
||||
// as the 'name' property in JSON. (Unless explicitly given)
|
||||
asset.name = NameFromAssetPath(context.assetPath);
|
||||
|
||||
// Add asset.
|
||||
////REVIEW: the icons won't change if the user changes skin; not sure it makes sense to differentiate here
|
||||
context.AddObjectToAsset("<root>", asset, InputActionAssetIconLoader.LoadAssetIcon());
|
||||
context.SetMainObject(asset);
|
||||
|
||||
// Make sure all the elements in the asset have GUIDs and that they are indeed unique.
|
||||
// Create sub-assets for each action to allow search and editor referencing/picking.
|
||||
SetupAsset(asset, context.AddObjectToAsset);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
context.LogImportError($"Could not parse input actions in JSON format from '{context.assetPath}' ({exception})");
|
||||
DestroyImmediate(asset);
|
||||
asset = null;
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
public override void OnImportAsset(AssetImportContext ctx)
|
||||
{
|
||||
if (ctx == null)
|
||||
throw new ArgumentNullException(nameof(ctx));
|
||||
|
||||
foreach (var callback in s_OnImportCallbacks)
|
||||
callback();
|
||||
|
||||
var asset = CreateFromJson(ctx);
|
||||
if (asset == null)
|
||||
return;
|
||||
|
||||
if (m_GenerateWrapperCode && HasMapWithSameNameAsAsset(asset, m_WrapperClassName))
|
||||
{
|
||||
ctx.LogImportError(
|
||||
$"{asset.name}: An action map in an .inputactions asset cannot be named the same as the asset itself if 'Generate C# Class' is used. "
|
||||
+ "You can rename the action map in the asset, rename the asset itself or assign a different C# class name in the import settings.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetupAsset(InputActionAsset asset)
|
||||
{
|
||||
SetupAsset(asset, (identifier, subAsset, icon) =>
|
||||
AssetDatabase.AddObjectToAsset(subAsset, asset));
|
||||
}
|
||||
|
||||
private delegate void AddObjectToAsset(string identifier, Object subAsset, Texture2D icon);
|
||||
|
||||
private static void SetupAsset(InputActionAsset asset, AddObjectToAsset addObjectToAsset)
|
||||
{
|
||||
FixMissingGuids(asset);
|
||||
CreateInputActionReferences(asset, addObjectToAsset);
|
||||
}
|
||||
|
||||
private static void FixMissingGuids(InputActionAsset asset)
|
||||
{
|
||||
// Make sure all the elements in the asset have GUIDs and that they are indeed unique.
|
||||
foreach (var map in asset.actionMaps)
|
||||
{
|
||||
// Make sure action map has GUID.
|
||||
if (string.IsNullOrEmpty(map.m_Id) || asset.actionMaps.Count(x => x.m_Id == map.m_Id) > 1)
|
||||
map.GenerateId();
|
||||
|
||||
// Make sure all actions have GUIDs.
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
var actionId = action.m_Id;
|
||||
if (string.IsNullOrEmpty(actionId) || asset.actionMaps.Sum(m => m.actions.Count(a => a.m_Id == actionId)) > 1)
|
||||
action.GenerateId();
|
||||
}
|
||||
|
||||
// Make sure all bindings have GUIDs.
|
||||
for (var i = 0; i < map.m_Bindings.LengthSafe(); ++i)
|
||||
{
|
||||
var bindingId = map.m_Bindings[i].m_Id;
|
||||
if (string.IsNullOrEmpty(bindingId) || asset.actionMaps.Sum(m => m.bindings.Count(b => b.m_Id == bindingId)) > 1)
|
||||
map.m_Bindings[i].GenerateId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateInputActionReferences(InputActionAsset asset, AddObjectToAsset addObjectToAsset)
|
||||
{
|
||||
var actionIcon = InputActionAssetIconLoader.LoadActionIcon();
|
||||
foreach (var map in asset.actionMaps)
|
||||
{
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
// Note that it is important action is set before being added to asset, otherwise the immutability
|
||||
// check within InputActionReference would throw.
|
||||
var actionReference = ScriptableObject.CreateInstance<InputActionReference>();
|
||||
actionReference.Set(action);
|
||||
addObjectToAsset(action.m_Id, actionReference, actionIcon);
|
||||
|
||||
// Backwards-compatibility (added for 1.0.0-preview.7).
|
||||
// We used to call AddObjectToAsset using objectName instead of action.m_Id as the object name. This fed
|
||||
// the action name (*and* map name) into the hash generation that was used as the basis for the file ID
|
||||
// object the InputActionReference object. Thus, if the map and/or action name changed, the file ID would
|
||||
// change and existing references to the InputActionReference object would become invalid.
|
||||
//
|
||||
// What we do here is add another *hidden* InputActionReference object with the same content to the
|
||||
// asset. This one will use the old file ID and thus preserve backwards-compatibility. We should be able
|
||||
// to remove this for 2.0.
|
||||
//
|
||||
// Case: https://fogbugz.unity3d.com/f/cases/1229145/
|
||||
var backcompatActionReference = Instantiate(actionReference);
|
||||
backcompatActionReference.name = actionReference.name; // Get rid of the (Clone) suffix.
|
||||
backcompatActionReference.hideFlags = HideFlags.HideInHierarchy;
|
||||
addObjectToAsset(actionReference.name, backcompatActionReference, actionIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasMapWithSameNameAsAsset(InputActionAsset asset, string codeClassName)
|
||||
{
|
||||
// When using code generation, it is an error for any action map to be named the same as the asset itself.
|
||||
// https://fogbugz.unity3d.com/f/cases/1212052/
|
||||
var className = !string.IsNullOrEmpty(codeClassName) ? codeClassName : CSharpCodeHelpers.MakeTypeName(asset.name);
|
||||
return (asset.actionMaps.Any(x =>
|
||||
CSharpCodeHelpers.MakeTypeName(x.name) == className ||
|
||||
CSharpCodeHelpers.MakeIdentifier(x.name) == className));
|
||||
}
|
||||
|
||||
private static void GenerateWrapperCode(string assetPath, InputActionAsset asset, string codeNamespace, string codeClassName, string codePath)
|
||||
{
|
||||
if (HasMapWithSameNameAsAsset(asset, codeClassName))
|
||||
return;
|
||||
|
||||
var wrapperFilePath = codePath;
|
||||
if (string.IsNullOrEmpty(wrapperFilePath))
|
||||
{
|
||||
// Placed next to .inputactions file.
|
||||
var directory = Path.GetDirectoryName(assetPath);
|
||||
var fileName = Path.GetFileNameWithoutExtension(assetPath);
|
||||
wrapperFilePath = Path.Combine(directory, fileName) + ".cs";
|
||||
}
|
||||
else if (wrapperFilePath.StartsWith("./") || wrapperFilePath.StartsWith(".\\") ||
|
||||
wrapperFilePath.StartsWith("../") || wrapperFilePath.StartsWith("..\\"))
|
||||
{
|
||||
// User-specified file relative to location of .inputactions file.
|
||||
var directory = Path.GetDirectoryName(assetPath);
|
||||
wrapperFilePath = Path.Combine(directory, wrapperFilePath);
|
||||
}
|
||||
else if (!wrapperFilePath.StartsWith("assets/", StringComparison.InvariantCultureIgnoreCase) &&
|
||||
!wrapperFilePath.StartsWith("assets\\", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
// User-specified file in Assets/ folder.
|
||||
wrapperFilePath = Path.Combine("Assets", wrapperFilePath);
|
||||
}
|
||||
|
||||
var dir = Path.GetDirectoryName(wrapperFilePath);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
// Check for the case where the target file already exists.
|
||||
// If it does, we don't want to overwrite it unless it's been generated by us before.
|
||||
if (File.Exists(wrapperFilePath))
|
||||
{
|
||||
var text = File.ReadAllText(wrapperFilePath);
|
||||
var autoGeneratedMarker = "This code was auto-generated by com.unity.inputsystem";
|
||||
if (!text.Contains(autoGeneratedMarker))
|
||||
{
|
||||
throw new Exception($"The target file for Input Actions code generation already exists: {wrapperFilePath}. Since it doesn't look to contain Input generated code that we can safely overwrite, we stopped to prevent any data loss. Consider renaming. ");
|
||||
}
|
||||
}
|
||||
|
||||
var options = new InputActionCodeGenerator.Options
|
||||
{
|
||||
sourceAssetPath = assetPath,
|
||||
namespaceName = codeNamespace,
|
||||
className = codeClassName,
|
||||
};
|
||||
|
||||
|
||||
if (InputActionCodeGenerator.GenerateWrapperCode(wrapperFilePath, asset, options))
|
||||
{
|
||||
// This isn't ideal and may have side effects, but we cannot avoid compiling again.
|
||||
// Previously we attempted to run a EditorApplication.delayCall += AssetDatabase.Refresh
|
||||
// but this would lead to "error: Error building Player because scripts are compiling" in CI.
|
||||
// Previous comment here warned against not being able to reimport here directly, but it seems it's ok.
|
||||
AssetDatabase.ImportAsset(wrapperFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<InputActionReference> LoadInputActionReferencesFromAsset(string assetPath)
|
||||
{
|
||||
// Get all InputActionReferences are stored at the same asset path as InputActionAsset
|
||||
// Note we exclude 'hidden' action references (which are present to support one of the pre releases)
|
||||
return AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(
|
||||
o => o is InputActionReference &&
|
||||
!((InputActionReference)o).hideFlags.HasFlag(HideFlags.HideInHierarchy))
|
||||
.Cast<InputActionReference>();
|
||||
}
|
||||
|
||||
private static readonly string[] s_DefaultAssetSearchFolders = new string[] { "Assets" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets all <see cref="InputActionReference"/> instances available in assets in the project.
|
||||
/// By default, it only gets the assets located in the "Assets" folder.
|
||||
/// </summary>
|
||||
/// <param name="foldersPath">Optional array of directory paths to be searched.</param>
|
||||
/// <param name="skipProjectWide">If true, excludes project-wide input actions from the result.</param>
|
||||
/// <returns></returns>
|
||||
internal static IEnumerable<InputActionReference> LoadInputActionReferencesFromAssetDatabase(
|
||||
string[] foldersPath = null, bool skipProjectWide = false)
|
||||
{
|
||||
// Get all InputActionReference from assets in "Asset" folder by default.
|
||||
// It does not search inside "Packages" folder.
|
||||
const string inputActionReferenceFilter = "t:" + nameof(InputActionReference);
|
||||
var inputActionReferenceGUIDs = AssetDatabase.FindAssets(inputActionReferenceFilter,
|
||||
foldersPath ?? s_DefaultAssetSearchFolders);
|
||||
|
||||
// To find all the InputActionReferences, the GUID of the asset containing at least one action reference is
|
||||
// used to find the asset path. This is because InputActionReferences are stored in the asset database as sub-assets of InputActionAsset.
|
||||
// Then the whole asset is loaded and all the InputActionReferences are extracted from it.
|
||||
// Also, the action references are duplicated to have backwards compatibility with the 1.0.0-preview.7. That
|
||||
// is why we look for references withouth the `HideFlags.HideInHierarchy` flag.
|
||||
var inputActionReferencesList = new List<InputActionReference>();
|
||||
foreach (var guid in inputActionReferenceGUIDs)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
foreach (var assetInputActionReference in LoadInputActionReferencesFromAsset(assetPath))
|
||||
{
|
||||
if (skipProjectWide && assetInputActionReference.m_Asset == InputSystem.actions)
|
||||
continue;
|
||||
|
||||
inputActionReferencesList.Add(assetInputActionReference);
|
||||
}
|
||||
}
|
||||
return inputActionReferencesList;
|
||||
}
|
||||
|
||||
// Add item to plop an .inputactions asset into the project.
|
||||
[MenuItem("Assets/Create/Input Actions")]
|
||||
public static void CreateInputAsset()
|
||||
{
|
||||
ProjectWindowUtil.CreateAssetWithContent("New Actions." + InputActionAsset.Extension,
|
||||
InputActionAsset.kDefaultAssetLayoutJson, InputActionAssetIconLoader.LoadAssetIcon());
|
||||
}
|
||||
|
||||
// File extension of the associated asset
|
||||
private const string kFileExtension = "." + InputActionAsset.Extension;
|
||||
|
||||
// Evaluates whether the given path is a path to an asset of the associated type based on extension.
|
||||
public static bool IsInputActionAssetPath(string path)
|
||||
{
|
||||
return path != null && path.EndsWith(kFileExtension, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
// Returns a suitable object name for an asset based on its path.
|
||||
public static string NameFromAssetPath(string assetPath)
|
||||
{
|
||||
Debug.Assert(IsInputActionAssetPath(assetPath));
|
||||
return Path.GetFileNameWithoutExtension(assetPath);
|
||||
}
|
||||
|
||||
private static bool ContainsInputActionAssetPath(string[] assetPaths)
|
||||
{
|
||||
foreach (var assetPath in assetPaths)
|
||||
{
|
||||
if (IsInputActionAssetPath(assetPath))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// This processor was added to address this issue:
|
||||
// https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-749
|
||||
//
|
||||
// When an action asset is renamed, copied, or moved in the Editor, the "Name" field in the JSON will
|
||||
// hold the old name and won't match the asset objects name in memory which is set based on the filename
|
||||
// by the scripted imported. To avoid this, this asset post-processor detects any imported or moved assets
|
||||
// with a JSON name property not matching the importer assigned name and updates the JSON name based on this.
|
||||
// This basically solves any problem related to unmodified assets.
|
||||
//
|
||||
// Note that JSON names have no relevance for editor workflows and are basically ignored by the importer.
|
||||
// Note that JSON names may be the only way to identify assets loaded from non-file sources or via
|
||||
// UnityEngine.Resources in run-time.
|
||||
//
|
||||
// Note that if an asset is is imported and a name mismatch is detected, the asset will be modified and
|
||||
// imported again, which will yield yet another callback to the post-processor. For the second iteration,
|
||||
// the name will no longer be a mismatch and the cycle will be aborted.
|
||||
private class InputActionJsonNameModifierAssetProcessor : AssetPostprocessor
|
||||
{
|
||||
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets,
|
||||
string[] movedAssets, string[] movedFromAssetPaths)
|
||||
{
|
||||
var needToInvalidate = false;
|
||||
foreach (var assetPath in importedAssets)
|
||||
{
|
||||
if (IsInputActionAssetPath(assetPath))
|
||||
{
|
||||
// Generate C# code from asset if configured via importer settings.
|
||||
// We generate from a parsed temporary asset here since loading the asset won't work here.
|
||||
var importer = GetAtPath(assetPath) as InputActionImporter;
|
||||
if (importer != null && importer.m_GenerateWrapperCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var asset = InputActionAsset.FromJson(File.ReadAllText(assetPath));
|
||||
GenerateWrapperCode(assetPath, asset, importer.m_WrapperCodeNamespace,
|
||||
importer.m_WrapperClassName, importer.m_WrapperCodePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
needToInvalidate = true;
|
||||
CheckAndRenameJsonNameIfDifferent(assetPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!needToInvalidate)
|
||||
needToInvalidate = ContainsInputActionAssetPath(deletedAssets);
|
||||
if (!needToInvalidate)
|
||||
needToInvalidate = ContainsInputActionAssetPath(movedAssets);
|
||||
if (!needToInvalidate)
|
||||
needToInvalidate = ContainsInputActionAssetPath(movedFromAssetPaths);
|
||||
|
||||
// Invalidate all references to make sure there are no dangling references after reimport among
|
||||
// our "live objects. We only need to invalidate loaded sub-assets and not assets/prefabs/SO etc
|
||||
// since they may still contain effectively obsolete InputActionReferences but those references
|
||||
// will resolve.
|
||||
if (needToInvalidate)
|
||||
InputActionReference.InvalidateAll();
|
||||
}
|
||||
|
||||
private static void CheckAndRenameJsonNameIfDifferent(string assetPath)
|
||||
{
|
||||
InputActionAsset asset = null;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(assetPath))
|
||||
return;
|
||||
|
||||
// Evaluate whether JSON name corresponds to desired name
|
||||
asset = InputActionAsset.FromJson(File.ReadAllText(assetPath));
|
||||
var desiredName = Path.GetFileNameWithoutExtension(assetPath);
|
||||
if (asset.name == desiredName)
|
||||
return;
|
||||
|
||||
// Update JSON name by modifying the asset
|
||||
asset.name = desiredName;
|
||||
if (!EditorHelpers.WriteAsset(assetPath, asset.ToJson()))
|
||||
{
|
||||
Debug.LogError($"Unable to change JSON name for asset at \"{assetPath}\" since the asset-path could not be checked-out as editable in the underlying version-control system.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (asset != null)
|
||||
DestroyImmediate(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8404be70184654265930450def6a9037
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,150 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using UnityEditor;
|
||||
using UnityEditor.AssetImporters;
|
||||
|
||||
////TODO: support for multi-editing
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor that allows modifying importer settings for an <see cref="InputActionImporter"/>.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(InputActionImporter))]
|
||||
internal class InputActionImporterEditor : ScriptedImporterEditor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
var inputActionAsset = GetAsset();
|
||||
|
||||
// ScriptedImporterEditor in 2019.2 now requires explicitly updating the SerializedObject
|
||||
// like in other types of editors.
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (inputActionAsset == null)
|
||||
EditorGUILayout.HelpBox("The currently selected object is not an editable input action asset.",
|
||||
MessageType.Info);
|
||||
|
||||
// Button to pop up window to edit the asset.
|
||||
using (new EditorGUI.DisabledScope(inputActionAsset == null))
|
||||
{
|
||||
if (GUILayout.Button(GetOpenEditorButtonText(inputActionAsset), GUILayout.Height(30)))
|
||||
OpenEditor(inputActionAsset);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Project-wide Input Actions Asset UI.
|
||||
InputAssetEditorUtils.DrawMakeActiveGui(InputSystem.actions, inputActionAsset,
|
||||
inputActionAsset ? inputActionAsset.name : "Null", "Project-wide Input Actions",
|
||||
(value) => InputSystem.actions = value, !EditorApplication.isPlayingOrWillChangePlaymode);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// Importer settings UI.
|
||||
var generateWrapperCodeProperty = serializedObject.FindProperty("m_GenerateWrapperCode");
|
||||
EditorGUILayout.PropertyField(generateWrapperCodeProperty, m_GenerateWrapperCodeLabel);
|
||||
if (generateWrapperCodeProperty.boolValue)
|
||||
{
|
||||
var wrapperCodePathProperty = serializedObject.FindProperty("m_WrapperCodePath");
|
||||
var wrapperClassNameProperty = serializedObject.FindProperty("m_WrapperClassName");
|
||||
var wrapperCodeNamespaceProperty = serializedObject.FindProperty("m_WrapperCodeNamespace");
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
string defaultFileName = "";
|
||||
if (inputActionAsset != null)
|
||||
{
|
||||
var assetPath = AssetDatabase.GetAssetPath(inputActionAsset);
|
||||
defaultFileName = Path.ChangeExtension(assetPath, ".cs");
|
||||
}
|
||||
|
||||
wrapperCodePathProperty.PropertyFieldWithDefaultText(m_WrapperCodePathLabel, defaultFileName);
|
||||
|
||||
if (GUILayout.Button("…", EditorStyles.miniButton, GUILayout.MaxWidth(20)))
|
||||
{
|
||||
var fileName = EditorUtility.SaveFilePanel("Location for generated C# file",
|
||||
Path.GetDirectoryName(defaultFileName),
|
||||
Path.GetFileName(defaultFileName), "cs");
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
if (fileName.StartsWith(Application.dataPath))
|
||||
fileName = "Assets/" + fileName.Substring(Application.dataPath.Length + 1);
|
||||
|
||||
wrapperCodePathProperty.stringValue = fileName;
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
string typeName = null;
|
||||
if (inputActionAsset != null)
|
||||
typeName = CSharpCodeHelpers.MakeTypeName(inputActionAsset?.name);
|
||||
wrapperClassNameProperty.PropertyFieldWithDefaultText(m_WrapperClassNameLabel, typeName ?? "<Class name>");
|
||||
|
||||
if (!CSharpCodeHelpers.IsEmptyOrProperIdentifier(wrapperClassNameProperty.stringValue))
|
||||
EditorGUILayout.HelpBox("Must be a valid C# identifier", MessageType.Error);
|
||||
|
||||
wrapperCodeNamespaceProperty.PropertyFieldWithDefaultText(m_WrapperCodeNamespaceLabel, "<Global namespace>");
|
||||
|
||||
if (!CSharpCodeHelpers.IsEmptyOrProperNamespaceName(wrapperCodeNamespaceProperty.stringValue))
|
||||
EditorGUILayout.HelpBox("Must be a valid C# namespace name", MessageType.Error);
|
||||
}
|
||||
|
||||
// Using ApplyRevertGUI requires calling Update and ApplyModifiedProperties around the serializedObject,
|
||||
// and will print warning messages otherwise (see warning message in ApplyRevertGUI implementation).
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
ApplyRevertGUI();
|
||||
}
|
||||
|
||||
private InputActionAsset GetAsset()
|
||||
{
|
||||
return assetTarget as InputActionAsset;
|
||||
}
|
||||
|
||||
protected override bool ShouldHideOpenButton()
|
||||
{
|
||||
return IsProjectWideActionsAsset();
|
||||
}
|
||||
|
||||
private bool IsProjectWideActionsAsset()
|
||||
{
|
||||
return IsProjectWideActionsAsset(GetAsset());
|
||||
}
|
||||
|
||||
private static bool IsProjectWideActionsAsset(InputActionAsset asset)
|
||||
{
|
||||
return !ReferenceEquals(asset, null) && InputSystem.actions == asset;
|
||||
}
|
||||
|
||||
private string GetOpenEditorButtonText(InputActionAsset asset)
|
||||
{
|
||||
if (IsProjectWideActionsAsset(asset))
|
||||
return "Edit in Project Settings Window";
|
||||
|
||||
return "Edit Asset";
|
||||
}
|
||||
|
||||
private static void OpenEditor(InputActionAsset asset)
|
||||
{
|
||||
// Redirect to Project-settings Input Actions editor if this is the project-wide actions asset
|
||||
if (IsProjectWideActionsAsset(asset))
|
||||
{
|
||||
SettingsService.OpenProjectSettings(InputSettingsPath.kSettingsRootPath);
|
||||
return;
|
||||
}
|
||||
|
||||
InputActionsEditorWindow.OpenEditor(asset);
|
||||
}
|
||||
|
||||
private readonly GUIContent m_GenerateWrapperCodeLabel = EditorGUIUtility.TrTextContent("Generate C# Class");
|
||||
private readonly GUIContent m_WrapperCodePathLabel = EditorGUIUtility.TrTextContent("C# Class File");
|
||||
private readonly GUIContent m_WrapperClassNameLabel = EditorGUIUtility.TrTextContent("C# Class Name");
|
||||
private readonly GUIContent m_WrapperCodeNamespaceLabel = EditorGUIUtility.TrTextContent("C# Class Namespace");
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50db2526021f1429e84687ad14aa0855
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb6feb90190641c19dce92f09614edd4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine.InputSystem.LowLevel;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Reporting;
|
||||
using UnityEditor.Compilation;
|
||||
using UnityEditor.UnityLinker;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Input system uses runtime reflection to instantiate and discover some capabilities like layouts, processors, interactions, etc.
|
||||
/// Managed linker on high stripping modes is very keen on removing parts of classes or whole classes.
|
||||
/// One way to preserve the classes is to put [Preserve] on class itself and every field/property we're interested in,
|
||||
/// this was proven to be error prone as it's easy to forget an attribute and tedious as everything needs an attribute now.
|
||||
///
|
||||
/// Instead this LinkFileGenerator inspects all types in the domain, and if they could be used via reflection,
|
||||
/// we preserve them in all entirety.
|
||||
///
|
||||
/// In a long run we would like to remove usage of reflection all together, and then this mechanism will be gone too.
|
||||
///
|
||||
/// Beware, this uses "AppDomain.CurrentDomain.GetAssemblies" which returns editor assemblies,
|
||||
/// but not all classes are available on all platforms, most of platform specific code is wrapped into defines like
|
||||
/// "#if UNITY_EDITOR || UNITY_IOS || PACKAGE_DOCS_GENERATION", and when compiling for Android,
|
||||
/// that particular class wouldn't be available in the final executable, though our link.xml here would still specify it,
|
||||
/// potentially creating linker warnings that we need to later ignore.
|
||||
/// </summary>
|
||||
internal class LinkFileGenerator : IUnityLinkerProcessor
|
||||
{
|
||||
public int callbackOrder => 0;
|
||||
|
||||
public string GenerateAdditionalLinkXmlFile(BuildReport report, UnityLinkerBuildPipelineData data)
|
||||
{
|
||||
var currentAssemblyName = typeof(UnityEngine.InputSystem.InputSystem).Assembly.GetName().Name;
|
||||
|
||||
var typesByAssemblies = new Dictionary<System.Reflection.Assembly, Type[]>();
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip any assembly that doesn't reference the input system assembly.
|
||||
if (assembly.GetName().Name != currentAssemblyName && !assembly
|
||||
.GetReferencedAssemblies().Any(x => x.Name == currentAssemblyName))
|
||||
continue;
|
||||
|
||||
var types = assembly.GetTypes().Where(ShouldPreserveType).ToArray();
|
||||
if (types.Length > 0)
|
||||
typesByAssemblies.Add(assembly, types);
|
||||
}
|
||||
catch (ReflectionTypeLoadException)
|
||||
{
|
||||
Debug.LogWarning($"Couldn't load types from assembly: {assembly.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<linker>");
|
||||
|
||||
foreach (var assembly in typesByAssemblies.Keys.OrderBy(a => a.GetName().Name))
|
||||
{
|
||||
sb.AppendLine($" <assembly fullname=\"{assembly.GetName().Name}\">");
|
||||
|
||||
var types = typesByAssemblies[assembly];
|
||||
foreach (var type in types.OrderBy(t => t.FullName))
|
||||
sb.AppendLine(
|
||||
$" <type fullname=\"{FormatForXml(ToCecilName(type.FullName))}\" preserve=\"all\"/>");
|
||||
|
||||
sb.AppendLine(" </assembly>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</linker>");
|
||||
|
||||
var linkXmlDirectory = Path.Combine(Application.dataPath, "..", "Library", "InputSystem");
|
||||
var linkXmlFile = Path.Combine(linkXmlDirectory, $"{data.target}Link.xml");
|
||||
|
||||
Directory.CreateDirectory(linkXmlDirectory);
|
||||
File.WriteAllText(linkXmlFile, sb.ToString());
|
||||
return linkXmlFile;
|
||||
}
|
||||
|
||||
static bool IsTypeUsedViaReflectionByInputSystem(Type type)
|
||||
{
|
||||
return type.IsSubclassOf(typeof(InputControl)) ||
|
||||
typeof(IInputStateTypeInfo).IsAssignableFrom(type) ||
|
||||
typeof(IInputInteraction).IsAssignableFrom(type) ||
|
||||
typeof(InputProcessor).IsAssignableFrom(type) ||
|
||||
typeof(InputBindingComposite).IsAssignableFrom(type) ||
|
||||
type.GetCustomAttributes<InputControlAttribute>().Any();
|
||||
}
|
||||
|
||||
static bool IsFieldRelatedToControlLayouts(FieldInfo field)
|
||||
{
|
||||
return IsTypeUsedViaReflectionByInputSystem(field.GetType()) ||
|
||||
field.GetCustomAttributes<InputControlAttribute>().Any();
|
||||
}
|
||||
|
||||
static bool IsPropertyRelatedToControlLayouts(PropertyInfo property)
|
||||
{
|
||||
return IsTypeUsedViaReflectionByInputSystem(property.GetType()) ||
|
||||
property.GetCustomAttributes<InputControlAttribute>().Any();
|
||||
}
|
||||
|
||||
static bool ShouldPreserveType(Type type)
|
||||
{
|
||||
if (IsTypeUsedViaReflectionByInputSystem(type))
|
||||
return true;
|
||||
|
||||
foreach (var field in type.GetFields())
|
||||
if (IsFieldRelatedToControlLayouts(field))
|
||||
return true;
|
||||
|
||||
foreach (var property in type.GetProperties())
|
||||
if (IsPropertyRelatedToControlLayouts(property))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static string ToCecilName(string fullTypeName)
|
||||
{
|
||||
return fullTypeName.Replace('+', '/');
|
||||
}
|
||||
|
||||
static string FormatForXml(string value)
|
||||
{
|
||||
return value.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
}
|
||||
|
||||
public void OnBeforeRun(BuildReport report, UnityLinkerBuildPipelineData data)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnAfterRun(BuildReport report, UnityLinkerBuildPipelineData data)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cec9e0ea8cd94017b8667cd540d61948
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b01eed8192c9f442b820a7af87ac73f8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal interface IInputControlPickerLayout
|
||||
{
|
||||
void AddControlItem(InputControlPickerDropdown dropdown, DeviceDropdownItem parent,
|
||||
ControlDropdownItem parentControl,
|
||||
InputControlLayout.ControlItem control, string device, string usage, bool searchable);
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f4390a0cc25d424d9adc1d6d6699a1f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal abstract class InputControlDropdownItem : AdvancedDropdownItem
|
||||
{
|
||||
protected string m_ControlPath;
|
||||
protected string m_Device;
|
||||
protected string m_Usage;
|
||||
protected bool m_Searchable;
|
||||
|
||||
public string controlPath => m_ControlPath;
|
||||
|
||||
public virtual string controlPathWithDevice
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = new StringBuilder($"<{m_Device}>");
|
||||
if (!string.IsNullOrEmpty(m_Usage))
|
||||
path.Append($"{{{m_Usage}}}");
|
||||
if (!string.IsNullOrEmpty(m_ControlPath))
|
||||
path.Append($"/{m_ControlPath}");
|
||||
return path.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public override string searchableName
|
||||
{
|
||||
get
|
||||
{
|
||||
// ToHumanReadableString is expensive, especially given that we build the whole tree
|
||||
// every time the control picker comes up. Build searchable names only on demand
|
||||
// to save some time.
|
||||
if (m_SearchableName == null)
|
||||
{
|
||||
if (m_Searchable)
|
||||
m_SearchableName = InputControlPath.ToHumanReadableString(controlPathWithDevice);
|
||||
else
|
||||
m_SearchableName = string.Empty;
|
||||
}
|
||||
return m_SearchableName;
|
||||
}
|
||||
}
|
||||
|
||||
protected InputControlDropdownItem(string name)
|
||||
: base(name) {}
|
||||
}
|
||||
|
||||
// NOTE: Optional control items, unlike normal control items, are displayed with their internal control
|
||||
// names rather that their display names. The reason is that we're looking at controls that have
|
||||
// the same internal name in one or more derived layouts but each of those derived layouts may
|
||||
// give the control a different display name.
|
||||
//
|
||||
// Also, if we generate a control path for an optional binding, InputControlPath.ToHumanReadableName()
|
||||
// not find the referenced control on the referenced device layout and will thus not be able to
|
||||
// find a display name for it either. So, in the binding UI, these paths will also show with their
|
||||
// internal control names rather than display names.
|
||||
internal sealed class OptionalControlDropdownItem : InputControlDropdownItem
|
||||
{
|
||||
public OptionalControlDropdownItem(EditorInputControlLayoutCache.OptionalControl optionalControl, string deviceControlId, string commonUsage)
|
||||
: base(optionalControl.name)
|
||||
{
|
||||
m_ControlPath = optionalControl.name;
|
||||
m_Device = deviceControlId;
|
||||
m_Usage = commonUsage;
|
||||
// Not searchable.
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ControlUsageDropdownItem : InputControlDropdownItem
|
||||
{
|
||||
public override string controlPathWithDevice => BuildControlPath();
|
||||
private string BuildControlPath()
|
||||
{
|
||||
if (m_Device == "*")
|
||||
{
|
||||
var path = new StringBuilder(m_Device);
|
||||
if (!string.IsNullOrEmpty(m_Usage))
|
||||
path.Append($"{{{m_Usage}}}");
|
||||
if (!string.IsNullOrEmpty(m_ControlPath))
|
||||
path.Append($"/{m_ControlPath}");
|
||||
return path.ToString();
|
||||
}
|
||||
else
|
||||
return base.controlPathWithDevice;
|
||||
}
|
||||
|
||||
public ControlUsageDropdownItem(string device, string usage, string controlUsage)
|
||||
: base(usage)
|
||||
{
|
||||
m_Device = string.IsNullOrEmpty(device) ? "*" : device;
|
||||
m_Usage = usage;
|
||||
m_ControlPath = $"{{{ controlUsage }}}";
|
||||
name = controlUsage;
|
||||
id = controlPathWithDevice.GetHashCode();
|
||||
m_Searchable = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DeviceDropdownItem : InputControlDropdownItem
|
||||
{
|
||||
public DeviceDropdownItem(InputControlLayout layout, string usage = null, bool searchable = true)
|
||||
: base(layout.m_DisplayName ?? ObjectNames.NicifyVariableName(layout.name))
|
||||
{
|
||||
m_Device = layout.name;
|
||||
m_Usage = usage;
|
||||
if (usage != null)
|
||||
name += " (" + usage + ")";
|
||||
id = name.GetHashCode();
|
||||
m_Searchable = searchable;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ControlDropdownItem : InputControlDropdownItem
|
||||
{
|
||||
public ControlDropdownItem(ControlDropdownItem parent, string controlName, string displayName, string device, string usage, bool searchable)
|
||||
: base("")
|
||||
{
|
||||
m_Device = device;
|
||||
m_Usage = usage;
|
||||
m_Searchable = searchable;
|
||||
|
||||
if (parent != null)
|
||||
m_ControlPath = $"{parent.controlPath}/{controlName}";
|
||||
else
|
||||
m_ControlPath = controlName;
|
||||
|
||||
name = !string.IsNullOrEmpty(displayName) ? displayName : ObjectNames.NicifyVariableName(controlName);
|
||||
|
||||
id = controlPathWithDevice.GetHashCode();
|
||||
indent = parent?.indent + 1 ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83070f7befc0b4deeb2cfde76b5a9002
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,223 @@
|
||||
#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor UI for editing control paths.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the implementation underlying <see cref="InputControlPathDrawer"/>. It is useful primarily when
|
||||
/// greater control is required than is offered by the <see cref="PropertyDrawer"/> mechanism. In particular,
|
||||
/// it allows applying additional constraints such as requiring control paths to match ...
|
||||
/// </remarks>
|
||||
public sealed class InputControlPathEditor : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize the control path editor.
|
||||
/// </summary>
|
||||
/// <param name="pathProperty"><see cref="string"/> type property that will receive the picked input control path.</param>
|
||||
/// <param name="pickerState">Persistent editing state of the path editor. Used to retain state across domain reloads.</param>
|
||||
/// <param name="onModified">Delegate that is called when the path has been modified.</param>
|
||||
/// <param name="label">Optional label to display instead of display name of <paramref name="pathProperty"/>.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="pathProperty"/> is <c>null</c>.</exception>
|
||||
public InputControlPathEditor(SerializedProperty pathProperty, InputControlPickerState pickerState, Action onModified, GUIContent label = null)
|
||||
{
|
||||
if (pathProperty == null)
|
||||
throw new ArgumentNullException(nameof(pathProperty));
|
||||
|
||||
this.pathProperty = pathProperty;
|
||||
this.onModified = onModified;
|
||||
m_PickerState = pickerState ?? new InputControlPickerState();
|
||||
m_PathLabel = label ?? new GUIContent(pathProperty.displayName, pathProperty.GetTooltip());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_PickerDropdown?.Dispose();
|
||||
}
|
||||
|
||||
public void SetControlPathsToMatch(IEnumerable<string> controlPaths)
|
||||
{
|
||||
m_ControlPathsToMatch = controlPaths.ToArray();
|
||||
m_PickerDropdown?.SetControlPathsToMatch(m_ControlPathsToMatch);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constrain the type of control layout that can be picked.
|
||||
/// </summary>
|
||||
/// <param name="expectedControlLayout">Name of the layout. This it the name as registered with
|
||||
/// <see cref="InputSystem.RegisterLayout"/>.</param>.
|
||||
/// <remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // Pick only button controls.
|
||||
/// editor.SetExpectedControlLayout("Button");
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </remarks>
|
||||
public void SetExpectedControlLayout(string expectedControlLayout)
|
||||
{
|
||||
m_ExpectedControlLayout = expectedControlLayout;
|
||||
m_PickerDropdown?.SetExpectedControlLayout(m_ExpectedControlLayout);
|
||||
}
|
||||
|
||||
public void SetExpectedControlLayoutFromAttribute()
|
||||
{
|
||||
var field = pathProperty.GetField();
|
||||
if (field == null)
|
||||
return;
|
||||
|
||||
var attribute = field.GetCustomAttribute<InputControlAttribute>();
|
||||
if (attribute != null)
|
||||
SetExpectedControlLayout(attribute.layout);
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
////FIXME: for some reason, the left edge doesn't align properly in GetRect()'s result; indentation issue?
|
||||
var rect = GUILayoutUtility.GetRect(0, EditorGUIUtility.singleLineHeight);
|
||||
rect.x += EditorGUIUtility.standardVerticalSpacing + 2;
|
||||
rect.width -= EditorGUIUtility.standardVerticalSpacing * 2 + 4;
|
||||
OnGUI(rect);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
public void OnGUI(Rect rect, GUIContent label = null, SerializedProperty property = null, Action modifiedCallback = null)
|
||||
{
|
||||
var pathLabel = label ?? m_PathLabel;
|
||||
var serializedProperty = property ?? pathProperty;
|
||||
|
||||
var lineRect = rect;
|
||||
var labelRect = lineRect;
|
||||
labelRect.width = EditorStyles.label.CalcSize(pathLabel).x + 20; // Fit to label with some padding
|
||||
EditorGUI.LabelField(labelRect, pathLabel);
|
||||
lineRect.x += labelRect.width;
|
||||
lineRect.width -= labelRect.width;
|
||||
|
||||
var bindingTextRect = lineRect;
|
||||
var editButtonRect = lineRect;
|
||||
|
||||
bindingTextRect.x = labelRect.x + labelRect.width; // Place directly after labelRect
|
||||
editButtonRect.x += lineRect.width - 20; // Place at the edge of the window to appear after bindingTextRect
|
||||
bindingTextRect.width = editButtonRect.x - bindingTextRect.x; // bindingTextRect fills remaining space between label and editButton
|
||||
editButtonRect.width = 20;
|
||||
editButtonRect.height = 15;
|
||||
|
||||
var path = String.Empty;
|
||||
try
|
||||
{
|
||||
path = serializedProperty.stringValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// This try-catch block is a temporary fix for ISX-1436
|
||||
// The plan is to convert InputControlPathEditor entirely to UITK and therefore this fix will
|
||||
// no longer be required.
|
||||
return;
|
||||
}
|
||||
|
||||
////TODO: this should be cached; generates needless GC churn
|
||||
var displayName = InputControlPath.ToHumanReadableString(path);
|
||||
|
||||
// Either show dropdown control that opens path picker or show path directly as
|
||||
// text, if manual path editing is toggled on.
|
||||
if (m_PickerState.manualPathEditMode)
|
||||
{
|
||||
////FIXME: for some reason the text field does not fill all the rect but rather adds large padding on the left
|
||||
bindingTextRect.x -= 15;
|
||||
bindingTextRect.width += 15;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
path = EditorGUI.DelayedTextField(bindingTextRect, path);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
serializedProperty.stringValue = path;
|
||||
serializedProperty.serializedObject.ApplyModifiedProperties();
|
||||
(modifiedCallback ?? onModified).Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dropdown that shows binding text and allows opening control picker.
|
||||
if (EditorGUI.DropdownButton(bindingTextRect, new GUIContent(displayName), FocusType.Keyboard))
|
||||
{
|
||||
SetExpectedControlLayoutFromAttribute(serializedProperty);
|
||||
////TODO: for bindings that are part of composites, use the layout information from the [InputControl] attribute on the field
|
||||
ShowDropdown(bindingTextRect, serializedProperty, modifiedCallback ?? onModified);
|
||||
}
|
||||
}
|
||||
|
||||
// Button to toggle between text edit mode.
|
||||
m_PickerState.manualPathEditMode = GUI.Toggle(editButtonRect, m_PickerState.manualPathEditMode, "T",
|
||||
EditorStyles.miniButton);
|
||||
}
|
||||
|
||||
private void ShowDropdown(Rect rect, SerializedProperty serializedProperty, Action modifiedCallback)
|
||||
{
|
||||
InputActionsEditorSettingsProvider.SetIMGUIDropdownVisible(true, false);
|
||||
IsShowingDropdown = true;
|
||||
|
||||
if (m_PickerDropdown == null)
|
||||
{
|
||||
m_PickerDropdown = new InputControlPickerDropdown(
|
||||
m_PickerState,
|
||||
path =>
|
||||
{
|
||||
serializedProperty.stringValue = path;
|
||||
m_PickerState.manualPathEditMode = false;
|
||||
modifiedCallback();
|
||||
});
|
||||
}
|
||||
|
||||
m_PickerDropdown.SetPickedCallback(path =>
|
||||
{
|
||||
serializedProperty.stringValue = path;
|
||||
m_PickerState.manualPathEditMode = false;
|
||||
modifiedCallback();
|
||||
});
|
||||
|
||||
m_PickerDropdown.SetControlPathsToMatch(m_ControlPathsToMatch);
|
||||
m_PickerDropdown.SetExpectedControlLayout(m_ExpectedControlLayout);
|
||||
|
||||
m_PickerDropdown.Show(rect);
|
||||
|
||||
IsShowingDropdown = false;
|
||||
}
|
||||
|
||||
private void SetExpectedControlLayoutFromAttribute(SerializedProperty property)
|
||||
{
|
||||
var field = property.GetField();
|
||||
if (field == null)
|
||||
return;
|
||||
|
||||
var attribute = field.GetCustomAttribute<InputControlAttribute>();
|
||||
if (attribute != null)
|
||||
SetExpectedControlLayout(attribute.layout);
|
||||
}
|
||||
|
||||
public SerializedProperty pathProperty { get; }
|
||||
public Action onModified { get; }
|
||||
|
||||
private GUIContent m_PathLabel;
|
||||
private string m_ExpectedControlLayout;
|
||||
private string[] m_ControlPathsToMatch;
|
||||
|
||||
private InputControlPickerDropdown m_PickerDropdown;
|
||||
private readonly InputControlPickerState m_PickerState;
|
||||
|
||||
/// <summary>
|
||||
/// This property is only set from this class in order to communicate that we're showing the dropdown at the moment
|
||||
/// It's employed to skip auto-saving, because that complicates updating the internal SerializedProperties.
|
||||
/// Unfortunately, we can't use IMGUIDropdownVisible from the setings provider because of the early-out logic in there.
|
||||
/// </summary>
|
||||
internal static bool IsShowingDropdown { get; private set; }
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 924d7617bcdc84f8ea1c0d0ce6466a30
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
|
||||
using System;
|
||||
|
||||
////REVIEW: should this be a PopupWindowContent?
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// A popup that allows picking input controls graphically.
|
||||
/// </summary>
|
||||
public sealed class InputControlPicker : IDisposable
|
||||
{
|
||||
public InputControlPicker(Mode mode, Action<string> onPick, InputControlPickerState state)
|
||||
{
|
||||
m_State = state ?? new InputControlPickerState();
|
||||
m_Dropdown = new InputControlPickerDropdown(state, onPick, mode: mode);
|
||||
}
|
||||
|
||||
public void Show(Rect rect)
|
||||
{
|
||||
m_Dropdown.Show(rect);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_Dropdown?.Dispose();
|
||||
}
|
||||
|
||||
public InputControlPickerState state => m_State;
|
||||
|
||||
private readonly InputControlPickerDropdown m_Dropdown;
|
||||
private readonly InputControlPickerState m_State;
|
||||
|
||||
public enum Mode
|
||||
{
|
||||
PickControl,
|
||||
PickDevice,
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e35c175d4f60c4820a36054488cb5524
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,645 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine.InputSystem.Controls;
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
////TODO: have tooltips on each entry in the picker
|
||||
|
||||
////TODO: find better way to present controls when filtering to specific devices
|
||||
|
||||
////REVIEW: if there's only a single device in the picker, automatically go into it?
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class InputControlPickerDropdown : AdvancedDropdown, IDisposable
|
||||
{
|
||||
public InputControlPickerDropdown(
|
||||
InputControlPickerState state,
|
||||
Action<string> onPickCallback,
|
||||
InputControlPicker.Mode mode = InputControlPicker.Mode.PickControl)
|
||||
: base(state.advancedDropdownState)
|
||||
{
|
||||
m_Gui = new InputControlPickerGUI(this);
|
||||
|
||||
minimumSize = new Vector2(275, 300);
|
||||
maximumSize = new Vector2(0, 300);
|
||||
|
||||
m_OnPickCallback = onPickCallback;
|
||||
m_Mode = mode;
|
||||
}
|
||||
|
||||
public void SetControlPathsToMatch(string[] controlPathsToMatch)
|
||||
{
|
||||
m_ControlPathsToMatch = controlPathsToMatch;
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void SetExpectedControlLayout(string expectedControlLayout)
|
||||
{
|
||||
m_ExpectedControlLayout = expectedControlLayout;
|
||||
|
||||
if (string.Equals(expectedControlLayout, "InputDevice", StringComparison.InvariantCultureIgnoreCase))
|
||||
m_ExpectedControlType = typeof(InputDevice);
|
||||
else
|
||||
m_ExpectedControlType = !string.IsNullOrEmpty(expectedControlLayout)
|
||||
? InputSystem.s_Manager.m_Layouts.GetControlTypeForLayout(new InternedString(expectedControlLayout))
|
||||
: null;
|
||||
|
||||
// If the layout is for a device, automatically switch to device
|
||||
// picking mode.
|
||||
if (m_ExpectedControlType != null && typeof(InputDevice).IsAssignableFrom(m_ExpectedControlType))
|
||||
m_Mode = InputControlPicker.Mode.PickDevice;
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void SetPickedCallback(Action<string> action)
|
||||
{
|
||||
m_OnPickCallback = action;
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
InputActionsEditorSettingsProvider.SetIMGUIDropdownVisible(false, false);
|
||||
m_RebindingOperation?.Dispose();
|
||||
m_RebindingOperation = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
m_RebindingOperation?.Dispose();
|
||||
}
|
||||
|
||||
protected override AdvancedDropdownItem BuildRoot()
|
||||
{
|
||||
var root = new AdvancedDropdownItem(string.Empty);
|
||||
|
||||
// Usages.
|
||||
if (m_Mode != InputControlPicker.Mode.PickDevice)
|
||||
{
|
||||
var usages = BuildTreeForControlUsages();
|
||||
if (usages.children.Any())
|
||||
{
|
||||
root.AddChild(usages);
|
||||
root.AddSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
// Devices.
|
||||
AddItemsForDevices(root);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected override AdvancedDropdownItem BuildCustomSearch(string searchString,
|
||||
IEnumerable<AdvancedDropdownItem> elements)
|
||||
{
|
||||
if (!isListening)
|
||||
return null;
|
||||
|
||||
var root = new AdvancedDropdownItem(!string.IsNullOrEmpty(m_ExpectedControlLayout)
|
||||
? $"Listening for {m_ExpectedControlLayout}..."
|
||||
: "Listening for input...");
|
||||
|
||||
if (searchString == "\u0017")
|
||||
return root;
|
||||
|
||||
var paths = searchString.Substring(1).Split('\u0017');
|
||||
foreach (var element in elements)
|
||||
{
|
||||
if (element is ControlDropdownItem controlItem && paths.Any(x => controlItem.controlPathWithDevice == x))
|
||||
root.AddChild(element);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected override void ItemSelected(AdvancedDropdownItem item)
|
||||
{
|
||||
InputActionsEditorSettingsProvider.SetIMGUIDropdownVisible(false, true);
|
||||
var path = ((InputControlDropdownItem)item).controlPathWithDevice;
|
||||
m_OnPickCallback(path);
|
||||
}
|
||||
|
||||
private AdvancedDropdownItem BuildTreeForControlUsages(string device = "", string usage = "")
|
||||
{
|
||||
var usageRoot = new AdvancedDropdownItem("Usages");
|
||||
foreach (var usageAndLayouts in EditorInputControlLayoutCache.allUsages)
|
||||
{
|
||||
if (usageAndLayouts.Item2.Any(LayoutMatchesExpectedControlLayoutFilter))
|
||||
{
|
||||
var child = new ControlUsageDropdownItem(device, usage, usageAndLayouts.Item1);
|
||||
usageRoot.AddChild(child);
|
||||
}
|
||||
}
|
||||
return usageRoot;
|
||||
}
|
||||
|
||||
private void AddItemsForDevices(AdvancedDropdownItem parent)
|
||||
{
|
||||
// Add devices that are marked as generic types of devices directly to the parent.
|
||||
// E.g. adds "Gamepad" and then underneath all the more specific types of gamepads.
|
||||
foreach (var deviceLayout in EditorInputControlLayoutCache.allLayouts
|
||||
.Where(x => x.isDeviceLayout && !x.isOverride && x.isGenericTypeOfDevice && !x.hideInUI)
|
||||
.OrderBy(a => a.displayName))
|
||||
{
|
||||
AddDeviceTreeItemRecursive(deviceLayout, parent);
|
||||
}
|
||||
|
||||
// We have devices that are based directly on InputDevice but are not marked as generic types
|
||||
// of devices (e.g. Vive Lighthouses). We do not want them to clutter the list at the root so we
|
||||
// put all of them in a group called "Other" at the end of the list.
|
||||
var otherGroup = new AdvancedDropdownItem("Other");
|
||||
foreach (var deviceLayout in EditorInputControlLayoutCache.allLayouts
|
||||
.Where(x => x.isDeviceLayout && !x.isOverride && !x.isGenericTypeOfDevice &&
|
||||
(x.type.BaseType == typeof(InputDevice) || x.type == typeof(InputDevice)) &&
|
||||
!x.hideInUI && !x.baseLayouts.Any()).OrderBy(a => a.displayName))
|
||||
{
|
||||
AddDeviceTreeItemRecursive(deviceLayout, otherGroup);
|
||||
}
|
||||
|
||||
if (otherGroup.children.Any())
|
||||
parent.AddChild(otherGroup);
|
||||
}
|
||||
|
||||
private void AddDeviceTreeItemRecursive(InputControlLayout layout, AdvancedDropdownItem parent, bool searchable = true)
|
||||
{
|
||||
// Find all layouts directly based on this one (ignoring overrides).
|
||||
var childLayouts = EditorInputControlLayoutCache.allLayouts
|
||||
.Where(x => x.isDeviceLayout && !x.isOverride && !x.hideInUI && x.baseLayouts.Contains(layout.name)).OrderBy(x => x.displayName);
|
||||
|
||||
// See if the entire tree should be excluded.
|
||||
var shouldIncludeDeviceLayout = ShouldIncludeDeviceLayout(layout);
|
||||
var shouldIncludeAtLeastOneChildLayout = childLayouts.Any(ShouldIncludeDeviceLayout);
|
||||
|
||||
if (!shouldIncludeDeviceLayout && !shouldIncludeAtLeastOneChildLayout)
|
||||
return;
|
||||
|
||||
// Add toplevel item for device.
|
||||
var deviceItem = new DeviceDropdownItem(layout, searchable: searchable);
|
||||
|
||||
var defaultControlPickerLayout = new DefaultInputControlPickerLayout();
|
||||
|
||||
// Add common usage variants of the device
|
||||
if (layout.commonUsages.Count > 0)
|
||||
{
|
||||
foreach (var usage in layout.commonUsages)
|
||||
{
|
||||
var usageItem = new DeviceDropdownItem(layout, usage);
|
||||
|
||||
// Add control usages to the device variants
|
||||
var deviceVariantControlUsages = BuildTreeForControlUsages(layout.name, usage);
|
||||
if (deviceVariantControlUsages.children.Any())
|
||||
{
|
||||
usageItem.AddChild(deviceVariantControlUsages);
|
||||
usageItem.AddSeparator();
|
||||
}
|
||||
|
||||
if (m_Mode == InputControlPicker.Mode.PickControl)
|
||||
AddControlTreeItemsRecursive(defaultControlPickerLayout, layout, usageItem, layout.name, usage, searchable);
|
||||
deviceItem.AddChild(usageItem);
|
||||
}
|
||||
deviceItem.AddSeparator();
|
||||
}
|
||||
|
||||
// Add control usages
|
||||
var deviceControlUsages = BuildTreeForControlUsages(layout.name);
|
||||
if (deviceControlUsages.children.Any())
|
||||
{
|
||||
deviceItem.AddChild(deviceControlUsages);
|
||||
deviceItem.AddSeparator();
|
||||
}
|
||||
|
||||
// Add controls.
|
||||
if (m_Mode != InputControlPicker.Mode.PickDevice)
|
||||
{
|
||||
// The keyboard is special in that we want to allow binding by display name (i.e. character
|
||||
// generated by a key) instead of only by physical key location. Also, we want to give an indication
|
||||
// of which specific key an entry refers to by taking the current keyboard layout into account.
|
||||
//
|
||||
// So what we do is add an extra level to the keyboard where key's can be bound by character
|
||||
// according to the current layout. And in the top level of the keyboard we display keys with
|
||||
// both physical and logical names.
|
||||
if (layout.type == typeof(Keyboard) && InputSystem.GetDevice<Keyboard>() != null)
|
||||
{
|
||||
var byLocationGroup = new AdvancedDropdownItem("By Location of Key (Using US Layout)");
|
||||
var byCharacterGroup = new AdvancedDropdownItem("By Character Mapped to Key");
|
||||
|
||||
deviceItem.AddChild(byLocationGroup);
|
||||
deviceItem.AddChild(byCharacterGroup);
|
||||
|
||||
var keyboard = InputSystem.GetDevice<Keyboard>();
|
||||
|
||||
AddCharacterKeyBindingsTo(byCharacterGroup, keyboard);
|
||||
AddPhysicalKeyBindingsTo(byLocationGroup, keyboard, searchable);
|
||||
|
||||
// AnyKey won't appear in either group. Add it explicitly.
|
||||
AddControlItem(defaultControlPickerLayout, deviceItem, null,
|
||||
layout.FindControl(new InternedString("anyKey")).Value, layout.name, null, searchable);
|
||||
}
|
||||
else if (layout.type == typeof(Touchscreen))
|
||||
{
|
||||
AddControlTreeItemsRecursive(new TouchscreenControlPickerLayout(), layout, deviceItem, layout.name, null, searchable);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddControlTreeItemsRecursive(defaultControlPickerLayout, layout, deviceItem, layout.name, null, searchable);
|
||||
}
|
||||
}
|
||||
|
||||
// Add child items.
|
||||
var isFirstChild = true;
|
||||
foreach (var childLayout in childLayouts)
|
||||
{
|
||||
if (!ShouldIncludeDeviceLayout(childLayout))
|
||||
continue;
|
||||
|
||||
if (isFirstChild)
|
||||
deviceItem.AddSeparator("More Specific " + deviceItem.name.GetPlural());
|
||||
isFirstChild = false;
|
||||
|
||||
AddDeviceTreeItemRecursive(childLayout, deviceItem, searchable && !childLayout.isGenericTypeOfDevice);
|
||||
}
|
||||
|
||||
// When picking devices, it must be possible to select a device that itself has more specific types
|
||||
// of devices underneath it. However in the dropdown, such a device will be a foldout and not itself
|
||||
// be selectable. We solve this problem by adding an entry for the device underneath the device
|
||||
// itself (e.g. "Gamepad >> Gamepad").
|
||||
if (m_Mode == InputControlPicker.Mode.PickDevice && deviceItem.m_Children.Count > 0)
|
||||
{
|
||||
var item = new DeviceDropdownItem(layout);
|
||||
deviceItem.m_Children.Insert(0, item);
|
||||
}
|
||||
|
||||
if (deviceItem.m_Children.Count > 0 || m_Mode == InputControlPicker.Mode.PickDevice)
|
||||
parent.AddChild(deviceItem);
|
||||
}
|
||||
|
||||
private void AddControlTreeItemsRecursive(IInputControlPickerLayout controlPickerLayout, InputControlLayout layout,
|
||||
DeviceDropdownItem parent, string device, string usage, bool searchable, ControlDropdownItem parentControl = null)
|
||||
{
|
||||
foreach (var control in layout.controls.OrderBy(a => a.name))
|
||||
{
|
||||
if (control.isModifyingExistingControl)
|
||||
continue;
|
||||
|
||||
// Skip variants except the default variant and variants dictated by the layout itself.
|
||||
if (!control.variants.IsEmpty() && control.variants != InputControlLayout.DefaultVariant
|
||||
&& (layout.variants.IsEmpty() || !InputControlLayout.VariantsMatch(layout.variants, control.variants)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
controlPickerLayout.AddControlItem(this, parent, parentControl, control, device, usage, searchable);
|
||||
}
|
||||
|
||||
// Add optional controls for devices.
|
||||
var optionalControls = EditorInputControlLayoutCache.GetOptionalControlsForLayout(layout.name);
|
||||
if (optionalControls.Any() && layout.isDeviceLayout)
|
||||
{
|
||||
var optionalGroup = new AdvancedDropdownItem("Optional Controls");
|
||||
foreach (var optionalControl in optionalControls)
|
||||
{
|
||||
////FIXME: this should list children, too
|
||||
////FIXME: this should handle arrays, too
|
||||
if (LayoutMatchesExpectedControlLayoutFilter(optionalControl.layout))
|
||||
{
|
||||
var child = new OptionalControlDropdownItem(optionalControl, device, usage);
|
||||
child.icon = EditorInputControlLayoutCache.GetIconForLayout(optionalControl.layout);
|
||||
optionalGroup.AddChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (optionalGroup.children.Any())
|
||||
{
|
||||
var deviceName = EditorInputControlLayoutCache.TryGetLayout(device).m_DisplayName ??
|
||||
ObjectNames.NicifyVariableName(device);
|
||||
parent.AddSeparator("Controls Present on More Specific " + deviceName.GetPlural());
|
||||
parent.AddChild(optionalGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void AddControlItem(IInputControlPickerLayout controlPickerLayout,
|
||||
DeviceDropdownItem parent, ControlDropdownItem parentControl,
|
||||
InputControlLayout.ControlItem control, string device, string usage, bool searchable,
|
||||
string controlNameOverride = default)
|
||||
{
|
||||
var controlName = controlNameOverride ?? control.name;
|
||||
|
||||
// If it's an array, generate a control entry for each array element.
|
||||
for (var i = 0; i < (control.isArray ? control.arraySize : 1); ++i)
|
||||
{
|
||||
var name = control.isArray ? controlName + i : controlName;
|
||||
var displayName = !string.IsNullOrEmpty(control.displayName)
|
||||
? (control.isArray ? $"{control.displayName} #{i}" : control.displayName)
|
||||
: name;
|
||||
|
||||
var child = new ControlDropdownItem(parentControl, name, displayName,
|
||||
device, usage, searchable);
|
||||
child.icon = EditorInputControlLayoutCache.GetIconForLayout(control.layout);
|
||||
var controlLayout = EditorInputControlLayoutCache.TryGetLayout(control.layout);
|
||||
|
||||
if (LayoutMatchesExpectedControlLayoutFilter(control.layout))
|
||||
parent.AddChild(child);
|
||||
else if (controlLayout.controls.Any(x => LayoutMatchesExpectedControlLayoutFilter(x.layout)))
|
||||
{
|
||||
child.enabled = false;
|
||||
parent.AddChild(child);
|
||||
}
|
||||
// Add children.
|
||||
if (controlLayout != null)
|
||||
AddControlTreeItemsRecursive(controlPickerLayout, controlLayout, parent, device, usage,
|
||||
searchable, child);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddPhysicalKeyBindingsTo(AdvancedDropdownItem parent, Keyboard keyboard, bool searchable)
|
||||
{
|
||||
foreach (var key in keyboard.children.OfType<KeyControl>())
|
||||
{
|
||||
// If the key has a display name that differs from the key name, show it in the UI.
|
||||
var displayName = key.m_DisplayNameFromLayout;
|
||||
var keyDisplayName = key.displayName;
|
||||
if (keyDisplayName.All(x => x.IsPrintable()) && string.Compare(keyDisplayName, displayName,
|
||||
StringComparison.InvariantCultureIgnoreCase) != 0)
|
||||
displayName = $"{displayName} (Current Layout: {key.displayName})";
|
||||
|
||||
// For left/right modifier keys, prepend artificial combined version.
|
||||
ButtonControl combinedVersion = null;
|
||||
if (key == keyboard.leftShiftKey)
|
||||
combinedVersion = keyboard.shiftKey;
|
||||
else if (key == keyboard.leftAltKey)
|
||||
combinedVersion = keyboard.altKey;
|
||||
else if (key == keyboard.leftCtrlKey)
|
||||
combinedVersion = keyboard.ctrlKey;
|
||||
if (combinedVersion != null)
|
||||
parent.AddChild(new ControlDropdownItem(null, combinedVersion.name, combinedVersion.displayName, keyboard.layout,
|
||||
"", searchable));
|
||||
|
||||
var item = new ControlDropdownItem(null, key.name, displayName,
|
||||
keyboard.layout, "", searchable);
|
||||
|
||||
parent.AddChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCharacterKeyBindingsTo(AdvancedDropdownItem parent, Keyboard keyboard)
|
||||
{
|
||||
foreach (var key in keyboard.children.OfType<KeyControl>())
|
||||
{
|
||||
if (!key.keyCode.IsTextInputKey())
|
||||
continue;
|
||||
|
||||
// We can only bind to characters that can be printed.
|
||||
var displayName = key.displayName;
|
||||
if (!displayName.All(x => x.IsPrintable()))
|
||||
continue;
|
||||
|
||||
if (displayName.Contains(')'))
|
||||
displayName = string.Join("", displayName.Select(x => "\\" + x));
|
||||
|
||||
////TODO: should be searchable; when searching, needs different display name
|
||||
var item = new ControlDropdownItem(null, $"#({displayName})", "", keyboard.layout, "", false);
|
||||
item.name = key.displayName;
|
||||
|
||||
parent.AddChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
private bool LayoutMatchesExpectedControlLayoutFilter(string layout)
|
||||
{
|
||||
if (m_ExpectedControlType == null)
|
||||
return true;
|
||||
|
||||
var layoutType = InputSystem.s_Manager.m_Layouts.GetControlTypeForLayout(new InternedString(layout));
|
||||
return m_ExpectedControlType.IsAssignableFrom(layoutType);
|
||||
}
|
||||
|
||||
private bool ShouldIncludeDeviceLayout(InputControlLayout layout)
|
||||
{
|
||||
if (layout.hideInUI)
|
||||
return false;
|
||||
|
||||
// By default, if a device has no (usable) controls, we don't want it listed in the control picker
|
||||
// except if we're picking devices.
|
||||
if (!layout.controls.Any(x => LayoutMatchesExpectedControlLayoutFilter(x.layout)) && layout.controls.Any(x => true) &&
|
||||
m_Mode != InputControlPicker.Mode.PickDevice)
|
||||
return false;
|
||||
|
||||
// If we have a device filter, see if we should ignore the device.
|
||||
if (m_ControlPathsToMatch != null && m_ControlPathsToMatch.Length > 0)
|
||||
{
|
||||
var matchesAnyInDeviceFilter = false;
|
||||
foreach (var entry in m_ControlPathsToMatch)
|
||||
{
|
||||
// Include the layout if it's in the inheritance hierarchy of the layout we expect (either below
|
||||
// or above it or, well, just right on it).
|
||||
var expectedLayout = InputControlPath.TryGetDeviceLayout(entry);
|
||||
if (!string.IsNullOrEmpty(expectedLayout) &&
|
||||
(expectedLayout == layout.name ||
|
||||
InputControlLayout.s_Layouts.IsBasedOn(layout.name, new InternedString(expectedLayout)) ||
|
||||
InputControlLayout.s_Layouts.IsBasedOn(new InternedString(expectedLayout), layout.name)))
|
||||
{
|
||||
matchesAnyInDeviceFilter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchesAnyInDeviceFilter)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void StartListening()
|
||||
{
|
||||
if (m_RebindingOperation == null)
|
||||
m_RebindingOperation = new InputActionRebindingExtensions.RebindingOperation();
|
||||
|
||||
////TODO: for keyboard, generate both possible paths (physical and by display name)
|
||||
|
||||
m_RebindingOperation.Reset();
|
||||
m_RebindingOperation
|
||||
.WithExpectedControlType(m_ExpectedControlLayout)
|
||||
// Require minimum actuation of 0.15f. This is after deadzoning has been applied.
|
||||
.WithMagnitudeHavingToBeGreaterThan(0.15f)
|
||||
////REVIEW: should we exclude only the system's active pointing device?
|
||||
// With the mouse operating the UI, its cursor control is too fickle a thing to
|
||||
// bind to. Ignore mouse position and delta and clicks.
|
||||
// NOTE: We go for all types of pointers here, not just mice.
|
||||
.WithControlsExcluding("<Pointer>/position")
|
||||
.WithControlsExcluding("<Pointer>/delta")
|
||||
.WithControlsExcluding("<Pointer>/press")
|
||||
.WithControlsExcluding("<Pointer>/clickCount")
|
||||
.WithControlsExcluding("<Pointer>/{PrimaryAction}")
|
||||
.WithControlsExcluding("<Mouse>/scroll")
|
||||
.OnPotentialMatch(
|
||||
operation =>
|
||||
{
|
||||
// We never really complete the pick but keep listening for as long as the "Interactive"
|
||||
// button is toggled on.
|
||||
|
||||
Repaint();
|
||||
})
|
||||
.OnCancel(
|
||||
operation =>
|
||||
{
|
||||
Repaint();
|
||||
})
|
||||
.OnApplyBinding(
|
||||
(operation, newPath) =>
|
||||
{
|
||||
// This is never invoked (because we don't complete the pick) but we need it nevertheless
|
||||
// as RebindingOperation requires the callback if we don't supply an action to apply the binding to.
|
||||
});
|
||||
|
||||
// If we have control paths to match, pass them on.
|
||||
if (m_ControlPathsToMatch.LengthSafe() > 0)
|
||||
m_ControlPathsToMatch.Select(x => m_RebindingOperation.WithControlsHavingToMatchPath(x));
|
||||
|
||||
m_RebindingOperation.Start();
|
||||
}
|
||||
|
||||
private void StopListening()
|
||||
{
|
||||
m_RebindingOperation?.Cancel();
|
||||
}
|
||||
|
||||
// This differs from RebindingOperation.GeneratePathForControl in that it cycles through all
|
||||
// layouts in the inheritance chain and generates a path for each one that contains the given control.
|
||||
private static IEnumerable<string> GeneratePossiblePathsForControl(InputControl control)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var deviceLayoutName = control.device.m_Layout;
|
||||
do
|
||||
{
|
||||
// Skip layout if it is supposed to be hidden in the UI.
|
||||
var layout = EditorInputControlLayoutCache.TryGetLayout(deviceLayoutName);
|
||||
if (layout.hideInUI)
|
||||
continue;
|
||||
|
||||
builder.Length = 0;
|
||||
yield return control.BuildPath(deviceLayoutName, builder);
|
||||
}
|
||||
while (InputControlLayout.s_Layouts.baseLayoutTable.TryGetValue(deviceLayoutName, out deviceLayoutName));
|
||||
}
|
||||
|
||||
private Action<string> m_OnPickCallback;
|
||||
private InputControlPicker.Mode m_Mode;
|
||||
private string[] m_ControlPathsToMatch;
|
||||
private string m_ExpectedControlLayout;
|
||||
private Type m_ExpectedControlType;
|
||||
private InputActionRebindingExtensions.RebindingOperation m_RebindingOperation;
|
||||
|
||||
private bool isListening => m_RebindingOperation != null && m_RebindingOperation.started;
|
||||
|
||||
private class InputControlPickerGUI : AdvancedDropdownGUI
|
||||
{
|
||||
private readonly InputControlPickerDropdown m_Owner;
|
||||
|
||||
public InputControlPickerGUI(InputControlPickerDropdown owner)
|
||||
{
|
||||
m_Owner = owner;
|
||||
}
|
||||
|
||||
internal override void BeginDraw(EditorWindow window)
|
||||
{
|
||||
if (Event.current.isKey && Event.current.keyCode == KeyCode.Escape)
|
||||
{
|
||||
window.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_Owner.isListening)
|
||||
{
|
||||
// Eat key events to suppress the editor from passing them to the OS
|
||||
// (causing beeps or menu commands being triggered).
|
||||
if (Event.current.isKey)
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
|
||||
internal override string DrawSearchFieldControl(string searchString)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
var isListening = false;
|
||||
|
||||
// When picking controls, have a "Listen" button that allows listening for input.
|
||||
if (m_Owner.m_Mode == InputControlPicker.Mode.PickControl)
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope(GUILayout.MaxWidth(50)))
|
||||
{
|
||||
GUILayout.Space(4);
|
||||
var isListeningOld = m_Owner.isListening;
|
||||
var isListeningNew = GUILayout.Toggle(isListeningOld, "Listen",
|
||||
EditorStyles.miniButton, GUILayout.MaxWidth(50));
|
||||
|
||||
if (isListeningOld != isListeningNew)
|
||||
{
|
||||
if (isListeningNew)
|
||||
{
|
||||
m_Owner.StartListening();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Owner.StopListening();
|
||||
searchString = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
isListening = isListeningNew;
|
||||
}
|
||||
}
|
||||
|
||||
////FIXME: the search box doesn't clear out when listening; no idea why the new string isn't taking effect
|
||||
EditorGUI.BeginDisabledGroup(isListening);
|
||||
var newSearchString = base.DrawSearchFieldControl(isListening ? string.Empty : searchString);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
if (isListening)
|
||||
{
|
||||
var rebind = m_Owner.m_RebindingOperation;
|
||||
return "\u0017" + string.Join("\u0017",
|
||||
rebind.candidates.SelectMany(x => GeneratePossiblePathsForControl(x).Reverse()));
|
||||
}
|
||||
|
||||
return newSearchString;
|
||||
}
|
||||
}
|
||||
|
||||
internal override void DrawItem(AdvancedDropdownItem item, string name, Texture2D icon, bool enabled,
|
||||
bool drawArrow, bool selected, bool hasSearch, bool richText = false)
|
||||
{
|
||||
if (hasSearch && item is InputControlDropdownItem viewItem)
|
||||
name = viewItem.searchableName;
|
||||
|
||||
base.DrawItem(item, name, icon, enabled, drawArrow, selected, hasSearch);
|
||||
}
|
||||
|
||||
internal override void DrawFooter(AdvancedDropdownItem selectedItem)
|
||||
{
|
||||
//dun work because there is no selection
|
||||
if (selectedItem is ControlDropdownItem controlItem)
|
||||
{
|
||||
var content = new GUIContent(controlItem.controlPath);
|
||||
var rect = GUILayoutUtility.GetRect(content, headerStyle, GUILayout.ExpandWidth(true));
|
||||
EditorGUI.TextField(rect, controlItem.controlPath, headerStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Styles
|
||||
{
|
||||
public static readonly GUIStyle waitingForInputLabel = new GUIStyle("WhiteBoldLabel").WithFontSize(22);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db7a158b580fb4518ae82aa029a54892
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
#if UNITY_EDITOR || PACKAGE_DOCS_GENERATION
|
||||
using System;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Persistent state for <see cref="InputControlPathEditor"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class encapsulates the viewing state for an input control picker.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class InputControlPickerState
|
||||
{
|
||||
internal AdvancedDropdownState advancedDropdownState => m_AdvancedDropdownState;
|
||||
|
||||
internal bool manualPathEditMode
|
||||
{
|
||||
get => m_ManualPathEditMode;
|
||||
set => m_ManualPathEditMode = value;
|
||||
}
|
||||
|
||||
[SerializeField] private AdvancedDropdownState m_AdvancedDropdownState = new AdvancedDropdownState();
|
||||
[SerializeField] private bool m_ManualPathEditMode;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfa8add0e3ed349708aeadc8b8c1ad0f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b630b769a2a8aa499ce2d2093f40500
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class DefaultInputControlPickerLayout : IInputControlPickerLayout
|
||||
{
|
||||
public void AddControlItem(InputControlPickerDropdown dropdown, DeviceDropdownItem parent,
|
||||
ControlDropdownItem parentControl,
|
||||
InputControlLayout.ControlItem control, string device, string usage, bool searchable)
|
||||
{
|
||||
dropdown.AddControlItem(this, parent, parentControl, control, device, usage, searchable);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7717bf968da90024b9c047ba1a3394ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,36 @@
|
||||
#if UNITY_EDITOR
|
||||
using UnityEngine.InputSystem.Layouts;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class TouchscreenControlPickerLayout : IInputControlPickerLayout
|
||||
{
|
||||
public void AddControlItem(InputControlPickerDropdown dropdown, DeviceDropdownItem parent, ControlDropdownItem parentControl,
|
||||
InputControlLayout.ControlItem control, string device, string usage, bool searchable)
|
||||
{
|
||||
// for the Press control, show two variants, one for single touch presses, and another for multi-touch presses
|
||||
if (control.displayName == "Press")
|
||||
{
|
||||
dropdown.AddControlItem(this, parent, parentControl, new InputControlLayout.ControlItem
|
||||
{
|
||||
name = new InternedString("Press"),
|
||||
displayName = new InternedString("Press (Single touch)"),
|
||||
layout = control.layout
|
||||
}, device, usage, searchable);
|
||||
|
||||
dropdown.AddControlItem(this, parent, parentControl, new InputControlLayout.ControlItem
|
||||
{
|
||||
name = new InternedString("Press"),
|
||||
displayName = new InternedString("Press (Multi-touch)"),
|
||||
layout = control.layout
|
||||
}, device, usage, searchable, "touch*/Press");
|
||||
}
|
||||
else
|
||||
{
|
||||
dropdown.AddControlItem(this, parent, parentControl, control, device, usage, searchable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 224236193350c244abab484f4514df16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 461c380dccba54a569c07d9810dd3b32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,62 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
|
||||
////TODO: survive domain reload properly
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class InputActionDebuggerWindow : EditorWindow
|
||||
{
|
||||
[NonSerialized] private InputAction m_Action = null;
|
||||
|
||||
public static void CreateOrShowExisting(InputAction action)
|
||||
{
|
||||
if (action == null)
|
||||
throw new System.ArgumentNullException(nameof(action));
|
||||
|
||||
// See if we have an existing window for the action and if so pop it in front.
|
||||
if (s_OpenDebuggerWindows != null)
|
||||
{
|
||||
for (var i = 0; i < s_OpenDebuggerWindows.Count; ++i)
|
||||
{
|
||||
var existingWindow = s_OpenDebuggerWindows[i];
|
||||
if (ReferenceEquals(existingWindow.m_Action, action))
|
||||
{
|
||||
existingWindow.Show();
|
||||
existingWindow.Focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No, so create a new one.
|
||||
var window = CreateInstance<InputActionDebuggerWindow>();
|
||||
window.Show();
|
||||
window.titleContent = new GUIContent(action.name);
|
||||
window.AddToList();
|
||||
}
|
||||
|
||||
public void OnGUI()
|
||||
{
|
||||
}
|
||||
|
||||
private static List<InputActionDebuggerWindow> s_OpenDebuggerWindows;
|
||||
|
||||
private void AddToList()
|
||||
{
|
||||
if (s_OpenDebuggerWindows == null)
|
||||
s_OpenDebuggerWindows = new List<InputActionDebuggerWindow>();
|
||||
if (!s_OpenDebuggerWindows.Contains(this))
|
||||
s_OpenDebuggerWindows.Add(this);
|
||||
}
|
||||
|
||||
private void RemoveFromList()
|
||||
{
|
||||
if (s_OpenDebuggerWindows != null)
|
||||
s_OpenDebuggerWindows.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // UNITY_EDITOR
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d659406a304446c599b9ec454663c46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||