Files
2026-03-09 17:50:20 +08:00

384 lines
15 KiB
C#

using System;
using System.IO;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityTcp.Editor.Helpers;
namespace UnityTcp.Editor.Tools
{
/// <summary>
/// [EXPERIMENTAL] Handles UI Toolkit operations (UXML, USS, PanelSettings).
/// </summary>
public static class ManageUIToolkit
{
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
try
{
switch (action)
{
case "ensure_panel_settings_asset":
return EnsurePanelSettingsAsset(@params);
case "link_uss_to_uxml":
return LinkUssToUxml(@params);
case "create_uxml":
return CreateUxml(@params);
case "create_uss":
return CreateUss(@params);
default:
return Response.Error(
$"Unknown action: '{action}'. Valid actions: ensure_panel_settings_asset, link_uss_to_uxml, create_uxml, create_uss."
);
}
}
catch (Exception e)
{
Debug.LogError($"[ManageUIToolkit] Action '{action}' failed: {e}");
return Response.Error($"[EXPERIMENTAL] UI Toolkit operation failed: {e.Message}");
}
}
private static object EnsurePanelSettingsAsset(JObject @params)
{
try
{
var writeCheck = WriteGuard.CheckWriteAllowed("ensure_panel_settings_asset");
if (writeCheck != null) return writeCheck;
string path = @params["path"]?.ToString();
if (string.IsNullOrEmpty(path))
return Response.Error("'path' parameter required.");
if (!path.EndsWith(".asset"))
path += ".asset";
// Check if already exists
var existingAsset = AssetDatabase.LoadAssetAtPath<PanelSettings>(path);
if (existingAsset != null)
{
return new
{
success = true,
message = "[EXPERIMENTAL] PanelSettings asset already exists.",
data = new { path = path, alreadyExists = true },
state_delta = StateComposer.CreateAssetDelta(new[] {
new { path = path, imported = false, hasMeta = true }
})
};
}
// Create directory if needed
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
// Create PanelSettings asset
var panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
AssetDatabase.CreateAsset(panelSettings, path);
AssetDatabase.SaveAssets();
StateComposer.IncrementRevision();
return new
{
success = true,
message = "[EXPERIMENTAL] PanelSettings asset created.",
data = new { path = path, alreadyExists = false },
state_delta = StateComposer.CreateAssetDelta(new[] {
new { path = path, imported = true, hasMeta = true }
})
};
}
catch (Exception e)
{
return Response.Error($"[EXPERIMENTAL] Failed to ensure PanelSettings asset: {e.Message}");
}
}
private static object LinkUssToUxml(JObject @params)
{
try
{
var writeCheck = WriteGuard.CheckWriteAllowed("link_uss_to_uxml");
if (writeCheck != null) return writeCheck;
// Support both parameter naming conventions: uxml/uss and uxml_path/uss_path,
// as well as GUID-based references via uxml_guid/uss_guid.
string uxmlShorthand = @params["uxml"]?.ToString();
string uxmlPathParam = @params["uxml_path"]?.ToString();
string uxmlGuidParam = @params["uxml_guid"]?.ToString();
string ussShorthand = @params["uss"]?.ToString();
string ussPathParam = @params["uss_path"]?.ToString();
string ussGuidParam = @params["uss_guid"]?.ToString();
bool hasUxmlIdentifier =
!string.IsNullOrEmpty(uxmlShorthand)
|| !string.IsNullOrEmpty(uxmlPathParam)
|| !string.IsNullOrEmpty(uxmlGuidParam);
bool hasUssIdentifier =
!string.IsNullOrEmpty(ussShorthand)
|| !string.IsNullOrEmpty(ussPathParam)
|| !string.IsNullOrEmpty(ussGuidParam);
if (!hasUxmlIdentifier)
return Response.Error("Either 'uxml', 'uxml_path', or 'uxml_guid' parameter is required.");
if (!hasUssIdentifier)
return Response.Error("Either 'uss', 'uss_path', or 'uss_guid' parameter is required.");
string uxmlGuidUsed;
string ussGuidUsed;
string uxmlPath = ResolveAssetPath(uxmlShorthand, uxmlPathParam, uxmlGuidParam, out uxmlGuidUsed);
string ussPath = ResolveAssetPath(ussShorthand, ussPathParam, ussGuidParam, out ussGuidUsed);
if (string.IsNullOrEmpty(uxmlPath))
{
if (!string.IsNullOrEmpty(uxmlGuidUsed))
{
return Response.Error($"UXML asset not found for GUID: {uxmlGuidUsed}");
}
return Response.Error("UXML path could not be resolved.");
}
if (string.IsNullOrEmpty(ussPath))
{
if (!string.IsNullOrEmpty(ussGuidUsed))
{
return Response.Error($"USS asset not found for GUID: {ussGuidUsed}");
}
return Response.Error("USS path could not be resolved.");
}
var uxmlAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
if (uxmlAsset == null)
return Response.Error($"UXML not found at: {uxmlPath}");
var ussAsset = AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
if (ussAsset == null)
return Response.Error($"USS not found at: {ussPath}");
// Read UXML file content
string uxmlContent = File.ReadAllText(uxmlPath);
// Check if USS is already linked
string ussFileName = Path.GetFileName(ussPath);
if (uxmlContent.Contains($"src=\"{ussFileName}\""))
{
return new
{
success = true,
message = "[EXPERIMENTAL] USS already linked to UXML.",
data = new { uxml = uxmlPath, uss = ussPath, alreadyLinked = true },
state_delta = StateComposer.CreateAssetDelta(new[] {
new { path = uxmlPath, imported = false, hasMeta = true }
})
};
}
// Add USS reference to UXML
// Insert after <ui:UXML> tag
int insertPos = uxmlContent.IndexOf("<ui:UXML");
if (insertPos >= 0)
{
insertPos = uxmlContent.IndexOf('>', insertPos) + 1;
string styleTag = $"\n <Style src=\"{ussFileName}\" />";
uxmlContent = uxmlContent.Insert(insertPos, styleTag);
File.WriteAllText(uxmlPath, uxmlContent);
AssetDatabase.ImportAsset(uxmlPath);
StateComposer.IncrementRevision();
return new
{
success = true,
message = "[EXPERIMENTAL] USS linked to UXML.",
data = new { uxml = uxmlPath, uss = ussPath, alreadyLinked = false },
state_delta = StateComposer.CreateAssetDelta(new[] {
new { path = uxmlPath, imported = true, hasMeta = true }
})
};
}
else
{
return Response.Error("Failed to parse UXML file.");
}
}
catch (Exception e)
{
return Response.Error($"[EXPERIMENTAL] Failed to link USS to UXML: {e.Message}");
}
}
/// <summary>
/// Resolves an asset path from either a shorthand (path or GUID), an explicit path, or a GUID.
/// - If explicit path is provided, it is returned as-is.
/// - If shorthand contains '/', it is treated as a path.
/// - Otherwise shorthand / guid are treated as GUIDs and resolved via AssetDatabase.GUIDToAssetPath.
/// Returns null if the asset path cannot be resolved.
/// </summary>
private static string ResolveAssetPath(string shorthand, string pathParam, string guidParam, out string guidUsed)
{
guidUsed = null;
// Prefer explicit path if provided
if (!string.IsNullOrEmpty(pathParam))
{
return pathParam;
}
// Handle shorthand: path or GUID
if (!string.IsNullOrEmpty(shorthand))
{
if (shorthand.Contains("/"))
{
// Looks like a path (ValidateToolParams already enforced "Assets/" prefix when appropriate)
return shorthand;
}
// Treat shorthand as GUID
guidUsed = shorthand;
string resolvedFromShorthand = AssetDatabase.GUIDToAssetPath(shorthand);
if (!string.IsNullOrEmpty(resolvedFromShorthand))
{
return resolvedFromShorthand;
}
// Fall through and let guidParam try (could be different)
}
if (!string.IsNullOrEmpty(guidParam))
{
guidUsed = guidParam;
string resolvedFromGuid = AssetDatabase.GUIDToAssetPath(guidParam);
if (!string.IsNullOrEmpty(resolvedFromGuid))
{
return resolvedFromGuid;
}
}
return null;
}
private static object CreateUxml(JObject @params)
{
try
{
var writeCheck = WriteGuard.CheckWriteAllowed("create_uxml");
if (writeCheck != null) return writeCheck;
string path = @params["path"]?.ToString();
if (string.IsNullOrEmpty(path))
return Response.Error("'path' parameter required.");
if (!path.EndsWith(".uxml"))
path += ".uxml";
if (File.Exists(path))
return Response.Error($"UXML file already exists at: {path}");
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
string template = @"<?xml version=""1.0"" encoding=""utf-8""?>
<ui:UXML
xmlns:ui=""UnityEngine.UIElements""
xmlns:uie=""UnityEditor.UIElements"">
<ui:VisualElement name=""root"" style=""flex-grow: 1;"">
<ui:Label text=""Hello UI Toolkit"" name=""title-label"" />
</ui:VisualElement>
</ui:UXML>";
File.WriteAllText(path, template);
AssetDatabase.ImportAsset(path);
StateComposer.IncrementRevision();
return new
{
success = true,
message = "[EXPERIMENTAL] UXML file created.",
data = new { path = path },
state_delta = StateComposer.CreateAssetDelta(new[] {
new { path = path, imported = true, hasMeta = true }
})
};
}
catch (Exception e)
{
return Response.Error($"[EXPERIMENTAL] Failed to create UXML: {e.Message}");
}
}
private static object CreateUss(JObject @params)
{
try
{
var writeCheck = WriteGuard.CheckWriteAllowed("create_uss");
if (writeCheck != null) return writeCheck;
string path = @params["path"]?.ToString();
if (string.IsNullOrEmpty(path))
return Response.Error("'path' parameter required.");
if (!path.EndsWith(".uss"))
path += ".uss";
if (File.Exists(path))
return Response.Error($"USS file already exists at: {path}");
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
string template = @"/* UI Toolkit Style Sheet */
.root {
flex-grow: 1;
background-color: rgb(56, 56, 56);
}
#title-label {
font-size: 20px;
-unity-font-style: bold;
color: rgb(255, 255, 255);
padding: 10px;
}
";
File.WriteAllText(path, template);
AssetDatabase.ImportAsset(path);
StateComposer.IncrementRevision();
return new
{
success = true,
message = "[EXPERIMENTAL] USS file created.",
data = new { path = path },
state_delta = StateComposer.CreateAssetDelta(new[] {
new { path = path, imported = true, hasMeta = true }
})
};
}
catch (Exception e)
{
return Response.Error($"[EXPERIMENTAL] Failed to create USS: {e.Message}");
}
}
}
}