对齐 FairyView 宿主命名。环线/充电/按钮/门进程的 Stop 改为 override,避免隐藏基类;Kiva.ForceStop 改为 override。 Co-authored-by: Cursor <cursoragent@cursor.com>
409 lines
17 KiB
C#
409 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using FairyView;
|
|
using StandardScene.Utils;
|
|
|
|
namespace StandardScene.ExtendDevice.ButtonBox
|
|
{
|
|
/// <summary>
|
|
/// 按钮盒配置管理(CycleGUI 版,替代原 WinForms <c>ButtonBoxManager</c> 窗体)。
|
|
/// </summary>
|
|
public class ButtonBoxManager
|
|
{
|
|
private const string DataFileName = "ButtonBoxConfig.json";
|
|
private string DataFilePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DataFileName);
|
|
private readonly object SaveLock = new object();
|
|
|
|
private Panel _panel;
|
|
private List<ButtonBoxModel> _boxes = new List<ButtonBoxModel>();
|
|
private int _selectedBoxIdx = -1;
|
|
private int _selectedBtnIdx = -1;
|
|
private string _status = "";
|
|
|
|
private string _boxIp = "";
|
|
private string _boxPort = "502";
|
|
private string _boxIndex = "";
|
|
private int _typeIdx;
|
|
private string[] _typeNames = Array.Empty<string>();
|
|
|
|
private string _btnIndex = "";
|
|
private string _triggerMission = "";
|
|
private string _triggerMethod = "";
|
|
private string _triggerParams = "";
|
|
private string _triggerDelay = "0";
|
|
private int _triggerStateIdx;
|
|
private readonly string[] _triggerStateNames = Enum.GetNames(typeof(ButtonState));
|
|
|
|
private static ButtonBoxManager _instance;
|
|
|
|
public static void OpenViewer() => Open();
|
|
|
|
public static void Open() => (_instance ??= new ButtonBoxManager()).OpenCore();
|
|
|
|
private void OpenCore()
|
|
{
|
|
if (_panel != null)
|
|
{
|
|
try { _panel.BringToFront(); return; }
|
|
catch { _panel = null; }
|
|
}
|
|
|
|
_typeNames = DiscoverTypes();
|
|
LoadData();
|
|
_selectedBoxIdx = -1;
|
|
_selectedBtnIdx = -1;
|
|
ClearBoxFields();
|
|
ClearButtonFields();
|
|
|
|
var panel = GUI.DeclarePanel()
|
|
.ShowTitle("按钮盒管理")
|
|
.ForceFloat()
|
|
.InitSize(1200, 760)
|
|
.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())
|
|
{
|
|
SaveData();
|
|
panel.Exit();
|
|
_panel = null;
|
|
return;
|
|
}
|
|
|
|
pb.SeparatorText("按钮盒");
|
|
pb.Table("btnbox-list", new[] { "盒编码", "IP", "端口", "类型", "盒操作" },
|
|
_boxes.Count, (row, i) =>
|
|
{
|
|
var b = _boxes[i];
|
|
row.Label($"{b.Index}");
|
|
row.Label(b.Ip);
|
|
row.Label($"{b.Port}");
|
|
row.Label(b.Type);
|
|
if (row.ButtonGroup(new[] { "选盒" }, new[] { "选择该按钮盒" }) == 0)
|
|
{
|
|
_selectedBoxIdx = i;
|
|
_selectedBtnIdx = -1;
|
|
LoadBoxFields(b);
|
|
ClearButtonFields();
|
|
}
|
|
}, height: FairyUiHelper.TableHeightPx(8), enableSearch: true);
|
|
|
|
pb.SeparatorText("按钮盒编辑");
|
|
var (ip, _) = pb.TextInput("1. IP", _boxIp, alwaysReturnString: true);
|
|
_boxIp = ip;
|
|
pb.SameLine(12);
|
|
var (port, _) = pb.TextInput("2. 端口", _boxPort, alwaysReturnString: true);
|
|
_boxPort = port;
|
|
var (idx, _) = pb.TextInput("3. 盒编码", _boxIndex, alwaysReturnString: true);
|
|
_boxIndex = idx;
|
|
if (_typeNames.Length > 0)
|
|
pb.DropdownBox("4. 类型", _typeNames, ref _typeIdx);
|
|
|
|
if (pb.Button("添加按钮盒", distinct: "bb-add-box")) AddBox();
|
|
pb.SameLine(8);
|
|
if (pb.Button("保存按钮盒", distinct: "bb-save-box")) SaveBox();
|
|
pb.SameLine(8);
|
|
if (pb.Button("删除按钮盒", distinct: "bb-del-box")) ConfirmDeleteBox();
|
|
|
|
var buttons = GetSelectedBox()?.Buttons ?? new List<ButtonModel>();
|
|
pb.SeparatorText($"按钮列表(盒 #{GetSelectedBox()?.Index ?? 0},共 {buttons.Count} 个)");
|
|
pb.Table("btn-list", new[] { "按钮编码", "触发状态", "延迟", "Mission", "Method", "按钮操作" },
|
|
buttons.Count, (row, i) =>
|
|
{
|
|
var btn = buttons[i];
|
|
row.Label($"{btn.Index}");
|
|
row.Label(btn.TriggerState);
|
|
row.Label($"{btn.TriggerDelay}");
|
|
row.Label(btn.TriggerMission);
|
|
row.Label(btn.TriggerMethod);
|
|
if (row.ButtonGroup(new[] { "选按钮" }, new[] { "选择该按钮" }) == 0)
|
|
{
|
|
_selectedBtnIdx = i;
|
|
LoadButtonFields(btn);
|
|
}
|
|
}, height: FairyUiHelper.TableHeightPx(6), enableSearch: true);
|
|
|
|
pb.SeparatorText("按钮编辑");
|
|
var (bIdx, _) = pb.TextInput("5. 按钮编码", _btnIndex, alwaysReturnString: true);
|
|
_btnIndex = bIdx;
|
|
pb.DropdownBox("6. 触发状态", _triggerStateNames, ref _triggerStateIdx);
|
|
var (delay, _) = pb.TextInput("7. 触发延迟", _triggerDelay, alwaysReturnString: true);
|
|
_triggerDelay = delay;
|
|
var (mission, _) = pb.TextInput("8. TriggerMission", _triggerMission, alwaysReturnString: true);
|
|
_triggerMission = mission;
|
|
var (method, _) = pb.TextInput("9. TriggerMethod", _triggerMethod, alwaysReturnString: true);
|
|
_triggerMethod = method;
|
|
var (parms, _) = pb.TextInput("10. TriggerMethodParams", _triggerParams, alwaysReturnString: true);
|
|
_triggerParams = parms;
|
|
|
|
if (pb.Button("添加按钮", distinct: "bb-add-btn")) AddButton();
|
|
pb.SameLine(8);
|
|
if (pb.Button("保存按钮", distinct: "bb-save-btn")) SaveButton();
|
|
pb.SameLine(8);
|
|
if (pb.Button("删除按钮", distinct: "bb-del-btn")) ConfirmDeleteButton();
|
|
|
|
if (!string.IsNullOrEmpty(_status))
|
|
{
|
|
pb.Separator();
|
|
pb.Label(_status);
|
|
}
|
|
});
|
|
}
|
|
|
|
private ButtonBoxModel GetSelectedBox() =>
|
|
_selectedBoxIdx >= 0 && _selectedBoxIdx < _boxes.Count ? _boxes[_selectedBoxIdx] : null;
|
|
|
|
private ButtonModel GetSelectedButton()
|
|
{
|
|
var box = GetSelectedBox();
|
|
return box != null && _selectedBtnIdx >= 0 && _selectedBtnIdx < box.Buttons.Count
|
|
? box.Buttons[_selectedBtnIdx] : null;
|
|
}
|
|
|
|
private string[] DiscoverTypes()
|
|
{
|
|
try
|
|
{
|
|
var names = Simple3.Utils.UiTypeDiscovery.AllTypes()
|
|
.Where(t => t.IsClass && !t.IsAbstract
|
|
&& t.Namespace == typeof(BasicButtonBox).Namespace
|
|
&& t.IsSubclassOf(typeof(BasicButtonBox)))
|
|
.Select(t => t.Name)
|
|
.OrderBy(n => n)
|
|
.ToArray();
|
|
return names.Length > 0 ? names : new[] { "BasicButtonBox" };
|
|
}
|
|
catch
|
|
{
|
|
return new[] { "BasicButtonBox" };
|
|
}
|
|
}
|
|
|
|
private void LoadData()
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(DataFilePath)) { _boxes = new List<ButtonBoxModel>(); return; }
|
|
var json = File.ReadAllText(DataFilePath, Encoding.UTF8);
|
|
_boxes = string.IsNullOrWhiteSpace(json)
|
|
? new List<ButtonBoxModel>()
|
|
: json.JsonTo<List<ButtonBoxModel>>() ?? new List<ButtonBoxModel>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_boxes = new List<ButtonBoxModel>();
|
|
_status = $"加载失败: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private void SaveData()
|
|
{
|
|
var json = _boxes.ToJson();
|
|
var path = DataFilePath;
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
lock (SaveLock)
|
|
File.WriteAllText(path, json, Encoding.UTF8);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_status = $"保存失败: {ex.Message}";
|
|
_panel?.Repaint();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void LoadBoxFields(ButtonBoxModel b)
|
|
{
|
|
_boxIp = b.Ip;
|
|
_boxPort = b.Port.ToString();
|
|
_boxIndex = b.Index.ToString();
|
|
_typeIdx = Math.Max(0, Array.IndexOf(_typeNames, b.Type));
|
|
if (_typeIdx < 0) _typeIdx = 0;
|
|
}
|
|
|
|
private void ClearBoxFields()
|
|
{
|
|
_boxIp = "";
|
|
_boxPort = "502";
|
|
_boxIndex = "";
|
|
_typeIdx = 0;
|
|
}
|
|
|
|
private void LoadButtonFields(ButtonModel b)
|
|
{
|
|
_btnIndex = b.Index.ToString();
|
|
_triggerMission = b.TriggerMission ?? "";
|
|
_triggerMethod = b.TriggerMethod ?? "";
|
|
_triggerParams = b.TriggerMethodParams ?? "";
|
|
_triggerDelay = b.TriggerDelay.ToString();
|
|
_triggerStateIdx = Math.Max(0, Array.IndexOf(_triggerStateNames, b.TriggerState));
|
|
}
|
|
|
|
private void ClearButtonFields()
|
|
{
|
|
_btnIndex = "";
|
|
_triggerMission = "";
|
|
_triggerMethod = "";
|
|
_triggerParams = "";
|
|
_triggerDelay = "0";
|
|
_triggerStateIdx = 0;
|
|
}
|
|
|
|
private void AddBox()
|
|
{
|
|
if (!TryParseBoxInput(out var index, out var ip, out var port, out var type, false, out var err))
|
|
{ FairyUiHelper.Alert("错误", err); return; }
|
|
if (_boxes.Any(b => b.Index == index)) { FairyUiHelper.Alert("错误", $"编码 {index} 已存在"); return; }
|
|
if (_boxes.Any(b => b.Ip == ip)) { FairyUiHelper.Alert("错误", $"IP {ip} 已存在"); return; }
|
|
|
|
var box = new ButtonBoxModel { Index = index, Ip = ip, Port = port, Type = type };
|
|
_boxes.Add(box);
|
|
_selectedBoxIdx = _boxes.Count - 1;
|
|
SaveData();
|
|
_status = "已添加按钮盒";
|
|
_panel?.Repaint();
|
|
}
|
|
|
|
private void SaveBox()
|
|
{
|
|
var cur = GetSelectedBox();
|
|
if (cur == null) { FairyUiHelper.Alert("提示", "请先选择按钮盒"); return; }
|
|
if (!TryParseBoxInput(out var index, out var ip, out var port, out var type, true, out var err))
|
|
{ FairyUiHelper.Alert("错误", err); return; }
|
|
if (_boxes.Any(b => b.Index == index && b != cur)) { FairyUiHelper.Alert("错误", $"编码 {index} 已存在"); return; }
|
|
if (_boxes.Any(b => b.Ip == ip && b != cur)) { FairyUiHelper.Alert("错误", $"IP {ip} 已存在"); return; }
|
|
|
|
cur.Index = index; cur.Ip = ip; cur.Port = port; cur.Type = type;
|
|
SaveData();
|
|
FairyUiHelper.Alert("提示", "保存成功");
|
|
_panel?.Repaint();
|
|
}
|
|
|
|
private void ConfirmDeleteBox()
|
|
{
|
|
var cur = GetSelectedBox();
|
|
if (cur == null) { FairyUiHelper.Alert("提示", "请选择要删除的按钮盒"); return; }
|
|
FairyUiHelper.ConfirmThen($"删除编码为 {cur.Index} 的按钮盒?", () =>
|
|
{
|
|
_boxes.Remove(cur);
|
|
_selectedBoxIdx = -1;
|
|
_selectedBtnIdx = -1;
|
|
ClearBoxFields();
|
|
ClearButtonFields();
|
|
SaveData();
|
|
_status = "已删除按钮盒";
|
|
_panel?.Repaint();
|
|
});
|
|
}
|
|
|
|
private void AddButton()
|
|
{
|
|
var box = GetSelectedBox();
|
|
if (box == null) { FairyUiHelper.Alert("提示", "请先选择按钮盒"); return; }
|
|
if (!TryParseButtonInput(out var index, out var delay, out var err))
|
|
{ FairyUiHelper.Alert("错误", err); return; }
|
|
if (box.Buttons.Any(b => b.Index == index)) { FairyUiHelper.Alert("错误", $"按钮编码 {index} 已存在"); return; }
|
|
|
|
box.Buttons.Add(new ButtonModel
|
|
{
|
|
Index = index,
|
|
TriggerMission = _triggerMission.Trim(),
|
|
TriggerMethod = _triggerMethod.Trim(),
|
|
TriggerMethodParams = _triggerParams.Trim(),
|
|
TriggerState = _triggerStateNames[_triggerStateIdx],
|
|
TriggerDelay = delay
|
|
});
|
|
_selectedBtnIdx = box.Buttons.Count - 1;
|
|
SaveData();
|
|
_status = "已添加按钮";
|
|
_panel?.Repaint();
|
|
}
|
|
|
|
private void SaveButton()
|
|
{
|
|
var box = GetSelectedBox();
|
|
var btn = GetSelectedButton();
|
|
if (box == null || btn == null) { FairyUiHelper.Alert("提示", "请先选择按钮"); return; }
|
|
if (!TryParseButtonInput(out var index, out var delay, out var err))
|
|
{ FairyUiHelper.Alert("错误", err); return; }
|
|
if (box.Buttons.Any(b => b.Index == index && b != btn)) { FairyUiHelper.Alert("错误", $"按钮编码 {index} 已存在"); return; }
|
|
|
|
btn.Index = index;
|
|
btn.TriggerMission = _triggerMission.Trim();
|
|
btn.TriggerMethod = _triggerMethod.Trim();
|
|
btn.TriggerMethodParams = _triggerParams.Trim();
|
|
btn.TriggerState = _triggerStateNames[_triggerStateIdx];
|
|
btn.TriggerDelay = delay;
|
|
SaveData();
|
|
FairyUiHelper.Alert("提示", "保存成功");
|
|
_panel?.Repaint();
|
|
}
|
|
|
|
private void ConfirmDeleteButton()
|
|
{
|
|
var box = GetSelectedBox();
|
|
var btn = GetSelectedButton();
|
|
if (box == null || btn == null) { FairyUiHelper.Alert("提示", "请选择要删除的按钮"); return; }
|
|
FairyUiHelper.ConfirmThen($"删除编码为 {btn.Index} 的按钮?", () =>
|
|
{
|
|
box.Buttons.Remove(btn);
|
|
_selectedBtnIdx = -1;
|
|
ClearButtonFields();
|
|
SaveData();
|
|
_status = "已删除按钮";
|
|
_panel?.Repaint();
|
|
});
|
|
}
|
|
|
|
private bool TryParseBoxInput(out int index, out string ip, out int port, out string type, bool requireSelection, out string err)
|
|
{
|
|
index = 0; ip = ""; port = 502; type = ""; err = "";
|
|
ip = string.IsNullOrWhiteSpace(_boxIp) ? "192.168.1.100" : _boxIp.Trim();
|
|
if (!IsValidIp(ip)) { err = "无效的 IP 地址"; return false; }
|
|
if (!int.TryParse(_boxPort.Trim(), out port)) { err = "端口必须是数字"; return false; }
|
|
if (!int.TryParse(_boxIndex.Trim(), out index))
|
|
{
|
|
index = _boxes.Count > 0 ? _boxes.Max(b => b.Index) + 1 : 1;
|
|
if (!string.IsNullOrWhiteSpace(_boxIndex.Trim())) { err = "编码必须是数字"; return false; }
|
|
}
|
|
type = _typeNames.Length > 0 ? _typeNames[_typeIdx] : "BasicButtonBox";
|
|
if (requireSelection && GetSelectedBox() == null) { err = "请先选择按钮盒"; return false; }
|
|
return true;
|
|
}
|
|
|
|
private bool TryParseButtonInput(out int index, out ushort delay, out string err)
|
|
{
|
|
index = 0; delay = 0; err = "";
|
|
var box = GetSelectedBox();
|
|
if (!int.TryParse(_btnIndex.Trim(), out index))
|
|
{
|
|
index = box?.Buttons.Count > 0 ? box.Buttons.Max(b => b.Index) + 1 : 1;
|
|
if (!string.IsNullOrWhiteSpace(_btnIndex.Trim())) { err = "按钮编码必须是数字"; return false; }
|
|
}
|
|
if (!ushort.TryParse(string.IsNullOrWhiteSpace(_triggerDelay) ? "0" : _triggerDelay.Trim(), out delay))
|
|
{ err = "触发延迟必须是 0-65535"; return false; }
|
|
return true;
|
|
}
|
|
|
|
private bool IsValidIp(string ip)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ip)) return false;
|
|
var pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
|
|
return Regex.IsMatch(ip, pattern) && IPAddress.TryParse(ip, out var addr)
|
|
&& addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork;
|
|
}
|
|
}
|
|
}
|