Files
XericUIActionVessel/Editor/XTable/XericUIActionTableSceneEditor.cs

1167 lines
36 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEditor;
using UnityEngine;
using XericLibrary.Runtime.MacroLibrary;
using XericLibrary.Runtime.SuperStyleSheet;
using XericUI.XTable.Core;
using XericUI.XTable.Mapping;
using XericUI.XTable.Rendering.Component;
namespace XericUIEditor.XTable
{
/// <summary>
/// Scene View 表格编辑覆盖层——
/// 左侧行索引、顶部列索引A..Z、底部输入框。
/// 点击行列交叉区域选中单元格底部输入框编辑文本方向键导航Shift+方向键多选。
/// 不创建任何场景对象,通过 Handles.BeginGUI/EndGUI 绘制。
/// </summary>
[InitializeOnLoad]
public static class XericUIActionTableSceneEditor
{
// ─────────────────────────── 交互状态机 ───────────────────────────
private enum EditorState
{
TableNavigation, // 默认:键盘/鼠标在表格上导航与选择
SizeEditing, // 气泡输入框中编辑行高/列宽
CellEditing // 底部工具栏中编辑单元格文本内容
}
private static EditorState s_State;
private static string s_CurrentControlName; // 当前聚焦的控件名(每帧由焦点决定)
private static bool s_Enabled;
private const string ENABLED_KEY = "XericUI_TableSceneEditor_Enabled";
private static string s_EditText = "";
private static string s_SizeEditText = ""; // 气泡输入框当前编辑文本
private static int s_SizeEditTargetIndex = -1;
private static string s_LastFocusedControl = "";
// 拖拽框选状态
private static bool s_IsDragging;
private static int s_DragStartRow = -1;
private static int s_DragStartCol = -1;
private static Rect s_TableRect;
private static float s_XScale, s_YScale;
private static float[] s_RowHeights, s_ColWidths;
private static XericUIActionTable s_ActiveTable;
private static readonly Color s_LabelBg = new Color(0.12f, 0.12f, 0.18f, 0.9f);
private static readonly Color s_SelectedLabel = new Color(0.2f, 0.5f, 0.9f, 0.6f);
private static readonly Color s_MultiSelectLabel = new Color(0.2f, 0.5f, 0.9f, 0.35f);
private static readonly Color s_LabelText = new Color(0.75f, 0.75f, 0.8f);
private static readonly Color s_InputBg = new Color(0.08f, 0.08f, 0.14f, 0.92f);
private static readonly Color s_SelectionColor = new Color(0.18f, 0.45f, 0.85f, 0.25f);
private static readonly Color s_MultiSelectRangeColor = new Color(0.18f, 0.45f, 0.85f, 0.12f);
private static readonly Color s_BubbleBg = new Color(0.08f, 0.1f, 0.2f, 0.95f);
private static readonly Color s_BubbleBorder = new Color(0.3f, 0.5f, 0.9f, 0.8f);
private static readonly Color s_ApplyBtnColor = new Color(0.15f, 0.4f, 0.15f, 0.9f);
private static readonly Color s_AddBtnBg = new Color(0.15f, 0.3f, 0.15f, 0.8f);
private static readonly Color s_AddBtnHover = new Color(0.2f, 0.45f, 0.2f, 0.9f);
private const float ROW_H = 22f;
private const float COL_W = 36f;
private const float MARGIN = 3f;
static XericUIActionTableSceneEditor()
{
s_Enabled = EditorPrefs.GetBool(ENABLED_KEY, true);
SceneView.duringSceneGui += OnSceneGUI;
}
[MenuItem("XericLibrary/UI/Table/表格编辑器覆盖层", false, 200)]
private static void ToggleEnabled()
{
s_Enabled = !s_Enabled;
EditorPrefs.SetBool(ENABLED_KEY, s_Enabled);
Menu.SetChecked("XericLibrary/UI/Table/表格编辑器覆盖层", s_Enabled);
SceneView.RepaintAll();
}
[MenuItem("XericLibrary/UI/Table/表格编辑器覆盖层", true)]
private static bool ToggleEnabledValidate()
{
Menu.SetChecked("XericLibrary/UI/Table/表格编辑器覆盖层", s_Enabled);
return true;
}
// ═══════════════════════════════════════════════════════════════
// 状态机入口
// ═══════════════════════════════════════════════════════════════
private static void UpdateState()
{
string focused = GUI.GetNameOfFocusedControl();
// 检测焦点切换 → 种子编辑文本
if (focused != s_LastFocusedControl)
{
s_LastFocusedControl = focused;
OnFocusChanged(focused);
}
if (string.IsNullOrEmpty(focused))
s_State = EditorState.TableNavigation;
else if (focused.StartsWith("XTableColSizeEdit_") || focused.StartsWith("XTableRowSizeEdit_"))
s_State = EditorState.SizeEditing;
else if (focused == "XTableSceneEditInput")
s_State = EditorState.CellEditing;
else
s_State = EditorState.TableNavigation;
s_CurrentControlName = focused;
}
private static void OnFocusChanged(string newFocus)
{
// 气泡输入框获得焦点:种子为当前尺寸值
if (newFocus.StartsWith("XTableColSizeEdit_") && s_ActiveTable != null)
{
if (int.TryParse(newFocus.Substring("XTableColSizeEdit_".Length), out int idx))
{
s_SizeEditTargetIndex = idx;
s_SizeEditText = s_ColWidths != null && idx < s_ColWidths.Length
? s_ColWidths[idx].ToString("F0") : "";
}
}
else if (newFocus.StartsWith("XTableRowSizeEdit_") && s_ActiveTable != null)
{
if (int.TryParse(newFocus.Substring("XTableRowSizeEdit_".Length), out int idx))
{
s_SizeEditTargetIndex = idx;
s_SizeEditText = s_RowHeights != null && idx < s_RowHeights.Length
? s_RowHeights[idx].ToString("F0") : "";
}
}
// 底部内容输入框获得焦点:种子为当前单元格内容
else if (newFocus == "XTableSceneEditInput" && s_ActiveTable != null)
{
SyncEditText(s_ActiveTable);
}
}
/// <summary>将 s_EditText 同步为当前选中单元格的内容(选中变更时调用)</summary>
private static void SyncEditText(XericUIActionTable table)
{
int sr = table.SelectedRow;
int sc = table.SelectedCol;
if (sr >= 0 && sc >= 0)
{
var data = table.GetCellData(sr, sc);
s_EditText = (data != null) ? (data.Text ?? "") : "";
}
else
{
s_EditText = "";
}
}
// ═══════════════════════════════════════════════════════════════
// 主入口
// ═══════════════════════════════════════════════════════════════
private static void OnSceneGUI(SceneView sceneView)
{
if (!s_Enabled) return;
GameObject sel = Selection.activeGameObject;
if (sel == null) return;
XericUIActionTable table = sel.GetComponentInChildren<XericUIActionTable>();
if (table == null)
table = sel.GetComponentInParent<XericUIActionTable>();
if (table == null || !table.isActiveAndEnabled) return;
#if UNITY_EDITOR
Canvas.ForceUpdateCanvases();
#endif
// 使用 Content 的 RectTransform 进行定位计算(而非表格组件所在的 ScrollRect 根节点)
RectTransform tableRt = table.ContentTransform.RectTransform();
if (tableRt == null) tableRt = table.GetComponent<RectTransform>();
float[] rh = table.RowHeights;
float[] cw = table.ColWidths;
if (rh == null || cw == null || rh.Length == 0 || cw.Length == 0) return;
Vector3[] corners = new Vector3[4];
tableRt.GetWorldCorners(corners);
Vector2 guiTL = HandleUtility.WorldToGUIPoint(corners[1]);
Vector2 guiBR = HandleUtility.WorldToGUIPoint(corners[3]);
float tableW = guiBR.x - guiTL.x;
float tableH = guiBR.y - guiTL.y;
if (tableW <= 0 || tableH <= 0) return;
float totalW = 0, totalH = 0;
foreach (float w in cw) totalW += w;
foreach (float h in rh) totalH += h;
if (totalW <= 0 || totalH <= 0) return;
float xScale = tableW / totalW;
float yScale = tableH / totalH;
Rect tableRect = new Rect(guiTL.x, guiTL.y, tableW, tableH);
// 更新缓存
s_TableRect = tableRect;
s_XScale = xScale;
s_YScale = yScale;
s_RowHeights = rh;
s_ColWidths = cw;
s_ActiveTable = table;
Handles.BeginGUI();
// ★ 状态机:必须最先执行,后续所有逻辑依赖 s_State
UpdateState();
// Unity 2021 兼容:阻止鼠标事件穿透 overlay 到背后 UI 对象
// 原理:在 overlay 区域注册默认控件Scene View 拣选系统检测到控件后
// 不再选中背后 GameObjectCanvas、ScrollRect 等)
if (s_State == EditorState.TableNavigation)
{
// 覆盖区域:行标签 + 列表头 + 表格主体 + 底部工具栏
Rect overlayArea = new Rect(
tableRect.x - COL_W - MARGIN, // 包含行标签
tableRect.y - ROW_H - MARGIN, // 包含列表头
tableRect.width + COL_W + MARGIN + 4, // 包含行标签
tableRect.height + ROW_H + MARGIN + 56); // 包含列表头+工具栏
if (overlayArea.Contains(Event.current.mousePosition))
{
int blockId = GUIUtility.GetControlID(FocusType.Passive);
HandleUtility.AddDefaultControl(blockId);
}
}
DrawRowLabels(rh, tableRect, xScale, yScale, table);
DrawColumnLabels(cw, tableRect, xScale, yScale, table);
DrawGridLines(rh, cw, tableRect, xScale, yScale);
DrawSelectionHighlight(rh, cw, tableRect, xScale, yScale, table);
DrawAddButtons(rh, cw, tableRect, xScale, yScale, table);
DrawSizeEditBubble(rh, cw, tableRect, xScale, yScale, table);
DrawInputBar(table, tableRect.x, tableRect.y + tableRect.height + MARGIN);
// 非导航状态不处理鼠标/键盘表格操作
if (s_State == EditorState.TableNavigation)
{
HandleTableMouse(table, rh, cw, tableRect, xScale, yScale);
HandleKeyboard(table, rh, cw);
}
Handles.EndGUI();
}
#region
private static int IndexFromCoord(float coord, float[] sizes)
{
float acc = 0;
for (int i = 0; i < sizes.Length; i++)
{
if (coord >= acc && coord < acc + sizes[i]) return i;
acc += sizes[i];
}
return -1;
}
private static float SumBefore(float[] arr, int count)
{
float sum = 0;
for (int i = 0; i < count && i < arr.Length; i++) sum += arr[i];
return sum;
}
private static float SumRange(float[] arr, int start, int end)
{
float sum = 0;
for (int i = start; i < end && i < arr.Length; i++) sum += arr[i];
return sum;
}
private static bool IsRowInMultiSelect(XericUIActionTable table, int row)
{
if (!table.HasMultiSelection) return false;
int minR = Mathf.Min(table.SelectStartRow, table.SelectEndRow);
int maxR = Mathf.Max(table.SelectStartRow, table.SelectEndRow);
return row >= minR && row <= maxR;
}
private static bool IsColInMultiSelect(XericUIActionTable table, int col)
{
if (!table.HasMultiSelection) return false;
int minC = Mathf.Min(table.SelectStartCol, table.SelectEndCol);
int maxC = Mathf.Max(table.SelectStartCol, table.SelectEndCol);
return col >= minC && col <= maxC;
}
/// <summary>创建纯色 1x1 纹理,用于按钮背景</summary>
private static Texture2D MakeTex(int w, int h, Color col)
{
Color[] pix = new Color[w * h];
for (int i = 0; i < pix.Length; i++)
pix[i] = col;
Texture2D result = new Texture2D(w, h);
result.SetPixels(pix);
result.Apply();
return result;
}
#endregion
#region
private static void DrawRowLabels(float[] rh, Rect tableRect,
float xScale, float yScale, XericUIActionTable table)
{
var style = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = s_LabelText },
alignment = TextAnchor.MiddleCenter
};
float y = tableRect.y;
for (int r = 0; r < rh.Length; r++)
{
float cellH = rh[r] * yScale;
if (cellH < 2) { y += cellH; continue; }
Rect labelRect = new Rect(
tableRect.x - COL_W - MARGIN,
y,
COL_W,
cellH);
Color bgColor;
if (table.SelectedRow == r)
bgColor = s_SelectedLabel;
else if (IsRowInMultiSelect(table, r))
bgColor = s_MultiSelectLabel;
else
bgColor = s_LabelBg;
EditorGUI.DrawRect(labelRect, bgColor);
GUI.Label(labelRect, $"{r}", style);
// 表格导航状态下才响应鼠标
if (s_State != EditorState.TableNavigation) { y += cellH; continue; }
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 &&
labelRect.Contains(Event.current.mousePosition))
{
int col = table.SelectedCol;
if (col < 0) col = 0;
s_IsDragging = true;
s_DragStartRow = r;
s_DragStartCol = col;
if (Event.current.shift && table.SelectedRow >= 0)
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, r, col);
else
table.SelectionHandle.ExtendTo(r, col);
}
else
{
table.SelectCell(r, col);
}
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
}
y += cellH;
}
}
#endregion
#region
private static void DrawColumnLabels(float[] cw, Rect tableRect,
float xScale, float yScale, XericUIActionTable table)
{
var style = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = s_LabelText },
alignment = TextAnchor.MiddleCenter
};
float x = tableRect.x;
for (int c = 0; c < cw.Length; c++)
{
float cellW = cw[c] * xScale;
if (cellW < 2) { x += cellW; continue; }
Rect labelRect = new Rect(
x,
tableRect.y - ROW_H - MARGIN,
cellW,
ROW_H);
Color bgColor;
if (table.SelectedCol == c)
bgColor = s_SelectedLabel;
else if (IsColInMultiSelect(table, c))
bgColor = s_MultiSelectLabel;
else
bgColor = s_LabelBg;
EditorGUI.DrawRect(labelRect, bgColor);
string colLetter = XTableCoordinateFormat.ColumnIndexToLetter(c);
string text = cellW > 40 ? colLetter : (cellW > 18 ? colLetter : "");
GUI.Label(labelRect, text, style);
// 表格导航状态下才响应鼠标
if (s_State != EditorState.TableNavigation) { x += cellW; continue; }
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 &&
labelRect.Contains(Event.current.mousePosition))
{
int row = table.SelectedRow;
if (row < 0) row = 0;
s_IsDragging = true;
s_DragStartRow = row;
s_DragStartCol = c;
if (Event.current.shift && table.SelectedCol >= 0)
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, row, c);
else
table.SelectionHandle.ExtendTo(row, c);
}
else
{
table.SelectCell(row, c);
}
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
}
x += cellW;
}
}
#endregion
#region 线
private static void DrawGridLines(float[] rh, float[] cw, Rect tr,
float xScale, float yScale)
{
Color lineColor = new Color(0.5f, 0.5f, 0.55f, 0.18f);
// 水平网格线(跳过合并区域内的线)
float y = tr.y;
for (int r = 0; r < rh.Length; r++)
{
y += rh[r] * yScale;
// 检查此行是否在合并区域内(非底部边界的行)
bool skipLine = false;
if (s_ActiveTable != null && r + 1 < rh.Length)
{
var data = s_ActiveTable.TableData;
if (data != null && data.IsCellMerged(r + 1, 0))
skipLine = true;
}
if (!skipLine)
EditorGUI.DrawRect(new Rect(tr.x, y, tr.width, 1), lineColor);
}
// 垂直网格线(跳过合并区域内的线)
float x = tr.x;
for (int c = 0; c < cw.Length; c++)
{
x += cw[c] * xScale;
// 检查此列是否在合并区域内(非右侧边界的列)
bool skipLine = false;
if (s_ActiveTable != null && c + 1 < cw.Length)
{
var data = s_ActiveTable.TableData;
if (data != null && data.IsCellMerged(0, c + 1))
skipLine = true;
}
if (!skipLine)
EditorGUI.DrawRect(new Rect(x, tr.y, 1, tr.height), lineColor);
}
}
#endregion
#region
private static void DrawSelectionHighlight(float[] rh, float[] cw, Rect tr,
float xScale, float yScale, XericUIActionTable table)
{
int sr = table.SelectedRow;
int sc = table.SelectedCol;
if (sr < 0 || sc < 0 || sr >= rh.Length || sc >= cw.Length) return;
// 获取选中单元格的实际显示范围(合并源需要扩展)
int hlRowSpan = 1, hlColSpan = 1;
table.IsMergeSource(sr, sc, out hlRowSpan, out hlColSpan);
if (table.HasMultiSelection)
{
int minR = Mathf.Min(table.SelectStartRow, table.SelectEndRow);
int maxR = Mathf.Max(table.SelectStartRow, table.SelectEndRow);
int minC = Mathf.Min(table.SelectStartCol, table.SelectEndCol);
int maxC = Mathf.Max(table.SelectStartCol, table.SelectEndCol);
// 扩展多选区域以包含合并源的全部跨度
if (table.IsMergeSource(minR, minC, out int msRS, out int msCS))
{
maxR = Mathf.Max(maxR, minR + msRS - 1);
maxC = Mathf.Max(maxC, minC + msCS - 1);
}
minR = Mathf.Clamp(minR, 0, rh.Length - 1);
maxR = Mathf.Clamp(maxR, 0, rh.Length - 1);
minC = Mathf.Clamp(minC, 0, cw.Length - 1);
maxC = Mathf.Clamp(maxC, 0, cw.Length - 1);
float msx = tr.x + SumBefore(cw, minC) * xScale;
float msy = tr.y + SumBefore(rh, minR) * yScale;
float msw = SumRange(cw, minC, maxC + 1) * xScale;
float msh = SumRange(rh, minR, maxR + 1) * yScale;
EditorGUI.DrawRect(new Rect(msx + 1, msy + 1, msw - 2, msh - 2),
s_MultiSelectRangeColor);
}
// 单选高亮(合并源用完整跨度)
float sx = tr.x + SumBefore(cw, sc) * xScale;
float sy = tr.y + SumBefore(rh, sr) * yScale;
float sw = SumRange(cw, sc, sc + hlColSpan) * xScale;
float sh = SumRange(rh, sr, sr + hlRowSpan) * yScale;
EditorGUI.DrawRect(new Rect(sx + 1, sy + 1, sw - 2, sh - 2),
s_SelectionColor);
}
#endregion
#region
private static void DrawAddButtons(float[] rh, float[] cw, Rect tr,
float xScale, float yScale, XericUIActionTable table)
{
var plusStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 14,
fontStyle = FontStyle.Bold,
normal = { textColor = Color.white },
alignment = TextAnchor.MiddleCenter
};
float yEnd = tr.y + tr.height;
Rect rowBtn = new Rect(tr.x - COL_W - MARGIN, yEnd + MARGIN, COL_W, ROW_H);
bool rowHover = rowBtn.Contains(Event.current.mousePosition);
EditorGUI.DrawRect(rowBtn, rowHover ? s_AddBtnHover : s_AddBtnBg);
GUI.Label(rowBtn, "+", plusStyle);
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 && rowHover)
{
table.AddRow();
table.RefreshView();
Event.current.Use();
SceneView.RepaintAll();
}
float xEnd = tr.x + tr.width;
Rect colBtn = new Rect(xEnd + MARGIN, tr.y - ROW_H - MARGIN, COL_W, ROW_H);
bool colHover = colBtn.Contains(Event.current.mousePosition);
EditorGUI.DrawRect(colBtn, colHover ? s_AddBtnHover : s_AddBtnBg);
GUI.Label(colBtn, "+", plusStyle);
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 && colHover)
{
table.AddColumn();
table.RefreshView();
Event.current.Use();
SceneView.RepaintAll();
}
}
#endregion
#region
private const float BUBBLE_W = 60;
private const float BUBBLE_H = 22;
private const float BUBBLE_APPLY_BTN_W = 24;
private const float BUBBLE_DEL_BTN_W = 24;
private static GUIStyle s_BubbleApplyBtnStyle;
private static GUIStyle s_BubbleApplyBtnStyleActive;
private static GUIStyle s_BubbleDelBtnStyle;
private static void EnsureBubbleStyles()
{
if (s_BubbleApplyBtnStyle != null) return;
s_BubbleApplyBtnStyle = new GUIStyle(GUI.skin.button)
{
fontSize = 11,
fontStyle = FontStyle.Bold,
normal = { textColor = new Color(0.5f, 0.5f, 0.5f), background = MakeTex(1, 1, new Color(0.25f, 0.25f, 0.25f, 0.6f)) },
hover = { textColor = new Color(0.7f, 0.7f, 0.7f), background = MakeTex(1, 1, new Color(0.35f, 0.35f, 0.35f, 0.8f)) },
active = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.15f, 0.15f, 0.15f, 0.8f)) },
padding = new RectOffset(0, 0, 0, 0),
margin = new RectOffset(0, 0, 0, 0),
alignment = TextAnchor.MiddleCenter,
border = new RectOffset(0, 0, 0, 0)
};
s_BubbleApplyBtnStyleActive = new GUIStyle(s_BubbleApplyBtnStyle)
{
normal = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.2f, 0.6f, 0.2f, 0.9f)) },
hover = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.2f, 0.55f, 0.2f, 0.95f)) },
active = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.1f, 0.4f, 0.1f, 0.95f)) }
};
s_BubbleDelBtnStyle = new GUIStyle(s_BubbleApplyBtnStyle)
{
normal = { textColor = new Color(0.6f, 0.3f, 0.3f), background = MakeTex(1, 1, new Color(0.3f, 0.15f, 0.15f, 0.6f)) },
hover = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.7f, 0.2f, 0.2f, 0.95f)) },
active = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.5f, 0.1f, 0.1f, 0.95f)) }
};
}
private static void DrawSizeEditBubble(float[] rh, float[] cw, Rect tr,
float xScale, float yScale, XericUIActionTable table)
{
int selRow = table.SelectedRow;
int selCol = table.SelectedCol;
// 气泡固定总宽:输入框 + 应用按钮 + 删除按钮 + 间距
const float totalW = BUBBLE_W + BUBBLE_APPLY_BTN_W + BUBBLE_DEL_BTN_W + 4f;
// ── 列宽气泡:左对齐列左边缘,位于列标签上方 ──
if (selCol >= 0 && selCol < cw.Length && selRow < 0)
{
float cx = tr.x + SumBefore(cw, selCol) * xScale;
float bx = cx + 2f;
float by = tr.y - ROW_H - MARGIN - BUBBLE_H - 4f;
DrawBubble(table, bx, by, cw[selCol], totalW, isColumn: true, index: selCol);
}
// ── 行高气泡:左对齐行标签左侧 ──
if (selRow >= 0 && selRow < rh.Length && selCol < 0)
{
float ry = tr.y + SumBefore(rh, selRow) * yScale;
float rhScaled = rh[selRow] * yScale;
float bx = tr.x - COL_W - MARGIN - totalW - 4f;
float by = ry + (rhScaled - BUBBLE_H) / 2f;
DrawBubble(table, bx, by, rh[selRow], totalW, isColumn: false, index: selRow);
}
// ── 单元格选中时同时显示 ──
if (selRow >= 0 && selCol >= 0)
{
if (selCol < cw.Length)
{
float cx = tr.x + SumBefore(cw, selCol) * xScale;
float bx = cx + 2f;
float by = tr.y - ROW_H - MARGIN - BUBBLE_H - 4f;
DrawBubble(table, bx, by, cw[selCol], totalW, isColumn: true, index: selCol);
}
if (selRow < rh.Length)
{
float ry = tr.y + SumBefore(rh, selRow) * yScale;
float rhScaled = rh[selRow] * yScale;
float bx = tr.x - COL_W - MARGIN - totalW - 4f;
float by = ry + (rhScaled - BUBBLE_H) / 2f;
DrawBubble(table, bx, by, rh[selRow], totalW, isColumn: false, index: selRow);
}
}
}
private static void DrawBubble(XericUIActionTable table,
float x, float y, float currentValue,
float totalW, bool isColumn, int index)
{
string controlName = isColumn
? $"XTableColSizeEdit_{index}"
: $"XTableRowSizeEdit_{index}";
bool isFocused = GUI.GetNameOfFocusedControl() == controlName;
Rect bubbleRect = new Rect(x, y, totalW, BUBBLE_H);
EditorGUI.DrawRect(bubbleRect, s_BubbleBg);
EditorGUI.DrawRect(new Rect(x, y, totalW, 1), s_BubbleBorder);
EditorGUI.DrawRect(new Rect(x, y + BUBBLE_H - 1, totalW, 1), s_BubbleBorder);
EditorGUI.DrawRect(new Rect(x, y, 1, BUBBLE_H), s_BubbleBorder);
EditorGUI.DrawRect(new Rect(x + totalW - 1, y, 1, BUBBLE_H), s_BubbleBorder);
var inputStyle = new GUIStyle(GUI.skin.textField)
{
fontSize = 11,
margin = new RectOffset(1, 1, 1, 1),
alignment = TextAnchor.MiddleCenter
};
// 输入框占左
Rect inputRect = new Rect(x + 2, y + 2, BUBBLE_W - 4, BUBBLE_H - 4);
GUI.SetNextControlName(controlName);
if (isFocused)
s_SizeEditText = GUI.TextField(inputRect, s_SizeEditText, inputStyle);
else
GUI.TextField(inputRect, currentValue.ToString("F0"), inputStyle);
// 应用按钮 — 聚焦时绿色,否则灰色
EnsureBubbleStyles();
Rect applyBtnRect = new Rect(x + BUBBLE_W + 1f, y + 1, BUBBLE_APPLY_BTN_W, BUBBLE_H - 2);
var btnStyle = isFocused ? s_BubbleApplyBtnStyleActive : s_BubbleApplyBtnStyle;
if (GUI.Button(applyBtnRect, "✓", btnStyle))
{
ApplyBubbleEdit(table, isColumn, index);
GUI.FocusControl(null);
}
// 删除按钮 — 删除当前行/列
Rect delBtnRect = new Rect(x + BUBBLE_W + BUBBLE_APPLY_BTN_W + 2f, y + 1, BUBBLE_DEL_BTN_W, BUBBLE_H - 2);
if (GUI.Button(delBtnRect, "✕", s_BubbleDelBtnStyle))
{
ApplyBubbleDelete(table, isColumn, index);
}
// 回车提交
if (isFocused &&
Event.current.type == EventType.KeyDown &&
Event.current.keyCode == KeyCode.Return)
{
ApplyBubbleEdit(table, isColumn, index);
Event.current.Use();
}
}
private static void ApplyBubbleEdit(XericUIActionTable table, bool isColumn, int index)
{
string trimmed = s_SizeEditText.Trim();
if (float.TryParse(trimmed, out float value) && value > 0)
{
if (isColumn)
table.SetColWidth(index, value);
else
table.SetRowHeight(index, value);
table.RefreshView();
}
GUI.FocusControl(null);
s_State = EditorState.TableNavigation;
SceneView.RepaintAll();
}
private static void ApplyBubbleDelete(XericUIActionTable table, bool isColumn, int index)
{
if (isColumn)
table.RemoveColumn(index);
else
table.RemoveRow(index);
GUI.FocusControl(null);
s_State = EditorState.TableNavigation;
SceneView.RepaintAll();
}
#endregion
#region
private static void DrawInputBar(XericUIActionTable table, float x, float y)
{
const float barW = 450;
const float barH = 52;
const float rowH = 22;
const float pad = 2;
Rect barRect = new Rect(x, y, barW, barH);
EditorGUI.DrawRect(barRect, s_InputBg);
if (table.SelectedRow >= 0 && table.SelectedCol >= 0)
{
var labelStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 11,
normal = { textColor = s_LabelText },
alignment = TextAnchor.MiddleCenter
};
var inputStyle = new GUIStyle(GUI.skin.textField)
{
fontSize = 12,
margin = new RectOffset(1, 1, 1, 1)
};
var btnStyle = new GUIStyle(GUI.skin.button)
{
fontSize = 11,
margin = new RectOffset(1, 1, 1, 1),
padding = new RectOffset(4, 4, 2, 2)
};
// Row 1: 坐标标签 | 输入框 | 应用按钮 + 合并/拆分按钮
float actionBtnW = 50;
float row1Y = barRect.y + pad;
float row2Y = barRect.y + rowH + pad + 4;
string cellLabel = table.GetSelectionText();
// 合并按钮(多选时显示)
bool showMerge = table.HasMultiSelection &&
(table.SelectStartRow != table.SelectEndRow ||
table.SelectStartCol != table.SelectEndCol);
// 拆分按钮(选中单元格是合并源时显示)
bool isMergeSource = table.IsMergeSource(table.SelectedRow,
table.SelectedCol, out _, out _);
float mergeBtnW = showMerge || isMergeSource ? actionBtnW : 0;
float unmergeBtnW = isMergeSource ? actionBtnW : 0;
// 坐标标签
GUI.Label(new Rect(barRect.x + 4, row1Y, 60, rowH), cellLabel, labelStyle);
// 输入框
float inputW = barW - 68 - 44 - 2 - mergeBtnW - unmergeBtnW - pad * 2 - 4;
Rect inputRect = new Rect(barRect.x + 68, row1Y, inputW, rowH);
if (s_State == EditorState.CellEditing &&
Event.current.type == EventType.KeyDown &&
Event.current.keyCode == KeyCode.Return)
{
ApplyCellEdit(table);
Event.current.Use();
}
GUI.SetNextControlName("XTableSceneEditInput");
s_EditText = GUI.TextField(inputRect, s_EditText, inputStyle);
// 应用按钮
float rightX = barRect.x + barW - pad - 44;
if (GUI.Button(new Rect(rightX, row1Y, 44, rowH), "应用", btnStyle))
{
GUI.FocusControl(null);
ApplyCellEdit(table);
}
// 合并按钮
if (showMerge)
{
var mergeStyle = new GUIStyle(btnStyle)
{
normal = { background = MakeTex(1, 1, new Color(0.15f, 0.3f, 0.65f, 0.8f)), textColor = Color.white },
hover = { background = MakeTex(1, 1, new Color(0.2f, 0.4f, 0.85f, 0.9f)), textColor = Color.white }
};
if (GUI.Button(new Rect(rightX - mergeBtnW - 2, row1Y, mergeBtnW, rowH), "合并", mergeStyle))
{
int minR = Mathf.Min(table.SelectStartRow, table.SelectEndRow);
int maxR = Mathf.Max(table.SelectStartRow, table.SelectEndRow);
int minC = Mathf.Min(table.SelectStartCol, table.SelectEndCol);
int maxC = Mathf.Max(table.SelectStartCol, table.SelectEndCol);
table.MergeCells(minR, minC, maxR - minR + 1, maxC - minC + 1);
table.RefreshView();
SceneView.RepaintAll();
}
}
// 拆分按钮
if (isMergeSource)
{
var unmergeStyle = new GUIStyle(btnStyle)
{
normal = { background = MakeTex(1, 1, new Color(0.65f, 0.25f, 0.15f, 0.8f)), textColor = Color.white },
hover = { background = MakeTex(1, 1, new Color(0.85f, 0.35f, 0.2f, 0.9f)), textColor = Color.white }
};
float unmergeX = showMerge ? rightX - mergeBtnW - unmergeBtnW - 4 : rightX - unmergeBtnW - 2;
if (GUI.Button(new Rect(unmergeX, row1Y, unmergeBtnW, rowH), "拆分", unmergeStyle))
{
table.UnmergeCells(table.SelectedRow, table.SelectedCol);
table.RefreshView();
SceneView.RepaintAll();
}
}
// Row 2: 样式命名空间下拉框
var nsLabelStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = new Color(0.6f, 0.6f, 0.6f) },
alignment = TextAnchor.MiddleRight
};
GUI.Label(new Rect(barRect.x + 4, row2Y, 36, rowH), "样式:", nsLabelStyle);
string currentNS = table.GetCellStyleNamespace(
table.SelectedRow, table.SelectedCol) ?? "";
var namespaces = StyleManager.GetAvailableNamespaces();
int currentIdx = namespaces.IndexOf(currentNS);
if (currentIdx < 0) currentIdx = 0;
Rect dropRect = new Rect(barRect.x + 44, row2Y, barW - 44 - pad, rowH);
int newIdx = UnityEditor.EditorGUI.Popup(dropRect, currentIdx,
namespaces.ToArray(), UnityEditor.EditorStyles.popup);
if (newIdx != currentIdx && newIdx >= 0 && newIdx < namespaces.Count)
{
table.SetCellStyleNamespace(table.SelectedRow, table.SelectedCol,
namespaces[newIdx]);
}
}
else
{
var hintStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = new Color(0.5f, 0.5f, 0.5f) },
alignment = TextAnchor.MiddleCenter
};
GUI.Label(new Rect(barRect.x, barRect.y, barW, barH / 2),
"点击左侧行号或上方列名选中单元格 | Shift+方向键多选", hintStyle);
}
}
private static void ApplyCellEdit(XericUIActionTable table)
{
if (table.SelectedRow < 0 || table.SelectedCol < 0) return;
table.SetCellText(table.SelectedRow, table.SelectedCol, s_EditText);
table.RefreshView();
s_State = EditorState.TableNavigation;
SceneView.RepaintAll();
}
#endregion
#region TableNavigation
private static void HandleTableMouse(XericUIActionTable table,
float[] rh, float[] cw, Rect tr,
float xScale, float yScale)
{
Event e = Event.current;
switch (e.type)
{
case EventType.MouseDown:
if (e.button != 0) break;
if (!tr.Contains(e.mousePosition)) break;
CellFromMouse(tr, xScale, yScale, out int mdRow, out int mdCol);
if (mdRow < 0 || mdCol < 0) break;
s_IsDragging = true;
s_DragStartRow = mdRow;
s_DragStartCol = mdCol;
if (e.shift && table.SelectedRow >= 0 && table.SelectedCol >= 0)
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, mdRow, mdCol);
else
table.SelectionHandle.ExtendTo(mdRow, mdCol);
}
else
{
table.SelectCell(mdRow, mdCol);
}
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
break;
case EventType.MouseDrag:
if (!s_IsDragging || e.button != 0) break;
CellFromMouse(tr, xScale, yScale, out int dragRow, out int dragCol);
if (dragRow < 0 || dragCol < 0) break;
table.SelectionHandle.ExtendTo(dragRow, dragCol);
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
break;
case EventType.MouseUp:
if (e.button != 0) break;
s_IsDragging = false;
break;
}
}
private static void CellFromMouse(Rect tr, float xScale, float yScale,
out int row, out int col)
{
float rx = (Event.current.mousePosition.x - tr.x) / xScale;
float ry = (Event.current.mousePosition.y - tr.y) / yScale;
col = IndexFromCoord(rx, s_ColWidths);
row = IndexFromCoord(ry, s_RowHeights);
// 点击合并覆盖的单元格时,重定向到合并源
if (row >= 0 && col >= 0 && s_ActiveTable != null)
RedirectToMergeSource(ref row, ref col);
}
/// <summary>如果指定单元格被合并覆盖,重定向到合并源(锚点)</summary>
private static void RedirectToMergeSource(ref int row, ref int col)
{
var data = s_ActiveTable.TableData;
if (data == null) return;
XTableCoordinateUtility.CellToBlockCoordinate(
row, col, data.BlockSizeX, data.BlockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol);
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
if (!data.BlockMap.TryGetValue(zIndex, out XTableBlock block))
return;
var merge = block.GetMergeRedirect(localRow, localCol);
if (merge == null) return;
merge.GetRedirectTarget(out ulong targetBlockIndex, out int targetLocalRow, out int targetLocalCol);
if (merge.IsCrossBlock && data.BlockMap.TryGetValue(targetBlockIndex, out XTableBlock targetBlock))
{
// 跨块合并:从目标块的块坐标反算全局坐标
XTableBlockMapping.ZIndexToBlockCoord(targetBlockIndex,
out int targetBlockRow, out int targetBlockCol);
row = targetBlockRow * data.BlockSizeY + targetLocalRow;
col = targetBlockCol * data.BlockSizeX + targetLocalCol;
}
else
{
// 本地合并:源在同一块内,反算全局坐标
row = blockRow * data.BlockSizeY + targetLocalRow;
col = blockCol * data.BlockSizeX + targetLocalCol;
}
}
#endregion
#region TableNavigation
private static void HandleKeyboard(XericUIActionTable table, float[] rh, float[] cw)
{
Event e = Event.current;
if (e.type != EventType.KeyDown) return;
// Escape 清除选区
if (e.keyCode == KeyCode.Escape)
{
table.ClearSelection();
table.RefreshView();
s_EditText = "";
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
return;
}
int row = table.SelectedRow;
int col = table.SelectedCol;
bool shift = e.shift;
// 无选中时,方向键自动选中第一个单元格
if (row < 0 || col < 0)
{
if (rh.Length == 0 || cw.Length == 0) return;
row = 0;
col = 0;
switch (e.keyCode)
{
case KeyCode.UpArrow:
case KeyCode.DownArrow:
case KeyCode.LeftArrow:
case KeyCode.RightArrow:
break;
default:
return;
}
table.SelectCell(row, col);
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
return;
}
switch (e.keyCode)
{
case KeyCode.UpArrow:
if (!TryClampToValid(row - 1, col, rh, cw, out row, out col)) return;
break;
case KeyCode.DownArrow:
if (!TryClampToValid(row + 1, col, rh, cw, out row, out col)) return;
break;
case KeyCode.LeftArrow:
if (!TryClampToValid(row, col - 1, rh, cw, out row, out col)) return;
break;
case KeyCode.RightArrow:
if (!TryClampToValid(row, col + 1, rh, cw, out row, out col)) return;
break;
default:
return;
}
if (shift)
{
if (!table.HasMultiSelection && table.SelectedRow >= 0 && table.SelectedCol >= 0)
table.SelectCell(table.SelectedRow, table.SelectedCol, row, col);
else
table.SelectionHandle.ExtendTo(row, col);
}
else
{
RedirectToMergeSource(ref row, ref col);
table.SelectCell(row, col);
}
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
}
private static bool TryClampToValid(int row, int col, float[] rh, float[] cw,
out int outRow, out int outCol)
{
if (row < 0 || row >= rh.Length || col < 0 || col >= cw.Length)
{
outRow = row;
outCol = col;
return false;
}
outRow = row;
outCol = col;
return true;
}
#endregion
}
}