using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using CycleGUI;
using Newtonsoft.Json;
using StandardScene.Utils;
namespace StandardScene.Signal.Ui
{
///
/// 按 Model 的 反射生成可增删改表格。
///
///
/// 列顺序取属性 MetadataToken(源码声明顺序)。
/// 跳过不可写、索引器、 为 false、 的属性。
/// 编辑区用独立字典暂存,点「添加」或「保存当前行」才反写对象并 。
/// CycleGUI 控件 id 带 _idPrefix,三种表同时开窗时不会抢同一组控件状态。
///
public sealed class ModelTableEditor where T : class, new()
{
private static PropertyInfo[] Props => _props ??= Discover();
private static PropertyInfo[] _props;
private List _items = new List();
/// -1 表示未选中,此时「保存/删除」会提示先选行,「添加」写入新行。
private int _selected = -1;
private string _status = "";
private string _path = "";
private string _idPrefix = typeof(T).Name;
private Func _keySelector = _ => "";
/// 字符串/数值编辑缓存,键为属性名。
private readonly Dictionary _text = new Dictionary();
private readonly Dictionary _bools = new Dictionary();
/// 枚举下拉当前下标,写入时按 Enum.GetNames 转回枚举值。
private readonly Dictionary _enumIdx = new Dictionary();
public int Count => _items.Count;
public string Status => _status;
/// 绑定 JSON 路径并立即 Reload。同一编辑器换文件时调用。
public void Bind(string path, Func keySelector, string idPrefix)
{
_path = path ?? "";
_keySelector = keySelector ?? (_ => "");
_idPrefix = string.IsNullOrWhiteSpace(idPrefix) ? typeof(T).Name : idPrefix;
Reload();
}
/// 从磁盘重读,丢弃未保存的编辑区内容。
public void Reload()
{
_items = SignalConfigStore.Load(_path);
_selected = -1;
_status = $"已加载 {_items.Count} 条 · {_path}";
ClearEditor();
}
/// 列表 + 编辑区 + 添加/保存/删除/清空/刷新。每帧由面板 Define 调用。
public void Render(PanelBuilder pb)
{
var headers = Props.Select(DisplayOf).Concat(new[] { "行操作" }).ToArray();
pb.SeparatorText($"列表({_items.Count})");
pb.Table($"dc-list-{_idPrefix}", headers, _items.Count, (row, i) =>
{
var item = _items[i];
foreach (var p in Props)
row.Label(FormatCell(p.GetValue(item)));
if (row.ButtonGroup(new[] { "选择" }, new[] { "选择该行" }) == 0)
{
_selected = i;
LoadEditor(item);
}
}, height: 10, enableSearch: true);
pb.SeparatorText(_selected >= 0 ? $"编辑第 {_selected + 1} 行" : "编辑(未选中则「添加」写入新行)");
DrawEditor(pb);
if (pb.Button("添加", distinct: $"dc-{_idPrefix}-add")) Add();
pb.SameLine(8);
if (pb.Button("保存当前行", distinct: $"dc-{_idPrefix}-save")) SaveRow();
pb.SameLine(8);
if (pb.Button("删除当前行", distinct: $"dc-{_idPrefix}-del")) ConfirmDelete();
pb.SameLine(8);
if (pb.Button("清空编辑区", distinct: $"dc-{_idPrefix}-clear"))
{
_selected = -1;
ClearEditor();
_status = "已清空编辑区,可添加新行";
}
pb.SameLine(8);
if (pb.Button("从文件刷新", distinct: $"dc-{_idPrefix}-reload"))
Reload();
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
}
}
/// 发现可编辑列。Browsable=false 或 JsonIgnore 的运行态字段不会进表。
private static PropertyInfo[] Discover()
{
return typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
.Where(p => p.GetCustomAttribute()?.Browsable != false)
.Where(p => p.GetCustomAttribute() == null)
.OrderBy(p => p.MetadataToken)
.ToArray();
}
private static string DisplayOf(PropertyInfo p) =>
p.GetCustomAttribute()?.DisplayName ?? p.Name;
private static string FormatCell(object value)
{
if (value == null) return "";
if (value is bool b) return b ? "是" : "否";
return Convert.ToString(value) ?? "";
}
private void DrawEditor(PanelBuilder pb)
{
for (var i = 0; i < Props.Length; i++)
{
var p = Props[i];
var label = $"{_idPrefix}-{i + 1}. {DisplayOf(p)}";
var type = p.PropertyType;
if (type == typeof(bool))
{
if (!_bools.ContainsKey(p.Name)) _bools[p.Name] = false;
var v = _bools[p.Name];
pb.CheckBox(label, ref v);
_bools[p.Name] = v;
}
else if (type.IsEnum)
{
var names = Enum.GetNames(type);
if (names.Length == 0) continue;
if (!_enumIdx.ContainsKey(p.Name)) _enumIdx[p.Name] = 0;
var idx = _enumIdx[p.Name];
if (idx < 0 || idx >= names.Length) idx = 0;
pb.DropdownBox(label, names, ref idx);
_enumIdx[p.Name] = idx;
}
else
{
if (!_text.ContainsKey(p.Name)) _text[p.Name] = "";
var (text, _) = pb.TextInput(label, _text[p.Name], alwaysReturnString: true);
_text[p.Name] = text ?? "";
}
}
}
private void LoadEditor(T item)
{
foreach (var p in Props)
{
var value = p.GetValue(item);
if (p.PropertyType == typeof(bool))
_bools[p.Name] = value is true;
else if (p.PropertyType.IsEnum)
{
var names = Enum.GetNames(p.PropertyType);
var name = value?.ToString() ?? names.FirstOrDefault() ?? "";
var idx = Array.IndexOf(names, name);
_enumIdx[p.Name] = idx >= 0 ? idx : 0;
}
else
_text[p.Name] = value == null ? "" : Convert.ToString(value) ?? "";
}
}
private void ClearEditor()
{
_text.Clear();
_bools.Clear();
_enumIdx.Clear();
foreach (var p in Props)
{
if (p.PropertyType == typeof(bool))
_bools[p.Name] = false;
else if (p.PropertyType.IsEnum)
_enumIdx[p.Name] = 0;
else if (p.PropertyType == typeof(int) || p.PropertyType == typeof(byte) || p.PropertyType == typeof(ushort))
_text[p.Name] = "0";
else
_text[p.Name] = DefaultText(p);
}
}
private static string DefaultText(PropertyInfo p)
{
try
{
var fresh = new T();
var v = p.GetValue(fresh);
return v == null ? "" : Convert.ToString(v) ?? "";
}
catch
{
return "";
}
}
/// 把编辑区打成对象。主键空、整数解析失败时返回 false 并给出中文 err。
private bool TryBuild(out T item, out string err)
{
item = new T();
err = "";
foreach (var p in Props)
{
try
{
object boxed;
if (p.PropertyType == typeof(bool))
boxed = _bools.TryGetValue(p.Name, out var b) && b;
else if (p.PropertyType.IsEnum)
{
var names = Enum.GetNames(p.PropertyType);
var idx = _enumIdx.TryGetValue(p.Name, out var e) ? e : 0;
if (idx < 0 || idx >= names.Length) idx = 0;
boxed = Enum.Parse(p.PropertyType, names[idx]);
}
else if (p.PropertyType == typeof(int))
{
var raw = _text.TryGetValue(p.Name, out var t) ? t : "0";
if (!int.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n))
{
err = $"{DisplayOf(p)} 必须是整数";
return false;
}
boxed = n;
}
else if (p.PropertyType == typeof(ushort))
{
var raw = _text.TryGetValue(p.Name, out var t) ? t : "0";
if (!ushort.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n))
{
err = $"{DisplayOf(p)} 必须是 0-65535";
return false;
}
boxed = n;
}
else if (p.PropertyType == typeof(byte))
{
var raw = _text.TryGetValue(p.Name, out var t) ? t : "0";
if (!byte.TryParse(string.IsNullOrWhiteSpace(raw) ? "0" : raw.Trim(), out var n))
{
err = $"{DisplayOf(p)} 必须是 0-255";
return false;
}
boxed = n;
}
else
boxed = _text.TryGetValue(p.Name, out var s) ? s ?? "" : "";
p.SetValue(item, boxed);
}
catch (Exception ex)
{
err = $"{DisplayOf(p)} 写入失败:{ex.Message}";
return false;
}
}
var key = (_keySelector(item) ?? "").Trim();
if (string.IsNullOrEmpty(key))
{
err = "主键不能为空";
return false;
}
return true;
}
/// 主键已存在则拒绝。成功后立刻写盘并把选中行指到新行。
private void Add()
{
if (!TryBuild(out var item, out var err))
{
CycleUiHelper.Alert("错误", err);
return;
}
var key = _keySelector(item);
if (_items.Any(x => string.Equals(_keySelector(x), key, StringComparison.OrdinalIgnoreCase)))
{
CycleUiHelper.Alert("错误", $"主键「{key}」已存在");
return;
}
_items.Add(item);
_selected = _items.Count - 1;
SignalConfigStore.Save(_path, _items);
_status = $"已添加 {key}";
}
/// 覆盖当前选中行。主键与其它行冲突则拒绝。
private void SaveRow()
{
if (_selected < 0 || _selected >= _items.Count)
{
CycleUiHelper.Alert("提示", "请先选择要保存的行,或用「添加」写入新行");
return;
}
if (!TryBuild(out var item, out var err))
{
CycleUiHelper.Alert("错误", err);
return;
}
var key = _keySelector(item);
if (_items.Where((_, i) => i != _selected)
.Any(x => string.Equals(_keySelector(x), key, StringComparison.OrdinalIgnoreCase)))
{
CycleUiHelper.Alert("错误", $"主键「{key}」已存在");
return;
}
_items[_selected] = item;
SignalConfigStore.Save(_path, _items);
_status = $"已保存 {key}";
CycleUiHelper.Alert("提示", "保存成功");
}
/// 确认框通过后删除选中行并写盘。
private void ConfirmDelete()
{
if (_selected < 0 || _selected >= _items.Count)
{
CycleUiHelper.Alert("提示", "请先选择要删除的行");
return;
}
var key = _keySelector(_items[_selected]);
CycleUiHelper.ConfirmThen($"删除「{key}」?", () =>
{
_items.RemoveAt(_selected);
_selected = -1;
ClearEditor();
SignalConfigStore.Save(_path, _items);
_status = $"已删除 {key}";
});
}
}
}