using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
using BaseGames.Editor;
using BaseGames.UI;
namespace BaseGames.Editor.Input
{
///
/// 按键图标管理工作台(Input Icon Studio)。
///
/// 布局:
/// 工具栏 (InputReaderSO 选取 + 刷新按钮)
/// 设备标签栏 (键鼠 / Xbox / PlayStation / Switch)
/// TwoPaneSplitView:
/// 左列:Action 列表(带覆盖指示点 + 当前绑定路径预览)
/// 右列:
/// ① 当前 Action × 当前设备的绑定路径 + Sprite 字段
/// ② 48px 大图预览
/// ③ 所有设备快览行(4 行)
/// ④ 模拟 InteractPromptWidget 外观预览
///
/// 菜单:BaseGames / Input Icon Studio (priority=55)
///
public class InputIconStudioWindow : EditorWindow
{
private const string UssPath = "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss";
private const string PrefDevice = "InputIconStudio.Device";
private const string PrefAction = "InputIconStudio.Action";
private const float ListWidth = 280f;
// ── 静态开口 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Input Icon Studio", priority = 55)]
public static void Open()
{
var wnd = GetWindow();
wnd.titleContent = new GUIContent("Input Icon Studio",
EditorGUIUtility.IconContent("d_PreTextureRGB").image);
wnd.minSize = new Vector2(780, 480);
}
// ── 数据 ──────────────────────────────────────────────────────────────
private ScriptableObject _inputReaderAsset;
private InputActionAsset _actionAsset;
// 四套图标集 SO(每个设备一套,可为 null)
private InputDeviceIconSetSO _kbSet, _xboxSet, _psSet, _switchSet;
// 当前激活设备标签
private InputDeviceType _activeDevice = InputDeviceType.KeyboardMouse;
// 当前选中的 Action 名称
private string _selectedAction;
// 缓存所有 actions(仅 Gameplay map)
private readonly List _actionNames = new();
// ── UI 节点引用 ───────────────────────────────────────────────────────
private VisualElement _listContainer;
private VisualElement _detailPanel;
private VisualElement _deviceTabBar;
// ── 生命周期 ──────────────────────────────────────────────────────────
public void CreateGUI()
{
var uss = AssetDatabase.LoadAssetAtPath(UssPath);
if (uss != null) rootVisualElement.styleSheets.Add(uss);
rootVisualElement.style.flexDirection = FlexDirection.Column;
rootVisualElement.style.flexGrow = 1;
// 恢复上次选中状态
_activeDevice = (InputDeviceType)EditorPrefs.GetInt(PrefDevice, 0);
_selectedAction = EditorPrefs.GetString(PrefAction, null);
BuildToolbar();
BuildDeviceTabBar();
BuildMainSplit();
LoadAssetsFromProject();
}
private void OnDisable()
{
EditorPrefs.SetInt(PrefDevice, (int)_activeDevice);
EditorPrefs.SetString(PrefAction, _selectedAction ?? string.Empty);
}
// ── 工具栏 ────────────────────────────────────────────────────────────
private void BuildToolbar()
{
var toolbar = new VisualElement();
toolbar.AddToClassList("editor-toolbar");
// 标题图标
var icon = EditorGUIUtility.IconContent("d_PreTextureRGB");
if (icon.image != null)
{
var iconImg = new Image { image = icon.image };
iconImg.style.width = 18;
iconImg.style.height = 18;
iconImg.style.marginRight = 6;
toolbar.Add(iconImg);
}
var title = new Label("INPUT ICON STUDIO");
title.style.unityFontStyleAndWeight = FontStyle.Bold;
title.style.fontSize = 12;
title.style.opacity = 0.85f;
title.style.marginRight = 12;
toolbar.Add(title);
// InputReaderSO 选取器
var readerLabel = new Label("InputReaderSO:");
readerLabel.style.opacity = 0.65f;
readerLabel.style.marginRight = 4;
readerLabel.style.alignSelf = Align.Center;
toolbar.Add(readerLabel);
var readerField = new ObjectField
{
objectType = typeof(ScriptableObject),
allowSceneObjects = false,
value = _inputReaderAsset
};
readerField.style.width = 200;
readerField.style.flexShrink = 0;
readerField.RegisterValueChangedCallback(evt =>
{
_inputReaderAsset = evt.newValue as ScriptableObject;
ReloadActionAsset();
RebuildActionList();
RebuildDetail();
});
toolbar.Add(readerField);
// 弹性填充
var spacer = new VisualElement();
spacer.style.flexGrow = 1;
toolbar.Add(spacer);
// 刷新按钮
var btnRefresh = new Button(RefreshAll) { text = "⟳ 刷新" };
btnRefresh.tooltip = "重新扫描项目资产";
toolbar.Add(btnRefresh);
// 帮助按钮
var btnHelp = new Button(ShowHelp) { text = "?" };
btnHelp.style.width = 24;
btnHelp.tooltip = "使用说明";
toolbar.Add(btnHelp);
rootVisualElement.Add(toolbar);
}
// ── 设备标签栏 ────────────────────────────────────────────────────────
private void BuildDeviceTabBar()
{
_deviceTabBar = new VisualElement();
_deviceTabBar.AddToClassList("tab-bar");
_deviceTabBar.style.paddingLeft = 8;
_deviceTabBar.style.paddingRight = 8;
rootVisualElement.Add(_deviceTabBar);
RebuildDeviceTabs();
}
private void RebuildDeviceTabs()
{
_deviceTabBar.Clear();
var devices = new[]
{
(InputDeviceType.KeyboardMouse, "🖱 键鼠", "(W/A/S/D + Mouse)"),
(InputDeviceType.XboxController, "🎮 Xbox", "Xbox Controller"),
(InputDeviceType.PlayStationController,"🎮 PlayStation","DualShock / DualSense"),
(InputDeviceType.SwitchController, "🕹 Switch", "Pro Controller / Joy-Con"),
};
foreach (var (devType, label, hint) in devices)
{
var iconSet = GetIconSetForDevice(devType);
int total = GetEntryCount(iconSet);
int filled = GetFilledCount(iconSet);
// 覆盖率圆点颜色
var dot = total > 0 && filled == total ? "●" : (filled == 0 ? "○" : "◑");
var dotColor = total > 0 && filled == total
? new Color(0.3f, 0.85f, 0.45f)
: filled == 0
? new Color(0.8f, 0.3f, 0.3f)
: new Color(0.9f, 0.75f, 0.2f);
var btn = new Button(() => SelectDevice(devType));
btn.tooltip = hint;
btn.style.flexDirection = FlexDirection.Row;
btn.style.alignItems = Align.Center;
btn.AddToClassList("tab-button");
var dotLbl = new Label(dot);
dotLbl.style.color = new StyleColor(dotColor);
dotLbl.style.fontSize = 10;
dotLbl.style.marginRight = 4;
btn.Add(dotLbl);
var nameLbl = new Label(label);
btn.Add(nameLbl);
if (total > 0)
{
var countLbl = new Label($" {filled}/{total}");
countLbl.style.fontSize = 10;
countLbl.style.opacity = 0.55f;
btn.Add(countLbl);
}
if (devType == _activeDevice)
btn.AddToClassList("tab-button--active");
_deviceTabBar.Add(btn);
}
}
private void SelectDevice(InputDeviceType device)
{
_activeDevice = device;
RebuildDeviceTabs();
RebuildActionList();
RebuildDetail();
}
// ── 主分屏 ────────────────────────────────────────────────────────────
private void BuildMainSplit()
{
var split = new TwoPaneSplitView(0, ListWidth, TwoPaneSplitViewOrientation.Horizontal);
split.style.flexGrow = 1;
rootVisualElement.Add(split);
// ── 左:Action 列表 ────────────────────────────────────────────
var leftPane = new VisualElement();
leftPane.style.flexDirection = FlexDirection.Column;
split.Add(leftPane);
// 左列标题
var leftHeader = new VisualElement();
leftHeader.AddToClassList("pane-header");
leftHeader.style.flexDirection = FlexDirection.Row;
leftHeader.style.alignItems = Align.Center;
var leftTitle = new Label("Actions");
leftTitle.style.flexGrow = 1;
leftHeader.Add(leftTitle);
var btnSetAsset = new Button(PickIconSetForCurrentDevice)
{
text = "📂 指定图标集",
tooltip = "为当前设备绑定一个 InputDeviceIconSetSO 资产"
};
btnSetAsset.style.fontSize = 10;
btnSetAsset.style.height = 20;
leftHeader.Add(btnSetAsset);
leftPane.Add(leftHeader);
// 左列滚动区
var leftScroll = new ScrollView(ScrollViewMode.Vertical);
leftScroll.style.flexGrow = 1;
leftPane.Add(leftScroll);
_listContainer = new VisualElement();
_listContainer.style.paddingTop = 4;
_listContainer.style.paddingBottom = 4;
leftScroll.Add(_listContainer);
// ── 右:详情区 ─────────────────────────────────────────────────
var rightScroll = new ScrollView(ScrollViewMode.Vertical);
rightScroll.style.flexGrow = 1;
split.Add(rightScroll);
_detailPanel = new VisualElement();
_detailPanel.style.flexDirection = FlexDirection.Column;
_detailPanel.style.paddingBottom = 16;
rightScroll.Add(_detailPanel);
}
// ── Action 列表 ───────────────────────────────────────────────────────
private void RebuildActionList()
{
if (_listContainer == null) return;
_listContainer.Clear();
if (_actionNames.Count == 0)
{
var empty = new Label("未加载 Action Asset\n请在上方指定 InputReaderSO");
empty.style.opacity = 0.5f;
empty.style.marginTop = 20;
empty.style.whiteSpace = WhiteSpace.Normal;
empty.style.unityTextAlign = TextAnchor.MiddleCenter;
_listContainer.Add(empty);
return;
}
var iconSet = GetIconSetForDevice(_activeDevice);
foreach (var actionName in _actionNames)
{
var effectivePath = GetEffectivePath(actionName);
var sprite = iconSet?.GetIcon(effectivePath ?? string.Empty);
bool hasIcon = sprite != null;
var row = new Button(() =>
{
_selectedAction = actionName;
RebuildActionListSelection();
RebuildDetail();
});
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.paddingLeft = 10;
row.style.paddingRight = 8;
row.style.paddingTop = 6;
row.style.paddingBottom = 6;
row.style.borderRadius(0);
row.style.borderWidth(0);
row.style.marginBottom = 1;
row.style.backgroundColor = _selectedAction == actionName
? new StyleColor(new Color(0.35f, 0.55f, 0.85f, 0.22f))
: new StyleColor(Color.clear);
// 覆盖指示点
var dot = new Label(hasIcon ? "●" : "○");
dot.style.color = new StyleColor(hasIcon ? new Color(0.3f, 0.85f, 0.4f) : new Color(0.75f, 0.3f, 0.3f));
dot.style.fontSize = 10;
dot.style.width = 14;
dot.style.flexShrink = 0;
row.Add(dot);
// Action 名称
var nameLbl = new Label(actionName);
nameLbl.style.flexGrow = 1;
nameLbl.style.fontSize = 11;
row.Add(nameLbl);
// 缩略绑定路径
if (!string.IsNullOrEmpty(effectivePath))
{
var pathLbl = new Label(ShortPath(effectivePath));
pathLbl.style.fontSize = 10;
pathLbl.style.opacity = 0.5f;
pathLbl.style.marginLeft = 4;
row.Add(pathLbl);
}
// 微型图标预览
if (sprite != null)
{
var thumb = new Image { sprite = sprite };
thumb.style.width = 18;
thumb.style.height = 18;
thumb.style.marginLeft = 4;
thumb.style.flexShrink = 0;
row.Add(thumb);
}
row.name = "action-row-" + actionName;
_listContainer.Add(row);
}
}
private void RebuildActionListSelection()
{
if (_listContainer == null) return;
foreach (var child in _listContainer.Children())
{
if (child is not Button btn) continue;
var isSelected = btn.name == "action-row-" + _selectedAction;
btn.style.backgroundColor = isSelected
? new StyleColor(new Color(0.35f, 0.55f, 0.85f, 0.22f))
: new StyleColor(Color.clear);
}
}
// ── 详情面板 ──────────────────────────────────────────────────────────
private void RebuildDetail()
{
if (_detailPanel == null) return;
_detailPanel.Clear();
if (string.IsNullOrEmpty(_selectedAction))
{
var placeholder = new Label("← 从左侧选择一个 Action");
placeholder.style.opacity = 0.4f;
placeholder.style.marginTop = 60;
placeholder.style.unityTextAlign = TextAnchor.MiddleCenter;
_detailPanel.Add(placeholder);
return;
}
// ① 标题栏
BuildDetailHeader();
// ② 当前设备 Mapping 编辑区
BuildCurrentDeviceMapping();
// ③ 所有设备快览
BuildAllDevicesOverview();
// ④ 交互提示模拟预览
BuildPromptPreview();
// ⑤ 图标集资产状态
BuildIconSetStatus();
}
private void BuildDetailHeader()
{
var header = new VisualElement();
header.style.flexDirection = FlexDirection.Row;
header.style.alignItems = Align.Center;
header.style.paddingLeft = 12;
header.style.paddingRight = 12;
header.style.paddingTop = 10;
header.style.paddingBottom = 10;
header.style.borderBottomWidth = 1;
header.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.25f));
var actionLbl = new Label(_selectedAction);
actionLbl.style.fontSize = 15;
actionLbl.style.unityFontStyleAndWeight = FontStyle.Bold;
actionLbl.style.flexGrow = 1;
header.Add(actionLbl);
// 当前设备徽章
var devBadge = MakeDeviceBadge(_activeDevice);
header.Add(devBadge);
_detailPanel.Add(header);
}
private void BuildCurrentDeviceMapping()
{
var section = MakeSectionContainer("绑定路径与图标");
var iconSet = GetIconSetForDevice(_activeDevice);
var effectivePath = GetEffectivePath(_selectedAction);
// ── 绑定路径预览 ──────────────────────────────────────────────
var pathRow = new VisualElement();
pathRow.style.flexDirection = FlexDirection.Row;
pathRow.style.alignItems = Align.Center;
pathRow.style.marginBottom = 8;
var pathLabel = new Label("绑定路径");
pathLabel.style.opacity = 0.65f;
pathLabel.style.width = 80;
pathLabel.style.flexShrink = 0;
pathRow.Add(pathLabel);
var pathValue = new Label(effectivePath ?? "(未检测到绑定)");
pathValue.style.fontSize = 11;
pathValue.style.flexGrow = 1;
pathValue.style.opacity = string.IsNullOrEmpty(effectivePath) ? 0.4f : 1f;
pathValue.style.unityFontStyleAndWeight = FontStyle.Bold;
pathRow.Add(pathValue);
if (!string.IsNullOrEmpty(effectivePath))
{
var copyBtn = new Button(() => { GUIUtility.systemCopyBuffer = effectivePath; })
{ text = "复制", tooltip = "复制路径到剪贴板" };
copyBtn.style.height = 20;
copyBtn.style.fontSize = 10;
pathRow.Add(copyBtn);
}
section.Add(pathRow);
// ── 图标集 SO 字段 + 大图预览 ─────────────────────────────────
if (iconSet == null)
{
var warn = new HelpBox($"当前设备({DeviceLabel(_activeDevice)})尚未配置图标集 SO。\n" +
"请在工具栏 InputIconService 的 Inspector 中指定对应图标集,或点击左列「指定图标集」。",
HelpBoxMessageType.Warning);
warn.style.marginTop = 4;
section.Add(warn);
}
else
{
var iconEditRow = new VisualElement();
iconEditRow.style.flexDirection = FlexDirection.Row;
iconEditRow.style.alignItems = Align.Center;
// 图标 ObjectField
var existingSprite = string.IsNullOrEmpty(effectivePath)
? null
: iconSet.GetIcon(effectivePath);
var spriteField = new ObjectField("图标 Sprite")
{
objectType = typeof(Sprite),
allowSceneObjects = false,
value = existingSprite
};
spriteField.style.flexGrow = 1;
// 大图预览
var bigPreview = new Image();
bigPreview.style.width = 64;
bigPreview.style.height = 64;
bigPreview.style.marginLeft = 12;
bigPreview.style.flexShrink = 0;
bigPreview.style.borderRadius(4);
bigPreview.style.backgroundColor = new StyleColor(new Color(0f, 0f, 0f, 0.25f));
bigPreview.scaleMode = ScaleMode.ScaleToFit;
if (existingSprite != null) bigPreview.sprite = existingSprite;
spriteField.RegisterValueChangedCallback(evt =>
{
if (string.IsNullOrEmpty(effectivePath)) return;
SaveIconToSet(iconSet, effectivePath, evt.newValue as Sprite);
bigPreview.sprite = evt.newValue as Sprite;
RebuildDeviceTabs();
RebuildActionListSelection();
});
iconEditRow.Add(spriteField);
iconEditRow.Add(bigPreview);
section.Add(iconEditRow);
// 提示:若路径为空,说明该 Action 在此设备方案下无绑定
if (string.IsNullOrEmpty(effectivePath))
{
var note = new HelpBox("该 Action 在此控制方案下没有绑定。如有需要,请在 InputActionAsset 中添加对应的 Binding。",
HelpBoxMessageType.Info);
note.style.marginTop = 4;
section.Add(note);
}
}
_detailPanel.Add(section);
}
private void BuildAllDevicesOverview()
{
var section = MakeSectionContainer("所有设备快览");
var devices = new[]
{
InputDeviceType.KeyboardMouse,
InputDeviceType.XboxController,
InputDeviceType.PlayStationController,
InputDeviceType.SwitchController,
};
foreach (var devType in devices)
{
var iconSet = GetIconSetForDevice(devType);
var path = GetEffectivePath(_selectedAction);
var sprite = (iconSet != null && path != null) ? iconSet.GetIcon(path) : null;
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.paddingTop = 4;
row.style.paddingBottom = 4;
row.style.paddingLeft = 6;
row.style.paddingRight = 6;
row.style.borderBottomWidth = 1;
row.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.12f));
// 设备徽章
var badge = MakeDeviceBadge(devType);
badge.style.width = 110;
badge.style.flexShrink = 0;
row.Add(badge);
// 绑定路径
var pathLbl = new Label(path != null ? ShortPath(path) : "—");
pathLbl.style.flexGrow = 1;
pathLbl.style.fontSize = 10;
pathLbl.style.opacity = path == null ? 0.4f : 1f;
row.Add(pathLbl);
// 状态点
var statusLbl = new Label(sprite != null ? "✓" : (iconSet == null ? "—" : "✗"));
statusLbl.style.color = new StyleColor(
sprite != null ? new Color(0.3f, 0.85f, 0.4f) :
iconSet == null ? new Color(0.6f, 0.6f, 0.6f) :
new Color(0.85f, 0.3f, 0.3f));
statusLbl.style.width = 20;
statusLbl.style.unityTextAlign = TextAnchor.MiddleCenter;
row.Add(statusLbl);
// 小图预览
var thumb = new Image();
thumb.style.width = 28;
thumb.style.height = 28;
thumb.style.marginLeft = 6;
thumb.style.flexShrink = 0;
thumb.style.borderRadius(3);
thumb.style.backgroundColor = new StyleColor(new Color(0f, 0f, 0f, 0.2f));
thumb.scaleMode = ScaleMode.ScaleToFit;
if (sprite != null) thumb.sprite = sprite;
row.Add(thumb);
section.Add(row);
}
_detailPanel.Add(section);
}
private void BuildPromptPreview()
{
var section = MakeSectionContainer("交互提示预览(模拟)");
var iconSet = GetIconSetForDevice(_activeDevice);
var effectivePath = GetEffectivePath(_selectedAction);
var sprite = (iconSet != null && effectivePath != null) ? iconSet.GetIcon(effectivePath) : null;
// 模拟 InteractPromptWidget 的外观
var mockup = new VisualElement();
mockup.style.flexDirection = FlexDirection.Row;
mockup.style.alignItems = Align.Center;
mockup.style.alignSelf = Align.FlexStart;
mockup.style.paddingTop = 8;
mockup.style.paddingBottom = 8;
mockup.style.paddingLeft = 12;
mockup.style.paddingRight = 14;
mockup.style.marginTop = 4;
mockup.style.marginLeft = 8;
mockup.style.borderRadius(6);
mockup.style.backgroundColor = new StyleColor(new Color(0.08f, 0.08f, 0.10f, 0.85f));
mockup.style.borderWidth(1);
mockup.style.borderColor(new Color(0.5f, 0.5f, 0.6f, 0.35f));
// 按键图标区域
var keyBox = new VisualElement();
keyBox.style.width = 32;
keyBox.style.height = 32;
keyBox.style.borderRadius(4);
keyBox.style.backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.25f, 0.9f));
keyBox.style.borderWidth(1);
keyBox.style.borderColor(new Color(0.6f, 0.6f, 0.7f, 0.5f));
keyBox.style.justifyContent = Justify.Center;
keyBox.style.alignItems = Align.Center;
keyBox.style.marginRight = 10;
keyBox.style.flexShrink = 0;
if (sprite != null)
{
var keyImg = new Image { sprite = sprite };
keyImg.style.width = 24;
keyImg.style.height = 24;
keyImg.scaleMode = ScaleMode.ScaleToFit;
keyBox.Add(keyImg);
}
else
{
var missingLbl = new Label("?");
missingLbl.style.opacity = 0.4f;
missingLbl.style.unityTextAlign = TextAnchor.MiddleCenter;
keyBox.Add(missingLbl);
}
mockup.Add(keyBox);
// 文字区域
var textCol = new VisualElement();
textCol.style.flexDirection = FlexDirection.Column;
var actionText = new Label("交互"); // 模拟 LabelText
actionText.style.fontSize = 11;
actionText.style.unityFontStyleAndWeight = FontStyle.Bold;
actionText.style.color = new StyleColor(Color.white);
textCol.Add(actionText);
if (!string.IsNullOrEmpty(effectivePath))
{
var pathTip = new Label(ShortPath(effectivePath));
pathTip.style.fontSize = 9;
pathTip.style.opacity = 0.5f;
pathTip.style.color = new StyleColor(Color.white);
textCol.Add(pathTip);
}
mockup.Add(textCol);
section.Add(mockup);
if (sprite == null)
{
var note = new Label(iconSet == null
? "ⓘ 当前设备未配置图标集,无法预览。"
: "ⓘ 该路径在图标集中未找到对应 Sprite,运行时图标区域将隐藏。");
note.style.fontSize = 10;
note.style.opacity = 0.55f;
note.style.marginTop = 4;
note.style.whiteSpace = WhiteSpace.Normal;
section.Add(note);
}
_detailPanel.Add(section);
}
private void BuildIconSetStatus()
{
var section = MakeSectionContainer("图标集资产状态");
var rows = new[]
{
(InputDeviceType.KeyboardMouse, "键鼠", _kbSet),
(InputDeviceType.XboxController, "Xbox", _xboxSet),
(InputDeviceType.PlayStationController,"PlayStation", _psSet),
(InputDeviceType.SwitchController, "Switch", _switchSet),
};
foreach (var (devType, label, set) in rows)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.alignItems = Align.Center;
row.style.marginBottom = 3;
var badge = MakeDeviceBadge(devType);
badge.style.width = 100;
badge.style.flexShrink = 0;
row.Add(badge);
if (set != null)
{
int total = GetEntryCount(set);
int filled = GetFilledCount(set);
var statusChip = new Label($"{filled}/{total} 图标");
statusChip.AddToClassList(filled == total && total > 0 ? "status-chip--ok" : "status-chip--missing");
statusChip.style.fontSize = 10;
statusChip.style.marginLeft = 8;
row.Add(statusChip);
var pingBtn = new Button(() => { EditorGUIUtility.PingObject(set); Selection.activeObject = set; })
{ text = "定位", tooltip = $"在 Project 窗口中高亮 {set.name}" };
pingBtn.style.height = 18;
pingBtn.style.fontSize = 10;
pingBtn.style.marginLeft = 6;
row.Add(pingBtn);
}
else
{
var missingLbl = new Label("⚠ 未配置");
missingLbl.style.opacity = 0.6f;
missingLbl.style.fontSize = 10;
missingLbl.style.marginLeft = 8;
row.Add(missingLbl);
var createBtn = new Button(() => CreateIconSetForDevice(devType))
{ text = "+ 新建", tooltip = $"创建 {label} 图标集 SO 并放入项目" };
createBtn.style.height = 18;
createBtn.style.fontSize = 10;
createBtn.style.marginLeft = 6;
createBtn.AddToClassList("wizard-factory-btn");
row.Add(createBtn);
}
section.Add(row);
}
_detailPanel.Add(section);
}
// ── 加载项目资产 ──────────────────────────────────────────────────────
private void LoadAssetsFromProject()
{
// 找 InputReaderSO
if (_inputReaderAsset == null)
{
var guids = AssetDatabase.FindAssets("t:InputReaderSO");
if (guids.Length > 0)
_inputReaderAsset = AssetDatabase.LoadAssetAtPath(
AssetDatabase.GUIDToAssetPath(guids[0]));
}
ReloadActionAsset();
LoadIconSets();
RebuildDeviceTabs();
RebuildActionList();
RebuildDetail();
}
private void ReloadActionAsset()
{
_actionAsset = null;
_actionNames.Clear();
if (_inputReaderAsset == null) return;
var so = new SerializedObject(_inputReaderAsset);
var prop = so.FindProperty("_inputActions");
if (prop?.objectReferenceValue is InputActionAsset asset)
{
_actionAsset = asset;
var map = asset.FindActionMap("Gameplay", throwIfNotFound: false);
if (map != null)
foreach (var action in map.actions)
_actionNames.Add(action.name);
}
}
private void LoadIconSets()
{
var sets = EditorScaffoldUtils.FindAllAssetsOfType();
_kbSet = _xboxSet = _psSet = _switchSet = null;
// 按 DeviceType 字段匹配图标集 SO
foreach (var set in sets)
{
switch (set.DeviceType)
{
case InputDeviceType.KeyboardMouse: _kbSet ??= set; break;
case InputDeviceType.XboxController: _xboxSet ??= set; break;
case InputDeviceType.PlayStationController:_psSet ??= set; break;
case InputDeviceType.SwitchController: _switchSet??= set; break;
}
}
}
private void RefreshAll()
{
LoadAssetsFromProject();
}
// ── 每设备绑定路径查询 ────────────────────────────────────────────────
private string GetEffectivePath(string actionName)
{
if (_actionAsset == null || string.IsNullOrEmpty(actionName)) return null;
var action = _actionAsset.FindAction(actionName, throwIfNotFound: false);
if (action == null) return null;
string scheme = _activeDevice == InputDeviceType.KeyboardMouse ? "Keyboard&Mouse" : "Gamepad";
foreach (var binding in action.bindings)
{
if (binding.isComposite) continue;
if (!string.IsNullOrEmpty(scheme)
&& !string.IsNullOrEmpty(binding.groups)
&& !binding.groups.Contains(scheme, StringComparison.OrdinalIgnoreCase))
continue;
if (!string.IsNullOrEmpty(binding.effectivePath))
return binding.effectivePath;
}
return null;
}
// ── 保存图标到 SO ─────────────────────────────────────────────────────
private static void SaveIconToSet(InputDeviceIconSetSO iconSet, string path, Sprite sprite)
{
var so = new SerializedObject(iconSet);
var entries = so.FindProperty("_entries");
// 查找已有相同路径的条目
int found = -1;
for (int i = 0; i < entries.arraySize; i++)
{
var p = entries.GetArrayElementAtIndex(i).FindPropertyRelative("BindingPath").stringValue;
if (string.Equals(p, path, StringComparison.OrdinalIgnoreCase))
{
found = i;
break;
}
}
if (found < 0)
{
// 新增
entries.InsertArrayElementAtIndex(entries.arraySize);
found = entries.arraySize - 1;
entries.GetArrayElementAtIndex(found).FindPropertyRelative("BindingPath").stringValue = path;
}
entries.GetArrayElementAtIndex(found).FindPropertyRelative("Icon").objectReferenceValue = sprite;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(iconSet);
AssetDatabase.SaveAssets();
}
// ── 工具方法 ──────────────────────────────────────────────────────────
private InputDeviceIconSetSO GetIconSetForDevice(InputDeviceType device) => device switch
{
InputDeviceType.KeyboardMouse => _kbSet,
InputDeviceType.XboxController => _xboxSet,
InputDeviceType.PlayStationController => _psSet,
InputDeviceType.SwitchController => _switchSet,
_ => null,
};
private static int GetEntryCount(InputDeviceIconSetSO set)
{
if (set == null) return 0;
var so = new SerializedObject(set);
var arr = so.FindProperty("_entries");
return arr?.arraySize ?? 0;
}
private static int GetFilledCount(InputDeviceIconSetSO set)
{
if (set == null) return 0;
var so = new SerializedObject(set);
var arr = so.FindProperty("_entries");
if (arr == null) return 0;
int count = 0;
for (int i = 0; i < arr.arraySize; i++)
if (arr.GetArrayElementAtIndex(i).FindPropertyRelative("Icon").objectReferenceValue != null)
count++;
return count;
}
private void PickIconSetForCurrentDevice()
{
// 打开 Project 的 ObjectPicker 让用户选一个 InputDeviceIconSetSO
// 简单实现:扫描项目中所有 InputDeviceIconSetSO,弹出通用 GenericMenu
var all = EditorScaffoldUtils.FindAllAssetsOfType();
if (all.Count == 0)
{
EditorUtility.DisplayDialog("无图标集资产",
"项目中未找到 InputDeviceIconSetSO 资产,请先创建。", "确定");
return;
}
var menu = new GenericMenu();
foreach (var set in all)
{
var captured = set;
menu.AddItem(new GUIContent(set.name), false, () =>
{
AssignIconSetToDevice(_activeDevice, captured);
LoadIconSets();
RebuildDeviceTabs();
RebuildActionList();
RebuildDetail();
});
}
menu.AddSeparator("");
menu.AddItem(new GUIContent("新建..."), false, () => CreateIconSetForDevice(_activeDevice));
menu.ShowAsContext();
}
private void AssignIconSetToDevice(InputDeviceType device, InputDeviceIconSetSO set)
{
switch (device)
{
case InputDeviceType.KeyboardMouse: _kbSet = set; break;
case InputDeviceType.XboxController: _xboxSet = set; break;
case InputDeviceType.PlayStationController: _psSet = set; break;
case InputDeviceType.SwitchController: _switchSet = set; break;
}
}
private void CreateIconSetForDevice(InputDeviceType device)
{
string defaultName = $"ICN_{device}";
var asset = EditorScaffoldUtils.CreateSOAssetInteractive(
"Assets/_Game/Data/UI/InputIcons", defaultName);
if (asset != null)
{
var so = new SerializedObject(asset);
var prop = so.FindProperty("_deviceType");
if (prop != null) { prop.enumValueIndex = (int)device; so.ApplyModifiedProperties(); }
AssignIconSetToDevice(device, asset);
LoadIconSets();
RebuildDeviceTabs();
RebuildActionList();
RebuildDetail();
}
}
// ── UI 构建辅助 ───────────────────────────────────────────────────────
private static VisualElement MakeSectionContainer(string title)
{
var section = new VisualElement();
section.style.paddingLeft = 12;
section.style.paddingRight = 12;
section.style.paddingTop = 10;
section.style.paddingBottom = 10;
section.style.borderBottomWidth = 1;
section.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.18f));
var hdr = new Label(title);
hdr.AddToClassList("section-header");
hdr.style.marginBottom = 6;
section.Add(hdr);
return section;
}
private static VisualElement MakeDeviceBadge(InputDeviceType device)
{
var (label, bg) = device switch
{
InputDeviceType.KeyboardMouse => ("键鼠", new Color(0.2f, 0.4f, 0.7f, 0.5f)),
InputDeviceType.XboxController => ("Xbox", new Color(0.1f, 0.5f, 0.15f, 0.5f)),
InputDeviceType.PlayStationController => ("PS", new Color(0.1f, 0.25f, 0.65f, 0.5f)),
InputDeviceType.SwitchController => ("Switch", new Color(0.75f, 0.15f, 0.2f, 0.5f)),
_ => ("Unknown", new Color(0.4f, 0.4f, 0.4f, 0.4f)),
};
var badge = new Label(label);
badge.style.fontSize = 10;
badge.style.paddingTop = 2;
badge.style.paddingBottom = 2;
badge.style.paddingLeft = 7;
badge.style.paddingRight = 7;
badge.style.borderRadius(4);
badge.style.backgroundColor = new StyleColor(bg);
badge.style.borderWidth(1);
badge.style.borderColor(new Color(bg.r + 0.15f, bg.g + 0.15f, bg.b + 0.15f, 0.6f));
badge.style.unityFontStyleAndWeight = FontStyle.Bold;
return badge;
}
private static string DeviceLabel(InputDeviceType device) => device switch
{
InputDeviceType.KeyboardMouse => "键鼠",
InputDeviceType.XboxController => "Xbox",
InputDeviceType.PlayStationController => "PlayStation",
InputDeviceType.SwitchController => "Switch",
_ => device.ToString(),
};
/// 将完整绑定路径缩短为可读格式(去掉设备前缀)。
private static string ShortPath(string path)
{
if (string.IsNullOrEmpty(path)) return path;
var slash = path.LastIndexOf('/');
return slash >= 0 ? path[(slash + 1)..] : path;
}
private static void ShowHelp()
{
EditorUtility.DisplayDialog("Input Icon Studio — 使用说明",
"1. 在顶部选取 InputReaderSO 资产(项目中有唯一实例则自动加载)\n" +
"2. 通过设备标签页切换键鼠/Xbox/PS/Switch\n" +
"3. 左侧列表显示所有 Gameplay Actions:\n" +
" ● 绿色 = 当前设备已配置图标\n" +
" ○ 红色 = 未配置\n" +
"4. 选中 Action 后在右侧指定 Sprite\n" +
"5. 修改实时保存到对应的 InputDeviceIconSetSO\n\n" +
"💡 在 Inspector 中可对 InputDeviceIconSetSO 使用「从 Action Asset 填充路径」\n" +
" 批量生成所有路径条目,再逐一指定 Sprite。",
"知道了");
}
}
// ── 样式扩展方法(限本文件使用)─────────────────────────────────────────
internal static class VisualElementStyleExtensions
{
internal static void borderRadius(this IStyle style, float r)
{
style.borderTopLeftRadius = r;
style.borderTopRightRadius = r;
style.borderBottomLeftRadius = r;
style.borderBottomRightRadius = r;
}
internal static void borderWidth(this IStyle style, float w)
{
style.borderLeftWidth = w;
style.borderRightWidth = w;
style.borderTopWidth = w;
style.borderBottomWidth = w;
}
internal static void borderColor(this IStyle style, Color c)
{
var sc = new StyleColor(c);
style.borderLeftColor = sc;
style.borderRightColor = sc;
style.borderTopColor = sc;
style.borderBottomColor = sc;
}
internal static void borderRadius(this Button btn, float r)
=> btn.style.borderRadius(r);
internal static void borderWidth(this Button btn, float w)
=> btn.style.borderWidth(w);
}
}