新增信号交互进程插件
This commit is contained in:
@@ -1,51 +1,36 @@
|
||||
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"/>。
|
||||
/// CycleGUI 独立窗口打开某一类 Model 列表(进程工具栏「打开机构/握手点/放行点列表」触发)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 每种 T 共用一个静态窗口:已打开则前置,关掉后下次再 DeclarePanel。
|
||||
/// 真正的增删改在 <see cref="ModelTableEditor{T}"/>,保存直接写 JSON,
|
||||
/// 不自动 Reload 进程内 Worker,需再到进程上点「重新加载配置」。
|
||||
/// </remarks>
|
||||
public static class ModelListPanel<T> where T : class, new()
|
||||
{
|
||||
/// <summary>当前已声明的面板;关闭或异常后置 null,允许再次 Open。</summary>
|
||||
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;
|
||||
private static readonly ModelTableEditor<T> Editor = new ModelTableEditor<T>();
|
||||
|
||||
/// <param name="title">窗口标题。</param>
|
||||
/// <param name="path">JSON 绝对路径(应先经 SignalConfigStore.Resolve)。</param>
|
||||
/// <param name="keySelector">主键,添加/保存时查重(机构名、站点号等)。</param>
|
||||
public static void Open(string title, string path, Func<T, string> keySelector)
|
||||
{
|
||||
Editor.Bind(path, keySelector ?? (_ => ""), typeof(T).Name);
|
||||
|
||||
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)
|
||||
@@ -58,291 +43,12 @@ namespace StandardScene.Signal.Ui
|
||||
{
|
||||
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();
|
||||
Editor.Render(pb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user