Files
Fishing2/Packages/cn.tuanjie.codely.bridge/Editor/Windows/TcpBridgeControlWindow.cs
2026-03-09 17:50:20 +08:00

471 lines
21 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 UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Windows
{
public class TcpBridgeControlWindow : EditorWindow
{
private enum BridgeState { NotStarted, Starting, Started }
private double lastStatusUpdate;
private const double StatusUpdateInterval = 0.5;
private BridgeState currentState = BridgeState.NotStarted;
private bool isStarting = false;
private double startingTime = 0;
private const double StartingTimeout = 3.0;
// Window title icon
private Texture2D titleIcon;
// Client icons
private Texture2D codelyIcon;
private Texture2D vscodeIcon;
private Texture2D visualStudioIcon;
private Texture2D jetbrainsIcon;
private Texture2D cliIcon;
private Texture2D unityEditorIcon;
private Texture2D tuanjieIcon;
// Fonts
private Font firaMono;
private Font inter;
// Button textures
private Texture2D connectTex;
private Texture2D connectHoverTex;
private Texture2D connectPressedTex;
private Texture2D connectingTex;
private Texture2D disconnectTex;
private Texture2D disconnectHoverTex;
private Texture2D disconnectPressedTex;
// Connection tracking
private bool connectedToCLI;
private bool connectedToVSCode;
private bool connectedToVisualStudio;
private bool connectedToJetBrains;
private bool connectedToUnityEditor;
private bool connectedToTuanjie;
private static readonly Color GreenColor = new Color(0x01 / 255f, 0xA7 / 255f, 0x7F / 255f);
private static readonly Color GrayColor = new Color(0.5f, 0.5f, 0.5f);
private static readonly Color SubtextColor = new Color(0x9C / 255f, 0xA3 / 255f, 0xAF / 255f);
// ── Scale ────────────────────────────────────────────────────────────────
private const float ScaleFactor = 2 / 1.5f;
// ── Base sizes (ScaleFactor = 1) ─────────────────────────────────────────
private const float BaseWindowWidth = 430f;
private const float BaseLeftMargin = 25f;
private const float BaseTopPadding = 40f;
private const float BaseBottomPadding = 40f;
private const float BaseCodelyIconSize = 40f;
private const float BaseSpaceAfterIcon = 25f;
private const int BaseTitleFontSize = 20;
private const float BaseSpaceAfterTitle = 15f;
private const int BaseBodyFontSize = 17;
private const float BaseSpaceAfterBody = 30f;
private const int BaseStatusFontSize = 12;
private const float BaseSpaceBeforeStatus = 30f;
private const float BaseMinWindowHeight = 80f;
private const int BaseConnectedFontSize = 16;
private const float BaseConnectedItemSpacing = 6f;
private const float BaseClientIconSize = 24f;
private const float BaseClientIconLabelGap = 6f;
private const int BaseClientLabelFontSize = 13;
private const float BaseClientLabelHeight = 18f;
private const float BaseClientItemSpacing = 12f;
private const float BaseConnectBtnW = 159f;
private const float BaseConnectBtnH = 40f;
// ── Scaled values ─────────────────────────────────────────────────────────
private static float WindowWidth => BaseWindowWidth * ScaleFactor;
private static float LeftMargin => BaseLeftMargin * ScaleFactor;
private static float TopPadding => BaseTopPadding * ScaleFactor;
private static float BottomPadding => BaseBottomPadding * ScaleFactor;
private static float CodelyIconSize => BaseCodelyIconSize * ScaleFactor;
private static float SpaceAfterIcon => BaseSpaceAfterIcon * ScaleFactor;
private static int TitleFontSize => Mathf.RoundToInt(BaseTitleFontSize * ScaleFactor);
private static float SpaceAfterTitle => BaseSpaceAfterTitle * ScaleFactor;
private static int BodyFontSize => Mathf.RoundToInt(BaseBodyFontSize * ScaleFactor);
private static float SpaceAfterBody => BaseSpaceAfterBody * ScaleFactor;
private static int StatusFontSize => Mathf.RoundToInt(BaseStatusFontSize * ScaleFactor);
private static float SpaceBeforeStatus => BaseSpaceBeforeStatus * ScaleFactor;
private static float MinWindowHeight => BaseMinWindowHeight * ScaleFactor;
private static int ConnectedFontSize => Mathf.RoundToInt(BaseConnectedFontSize * ScaleFactor);
private static float ConnectedItemSpacing => BaseConnectedItemSpacing * ScaleFactor;
private static float ClientIconSize => BaseClientIconSize * ScaleFactor;
private static float ClientIconLabelGap => BaseClientIconLabelGap * ScaleFactor;
private static int ClientLabelFontSize => Mathf.RoundToInt(BaseClientLabelFontSize * ScaleFactor);
private static float ClientLabelHeight => BaseClientLabelHeight * ScaleFactor;
private static float ClientItemSpacing => BaseClientItemSpacing * ScaleFactor;
private static float ConnectBtnW => BaseConnectBtnW * ScaleFactor;
private static float ConnectBtnH => BaseConnectBtnH * ScaleFactor;
[MenuItem("Window/Codely Bridge", priority = 1001)]
public static void ShowWindow()
{
var window = GetWindow<TcpBridgeControlWindow>("Codely Bridge");
window.minSize = new Vector2(WindowWidth, MinWindowHeight);
window.maxSize = new Vector2(WindowWidth, 2000f);
window.Show();
}
private void OnEnable()
{
EditorPrefs.SetBool("UnityTcp.DebugLogs", false);
LoadIcons();
UpdateStatus();
UnityTcpBridge.OnClientPlatformsChanged += OnClientPlatformsChanged;
}
private void OnDisable()
{
UnityTcpBridge.OnClientPlatformsChanged -= OnClientPlatformsChanged;
}
private void OnClientPlatformsChanged() => UpdateConnectionStatus();
private static Texture2D Load(string path) =>
AssetDatabase.LoadAssetAtPath<Texture2D>(path);
private void LoadIcons()
{
const string b = "Packages/cn.tuanjie.codely.bridge/Editor/Icons/";
titleIcon = Load(b + "title_icon.png");
codelyIcon = Load(b + "codely.png");
vscodeIcon = Load(b + "vscode.png");
visualStudioIcon = Load(b + "visualstudio.png");
jetbrainsIcon = Load(b + "jetbrains.png");
cliIcon = Load(b + "cli.png");
unityEditorIcon = Load(b + "unity_editor.png");
tuanjieIcon = Load(b + "tuanjie.png");
connectTex = Load(b + "connect.png");
connectHoverTex = Load(b + "connect_hover.png");
connectPressedTex = Load(b + "connect_pressed.png");
connectingTex = Load(b + "connecting.png");
disconnectTex = Load(b + "disconnect.png");
disconnectHoverTex = Load(b + "disconnect_hover.png");
disconnectPressedTex = Load(b + "disconnect_pressed.png");
firaMono = AssetDatabase.LoadAssetAtPath<Font>("Packages/cn.tuanjie.codely.bridge/Editor/fonts/FiraMono-Regular.ttf");
inter = AssetDatabase.LoadAssetAtPath<Font>("Packages/cn.tuanjie.codely.bridge/Editor/fonts/Inter.ttc");
if (titleIcon != null)
titleContent = new GUIContent(" Codely Bridge", titleIcon);
}
private void OnGUI()
{
if (EditorApplication.timeSinceStartup - lastStatusUpdate > StatusUpdateInterval)
{
UpdateStatus();
lastStatusUpdate = EditorApplication.timeSinceStartup;
}
DrawUI();
}
private static readonly Color BgColor = new Color(0.18f, 0.18f, 0.18f);
private void DrawUI()
{
EditorGUI.DrawRect(new Rect(0, 0, position.width, position.height), BgColor);
bool started = currentState == BridgeState.Started;
// ── Top padding ──────────────────────────────────────────────────────
GUILayout.Space(TopPadding);
// ── Codely icon ──────────────────────────────────────────────────────
GUILayout.BeginHorizontal();
GUILayout.Space(LeftMargin);
if (codelyIcon != null)
GUILayout.Box(codelyIcon, GUIStyle.none, GUILayout.Width(CodelyIconSize), GUILayout.Height(CodelyIconSize));
GUILayout.EndHorizontal();
GUILayout.Space(SpaceAfterIcon);
// ── Title ────────────────────────────────────────────────────────────
GUILayout.BeginHorizontal();
GUILayout.Space(LeftMargin);
var titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = TitleFontSize, wordWrap = false, font = inter };
if (started) {
titleStyle.normal.textColor = GreenColor;
titleStyle.hover.textColor = GreenColor;
}
else {
titleStyle.normal.textColor = Color.white;
titleStyle.hover.textColor = Color.white;
}
GUILayout.Label(
started ? "Codely bridge is ready \u2713" : "Connect to Codely Agent",
titleStyle);
GUILayout.EndHorizontal();
GUILayout.Space(SpaceAfterTitle);
// ── Body (Form 1: client list | Form 2: text + status) ───────────────
bool hasClients = started && (connectedToCLI || connectedToVSCode || connectedToVisualStudio
|| connectedToJetBrains || connectedToUnityEditor || connectedToTuanjie);
GUILayout.BeginHorizontal();
GUILayout.Space(LeftMargin);
GUILayout.BeginVertical();
if (hasClients)
DrawClientListBody();
else
DrawNoClientBody();
GUILayout.EndVertical();
GUILayout.EndHorizontal();
GUILayout.Space(SpaceAfterBody);
// ── Action button ────────────────────────────────────────────────────
Texture2D btnNormal = null, btnPressed = null, btnHover = null;
bool btnEnabled = true;
System.Action btnAction = null;
switch (currentState)
{
case BridgeState.NotStarted:
btnNormal = connectTex;
btnPressed = connectPressedTex;
btnHover = connectHoverTex;
btnAction = StartBridge;
break;
case BridgeState.Starting:
btnNormal = connectingTex;
btnPressed = connectingTex;
btnHover = connectingTex;
btnEnabled = false;
break;
case BridgeState.Started:
btnNormal = disconnectTex;
btnPressed = disconnectPressedTex;
btnHover = disconnectHoverTex;
btnAction = StopBridge;
break;
}
GUILayout.BeginHorizontal();
GUILayout.Space(LeftMargin);
if (btnNormal != null)
{
float btnW = ConnectBtnW;
float btnH = ConnectBtnH;
var style = new GUIStyle(GUIStyle.none);
style.normal.background = btnNormal;
style.active.background = btnPressed ?? btnNormal;
style.hover.background = btnHover ?? btnNormal;
if (!btnEnabled) GUI.enabled = false;
if (GUILayout.Button(GUIContent.none, style, GUILayout.Width(btnW), GUILayout.Height(btnH))
&& btnAction != null)
btnAction();
if (!btnEnabled) GUI.enabled = true;
}
else
{
string fallback = currentState == BridgeState.NotStarted ? "Connect \u2192"
: currentState == BridgeState.Starting ? "Connecting..."
: "Disconnect";
if (!btnEnabled) GUI.enabled = false;
if (GUILayout.Button(fallback, GUILayout.Width(ConnectBtnW), GUILayout.Height(ConnectBtnH))
&& btnAction != null)
btnAction();
if (!btnEnabled) GUI.enabled = true;
}
GUILayout.EndHorizontal();
// ── Bottom padding ───────────────────────────────────────────────────
GUILayout.Space(BottomPadding);
// ── Auto-resize height to fit content ────────────────────────────────
if (Event.current.type == EventType.Repaint)
{
float targetH = Mathf.Max(MinWindowHeight, GUILayoutUtility.GetLastRect().yMax);
if (Mathf.Abs(position.height - targetH) > 1f)
{
minSize = new Vector2(WindowWidth, targetH);
maxSize = new Vector2(WindowWidth, targetH);
}
}
}
// Form 1 bridge started and at least one client is connected
private void DrawClientListBody()
{
var labelStyle = new GUIStyle(EditorStyles.label) { fontSize = ConnectedFontSize, font = inter };
labelStyle.normal.textColor = SubtextColor;
labelStyle.hover.textColor = SubtextColor;
GUILayout.BeginHorizontal();
// Left column: "Connected to :" label, top-aligned
float labelW = labelStyle.CalcSize(new GUIContent("Connected to :")).x + 4f;
GUILayout.Label("Connected to :", labelStyle, GUILayout.Width(labelW));
// Right column: all clients stacked vertically
GUILayout.BeginVertical();
if (connectedToVSCode) DrawClient(vscodeIcon, "VS Code");
if (connectedToVisualStudio) DrawClient(visualStudioIcon, "Visual Studio");
if (connectedToJetBrains) DrawClient(jetbrainsIcon, "JetBrains");
if (connectedToCLI) DrawClient(cliIcon, "CLI");
if (connectedToUnityEditor) DrawClient(unityEditorIcon, "Unity Editor");
if (connectedToTuanjie) DrawClient(tuanjieIcon, "Tuanjie");
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
// Form 2 not started, starting, or started but no clients yet
private void DrawNoClientBody()
{
string bodyText = currentState == BridgeState.Started
? "Waiting for clients..."
: "Start Codely Bridge to connect to Codely Agents.";
var bodyStyle = new GUIStyle(EditorStyles.label) { fontSize = BodyFontSize, wordWrap = true, font = inter };
bodyStyle.normal.textColor = SubtextColor;
bodyStyle.hover.textColor = SubtextColor;
GUILayout.Label(bodyText, bodyStyle);
GUILayout.Space(SpaceBeforeStatus);
string dots = new string('.', (int)(EditorApplication.timeSinceStartup * 2) % 3 + 1);
string statusText = currentState == BridgeState.Starting ? $"\u2022 Status: Connecting{dots}"
: currentState == BridgeState.Started ? "\u2022 Status: Waiting for clients..."
: "\u2022 Status: Disconnected";
GUILayout.Label(
statusText,
new GUIStyle(EditorStyles.label)
{
fontSize = StatusFontSize,
font = firaMono,
normal = { textColor = GrayColor },
hover = { textColor = GrayColor }
});
}
private void DrawClient(Texture2D icon, string label)
{
GUILayout.BeginHorizontal();
if (icon != null)
GUILayout.Box(icon, GUIStyle.none, GUILayout.Width(ClientIconSize), GUILayout.Height(ClientIconSize));
else
GUILayout.Space(ClientIconSize);
GUILayout.Space(ClientIconLabelGap);
GUILayout.Label(
label,
new GUIStyle(EditorStyles.label) { fontSize = ClientLabelFontSize, font = inter, alignment = TextAnchor.MiddleLeft, normal = { textColor = Color.white }, hover = { textColor = Color.white } },
GUILayout.Height(ClientLabelHeight));
GUILayout.EndHorizontal();
GUILayout.Space(ClientItemSpacing);
}
private void StartBridge()
{
try
{
UnityTcpBridge.Start();
isStarting = true;
startingTime = EditorApplication.timeSinceStartup;
currentState = BridgeState.Starting;
TcpLog.Info("Codely Bridge started via Control Window");
}
catch (System.Exception ex)
{
TcpLog.Error($"Failed to start Codely Bridge: {ex.Message}");
currentState = BridgeState.NotStarted;
isStarting = false;
}
}
private void StopBridge()
{
try
{
UnityTcpBridge.Stop(true);
TcpLog.Info("Codely Bridge manually stopped via Control Window");
currentState = BridgeState.NotStarted;
isStarting = false;
ResetConnections();
}
catch (System.Exception ex)
{
TcpLog.Error($"Failed to stop Codely Bridge: {ex.Message}");
}
}
private void ResetConnections()
{
connectedToCLI = connectedToVSCode = connectedToVisualStudio =
connectedToJetBrains = connectedToUnityEditor = connectedToTuanjie = false;
}
private void UpdateStatus()
{
bool isRunning = UnityTcpBridge.IsRunning;
if (isStarting && isRunning)
{
currentState = BridgeState.Started;
isStarting = false;
}
else if (isStarting && !isRunning)
{
if (EditorApplication.timeSinceStartup - startingTime > StartingTimeout)
{
currentState = BridgeState.NotStarted;
isStarting = false;
}
}
else if (!isStarting && isRunning && currentState != BridgeState.Started)
{
currentState = BridgeState.Started;
}
else if (!isRunning && currentState == BridgeState.Started)
{
currentState = BridgeState.NotStarted;
ResetConnections();
}
if (currentState == BridgeState.Started)
UpdateConnectionStatus();
}
private void UpdateConnectionStatus()
{
var clients = UnityTcpBridge.GetConnectedClients();
ResetConnections();
foreach (var platform in clients.Values)
{
var p = platform.ToLower();
if (p.Contains("cli"))
connectedToCLI = true;
else if (p.Contains("vscode") || p.Contains("vs code"))
connectedToVSCode = true;
else if (p.Contains("visualstudio") || p.Contains("visual studio"))
connectedToVisualStudio = true;
else if (p.Contains("jetbrains") || p.Contains("intellij") || p.Contains("rider"))
connectedToJetBrains = true;
else if (p.Contains("tuanjie") || p.Contains("codely") || p.Contains("agent"))
connectedToTuanjie = true;
else if (p.Contains("unity"))
connectedToUnityEditor = true;
}
}
private void OnInspectorUpdate() => Repaint();
}
}