Files
StandardSence/StandardScene.Signal/Ui/ModelListPanel.cs
T

350 lines
13 KiB
C#
Raw 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 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
{
/// <summary>
/// 按 Model 的 <see cref="DisplayNameAttribute"/> 反射生成可增删改列表(CycleGUI)。
/// 跳过 <see cref="BrowsableAttribute.Browsable"/> = false 与 <see cref="JsonIgnoreAttribute"/>。
/// </summary>
public static class ModelListPanel<T> where T : class, new()
{
private static Panel _panel;
private static List<T> _items = new List<T>();
private static int _selected = -1;
private static string _status = "";
private static string _title = "";
private static string _path = "";
private static Func<T, string> _keySelector = _ => "";
private static readonly Dictionary<string, string> Text = new Dictionary<string, string>();
private static readonly Dictionary<string, bool> Bools = new Dictionary<string, bool>();
private static readonly Dictionary<string, int> EnumIdx = new Dictionary<string, int>();
private static PropertyInfo[] Props => _props ??= Discover();
private static PropertyInfo[] _props;
public static void Open(string title, string path, Func<T, string> keySelector)
{
if (_panel != null)
{
try { _panel.BringToFront(); return; }
catch { _panel = null; }
}
_title = title;
_path = path;
_keySelector = keySelector ?? (_ => "");
_items = SignalConfigStore.Load<T>(path);
_selected = -1;
_status = $"已加载 {_items.Count} 条 · {_path}";
ClearEditor();
var typeKey = typeof(T).Name;
var panel = GUI.DeclarePanel()
.ShowTitle(title)
.SetDefaultDocking(Panel.Docking.None)
.InitSize(1100, 720)
.InitPos(false, 0, 0, 0.5f, 0.5f, 0.5f, 0.5f);
_panel = panel;
panel.IfTerminalQuit(() => { if (_panel == panel) _panel = null; });
panel.Define(pb =>
{
if (pb.Closing())
{
SignalConfigStore.Save(_path, _items);
panel.Exit();
_panel = null;
return;
}
var headers = Props.Select(DisplayOf).Concat(new[] { "行操作" }).ToArray();
pb.SeparatorText($"列表({_items.Count}");
pb.Table($"model-list-{typeKey}", 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: $"ml-{typeKey}-add")) Add();
pb.SameLine(8);
if (pb.Button("保存当前行", distinct: $"ml-{typeKey}-save")) SaveRow();
pb.SameLine(8);
if (pb.Button("删除当前行", distinct: $"ml-{typeKey}-del")) ConfirmDelete();
pb.SameLine(8);
if (pb.Button("清空编辑区", distinct: $"ml-{typeKey}-clear"))
{
_selected = -1;
ClearEditor();
_status = "已清空编辑区,可添加新行";
}
if (!string.IsNullOrEmpty(_status))
{
pb.Separator();
pb.Label(_status);
}
});
}
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<BrowsableAttribute>()?.Browsable != false)
.Where(p => p.GetCustomAttribute<JsonIgnoreAttribute>() == null)
.OrderBy(p => p.MetadataToken)
.ToArray();
}
private static string DisplayOf(PropertyInfo p) =>
p.GetCustomAttribute<DisplayNameAttribute>()?.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 static void DrawEditor(PanelBuilder pb)
{
for (var i = 0; i < Props.Length; i++)
{
var p = Props[i];
var label = $"{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 static 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 static 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 "";
}
}
private static 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 static 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}";
_panel?.Repaint();
}
private static 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("提示", "保存成功");
_panel?.Repaint();
}
private static 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}";
_panel?.Repaint();
});
}
}
}