This commit is contained in:
陈思海 2026-03-09 20:38:15 +08:00
parent 8547e57f55
commit 6f93b0fc15
17 changed files with 973 additions and 650 deletions

View File

@ -1,15 +1,15 @@
using System; using System;
using System.Linq;
using AlicizaX.InputGlyph;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
public static class GlyphService public static class GlyphService
{ {
/// <summary> // Cached device hint arrays to avoid allocations
/// 可选的全局数据库引用。你可以通过场景内的启动组件在 Awake 时赋值, private static readonly string[] KeyboardHints = { "Keyboard", "Mouse" };
/// 或者在调用每个方法时传入 InputGlyphDatabase 参数(见方法签名)。 private static readonly string[] XboxHints = { "XInput", "Xbox", "Gamepad" };
/// </summary> private static readonly string[] PlayStationHints = { "DualShock", "DualSense", "PlayStation", "Gamepad" };
private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' };
public static InputGlyphDatabase Database public static InputGlyphDatabase Database
{ {
get get
@ -26,22 +26,22 @@ public static class GlyphService
private static InputGlyphDatabase _database; private static InputGlyphDatabase _database;
public static string GetBindingControlPath(InputAction action, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) public static string GetBindingControlPath(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{ {
if (action == null) return string.Empty; if (action == null) return string.Empty;
var binding = GetBindingControl(action, deviceOverride); var binding = GetBindingControl(action, compositePartName, deviceOverride);
return binding.hasOverrides ? binding.effectivePath : binding.path; return binding.hasOverrides ? binding.effectivePath : binding.path;
} }
public static bool TryGetTMPTagForActionPath(InputActionReference reference, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null) public static bool TryGetTMPTagForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out string tag, out string displayFallback, InputGlyphDatabase db = null)
{ {
string path = GetBindingControlPath(reference, device); string path = GetBindingControlPath(reference, compositePartName, device);
return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db); return TryGetTMPTagForActionPath(path, device, out tag, out displayFallback, db);
} }
public static bool TryGetUISpriteForActionPath(InputActionReference reference, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null) public static bool TryGetUISpriteForActionPath(InputAction reference, string compositePartName, InputDeviceWatcher.InputDeviceCategory device, out Sprite sprite, InputGlyphDatabase db = null)
{ {
string path = GetBindingControlPath(reference, device); string path = GetBindingControlPath(reference, compositePartName, device);
return TryGetUISpriteForActionPath(path, device, out sprite, db); return TryGetUISpriteForActionPath(path, device, out sprite, db);
} }
@ -70,52 +70,68 @@ public static class GlyphService
} }
static InputBinding GetBindingControl(InputAction action, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null) static InputBinding GetBindingControl(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory? deviceOverride = null)
{ {
if (action == null) return default; if (action == null) return default;
var curCategory = deviceOverride ?? InputDeviceWatcher.CurrentCategory; var curCategory = deviceOverride ?? InputDeviceWatcher.CurrentCategory;
var hints = GetDeviceHintsForCategory(curCategory); var hints = GetDeviceHintsForCategory(curCategory);
foreach (var binding in action.bindings) foreach (var b in action.bindings)
{ {
var deviceName = binding.path ?? string.Empty; if (!string.IsNullOrEmpty(compositePartName))
if (hints.Any(h => deviceName.IndexOf(h, StringComparison.OrdinalIgnoreCase) >= 0))
{ {
return binding; if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
} }
// Replace LINQ Any() to avoid delegate allocation
if (!string.IsNullOrEmpty(b.path) && ContainsAnyHint(b.path, hints)) return b;
if (!string.IsNullOrEmpty(b.effectivePath) && ContainsAnyHint(b.effectivePath, hints)) return b;
} }
return default; return default;
} }
// Helper method to avoid LINQ Any() allocation
static bool ContainsAnyHint(string path, string[] hints)
{
for (int i = 0; i < hints.Length; i++)
{
if (path.IndexOf(hints[i], StringComparison.OrdinalIgnoreCase) >= 0)
return true;
}
return false;
}
static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat) static string[] GetDeviceHintsForCategory(InputDeviceWatcher.InputDeviceCategory cat)
{ {
switch (cat) switch (cat)
{ {
case InputDeviceWatcher.InputDeviceCategory.Keyboard: case InputDeviceWatcher.InputDeviceCategory.Keyboard:
return new[] { "Keyboard", "Mouse" }; return KeyboardHints;
case InputDeviceWatcher.InputDeviceCategory.Xbox: case InputDeviceWatcher.InputDeviceCategory.Xbox:
return new[] { "XInput", "Xbox", "Gamepad" }; return XboxHints;
case InputDeviceWatcher.InputDeviceCategory.PlayStation: case InputDeviceWatcher.InputDeviceCategory.PlayStation:
return new[] { "DualShock", "DualSense", "PlayStation", "Gamepad" }; return PlayStationHints;
default: default:
return new[] { "XInput", "Xbox", "Gamepad" }; return XboxHints;
} }
} }
public static string GetDisplayNameFromInputAction(InputAction reference) public static string GetDisplayNameFromInputAction(InputAction action, string compositePartName = null, InputDeviceWatcher.InputDeviceCategory deviceOverride = InputDeviceWatcher.InputDeviceCategory.Keyboard)
{ {
string controlPath = GetBindingControlPath(reference, InputDeviceWatcher.CurrentCategory); if (action == null) return string.Empty;
return GetDisplayNameFromControlPath(controlPath); var binding = GetBindingControl(action, compositePartName, deviceOverride);
return binding.ToDisplayString();
} }
public static string GetDisplayNameFromControlPath(string controlPath) public static string GetDisplayNameFromControlPath(string controlPath)
{ {
if (string.IsNullOrEmpty(controlPath)) return string.Empty; if (string.IsNullOrEmpty(controlPath)) return string.Empty;
var parts = controlPath.Split('/'); var parts = controlPath.Split('/');
var last = parts[parts.Length - 1].Trim(new char[] { '{', '}', '<', '>', '\'', '"' }); var last = parts[parts.Length - 1].Trim(TrimChars);
return last; return last;
} }
} }

View File

@ -9,54 +9,78 @@ using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using AlicizaX.InputGlyph; using AlicizaX;
using Cysharp.Threading.Tasks;
using RxUnit = System.Reactive.Unit; using RxUnit = System.Reactive.Unit;
namespace InputRemapper
{ public class InputBindingManager : MonoSingleton<InputBindingManager>
public class InputBindingManager : MonoBehaviour
{ {
public const string NULL_BINDING = "__NULL__"; public const string NULL_BINDING = "__NULL__";
private const string KEYBOARD_DEVICE = "<Keyboard>";
private const string MOUSE_DELTA = "<Mouse>/delta";
private const string MOUSE_SCROLL = "<Mouse>/scroll";
private const string MOUSE_SCROLL_X = "<Mouse>/scroll/x";
private const string MOUSE_SCROLL_Y = "<Mouse>/scroll/y";
private const string KEYBOARD_ESCAPE = "<Keyboard>/escape";
[Tooltip("InputActionAsset to manage")] [Tooltip("InputActionAsset to manage")]
public InputActionAsset actions; public InputActionAsset actions;
[SerializeField] private InputGlyphDatabase inputGlyphDatabase;
public string fileName = "input_bindings.json"; public string fileName = "input_bindings.json";
public bool debugMode = false; public bool debugMode = false;
public Dictionary<string, ActionMap> actionMap = new Dictionary<string, ActionMap>(); public Dictionary<string, ActionMap> actionMap = new Dictionary<string, ActionMap>();
public List<RebindContext> preparedRebinds = new List<RebindContext>(); public HashSet<RebindContext> preparedRebinds = new HashSet<RebindContext>();
internal InputActionRebindingExtensions.RebindingOperation rebindOperation; internal InputActionRebindingExtensions.RebindingOperation rebindOperation;
private readonly List<string> pressedActions = new List<string>();
private bool isApplyPending = false; private bool isApplyPending = false;
private string defaultBindingsJson = string.Empty; private string defaultBindingsJson = string.Empty;
private string cachedSavePath;
private Dictionary<string, (ActionMap map, ActionMap.Action action)> actionLookup = new Dictionary<string, (ActionMap, ActionMap.Action)>();
public ReplaySubject<RxUnit> OnInputsInit = new ReplaySubject<RxUnit>(1); public readonly ReplaySubject<RxUnit> OnInputsInit = new ReplaySubject<RxUnit>(1);
public Subject<bool> OnApply = new Subject<bool>(); public readonly Subject<(bool success, HashSet<RebindContext> appliedContexts)> OnApply = new Subject<(bool, HashSet<RebindContext>)>();
public Subject<RebindContext> OnRebindPrepare = new Subject<RebindContext>(); public readonly Subject<RebindContext> OnRebindPrepare = new Subject<RebindContext>();
public Subject<RxUnit> OnRebindStart = new Subject<RxUnit>(); public readonly Subject<RxUnit> OnRebindStart = new Subject<RxUnit>();
public Subject<bool> OnRebindEnd = new Subject<bool>(); public readonly Subject<(bool success, RebindContext context)> OnRebindEnd = new Subject<(bool, RebindContext)>();
public Subject<(RebindContext prepared, RebindContext conflict)> OnRebindConflict = new Subject<(RebindContext, RebindContext)>(); public readonly Subject<(RebindContext prepared, RebindContext conflict)> OnRebindConflict = new Subject<(RebindContext, RebindContext)>();
public string SavePath public string SavePath
{ {
get get
{ {
if (!string.IsNullOrEmpty(cachedSavePath))
return cachedSavePath;
#if UNITY_EDITOR #if UNITY_EDITOR
string folder = Application.dataPath; string folder = Application.dataPath;
#else #else
string folder = Application.persistentDataPath; string folder = Application.persistentDataPath;
#endif #endif
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); cachedSavePath = Path.Combine(folder, fileName);
return Path.Combine(folder, fileName); return cachedSavePath;
} }
} }
private void Awake() private void EnsureSaveDirectoryExists()
{ {
var directory = Path.GetDirectoryName(SavePath);
if (!Directory.Exists(directory))
Directory.CreateDirectory(directory);
}
protected override void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
if (actions == null) if (actions == null)
{ {
Debug.LogError("InputBindingManager: InputActionAsset not assigned."); Debug.LogError("InputBindingManager: InputActionAsset not assigned.");
@ -69,8 +93,9 @@ namespace InputRemapper
{ {
defaultBindingsJson = actions.SaveBindingOverridesAsJson(); defaultBindingsJson = actions.SaveBindingOverridesAsJson();
} }
catch catch (Exception ex)
{ {
Debug.LogWarning($"[InputBindingManager] Failed to save default bindings: {ex.Message}");
defaultBindingsJson = string.Empty; defaultBindingsJson = string.Empty;
} }
@ -83,7 +108,10 @@ namespace InputRemapper
{ {
actions.LoadBindingOverridesFromJson(json); actions.LoadBindingOverridesFromJson(json);
RefreshBindingPathsFromActions(); RefreshBindingPathsFromActions();
if (debugMode) Debug.Log($"Loaded overrides from {SavePath}"); if (debugMode)
{
Debug.Log($"Loaded overrides from {SavePath}");
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -96,8 +124,13 @@ namespace InputRemapper
actions.Enable(); actions.Enable();
} }
private void OnDestroy() protected override void OnDestroy()
{ {
if (_instance == this)
{
_instance = null;
}
rebindOperation?.Dispose(); rebindOperation?.Dispose();
rebindOperation = null; rebindOperation = null;
@ -111,18 +144,45 @@ namespace InputRemapper
private void BuildActionMap() private void BuildActionMap()
{ {
// Pre-allocate with known capacity to avoid resizing
int mapCount = actions.actionMaps.Count;
actionMap.Clear(); actionMap.Clear();
actionLookup.Clear();
// Estimate total action count for better allocation
int estimatedActionCount = 0;
foreach (var map in actions.actionMaps) foreach (var map in actions.actionMaps)
actionMap.Add(map.name, new ActionMap(map)); {
estimatedActionCount += map.actions.Count;
}
// Ensure capacity to avoid rehashing
if (actionMap.Count == 0)
{
actionMap = new Dictionary<string, ActionMap>(mapCount);
actionLookup = new Dictionary<string, (ActionMap, ActionMap.Action)>(estimatedActionCount);
}
foreach (var map in actions.actionMaps)
{
var actionMapObj = new ActionMap(map);
actionMap.Add(map.name, actionMapObj);
// Build lookup dictionary for O(1) action access
foreach (var actionPair in actionMapObj.actions)
{
actionLookup[actionPair.Key] = (actionMapObj, actionPair.Value);
}
}
} }
private void RefreshBindingPathsFromActions() private void RefreshBindingPathsFromActions()
{ {
foreach (var mapPair in actionMap) foreach (var mapPair in actionMap.Values)
{ {
foreach (var actionPair in mapPair.Value.actions) foreach (var actionPair in mapPair.actions.Values)
{ {
var a = actionPair.Value; var a = actionPair;
foreach (var bpair in a.bindings) foreach (var bpair in a.bindings)
{ {
bpair.Value.bindingPath.EffectivePath = a.action.bindings[bpair.Key].effectivePath; bpair.Value.bindingPath.EffectivePath = a.action.bindings[bpair.Key].effectivePath;
@ -131,26 +191,33 @@ namespace InputRemapper
} }
} }
public class ActionMap public sealed class ActionMap
{ {
public string name; public string name;
public Dictionary<string, Action> actions = new Dictionary<string, Action>(); public Dictionary<string, Action> actions;
public ActionMap(InputActionMap map) public ActionMap(InputActionMap map)
{ {
name = map.name; name = map.name;
foreach (var action in map.actions) actions.Add(action.name, new Action(action)); int actionCount = map.actions.Count;
actions = new Dictionary<string, Action>(actionCount);
foreach (var action in map.actions)
{
actions.Add(action.name, new Action(action));
}
} }
public class Action public sealed class Action
{ {
public InputAction action; public InputAction action;
public Dictionary<int, Binding> bindings = new Dictionary<int, Binding>(); public Dictionary<int, Binding> bindings;
public Action(InputAction action) public Action(InputAction action)
{ {
this.action = action; this.action = action;
int count = action.bindings.Count; int count = action.bindings.Count;
bindings = new Dictionary<int, Binding>(count);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
if (action.bindings[i].isComposite) if (action.bindings[i].isComposite)
@ -170,28 +237,39 @@ namespace InputRemapper
void AddBinding(InputBinding binding, int bindingIndex) void AddBinding(InputBinding binding, int bindingIndex)
{ {
bindings.Add(bindingIndex, new Binding bindings.Add(bindingIndex, new Binding(
{ binding.name,
name = binding.name, action.name,
parentAction = action.name, binding.name,
compositePart = binding.name, bindingIndex,
bindingIndex = bindingIndex, binding.groups?.Split(InputBinding.Separator) ?? Array.Empty<string>(),
group = binding.groups?.Split(InputBinding.Separator) ?? Array.Empty<string>(), new BindingPath(binding.path, binding.overridePath),
bindingPath = new BindingPath(binding.path, binding.overridePath), binding
inputBinding = binding ));
});
} }
} }
public struct Binding public readonly struct Binding
{ {
public string name; public readonly string name;
public string parentAction; public readonly string parentAction;
public string compositePart; public readonly string compositePart;
public int bindingIndex; public readonly int bindingIndex;
public string[] group; public readonly string[] group;
public BindingPath bindingPath; public readonly BindingPath bindingPath;
public InputBinding inputBinding; public readonly InputBinding inputBinding;
public Binding(string name, string parentAction, string compositePart, int bindingIndex,
string[] group, BindingPath bindingPath, InputBinding inputBinding)
{
this.name = name;
this.parentAction = parentAction;
this.compositePart = compositePart;
this.bindingIndex = bindingIndex;
this.group = group;
this.bindingPath = bindingPath;
this.inputBinding = inputBinding;
}
} }
} }
} }
@ -200,7 +278,7 @@ namespace InputRemapper
{ {
public string bindingPath; public string bindingPath;
public string overridePath; public string overridePath;
private readonly Subject<RxUnit> observer = new Subject<RxUnit>(); private Subject<RxUnit> observer;
public BindingPath(string bindingPath, string overridePath) public BindingPath(string bindingPath, string overridePath)
{ {
@ -214,18 +292,33 @@ namespace InputRemapper
set set
{ {
overridePath = (value == bindingPath) ? string.Empty : value; overridePath = (value == bindingPath) ? string.Empty : value;
observer.OnNext(RxUnit.Default); observer?.OnNext(RxUnit.Default);
} }
} }
public IObservable<string> EffectivePathObservable => observer.Select(_ => EffectivePath); public IObservable<string> EffectivePathObservable
{
get
{
observer ??= new Subject<RxUnit>();
return observer.Select(_ => EffectivePath);
}
}
public void Dispose()
{
observer?.OnCompleted();
observer?.Dispose();
observer = null;
}
} }
public class RebindContext public sealed class RebindContext
{ {
public InputAction action; public InputAction action;
public int bindingIndex; public int bindingIndex;
public string overridePath; public string overridePath;
private string cachedToString;
public RebindContext(InputAction action, int bindingIndex, string overridePath) public RebindContext(InputAction action, int bindingIndex, string overridePath)
{ {
@ -242,16 +335,28 @@ namespace InputRemapper
} }
public override int GetHashCode() => (action?.name ?? string.Empty, bindingIndex).GetHashCode(); public override int GetHashCode() => (action?.name ?? string.Empty, bindingIndex).GetHashCode();
public override string ToString() => $"{action?.name ?? "<null>"}:{bindingIndex}";
public override string ToString()
{
if (cachedToString == null && action != null)
{
cachedToString = $"{action.name}:{bindingIndex}";
}
return cachedToString ?? "<null>";
}
} }
/* ---------------- Public API ---------------- */ /* ---------------- Public API ---------------- */
public static InputAction Action(string actionName) public static InputAction Action(string actionName)
{ {
foreach (var map in Instance.actionMap) var instance = Instance;
if (instance == null) return null;
if (instance.actionLookup.TryGetValue(actionName, out var result))
{ {
if (map.Value.actions.TryGetValue(actionName, out var a)) return a.action; return result.action.action;
} }
Debug.LogError($"[InputBindingManager] Could not find action '{actionName}'"); Debug.LogError($"[InputBindingManager] Could not find action '{actionName}'");
@ -272,42 +377,61 @@ namespace InputRemapper
} }
Instance.actions.Disable(); Instance.actions.Disable();
Instance.PerformInteractiveRebinding(action, bindingIndex, "<Keyboard>", true); Instance.PerformInteractiveRebinding(action, bindingIndex, KEYBOARD_DEVICE, true);
Instance.OnRebindStart.OnNext(RxUnit.Default); Instance.OnRebindStart.OnNext(RxUnit.Default);
if (Instance.debugMode) Debug.Log("[InputBindingManager] Rebind started"); if (Instance.debugMode)
{
Debug.Log("[InputBindingManager] Rebind started");
}
} }
public static void CancelRebind() => Instance.rebindOperation?.Cancel(); public static void CancelRebind() => Instance.rebindOperation?.Cancel();
public static async Task<bool> ConfirmApply(bool clearConflicts = true) public static async UniTask<bool> ConfirmApply(bool clearConflicts = true)
{ {
if (!Instance.isApplyPending) return false; if (!Instance.isApplyPending) return false;
try try
{ {
// Create a copy of the prepared rebinds before clearing
var appliedContexts = new HashSet<RebindContext>(Instance.preparedRebinds);
foreach (var ctx in Instance.preparedRebinds) foreach (var ctx in Instance.preparedRebinds)
{ {
if (string.IsNullOrEmpty(ctx.overridePath)) if (!string.IsNullOrEmpty(ctx.overridePath))
{ {
if (ctx.overridePath == NULL_BINDING)
{
ctx.action.RemoveBindingOverride(ctx.bindingIndex);
}
else
{
ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath);
}
} }
else if (ctx.overridePath == NULL_BINDING) ctx.action.RemoveBindingOverride(ctx.bindingIndex);
else ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath);
var bp = GetBindingPath(ctx.action.name, ctx.bindingIndex); var bp = GetBindingPath(ctx.action.name, ctx.bindingIndex);
if (bp != null) bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; if (bp != null)
{
bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath;
}
} }
Instance.preparedRebinds.Clear(); Instance.preparedRebinds.Clear();
await Instance.WriteOverridesToDiskAsync(); await Instance.WriteOverridesToDiskAsync();
Instance.OnApply.OnNext(true); Instance.OnApply.OnNext((true, appliedContexts));
Instance.isApplyPending = false; Instance.isApplyPending = false;
if (Instance.debugMode) Debug.Log("[InputBindingManager] Apply confirmed and saved."); if (Instance.debugMode)
{
Debug.Log("[InputBindingManager] Apply confirmed and saved.");
}
return true; return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex); Debug.LogError("[InputBindingManager] Failed to apply binds: " + ex);
Instance.OnApply.OnNext(false); Instance.OnApply.OnNext((false, new HashSet<RebindContext>()));
return false; return false;
} }
} }
@ -315,23 +439,34 @@ namespace InputRemapper
public static void DiscardPrepared() public static void DiscardPrepared()
{ {
if (!Instance.isApplyPending) return; if (!Instance.isApplyPending) return;
// Create a copy of the prepared rebinds before clearing (for event notification)
var discardedContexts = new HashSet<RebindContext>(Instance.preparedRebinds);
Instance.preparedRebinds.Clear(); Instance.preparedRebinds.Clear();
Instance.isApplyPending = false; Instance.isApplyPending = false;
Instance.OnApply.OnNext(false); Instance.OnApply.OnNext((false, discardedContexts));
if (Instance.debugMode) Debug.Log("[InputBindingManager] Prepared rebinds discarded."); if (Instance.debugMode)
{
Debug.Log("[InputBindingManager] Prepared rebinds discarded.");
}
} }
private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true) private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true)
{ {
var op = action.PerformInteractiveRebinding(bindingIndex); var op = action.PerformInteractiveRebinding(bindingIndex);
if (!string.IsNullOrEmpty(deviceMatchPath)) op = op.WithControlsHavingToMatchPath(deviceMatchPath); if (!string.IsNullOrEmpty(deviceMatchPath))
{
op = op.WithControlsHavingToMatchPath(deviceMatchPath);
}
if (excludeMouseMovementAndScroll) if (excludeMouseMovementAndScroll)
{ {
op = op.WithControlsExcluding("<Mouse>/delta"); op = op.WithControlsExcluding(MOUSE_DELTA)
op = op.WithControlsExcluding("<Mouse>/scroll"); .WithControlsExcluding(MOUSE_SCROLL)
op = op.WithControlsExcluding("<Mouse>/scroll/x"); .WithControlsExcluding(MOUSE_SCROLL_X)
op = op.WithControlsExcluding("<Mouse>/scroll/y"); .WithControlsExcluding(MOUSE_SCROLL_Y);
} }
rebindOperation = op rebindOperation = op
@ -356,19 +491,27 @@ namespace InputRemapper
}) })
.OnComplete(opc => .OnComplete(opc =>
{ {
if (debugMode) Debug.Log("[InputBindingManager] Rebind completed"); if (debugMode)
{
Debug.Log("[InputBindingManager] Rebind completed");
}
actions.Enable(); actions.Enable();
OnRebindEnd.OnNext(true); OnRebindEnd.OnNext((true, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)));
CleanRebindOperation(); CleanRebindOperation();
}) })
.OnCancel(opc => .OnCancel(opc =>
{ {
if (debugMode) Debug.Log("[InputBindingManager] Rebind cancelled"); if (debugMode)
{
Debug.Log("[InputBindingManager] Rebind cancelled");
}
actions.Enable(); actions.Enable();
OnRebindEnd.OnNext(false); OnRebindEnd.OnNext((false, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)));
CleanRebindOperation(); CleanRebindOperation();
}) })
.WithCancelingThrough("<Keyboard>/escape") .WithCancelingThrough(KEYBOARD_ESCAPE)
.Start(); .Start();
} }
@ -395,17 +538,21 @@ namespace InputRemapper
private bool AnyBindingPath(string bindingPath, InputAction currentAction, int currentIndex, out (InputAction action, int bindingIndex) duplicate) private bool AnyBindingPath(string bindingPath, InputAction currentAction, int currentIndex, out (InputAction action, int bindingIndex) duplicate)
{ {
foreach (var map in actionMap) foreach (var map in actionMap.Values)
{ {
foreach (var actionPair in map.Value.actions) foreach (var actionPair in map.actions.Values)
{ {
foreach (var bindingPair in actionPair.Value.bindings) bool isSameAction = actionPair.action == currentAction;
foreach (var bindingPair in actionPair.bindings)
{ {
if (actionPair.Value.action == currentAction && bindingPair.Key == currentIndex) continue; // Skip if it's the same action and same binding index
var eff = bindingPair.Value.bindingPath.EffectivePath; if (isSameAction && bindingPair.Key == currentIndex)
if (eff == bindingPath) continue;
if (bindingPair.Value.bindingPath.EffectivePath == bindingPath)
{ {
duplicate = (actionPair.Value.action, bindingPair.Key); duplicate = (actionPair.action, bindingPair.Key);
return true; return true;
} }
} }
@ -418,7 +565,8 @@ namespace InputRemapper
private void PrepareRebind(RebindContext context) private void PrepareRebind(RebindContext context)
{ {
preparedRebinds.RemoveAll(x => x.Equals(context)); // Remove existing rebind for same action/binding if exists
preparedRebinds.Remove(context);
if (string.IsNullOrEmpty(context.overridePath)) if (string.IsNullOrEmpty(context.overridePath))
{ {
@ -434,19 +582,24 @@ namespace InputRemapper
preparedRebinds.Add(context); preparedRebinds.Add(context);
isApplyPending = true; isApplyPending = true;
OnRebindPrepare.OnNext(context); OnRebindPrepare.OnNext(context);
if (debugMode) Debug.Log($"Prepared rebind: {context} -> {context.overridePath}"); if (debugMode)
{
Debug.Log($"Prepared rebind: {context} -> {context.overridePath}");
}
} }
} }
private async Task WriteOverridesToDiskAsync() private async UniTask WriteOverridesToDiskAsync()
{ {
try try
{ {
var json = actions.SaveBindingOverridesAsJson(); var json = actions.SaveBindingOverridesAsJson();
var dir = Path.GetDirectoryName(SavePath); EnsureSaveDirectoryExists();
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json); using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json);
if (debugMode) Debug.Log($"Overrides saved to {SavePath}"); if (debugMode)
{
Debug.Log($"Overrides saved to {SavePath}");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -455,22 +608,34 @@ namespace InputRemapper
} }
} }
public async Task ResetToDefaultAsync() public async UniTask ResetToDefaultAsync()
{ {
try try
{ {
if (!string.IsNullOrEmpty(defaultBindingsJson)) actions.LoadBindingOverridesFromJson(defaultBindingsJson); if (!string.IsNullOrEmpty(defaultBindingsJson))
{
actions.LoadBindingOverridesFromJson(defaultBindingsJson);
}
else else
{ {
foreach (var map in actionMap) foreach (var map in actionMap.Values)
foreach (var a in map.Value.actions) {
for (int b = 0; b < a.Value.action.bindings.Count; b++) foreach (var a in map.actions.Values)
a.Value.action.RemoveBindingOverride(b); {
for (int b = 0; b < a.action.bindings.Count; b++)
{
a.action.RemoveBindingOverride(b);
}
}
}
} }
RefreshBindingPathsFromActions(); RefreshBindingPathsFromActions();
await WriteOverridesToDiskAsync(); await WriteOverridesToDiskAsync();
if (debugMode) Debug.Log("Reset to default and saved."); if (debugMode)
{
Debug.Log("Reset to default and saved.");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -480,17 +645,21 @@ namespace InputRemapper
public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0) public static BindingPath GetBindingPath(string actionName, int bindingIndex = 0)
{ {
foreach (var map in Instance.actionMap) var instance = Instance;
if (instance == null) return null;
if (instance.actionLookup.TryGetValue(actionName, out var result))
{ {
if (map.Value.actions.TryGetValue(actionName, out var action)) if (result.action.bindings.TryGetValue(bindingIndex, out var binding))
{ {
if (action.bindings.TryGetValue(bindingIndex, out var binding)) return binding.bindingPath; return binding.bindingPath;
} }
} }
return null; return null;
} }
// choose best binding index for keyboard; if compositePartName != null then look for part // choose best binding index for keyboard; if compositePartName != null then look for part
public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null)
{ {
@ -498,36 +667,51 @@ namespace InputRemapper
int fallbackPart = -1; int fallbackPart = -1;
int fallbackNonComposite = -1; int fallbackNonComposite = -1;
bool searchingForCompositePart = !string.IsNullOrEmpty(compositePartName);
for (int i = 0; i < action.bindings.Count; i++) for (int i = 0; i < action.bindings.Count; i++)
{ {
var b = action.bindings[i]; var b = action.bindings[i];
if (!string.IsNullOrEmpty(compositePartName)) // If searching for a specific composite part, skip non-matching bindings
if (searchingForCompositePart)
{ {
if (!b.isPartOfComposite) continue; if (!b.isPartOfComposite) continue;
if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue;
} }
// Check if this binding is for keyboard
bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase));
if (b.isPartOfComposite) if (b.isPartOfComposite)
{ {
if (fallbackPart == -1) fallbackPart = i; if (fallbackPart == -1) fallbackPart = i;
if (!string.IsNullOrEmpty(b.path) && b.path.StartsWith("<Keyboard>", StringComparison.OrdinalIgnoreCase)) return i; if (isKeyboardBinding) return i;
if (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith("<Keyboard>", StringComparison.OrdinalIgnoreCase)) return i;
} }
else else
{ {
if (fallbackNonComposite == -1) fallbackNonComposite = i; if (fallbackNonComposite == -1) fallbackNonComposite = i;
if (!string.IsNullOrEmpty(b.path) && b.path.StartsWith("<Keyboard>", StringComparison.OrdinalIgnoreCase)) return i; if (isKeyboardBinding) return i;
if (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith("<Keyboard>", StringComparison.OrdinalIgnoreCase)) return i;
} }
} }
if (fallbackNonComposite >= 0) return fallbackNonComposite; return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart;
return fallbackPart; }
public static InputBindingManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<InputBindingManager>();
}
return _instance;
}
} }
public static InputBindingManager Instance => _instance ??= FindObjectOfType<InputBindingManager>();
private static InputBindingManager _instance; private static InputBindingManager _instance;
} }
}

View File

@ -5,101 +5,184 @@ using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using UnityEngine.U2D; using UnityEngine.U2D;
namespace AlicizaX.InputGlyph
[Serializable]
public sealed class GlyphEntry
{ {
[Serializable] public Sprite Sprite;
public class GlyphEntry public InputAction action;
}
[Serializable]
public sealed class DeviceGlyphTable
{
public string deviceName;
public Texture2D spriteSheetTexture;
public Sprite platformIcons;
public List<GlyphEntry> entries = new List<GlyphEntry>();
}
[CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)]
public sealed class InputGlyphDatabase : ScriptableObject
{
private const string DEVICE_KEYBOARD = "Keyboard";
private const string DEVICE_XBOX = "Xbox";
private const string DEVICE_PLAYSTATION = "PlayStation";
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>();
// 当 FindEntryByControlPath 传空 path 时返回的占位 sprite
public Sprite placeholderSprite;
// Cache for faster lookups
private Dictionary<string, DeviceGlyphTable> _tableCache;
private Dictionary<(string path, InputDeviceWatcher.InputDeviceCategory device), Sprite> _spriteCache;
private void OnEnable()
{ {
public Sprite Sprite; BuildCache();
public InputAction action;
} }
[Serializable] private void BuildCache()
public class DeviceGlyphTable
{ {
// Table 名称(序列化) if (_tableCache == null)
public string deviceName; {
_tableCache = new Dictionary<string, DeviceGlyphTable>(tables.Count);
}
else
{
_tableCache.Clear();
}
// 支持三种来源: if (_spriteCache == null)
// 1) TMP Sprite AssetTextMeshPro sprite asset {
public TMP_SpriteAsset tmpAsset; _spriteCache = new Dictionary<(string, InputDeviceWatcher.InputDeviceCategory), Sprite>();
}
else
{
_spriteCache.Clear();
}
// 2) Unity SpriteAtlas可选 for (int i = 0; i < tables.Count; i++)
public SpriteAtlas spriteAtlas; {
var table = tables[i];
// 3) Texture2DSprite Mode = Multiple在 Sprite Editor 切好的切片 if (table != null && !string.IsNullOrEmpty(table.deviceName))
public Texture2D spriteSheetTexture; {
_tableCache[table.deviceName.ToLowerInvariant()] = table;
public List<GlyphEntry> entries = new List<GlyphEntry>(); }
}
} }
[CreateAssetMenu(fileName = "InputGlyphDatabase", menuName = "InputGlyphs/InputGlyphDatabase", order = 400)] public DeviceGlyphTable GetTable(string deviceName)
public class InputGlyphDatabase : ScriptableObject
{ {
public List<DeviceGlyphTable> tables = new List<DeviceGlyphTable>(); if (string.IsNullOrEmpty(deviceName)) return null;
if (tables == null) return null;
// 当 FindEntryByControlPath 传空 path 时返回的占位 sprite // Ensure cache is built
public Sprite placeholderSprite; if (_tableCache == null || _tableCache.Count == 0)
public DeviceGlyphTable GetTable(string deviceName)
{ {
if (string.IsNullOrEmpty(deviceName)) return null; BuildCache();
if (tables == null) return null;
for (int i = 0; i < tables.Count; ++i)
{
var t = tables[i];
if (t == null) continue;
if (string.Equals(t.deviceName, deviceName, StringComparison.OrdinalIgnoreCase))
return t;
}
return null;
} }
// 兼容枚举版本(示例) // Use cache for O(1) lookup
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) if (_tableCache.TryGetValue(deviceName.ToLowerInvariant(), out var table))
{ {
string name = "Other"; return table;
switch (device)
{
case InputDeviceWatcher.InputDeviceCategory.Keyboard: name = "Keyboard"; break;
case InputDeviceWatcher.InputDeviceCategory.Xbox: name = "Xbox"; break;
case InputDeviceWatcher.InputDeviceCategory.PlayStation: name = "PlayStation"; break;
default: name = "Xbox"; break;
}
return GetTable(name);
} }
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) return null;
}
public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device)
{
var table = GetTable(device);
if (table == null) return null;
return table.platformIcons;
}
public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device)
{
// Use constants to avoid string allocations
string name;
switch (device)
{ {
var entry = FindEntryByControlPath(controlPath, device); case InputDeviceWatcher.InputDeviceCategory.Keyboard:
if (string.IsNullOrEmpty(controlPath) || entry == null) name = DEVICE_KEYBOARD;
{ break;
return placeholderSprite; case InputDeviceWatcher.InputDeviceCategory.Xbox:
} name = DEVICE_XBOX;
return entry.Sprite; break;
case InputDeviceWatcher.InputDeviceCategory.PlayStation:
name = DEVICE_PLAYSTATION;
break;
default:
name = DEVICE_XBOX;
break;
} }
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device) return GetTable(name);
}
public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
if (string.IsNullOrEmpty(controlPath))
{ {
var t = GetTable(device); return placeholderSprite;
if (t != null) }
// Check cache first
var cacheKey = (controlPath, device);
if (_spriteCache != null && _spriteCache.TryGetValue(cacheKey, out var cachedSprite))
{
return cachedSprite ?? placeholderSprite;
}
var entry = FindEntryByControlPath(controlPath, device);
var sprite = entry?.Sprite ?? placeholderSprite;
// Cache the result (including null results to avoid repeated lookups)
if (_spriteCache != null)
{
_spriteCache[cacheKey] = sprite;
}
return sprite;
}
public GlyphEntry FindEntryByControlPath(string controlPath, InputDeviceWatcher.InputDeviceCategory device)
{
var t = GetTable(device);
if (t != null && t.entries != null)
{
for (int i = 0; i < t.entries.Count; ++i)
{ {
for (int i = 0; i < t.entries.Count; ++i) var e = t.entries[i];
if (e == null) continue;
if (e.action == null) continue;
var bindings = e.action.bindings;
int bindingCount = bindings.Count;
if (bindingCount <= 0) continue;
for (int j = 0; j < bindingCount; j++)
{ {
var e = t.entries[i]; var b = bindings[j];
if (e == null) continue; if (!string.IsNullOrEmpty(b.path) && string.Equals(b.path, controlPath, StringComparison.OrdinalIgnoreCase))
if (e.action == null) continue;
if (e.action.bindings.Count <= 0) continue;
foreach (var binding in e.action.bindings)
{ {
if (string.Equals(controlPath, binding.path, StringComparison.OrdinalIgnoreCase)) return e;
{ }
return e;
} if (!string.IsNullOrEmpty(b.effectivePath) && string.Equals(b.effectivePath, controlPath, StringComparison.OrdinalIgnoreCase))
{
return e;
} }
} }
} }
return null;
} }
return null;
} }
} }

View File

@ -1,12 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Reflection;
using AlicizaX.InputGlyph;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.U2D;
using TMPro;
[CustomEditor(typeof(InputGlyphDatabase))] [CustomEditor(typeof(InputGlyphDatabase))]
public class InputGlyphDatabaseEditor : Editor public class InputGlyphDatabaseEditor : Editor
@ -22,6 +17,9 @@ public class InputGlyphDatabaseEditor : Editor
List<string> searchStrings = new List<string>(); List<string> searchStrings = new List<string>();
List<int> currentPages = new List<int>(); List<int> currentPages = new List<int>();
// per-table temporary fields for adding single entry (only sprite now)
List<Sprite> newEntrySprites = new List<Sprite>();
const int itemsPerPage = 10; const int itemsPerPage = 10;
const int previewSize = 52; const int previewSize = 52;
@ -97,9 +95,11 @@ public class InputGlyphDatabaseEditor : Editor
var nameProp = t.FindPropertyRelative("deviceName"); var nameProp = t.FindPropertyRelative("deviceName");
if (nameProp != null && string.Equals(nameProp.stringValue, trimmed, StringComparison.OrdinalIgnoreCase)) if (nameProp != null && string.Equals(nameProp.stringValue, trimmed, StringComparison.OrdinalIgnoreCase))
{ {
exists = true; break; exists = true;
break;
} }
} }
if (exists) if (exists)
{ {
EditorUtility.DisplayDialog("Duplicate", "A table with that name already exists.", "OK"); EditorUtility.DisplayDialog("Duplicate", "A table with that name already exists.", "OK");
@ -111,10 +111,6 @@ public class InputGlyphDatabaseEditor : Editor
var newTable = tablesProp.GetArrayElementAtIndex(newIndex); var newTable = tablesProp.GetArrayElementAtIndex(newIndex);
var nameProp = newTable.FindPropertyRelative("deviceName"); var nameProp = newTable.FindPropertyRelative("deviceName");
if (nameProp != null) nameProp.stringValue = trimmed; if (nameProp != null) nameProp.stringValue = trimmed;
var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset");
if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null;
var atlasProp = newTable.FindPropertyRelative("spriteAtlas");
if (atlasProp != null) atlasProp.objectReferenceValue = null;
var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture");
if (sheetProp != null) sheetProp.objectReferenceValue = null; if (sheetProp != null) sheetProp.objectReferenceValue = null;
var entriesProp = newTable.FindPropertyRelative("entries"); var entriesProp = newTable.FindPropertyRelative("entries");
@ -130,6 +126,7 @@ public class InputGlyphDatabaseEditor : Editor
} }
} }
} }
if (GUILayout.Button("Cancel", EditorStyles.toolbarButton, GUILayout.Width(80))) if (GUILayout.Button("Cancel", EditorStyles.toolbarButton, GUILayout.Width(80)))
{ {
showAddField = false; showAddField = false;
@ -157,7 +154,7 @@ public class InputGlyphDatabaseEditor : Editor
if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22))) if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22)))
{ {
if (EditorUtility.DisplayDialog("Delete Table?", if (EditorUtility.DisplayDialog("Delete Table?",
$"Delete table '{name}' and all its entries? This cannot be undone.", "Delete", "Cancel")) $"Delete table '{name}' and all its entries? This cannot be undone.", "Delete", "Cancel"))
{ {
tablesProp.DeleteArrayElementAtIndex(i); tablesProp.DeleteArrayElementAtIndex(i);
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
@ -208,6 +205,11 @@ public class InputGlyphDatabaseEditor : Editor
EnsureEditorListsLength(); EnsureEditorListsLength();
// compute deviceName & runtime index for this table (used when deleting single entry)
var nameProp = tableProp.FindPropertyRelative("deviceName");
string deviceName = nameProp != null ? nameProp.stringValue : "";
int runtimeTableIndex = MapSerializedTableToRuntimeIndex(deviceName);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
GUIStyle searchStyle = EditorStyles.toolbarSearchField ?? EditorStyles.textField; GUIStyle searchStyle = EditorStyles.toolbarSearchField ?? EditorStyles.textField;
searchStrings[tabIndex] = GUILayout.TextField(searchStrings[tabIndex] ?? "", searchStyle); searchStrings[tabIndex] = GUILayout.TextField(searchStrings[tabIndex] ?? "", searchStyle);
@ -215,59 +217,6 @@ public class InputGlyphDatabaseEditor : Editor
EditorGUILayout.Space(6); EditorGUILayout.Space(6);
// TMP Asset row
var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset");
EditorGUILayout.BeginHorizontal();
GUILayout.Label("TMP Sprite Asset", GUILayout.Width(140));
EditorGUILayout.PropertyField(tmpAssetProp, GUIContent.none, GUILayout.ExpandWidth(true));
if (GUILayout.Button("Parse TMP Asset", GUILayout.Width(120)))
{
ParseTMPAssetIntoTableSerialized(tableProp);
}
if (GUILayout.Button("Clear", GUILayout.Width(80)))
{
var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp != null) entriesProp.arraySize = 0;
var nameProp = tableProp.FindPropertyRelative("deviceName");
if (nameProp != null && db != null)
{
var deviceName = nameProp.stringValue;
var table = db.GetTable(deviceName);
if (table != null) table.entries.Clear();
}
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
currentPages[tabIndex] = 0;
}
EditorGUILayout.EndHorizontal();
// SpriteAtlas row
var atlasProp = tableProp.FindPropertyRelative("spriteAtlas");
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Sprite Atlas", GUILayout.Width(140));
EditorGUILayout.PropertyField(atlasProp, GUIContent.none, GUILayout.ExpandWidth(true));
if (GUILayout.Button("Parse Sprite Atlas", GUILayout.Width(120)))
{
ParseSpriteAtlasIntoTableSerialized(tableProp);
}
if (GUILayout.Button("Clear", GUILayout.Width(80)))
{
var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp != null) entriesProp.arraySize = 0;
var nameProp = tableProp.FindPropertyRelative("deviceName");
if (nameProp != null && db != null)
{
var deviceName = nameProp.stringValue;
var table = db.GetTable(deviceName);
if (table != null) table.entries.Clear();
}
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
currentPages[tabIndex] = 0;
}
EditorGUILayout.EndHorizontal();
// SpriteSheet (Texture2D with Multiple) row
var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture");
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
GUILayout.Label("Sprite Sheet (Texture2D)", GUILayout.Width(140)); GUILayout.Label("Sprite Sheet (Texture2D)", GUILayout.Width(140));
@ -276,23 +225,74 @@ public class InputGlyphDatabaseEditor : Editor
{ {
ParseSpriteSheetIntoTableSerialized(tableProp); ParseSpriteSheetIntoTableSerialized(tableProp);
} }
if (GUILayout.Button("Clear", GUILayout.Width(80))) if (GUILayout.Button("Clear", GUILayout.Width(80)))
{ {
var entriesProp = tableProp.FindPropertyRelative("entries"); var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp != null) entriesProp.arraySize = 0; if (entriesProp != null) entriesProp.arraySize = 0;
var nameProp = tableProp.FindPropertyRelative("deviceName"); if (runtimeTableIndex >= 0 && db != null)
if (nameProp != null && db != null)
{ {
var deviceName = nameProp.stringValue; var table = db.tables[runtimeTableIndex];
var table = db.GetTable(deviceName);
if (table != null) table.entries.Clear(); if (table != null) table.entries.Clear();
} }
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db); EditorUtility.SetDirty(db);
currentPages[tabIndex] = 0; currentPages[tabIndex] = 0;
} }
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
var platformProp = tableProp.FindPropertyRelative("platformIcons");
EditorGUILayout.PropertyField(platformProp, new GUIContent("Platforms Icons"));
Sprite placeholder = platformProp.objectReferenceValue as Sprite;
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("Preview", EditorStyles.miniBoldLabel);
if (placeholder != null)
{
Texture2D preview = AssetPreview.GetAssetPreview(placeholder);
if (preview == null) preview = AssetPreview.GetMiniThumbnail(placeholder);
if (preview != null) GUILayout.Label(preview, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
else EditorGUILayout.ObjectField(placeholder, typeof(Sprite), false, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
}
else
{
EditorGUILayout.HelpBox("No PlatformIcons.", MessageType.Info);
}
EditorGUILayout.Space(6);
// ---- 新增:单个新增 Entry 的 UI只支持 Sprite ----
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Add Single Entry", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Sprite", GUILayout.Width(50));
newEntrySprites[tabIndex] = (Sprite)EditorGUILayout.ObjectField(newEntrySprites[tabIndex], typeof(Sprite), false, GUILayout.Width(80), GUILayout.Height(80));
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(6);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Add Entry", GUILayout.Width(110)))
{
if (newEntrySprites[tabIndex] == null)
{
EditorUtility.DisplayDialog("Missing Sprite", "Please assign a Sprite to add.", "OK");
}
else
{
AddEntryToTableSerialized(tableProp, newEntrySprites[tabIndex]);
// reset temp field
newEntrySprites[tabIndex] = null;
currentPages[tabIndex] = 0;
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
// ---- end add-single-entry UI ----
EditorGUILayout.Space(6); EditorGUILayout.Space(6);
var entries = tableProp.FindPropertyRelative("entries"); var entries = tableProp.FindPropertyRelative("entries");
@ -319,15 +319,30 @@ public class InputGlyphDatabaseEditor : Editor
currentPages[tabIndex] = Mathf.Clamp(currentPages[tabIndex], 0, totalPages - 1); currentPages[tabIndex] = Mathf.Clamp(currentPages[tabIndex], 0, totalPages - 1);
EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("<<", EditorStyles.miniButtonLeft, GUILayout.Width(36))) { currentPages[tabIndex] = 0; } if (GUILayout.Button("<<", EditorStyles.miniButtonLeft, GUILayout.Width(36)))
if (GUILayout.Button("<", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Max(0, currentPages[tabIndex] - 1); } {
currentPages[tabIndex] = 0;
}
if (GUILayout.Button("<", EditorStyles.miniButtonMid, GUILayout.Width(36)))
{
currentPages[tabIndex] = Mathf.Max(0, currentPages[tabIndex] - 1);
}
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(string.Format("Page {0}/{1}", currentPages[tabIndex] + 1, totalPages), GUILayout.Width(120)); EditorGUILayout.LabelField(string.Format("Page {0}/{1}", currentPages[tabIndex] + 1, totalPages), GUILayout.Width(120));
GUILayout.FlexibleSpace(); GUILayout.FlexibleSpace();
if (GUILayout.Button(">", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Min(totalPages - 1, currentPages[tabIndex] + 1); } if (GUILayout.Button(">", EditorStyles.miniButtonMid, GUILayout.Width(36)))
if (GUILayout.Button(">>", EditorStyles.miniButtonRight, GUILayout.Width(36))) { currentPages[tabIndex] = totalPages - 1; } {
currentPages[tabIndex] = Mathf.Min(totalPages - 1, currentPages[tabIndex] + 1);
}
if (GUILayout.Button(">>", EditorStyles.miniButtonRight, GUILayout.Width(36)))
{
currentPages[tabIndex] = totalPages - 1;
}
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
@ -341,8 +356,10 @@ public class InputGlyphDatabaseEditor : Editor
var eProp = entries.GetArrayElementAtIndex(i); var eProp = entries.GetArrayElementAtIndex(i);
if (eProp == null) continue; if (eProp == null) continue;
// display one entry with a small remove button on the right
using (new EditorGUILayout.HorizontalScope("box")) using (new EditorGUILayout.HorizontalScope("box"))
{ {
// left: preview column
using (new EditorGUILayout.VerticalScope(GUILayout.Width(80))) using (new EditorGUILayout.VerticalScope(GUILayout.Width(80)))
{ {
var spriteProp = eProp.FindPropertyRelative("Sprite"); var spriteProp = eProp.FindPropertyRelative("Sprite");
@ -368,11 +385,51 @@ public class InputGlyphDatabaseEditor : Editor
} }
} }
// middle: action column
EditorGUILayout.BeginVertical(); EditorGUILayout.BeginVertical();
var actionProp = eProp.FindPropertyRelative("action"); var actionProp = eProp.FindPropertyRelative("action");
EditorGUILayout.Space(2); EditorGUILayout.Space(2);
EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true)); EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true));
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
// right: small remove button
GUILayout.Space(6);
if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(24)))
{
string spriteName = null;
var sProp = eProp.FindPropertyRelative("Sprite");
if (sProp != null) spriteName = (sProp.objectReferenceValue as Sprite)?.name;
if (EditorUtility.DisplayDialog("Remove Entry?",
$"Remove entry '{(string.IsNullOrEmpty(spriteName) ? "<missing>" : spriteName)}' from table '{deviceName}'?", "Remove", "Cancel"))
{
// remove from serialized array
var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp != null && i >= 0 && i < entriesProp.arraySize)
{
entriesProp.DeleteArrayElementAtIndex(i);
// apply then remove from runtime to keep both in sync
serializedObject.ApplyModifiedProperties();
}
// remove from runtime list (db.tables)
if (runtimeTableIndex >= 0 && db != null && db.tables != null && runtimeTableIndex < db.tables.Count)
{
var runtimeTable = db.tables[runtimeTableIndex];
if (runtimeTable != null && i >= 0 && i < runtimeTable.entries.Count)
{
runtimeTable.entries.RemoveAt(i);
}
}
EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets();
// reset paging and return to avoid continuing to iterate mutated serialized array
currentPages[tabIndex] = 0;
return;
}
}
} }
EditorGUILayout.Space(4); EditorGUILayout.Space(4);
@ -409,10 +466,6 @@ public class InputGlyphDatabaseEditor : Editor
var newTable = tablesProp.GetArrayElementAtIndex(idx); var newTable = tablesProp.GetArrayElementAtIndex(idx);
var deviceNameProp = newTable.FindPropertyRelative("deviceName"); var deviceNameProp = newTable.FindPropertyRelative("deviceName");
if (deviceNameProp != null) deviceNameProp.stringValue = name; if (deviceNameProp != null) deviceNameProp.stringValue = name;
var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset");
if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null;
var atlasProp = newTable.FindPropertyRelative("spriteAtlas");
if (atlasProp != null) atlasProp.objectReferenceValue = null;
var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); var sheetProp = newTable.FindPropertyRelative("spriteSheetTexture");
if (sheetProp != null) sheetProp.objectReferenceValue = null; if (sheetProp != null) sheetProp.objectReferenceValue = null;
var entriesProp = newTable.FindPropertyRelative("entries"); var entriesProp = newTable.FindPropertyRelative("entries");
@ -427,12 +480,15 @@ public class InputGlyphDatabaseEditor : Editor
int count = tablesProp != null ? tablesProp.arraySize : 0; int count = tablesProp != null ? tablesProp.arraySize : 0;
if (searchStrings == null) searchStrings = new List<string>(); if (searchStrings == null) searchStrings = new List<string>();
if (currentPages == null) currentPages = new List<int>(); if (currentPages == null) currentPages = new List<int>();
if (newEntrySprites == null) newEntrySprites = new List<Sprite>();
while (searchStrings.Count < count) searchStrings.Add(""); while (searchStrings.Count < count) searchStrings.Add("");
while (currentPages.Count < count) currentPages.Add(0); while (currentPages.Count < count) currentPages.Add(0);
while (newEntrySprites.Count < count) newEntrySprites.Add(null);
while (searchStrings.Count > count) searchStrings.RemoveAt(searchStrings.Count - 1); while (searchStrings.Count > count) searchStrings.RemoveAt(searchStrings.Count - 1);
while (currentPages.Count > count) currentPages.RemoveAt(currentPages.Count - 1); while (currentPages.Count > count) currentPages.RemoveAt(currentPages.Count - 1);
while (newEntrySprites.Count > count) newEntrySprites.RemoveAt(newEntrySprites.Count - 1);
} }
void EnsureEditorListsLength() void EnsureEditorListsLength()
@ -441,247 +497,58 @@ public class InputGlyphDatabaseEditor : Editor
SyncEditorListsWithTables(); SyncEditorListsWithTables();
} }
// ----- Parse TMP SpriteAsset增强版 ----- // ----- 新增:把单个 Sprite 加入到序列化表和 runtime 表 -----
void ParseTMPAssetIntoTableSerialized(SerializedProperty tableProp) void AddEntryToTableSerialized(SerializedProperty tableProp, Sprite sprite)
{ {
if (tableProp == null) return; if (tableProp == null) return;
var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset"); var entriesProp = tableProp.FindPropertyRelative("entries");
var asset = tmpAssetProp.objectReferenceValue as TMP_SpriteAsset; if (entriesProp == null) return;
if (asset == null)
int insertIndex = entriesProp.arraySize;
entriesProp.InsertArrayElementAtIndex(insertIndex);
var newE = entriesProp.GetArrayElementAtIndex(insertIndex);
if (newE != null)
{ {
Debug.LogWarning("[InputGlyphDatabase] TMP Sprite Asset is null for table."); var spriteProp = newE.FindPropertyRelative("Sprite");
return; var actionProp = newE.FindPropertyRelative("action");
}
var nameProp = tableProp.FindPropertyRelative("deviceName"); if (spriteProp != null) spriteProp.objectReferenceValue = sprite;
string deviceName = nameProp != null ? nameProp.stringValue : "";
int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); // leave action serialized as-is (most projects can't serialize InputAction directly here)
if (tableIndex < 0) if (actionProp != null)
{
Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables.");
return;
}
var tableObj = db.tables[tableIndex];
tableObj.entries.Clear();
var chars = asset.spriteCharacterTable;
SpriteAtlas atlas = GetSpriteAtlasFromTMP(asset);
string assetPath = AssetDatabase.GetAssetPath(asset);
string assetFolder = !string.IsNullOrEmpty(assetPath) ? Path.GetDirectoryName(assetPath) : null;
int foundCount = 0;
for (int i = 0; i < chars.Count; ++i)
{
var ch = chars[i];
if (ch == null) continue;
string name = ch.name;
if (string.IsNullOrEmpty(name)) name = $"glyph_{i}";
Sprite s = null;
// 1) 尝试从 glyph / TMP_SpriteGlyph 中取 sprite
try
{
var glyph = ch.glyph as TMP_SpriteGlyph;
if (glyph != null)
{
var possible = typeof(TMP_SpriteGlyph).GetProperty("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (possible != null)
{
s = possible.GetValue(glyph, null) as Sprite;
}
else
{
var f = typeof(TMP_SpriteGlyph).GetField("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (f != null) s = f.GetValue(glyph) as Sprite;
}
}
}
catch { s = null; }
// 2) atlas 查找
if (s == null && atlas != null)
{
try { s = atlas.GetSprite(name); } catch { s = null; }
if (s == null)
{
try
{
var m = typeof(SpriteAtlas).GetMethod("GetSprite", new Type[] { typeof(string) });
if (m != null) s = m.Invoke(atlas, new object[] { name }) as Sprite;
}
catch { s = null; }
}
}
// 3) asset folder scope 查找
if (s == null && !string.IsNullOrEmpty(assetFolder))
{ {
try try
{ {
string[] scoped = AssetDatabase.FindAssets($"\"{name}\" t:Sprite", new[] { assetFolder }); actionProp.objectReferenceValue = null;
if (scoped != null && scoped.Length > 0)
{
foreach (var g in scoped)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(p);
if (sp != null && sp.name == name)
{
s = sp; break;
}
}
}
}
catch { s = null; }
}
// 4) 全项目查找
if (s == null)
{
try
{
string[] all = AssetDatabase.FindAssets($"{name} t:Sprite");
if (all != null && all.Length > 0)
{
foreach (var g in all)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(p);
if (sp != null && sp.name == name)
{
s = sp; break;
}
}
}
}
catch { s = null; }
}
// 5) LoadAllAssetsAtPath (TMP asset 本身) 作为最后手段
if (s == null && !string.IsNullOrEmpty(assetPath))
{
try
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
if (allAssets != null)
{
foreach (var obj in allAssets)
{
if (obj is Sprite sp && sp.name == name)
{
s = sp; break;
}
}
}
}
catch { s = null; }
}
GlyphEntry entry = new GlyphEntry();
entry.Sprite = s;
entry.action = null;
tableObj.entries.Add(entry);
if (s != null) foundCount++;
else Debug.LogWarning($"[InputGlyphDatabase] Failed to resolve sprite '{name}' for TMP asset '{asset.name}' (table '{deviceName}').");
}
// 按名字逐字符排序(不区分大小写)
tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name));
EditorUtility.SetDirty(db);
serializedObject.Update();
serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
Debug.Log($"[InputGlyphDatabase] Parsed TMP '{asset.name}' into table '{deviceName}'. chars={chars.Count}, resolvedSprites={foundCount}");
}
// ----- Parse SpriteAtlasUnity SpriteAtlas -----
void ParseSpriteAtlasIntoTableSerialized(SerializedProperty tableProp)
{
if (tableProp == null) return;
var atlasProp = tableProp.FindPropertyRelative("spriteAtlas");
var atlas = atlasProp != null ? atlasProp.objectReferenceValue as SpriteAtlas : null;
if (atlas == null)
{
Debug.LogWarning("[InputGlyphDatabase] SpriteAtlas is null for table.");
return;
}
var nameProp = tableProp.FindPropertyRelative("deviceName");
string deviceName = nameProp != null ? nameProp.stringValue : "";
int tableIndex = MapSerializedTableToRuntimeIndex(deviceName);
if (tableIndex < 0)
{
Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables.");
return;
}
var tableObj = db.tables[tableIndex];
tableObj.entries.Clear();
string[] guids = AssetDatabase.FindAssets("t:Sprite");
int added = 0;
try
{
for (int gi = 0; gi < guids.Length; ++gi)
{
var guid = guids[gi];
var path = AssetDatabase.GUIDToAssetPath(guid);
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (sp == null) continue;
bool belongs = false;
try
{
var got = atlas.GetSprite(sp.name);
if (got != null) belongs = true;
} }
catch catch
{ {
try
{
var m = typeof(SpriteAtlas).GetMethod("GetSprite", new Type[] { typeof(string) });
if (m != null)
{
var got2 = m.Invoke(atlas, new object[] { sp.name }) as Sprite;
if (got2 != null) belongs = true;
}
}
catch { }
}
if (belongs)
{
GlyphEntry e = new GlyphEntry();
e.Sprite = sp;
e.action = null;
tableObj.entries.Add(e);
added++;
} }
} }
} }
catch (Exception ex)
serializedObject.ApplyModifiedProperties();
// also add to runtime list
var nameProp = tableProp.FindPropertyRelative("deviceName");
string deviceName = nameProp != null ? nameProp.stringValue : "";
int tableIndex = MapSerializedTableToRuntimeIndex(deviceName);
if (tableIndex >= 0 && db != null && db.tables != null && tableIndex < db.tables.Count)
{ {
Debug.LogError("[InputGlyphDatabase] Exception while scanning sprites for atlas: " + ex); var tableObj = db.tables[tableIndex];
GlyphEntry e = new GlyphEntry();
e.Sprite = sprite;
e.action = null; // runtime only: none provided here
tableObj.entries.Add(e);
} }
// 按名字逐字符排序(不区分大小写)
tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name));
EditorUtility.SetDirty(db); EditorUtility.SetDirty(db);
serializedObject.Update();
serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
Debug.Log($"[InputGlyphDatabase] Parsed SpriteAtlas '{atlas.name}' into table '{deviceName}'. foundSprites={added}");
} }
// ----- Parse Sprite Sheet (Texture2D with Multiple) -----
// ----- Parse Sprite Sheet (Texture2D with Multiple) -----
// 只用名称匹配覆盖 Sprite不改变已有 action
void ParseSpriteSheetIntoTableSerialized(SerializedProperty tableProp) void ParseSpriteSheetIntoTableSerialized(SerializedProperty tableProp)
{ {
if (tableProp == null) return; if (tableProp == null) return;
@ -705,7 +572,11 @@ public class InputGlyphDatabaseEditor : Editor
} }
var tableObj = db.tables[tableIndex]; var tableObj = db.tables[tableIndex];
tableObj.entries.Clear(); if (tableObj == null)
{
Debug.LogError($"[InputGlyphDatabase] Runtime table object is null for '{deviceName}'.");
return;
}
string path = AssetDatabase.GetAssetPath(tex); string path = AssetDatabase.GetAssetPath(tex);
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
@ -721,35 +592,107 @@ public class InputGlyphDatabaseEditor : Editor
return; return;
} }
// 收集 sprites按照文件内顺序你如果想按名字排序可以在这里加
List<Sprite> sprites = new List<Sprite>(); List<Sprite> sprites = new List<Sprite>();
foreach (var a in assets) foreach (var a in assets)
{ {
if (a is Sprite sp) if (a is Sprite sp) sprites.Add(sp);
}
var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp == null)
{
Debug.LogWarning("[InputGlyphDatabase] entries property not found on table.");
return;
}
// 构建序列化表名 -> 索引 映射(忽略大小写)
var serializedNameToIndex = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < entriesProp.arraySize; ++i)
{
var eProp = entriesProp.GetArrayElementAtIndex(i);
if (eProp == null) continue;
var sProp = eProp.FindPropertyRelative("Sprite");
var sRef = sProp != null ? sProp.objectReferenceValue as Sprite : null;
if (sRef != null && !serializedNameToIndex.ContainsKey(sRef.name))
{ {
sprites.Add(sp); serializedNameToIndex[sRef.name] = i;
} }
} }
// 之前按视觉位置排序,改为先按名字逐字符排序(不区分大小写) // runtime 名称 -> 索引 映射
sprites.Sort((a, b) => CompareSpriteNames(a?.name, b?.name)); var runtimeNameToIndex = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < tableObj.entries.Count; ++i)
{
var re = tableObj.entries[i];
if (re != null && re.Sprite != null)
{
var rn = re.Sprite.name;
if (!runtimeNameToIndex.ContainsKey(rn))
runtimeNameToIndex[rn] = i;
}
}
int replaced = 0, added = 0;
foreach (var sp in sprites) foreach (var sp in sprites)
{ {
GlyphEntry e = new GlyphEntry(); if (sp == null) continue;
e.Sprite = sp; string nm = sp.name;
e.action = null;
tableObj.entries.Add(e); // -- 序列化层:同名则替换 Sprite 引用(不触碰 action否则新增元素并把 action 设为 null --
if (serializedNameToIndex.TryGetValue(nm, out int sIndex))
{
var eProp = entriesProp.GetArrayElementAtIndex(sIndex);
if (eProp != null)
{
var spriteProp = eProp.FindPropertyRelative("Sprite");
if (spriteProp != null) spriteProp.objectReferenceValue = sp;
// 不修改 actionProp保持原有 action如果有的话
}
replaced++;
}
else
{
int insertIndex = entriesProp.arraySize;
entriesProp.InsertArrayElementAtIndex(insertIndex);
var newE = entriesProp.GetArrayElementAtIndex(insertIndex);
if (newE != null)
{
var spriteProp = newE.FindPropertyRelative("Sprite");
var actionProp = newE.FindPropertyRelative("action");
if (spriteProp != null) spriteProp.objectReferenceValue = sp;
if (actionProp != null) actionProp.objectReferenceValue = null; // 新增项 action 为空
}
serializedNameToIndex[nm] = insertIndex;
added++;
}
// -- 运行时层:同名则替换 Sprite否则新增 runtime entryaction 设 null保持之前 runtime entry 的 action 不变) --
if (runtimeNameToIndex.TryGetValue(nm, out int rIndex))
{
var runtimeEntry = tableObj.entries[rIndex];
if (runtimeEntry != null) runtimeEntry.Sprite = sp;
}
else
{
GlyphEntry ge = new GlyphEntry();
ge.Sprite = sp;
ge.action = null;
tableObj.entries.Add(ge);
runtimeNameToIndex[nm] = tableObj.entries.Count - 1;
}
} }
// 额外在 runtime list 里也用相同排序(上面已经排序过 sprites // 应用并保存修改(序列化层与 runtime 层保持同步)
tableObj.entries.Sort((a, b) => CompareSpriteNames(a?.Sprite?.name, b?.Sprite?.name));
EditorUtility.SetDirty(db); EditorUtility.SetDirty(db);
serializedObject.Update(); serializedObject.Update();
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
Debug.Log($"[InputGlyphDatabase] Parsed sprite sheet '{tex.name}' into table '{deviceName}'. foundSprites={sprites.Count}"); Debug.Log($"[InputGlyphDatabase] Merged sprite sheet '{tex.name}' into table '{deviceName}'. spritesFound={sprites.Count}, replaced={replaced}, added={added}, totalEntries={tableObj.entries.Count}");
} }
int MapSerializedTableToRuntimeIndex(string deviceName) int MapSerializedTableToRuntimeIndex(string deviceName)
@ -760,56 +703,7 @@ public class InputGlyphDatabaseEditor : Editor
if (string.Equals(db.tables[ti].deviceName, deviceName, StringComparison.OrdinalIgnoreCase)) if (string.Equals(db.tables[ti].deviceName, deviceName, StringComparison.OrdinalIgnoreCase))
return ti; return ti;
} }
return -1; return -1;
} }
SpriteAtlas GetSpriteAtlasFromTMP(TMP_SpriteAsset asset)
{
if (asset == null) return null;
var t = asset.GetType();
foreach (var f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (typeof(SpriteAtlas).IsAssignableFrom(f.FieldType))
{
var val = f.GetValue(asset) as SpriteAtlas;
if (val != null) return val;
}
}
foreach (var p in t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (typeof(SpriteAtlas).IsAssignableFrom(p.PropertyType))
{
try
{
var val = p.GetValue(asset, null) as SpriteAtlas;
if (val != null) return val;
}
catch { }
}
}
return null;
}
int CompareSpriteNames(string a, string b)
{
// normalize null/empty
bool aEmpty = string.IsNullOrEmpty(a);
bool bEmpty = string.IsNullOrEmpty(b);
if (aEmpty && bEmpty) return 0;
if (aEmpty) return -1;
if (bEmpty) return 1;
int la = a.Length;
int lb = b.Length;
int n = Math.Min(la, lb);
for (int i = 0; i < n; ++i)
{
char ca = char.ToUpperInvariant(a[i]);
char cb = char.ToUpperInvariant(b[i]);
if (ca != cb) return ca - cb;
}
return la - lb;
}
} }

View File

@ -1,20 +1,22 @@
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using AlicizaX.InputGlyph;
[RequireComponent(typeof(Image))] public sealed class InputGlyphImage : MonoBehaviour
public class InputGlyphImage : MonoBehaviour
{ {
[SerializeField] private InputActionReference actionReference; [SerializeField] private InputActionReference actionReference;
[SerializeField] private Image targetImage; [SerializeField] private Image targetImage;
[SerializeField] private bool hideIfMissing = false; [SerializeField] private bool hideIfMissing = false;
[SerializeField] private GameObject hideTargetObject; [SerializeField] private GameObject hideTargetObject;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite;
void OnEnable() void OnEnable()
{ {
if (targetImage == null) targetImage = GetComponent<Image>(); if (targetImage == null) targetImage = GetComponent<Image>();
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt(); UpdatePrompt();
} }
@ -25,18 +27,34 @@ public class InputGlyphImage : MonoBehaviour
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{ {
UpdatePrompt(); if (_cachedCategory != cat)
{
_cachedCategory = cat;
UpdatePrompt();
}
} }
void UpdatePrompt() void UpdatePrompt()
{ {
if (actionReference == null || actionReference.action == null || targetImage == null) return; if (actionReference == null || actionReference.action == null || targetImage == null) return;
InputDeviceWatcher.InputDeviceCategory deviceCategory = InputDeviceWatcher.CurrentCategory;
if (GlyphService.TryGetUISpriteForActionPath(actionReference, deviceCategory, out Sprite sprite)) // Use cached category instead of re-querying CurrentCategory
if (GlyphService.TryGetUISpriteForActionPath(actionReference, "", _cachedCategory, out Sprite sprite))
{ {
targetImage.sprite = sprite; if (_cachedSprite != sprite)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
}
} }
if (hideTargetObject) hideTargetObject.SetActive(sprite != null && !hideIfMissing); if (hideTargetObject != null)
{
bool shouldBeActive = sprite != null && !hideIfMissing;
if (hideTargetObject.activeSelf != shouldBeActive)
{
hideTargetObject.SetActive(shouldBeActive);
}
}
} }
} }

View File

@ -1,22 +1,25 @@
using System; using System;
using System.Linq; using System.Linq;
using AlicizaX; using AlicizaX;
using AlicizaX.InputGlyph;
using UnityEngine; using UnityEngine;
using TMPro; using TMPro;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
[RequireComponent(typeof(TextMeshProUGUI))] [RequireComponent(typeof(TextMeshProUGUI))]
public class InputGlyphText : MonoBehaviour public sealed class InputGlyphText : MonoBehaviour
{ {
[SerializeField] private InputActionReference actionReference; [SerializeField] private InputActionReference actionReference;
private TMP_Text textField; private TMP_Text textField;
private string _oldText; private string _oldText;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private string _cachedFormattedText;
void OnEnable() void OnEnable()
{ {
if (textField == null) textField = GetComponent<TMP_Text>(); if (textField == null) textField = GetComponent<TMP_Text>();
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged; InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_oldText=textField.text; _oldText = textField.text;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt(); UpdatePrompt();
} }
@ -25,22 +28,36 @@ public class InputGlyphText : MonoBehaviour
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged; InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
} }
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat) void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{ {
UpdatePrompt(); if (_cachedCategory != cat)
{
_cachedCategory = cat;
UpdatePrompt();
}
} }
void UpdatePrompt() void UpdatePrompt()
{ {
if (actionReference == null || actionReference.action == null || textField == null) return; if (actionReference == null || actionReference.action == null || textField == null) return;
var device = InputDeviceWatcher.CurrentCategory;
if (GlyphService.TryGetTMPTagForActionPath(actionReference, device, out string tag, out string displayFallback)) // Use cached category instead of re-querying CurrentCategory
if (GlyphService.TryGetTMPTagForActionPath(actionReference, "", _cachedCategory, out string tag, out string displayFallback))
{ {
textField.text = Utility.Text.Format(_oldText, tag); string formattedText = Utility.Text.Format(_oldText, tag);
if (_cachedFormattedText != formattedText)
{
_cachedFormattedText = formattedText;
textField.text = formattedText;
}
return; return;
} }
textField.text = Utility.Text.Format(_oldText, displayFallback);; string fallbackText = Utility.Text.Format(_oldText, displayFallback);
if (_cachedFormattedText != fallbackText)
{
_cachedFormattedText = fallbackText;
textField.text = fallbackText;
}
} }
} }

View File

@ -0,0 +1,63 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
[RequireComponent(typeof(UXButton))]
public sealed class InputGlyphUXButton : MonoBehaviour
{
[SerializeField] private UXButton button;
[SerializeField] private Image targetImage;
private InputActionReference _actionReference;
private InputDeviceWatcher.InputDeviceCategory _cachedCategory;
private Sprite _cachedSprite;
#if UNITY_EDITOR
private void OnValidate()
{
if (button == null)
{
button = GetComponent<UXButton>();
}
}
#endif
void OnEnable()
{
if (button == null) button = GetComponent<UXButton>();
if (targetImage == null) targetImage = GetComponent<Image>();
_actionReference = button.HotKeyRefrence;
InputDeviceWatcher.OnDeviceChanged += OnDeviceChanged;
_cachedCategory = InputDeviceWatcher.InputDeviceCategory.Keyboard;
UpdatePrompt();
}
void OnDisable()
{
InputDeviceWatcher.OnDeviceChanged -= OnDeviceChanged;
}
void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory cat)
{
if (_cachedCategory != cat)
{
_cachedCategory = cat;
UpdatePrompt();
}
}
void UpdatePrompt()
{
if (_actionReference == null || _actionReference.action == null || targetImage == null) return;
// Use cached category instead of re-querying CurrentCategory
if (GlyphService.TryGetUISpriteForActionPath(_actionReference, "", _cachedCategory, out Sprite sprite))
{
if (_cachedSprite != sprite)
{
_cachedSprite = sprite;
targetImage.sprite = sprite;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f24ff96430d042109462f373aed1f2cc
timeCreated: 1765870646

View File

@ -1,16 +1,10 @@
// TestRebindScript.cs
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem; using UnityEngine.InputSystem;
using InputRemapper;
using UnityEngine.UI; using UnityEngine.UI;
/// <summary>
/// 测试用不需要繁琐处理
/// </summary>
public class TestRebindScript : MonoBehaviour public class TestRebindScript : MonoBehaviour
{ {
[Header("UI")] public UXButton btn; [Header("UI")] public UXButton btn;
@ -29,6 +23,7 @@ public class TestRebindScript : MonoBehaviour
private IDisposable prepareSub; private IDisposable prepareSub;
private IDisposable applySub; private IDisposable applySub;
private IDisposable rebindEndSub; private IDisposable rebindEndSub;
private IDisposable conflictSub;
private void Start() private void Start()
{ {
@ -38,6 +33,7 @@ public class TestRebindScript : MonoBehaviour
if (InputBindingManager.Instance != null) if (InputBindingManager.Instance != null)
{ {
// Subscribe to prepare events - already filtered by IsTargetContext
prepareSub = InputBindingManager.Instance.OnRebindPrepare.Subscribe(ctx => prepareSub = InputBindingManager.Instance.OnRebindPrepare.Subscribe(ctx =>
{ {
if (IsTargetContext(ctx)) if (IsTargetContext(ctx))
@ -48,8 +44,44 @@ public class TestRebindScript : MonoBehaviour
} }
}); });
applySub = InputBindingManager.Instance.OnApply.Subscribe(_ => UpdateBindingText()); // Subscribe to apply events - only update if this instance's binding was applied or discarded
rebindEndSub = InputBindingManager.Instance.OnRebindEnd.Subscribe(_ => UpdateBindingText()); applySub = InputBindingManager.Instance.OnApply.Subscribe(evt =>
{
var (success, appliedContexts) = evt;
if (appliedContexts != null)
{
// Only update if any of the applied/discarded contexts match this instance
foreach (var ctx in appliedContexts)
{
if (IsTargetContext(ctx))
{
UpdateBindingText();
break;
}
}
}
});
// Subscribe to rebind end events - only update if this instance's binding ended
rebindEndSub = InputBindingManager.Instance.OnRebindEnd.Subscribe(evt =>
{
var (success, context) = evt;
if (IsTargetContext(context))
{
UpdateBindingText();
}
});
// Subscribe to conflict events - update if this instance is involved in conflict
conflictSub = InputBindingManager.Instance.OnRebindConflict.Subscribe(evt =>
{
var (prepared, conflict) = evt;
// Update if either the prepared or conflict context matches this instance
if (IsTargetContext(prepared) || IsTargetContext(conflict))
{
UpdateBindingText();
}
});
} }
} }
@ -60,6 +92,7 @@ public class TestRebindScript : MonoBehaviour
prepareSub?.Dispose(); prepareSub?.Dispose();
applySub?.Dispose(); applySub?.Dispose();
rebindEndSub?.Dispose(); rebindEndSub?.Dispose();
conflictSub?.Dispose();
} }
private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _) private void OnDeviceChanged(InputDeviceWatcher.InputDeviceCategory _)
@ -73,12 +106,30 @@ public class TestRebindScript : MonoBehaviour
} }
private bool IsTargetContext(InputRemapper.InputBindingManager.RebindContext ctx) private bool IsTargetContext(InputBindingManager.RebindContext ctx)
{ {
if (ctx == null || ctx.action == null) return false; if (ctx == null || ctx.action == null) return false;
var action = GetAction(); var action = GetAction();
if (action == null) return false; if (action == null) return false;
return ctx.action == action;
// Must match the action
if (ctx.action != action) return false;
// If we have a composite part specified, we need to match the binding index
if (!string.IsNullOrEmpty(compositePartName))
{
// Get the binding at the context's index
if (ctx.bindingIndex < 0 || ctx.bindingIndex >= action.bindings.Count)
return false;
var binding = action.bindings[ctx.bindingIndex];
// Check if the binding's name matches our composite part
return string.Equals(binding.name, compositePartName, StringComparison.OrdinalIgnoreCase);
}
// If no composite part specified, just matching the action is enough
return true;
} }
private void OnBtnClicked() private void OnBtnClicked()
@ -98,7 +149,6 @@ public class TestRebindScript : MonoBehaviour
try try
{ {
var task = InputBindingManager.ConfirmApply(); var task = InputBindingManager.ConfirmApply();
if (task == null) return false;
return await task; return await task;
} }
catch (Exception ex) catch (Exception ex)
@ -111,7 +161,7 @@ public class TestRebindScript : MonoBehaviour
public void CancelPrepared() public void CancelPrepared()
{ {
InputBindingManager.DiscardPrepared(); InputBindingManager.DiscardPrepared();
UpdateBindingText(); // UpdateBindingText will be called automatically via OnApply event
} }
private void UpdateBindingText() private void UpdateBindingText()
@ -125,15 +175,15 @@ public class TestRebindScript : MonoBehaviour
} }
string disp = GlyphService.GetBindingControlPath(action, InputDeviceWatcher.CurrentCategory); bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action, compositePartName);
bindKeyText.text = GlyphService.GetDisplayNameFromInputAction(action);
try try
{ {
var deviceCat = InputDeviceWatcher.CurrentCategory; var deviceCat = InputDeviceWatcher.CurrentCategory;
string controlPath = GlyphService.GetBindingControlPath(action, deviceCat); InputActionReference refr=default;
if (!string.IsNullOrEmpty(controlPath) && GlyphService.TryGetUISpriteForActionPath(controlPath, deviceCat, out Sprite sprite)) // string controlPath = GlyphService.GetBindingControlPath(action, compositePartName, deviceCat);
if ( GlyphService.TryGetUISpriteForActionPath(action,compositePartName, deviceCat, out Sprite sprite))
{ {
if (targetImage != null) targetImage.sprite = sprite; if (targetImage != null) targetImage.sprite = sprite;
} }

View File

@ -1,9 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using AlicizaX.InputGlyph;
using AlicizaX.UI;
using AlicizaX.UI.Extension;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;

@ -1 +1 @@
Subproject commit 91cd9d2981e046ce0b03e3bbbd6c6a0294cd1150 Subproject commit 90291aa4a969a022e7a02d855748d55af03828b0

@ -1 +1 @@
Subproject commit 0a5ef9135ccb240227cf655e2251a63bf2d5da7a Subproject commit 74318889a2a1f3d93d1dbd7e35b610fce575043e

@ -1 +1 @@
Subproject commit f92e91920dc4c31b41e36e3fa40058d4e6aace34 Subproject commit b3f3f268bf27686916f963b219ffda4e8392b914

View File

@ -30,14 +30,14 @@ EditorUserSettings:
value: 56060350000d5b5a5908597a48255a44174e4d797a7d7e6475794f61e7b3643e value: 56060350000d5b5a5908597a48255a44174e4d797a7d7e6475794f61e7b3643e
flags: 0 flags: 0
RecentlyUsedSceneGuid-7: RecentlyUsedSceneGuid-7:
value: 50500404540c580d0f0b5e7543725b44424f4c7a7b7c7734747e4f36e4b1676d
flags: 0
RecentlyUsedSceneGuid-8:
value: 015450045700505d0f0a5f2313260a444e164b2e757b76652c2d4d32bab0313a value: 015450045700505d0f0a5f2313260a444e164b2e757b76652c2d4d32bab0313a
flags: 0 flags: 0
RecentlyUsedSceneGuid-9: RecentlyUsedSceneGuid-8:
value: 5a07065703500c59585e0e7748770d44444f4a737d2d7f35787d4f63e0b26668 value: 5a07065703500c59585e0e7748770d44444f4a737d2d7f35787d4f63e0b26668
flags: 0 flags: 0
RecentlyUsedSceneGuid-9:
value: 50500404540c580d0f0b5e7543725b44424f4c7a7b7c7734747e4f36e4b1676d
flags: 0
vcSharedLogLevel: vcSharedLogLevel:
value: 0d5e400f0650 value: 0d5e400f0650
flags: 0 flags: 0

View File

@ -19,7 +19,7 @@ MonoBehaviour:
width: 1920 width: 1920
height: 997 height: 997
m_ShowMode: 4 m_ShowMode: 4
m_Title: Project m_Title: Console
m_RootView: {fileID: 4} m_RootView: {fileID: 4}
m_MinSize: {x: 875, y: 300} m_MinSize: {x: 875, y: 300}
m_MaxSize: {x: 10000, y: 10000} m_MaxSize: {x: 10000, y: 10000}
@ -41,7 +41,7 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 696 y: 696
width: 421 width: 266
height: 251 height: 251
m_MinSize: {x: 51, y: 71} m_MinSize: {x: 51, y: 71}
m_MaxSize: {x: 4001, y: 4021} m_MaxSize: {x: 4001, y: 4021}
@ -70,7 +70,7 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 421 width: 266
height: 947 height: 947
m_MinSize: {x: 100, y: 100} m_MinSize: {x: 100, y: 100}
m_MaxSize: {x: 8096, y: 16192} m_MaxSize: {x: 8096, y: 16192}
@ -174,7 +174,7 @@ MonoBehaviour:
m_MinSize: {x: 400, y: 100} m_MinSize: {x: 400, y: 100}
m_MaxSize: {x: 32384, y: 16192} m_MaxSize: {x: 32384, y: 16192}
vertical: 0 vertical: 0
controlID: 69 controlID: 24
draggingID: 0 draggingID: 0
--- !u!114 &8 --- !u!114 &8
MonoBehaviour: MonoBehaviour:
@ -193,7 +193,7 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 421 width: 266
height: 696 height: 696
m_MinSize: {x: 201, y: 221} m_MinSize: {x: 201, y: 221}
m_MaxSize: {x: 4001, y: 4021} m_MaxSize: {x: 4001, y: 4021}
@ -219,9 +219,9 @@ MonoBehaviour:
- {fileID: 11} - {fileID: 11}
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 421 x: 266
y: 0 y: 0
width: 284 width: 388
height: 947 height: 947
m_MinSize: {x: 100, y: 100} m_MinSize: {x: 100, y: 100}
m_MaxSize: {x: 8096, y: 16192} m_MaxSize: {x: 8096, y: 16192}
@ -245,8 +245,8 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 0 y: 0
width: 284 width: 388
height: 249 height: 409
m_MinSize: {x: 202, y: 221} m_MinSize: {x: 202, y: 221}
m_MaxSize: {x: 4002, y: 4021} m_MaxSize: {x: 4002, y: 4021}
m_ActualView: {fileID: 17} m_ActualView: {fileID: 17}
@ -270,9 +270,9 @@ MonoBehaviour:
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 249 y: 409
width: 284 width: 388
height: 698 height: 538
m_MinSize: {x: 102, y: 121} m_MinSize: {x: 102, y: 121}
m_MaxSize: {x: 4002, y: 4021} m_MaxSize: {x: 4002, y: 4021}
m_ActualView: {fileID: 18} m_ActualView: {fileID: 18}
@ -295,9 +295,9 @@ MonoBehaviour:
m_Children: [] m_Children: []
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 705 x: 654
y: 0 y: 0
width: 431 width: 479
height: 947 height: 947
m_MinSize: {x: 232, y: 271} m_MinSize: {x: 232, y: 271}
m_MaxSize: {x: 10002, y: 10021} m_MaxSize: {x: 10002, y: 10021}
@ -321,9 +321,9 @@ MonoBehaviour:
m_Children: [] m_Children: []
m_Position: m_Position:
serializedVersion: 2 serializedVersion: 2
x: 1136 x: 1133
y: 0 y: 0
width: 784 width: 787
height: 947 height: 947
m_MinSize: {x: 276, y: 71} m_MinSize: {x: 276, y: 71}
m_MaxSize: {x: 4001, y: 4021} m_MaxSize: {x: 4001, y: 4021}
@ -354,7 +354,7 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 769 y: 769
width: 420 width: 265
height: 230 height: 230
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
@ -372,7 +372,7 @@ MonoBehaviour:
m_ShowGizmos: 0 m_ShowGizmos: 0
m_TargetDisplay: 0 m_TargetDisplay: 0
m_ClearColor: {r: 0, g: 0, b: 0, a: 0} m_ClearColor: {r: 0, g: 0, b: 0, a: 0}
m_TargetSize: {x: 420, y: 209} m_TargetSize: {x: 265, y: 209}
m_TextureFilterMode: 0 m_TextureFilterMode: 0
m_TextureHideFlags: 61 m_TextureHideFlags: 61
m_RenderIMGUI: 1 m_RenderIMGUI: 1
@ -387,8 +387,8 @@ MonoBehaviour:
m_VRangeLocked: 0 m_VRangeLocked: 0
hZoomLockedByDefault: 0 hZoomLockedByDefault: 0
vZoomLockedByDefault: 0 vZoomLockedByDefault: 0
m_HBaseRangeMin: -210 m_HBaseRangeMin: -132.5
m_HBaseRangeMax: 210 m_HBaseRangeMax: 132.5
m_VBaseRangeMin: -104.5 m_VBaseRangeMin: -104.5
m_VBaseRangeMax: 104.5 m_VBaseRangeMax: 104.5
m_HAllowExceedBaseRangeMin: 1 m_HAllowExceedBaseRangeMin: 1
@ -408,23 +408,23 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 21 y: 21
width: 420 width: 265
height: 209 height: 209
m_Scale: {x: 1, y: 1} m_Scale: {x: 1, y: 1}
m_Translation: {x: 210, y: 104.5} m_Translation: {x: 132.5, y: 104.5}
m_MarginLeft: 0 m_MarginLeft: 0
m_MarginRight: 0 m_MarginRight: 0
m_MarginTop: 0 m_MarginTop: 0
m_MarginBottom: 0 m_MarginBottom: 0
m_LastShownAreaInsideMargins: m_LastShownAreaInsideMargins:
serializedVersion: 2 serializedVersion: 2
x: -210 x: -132.5
y: -104.5 y: -104.5
width: 420 width: 265
height: 209 height: 209
m_MinimalGUI: 1 m_MinimalGUI: 1
m_defaultScale: 1 m_defaultScale: 1
m_LastWindowPixelSize: {x: 420, y: 230} m_LastWindowPixelSize: {x: 265, y: 230}
m_ClearInEditMode: 1 m_ClearInEditMode: 1
m_NoCameraWarning: 1 m_NoCameraWarning: 1
m_LowResolutionForAspectRatios: 01000000000000000000 m_LowResolutionForAspectRatios: 01000000000000000000
@ -522,7 +522,7 @@ MonoBehaviour:
serializedVersion: 2 serializedVersion: 2
x: 0 x: 0
y: 73 y: 73
width: 420 width: 265
height: 675 height: 675
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
@ -1163,10 +1163,10 @@ MonoBehaviour:
m_Tooltip: m_Tooltip:
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 421 x: 266
y: 73 y: 73
width: 282 width: 386
height: 228 height: 388
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
@ -1180,9 +1180,9 @@ MonoBehaviour:
m_SceneHierarchy: m_SceneHierarchy:
m_TreeViewState: m_TreeViewState:
scrollPos: {x: 0, y: 0} scrollPos: {x: 0, y: 0}
m_SelectedIDs: 626f0000 m_SelectedIDs: 2e1c0000
m_LastClickedID: 0 m_LastClickedID: 0
m_ExpandedIDs: eefafffff6fafffff8faffffd66c0000 m_ExpandedIDs: 28fbffff
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name:
@ -1226,10 +1226,10 @@ MonoBehaviour:
m_Tooltip: m_Tooltip:
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 421 x: 266
y: 322 y: 482
width: 282 width: 386
height: 677 height: 517
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
m_PreferredDataMode: 0 m_PreferredDataMode: 0
@ -1260,9 +1260,9 @@ MonoBehaviour:
m_Tooltip: m_Tooltip:
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 705 x: 654
y: 73 y: 73
width: 429 width: 477
height: 926 height: 926
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
@ -1285,7 +1285,7 @@ MonoBehaviour:
m_SkipHidden: 0 m_SkipHidden: 0
m_SearchArea: 2 m_SearchArea: 2
m_Folders: m_Folders:
- Packages/com.alicizax.unity.ui.extension/Editor/Res/ComponentIcon - Assets/Plugins/PrimeTween/Demo/Scripts/MeasureAllocations
m_Globs: [] m_Globs: []
m_OriginalText: m_OriginalText:
m_ImportLogFlags: 0 m_ImportLogFlags: 0
@ -1301,7 +1301,7 @@ MonoBehaviour:
scrollPos: {x: 0, y: 0} scrollPos: {x: 0, y: 0}
m_SelectedIDs: e48c0000 m_SelectedIDs: e48c0000
m_LastClickedID: 36068 m_LastClickedID: 36068
m_ExpandedIDs: 00000000226f0000246f0000266f0000286f00002a6f00002c6f00002e6f0000306f0000326f0000346f0000366f0000386f00003a6f00003c6f00003e6f0000406f0000426f0000446f0000466f0000486f00004a6f00004c6f00004e6f0000506f0000526f0000546f0000566f0000586f00005a6f00005c6f00005e6f0000606f0000626f0000646f0000666f0000686f00006a6f0000 m_ExpandedIDs: 00000000860d0000746d0000766d0000786d00007a6d00007c6d00007e6d0000806d0000826d0000846d0000866d0000886d00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name:
@ -1326,10 +1326,10 @@ MonoBehaviour:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_ResourceFile: m_ResourceFile:
m_AssetTreeState: m_AssetTreeState:
scrollPos: {x: 0, y: 720} scrollPos: {x: 0, y: 528}
m_SelectedIDs: m_SelectedIDs:
m_LastClickedID: 0 m_LastClickedID: 0
m_ExpandedIDs: ffffffff00000000226f0000246f0000266f0000286f00002a6f00002e6f0000306f0000326f0000346f0000366f0000386f00003a6f00003c6f00003e6f0000406f0000426f0000446f0000466f0000486f00004a6f00004e6f0000506f0000526f0000546f0000566f0000586f00005a6f00005c6f00005e6f0000606f0000626f0000646f0000666f0000686f00006a6f0000ffffff7f m_ExpandedIDs: ffffffff00000000860d0000746d0000766d0000786d00007a6d00007c6d00007e6d0000806d0000826d0000846d0000866d0000886d00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000c46f0000f47000001c710000
m_RenameOverlay: m_RenameOverlay:
m_UserAcceptedRename: 0 m_UserAcceptedRename: 0
m_Name: m_Name:
@ -1405,9 +1405,9 @@ MonoBehaviour:
m_Tooltip: m_Tooltip:
m_Pos: m_Pos:
serializedVersion: 2 serializedVersion: 2
x: 1136 x: 1133
y: 73 y: 73
width: 783 width: 786
height: 926 height: 926
m_SerializedDataModeController: m_SerializedDataModeController:
m_DataMode: 0 m_DataMode: 0
@ -1426,7 +1426,7 @@ MonoBehaviour:
m_ControlHash: 1412526313 m_ControlHash: 1412526313
m_PrefName: Preview_InspectorPreview m_PrefName: Preview_InspectorPreview
m_LastInspectedObjectInstanceID: -1 m_LastInspectedObjectInstanceID: -1
m_LastVerticalScrollValue: 432 m_LastVerticalScrollValue: 0
m_GlobalObjectId: m_GlobalObjectId:
m_InspectorMode: 0 m_InspectorMode: 0
m_LockTracker: m_LockTracker: