804 lines
31 KiB
C#
804 lines
31 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using MCPForUnity.Editor.Clients;
|
||
using MCPForUnity.Editor.Constants;
|
||
using MCPForUnity.Editor.Helpers;
|
||
using MCPForUnity.Editor.Models;
|
||
using MCPForUnity.Editor.Services;
|
||
using MCPForUnity.Editor.Services.Transport;
|
||
using MCPForUnity.Editor.Tools;
|
||
using UnityEditor;
|
||
using UnityEditor.UIElements;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace MCPForUnity.Editor.Windows.Components.Tools
|
||
{
|
||
/// <summary>
|
||
/// Controller for the Tools section inside the MCP for Unity editor window.
|
||
/// Provides discovery, filtering, and per-tool enablement toggles.
|
||
/// Tools are grouped by their Group property (core first, then alphabetical).
|
||
/// </summary>
|
||
public class McpToolsSection
|
||
{
|
||
private readonly Dictionary<string, Toggle> toolToggleMap = new();
|
||
private Toggle projectScopedToolsToggle;
|
||
private Label summaryLabel;
|
||
private Label noteLabel;
|
||
private Button enableAllButton;
|
||
private Button disableAllButton;
|
||
private Button rescanButton;
|
||
private Button reconfigureButton;
|
||
private VisualElement categoryContainer;
|
||
private List<ToolMetadata> allTools = new();
|
||
private readonly Dictionary<string, Toggle> groupToggleMap = new();
|
||
private readonly List<(Foldout foldout, string title, List<ToolMetadata> tools)> foldoutEntries = new();
|
||
|
||
/// <summary>Human-friendly names for tool groups shown in the UI.</summary>
|
||
private static readonly Dictionary<string, string> GroupDisplayNames = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
{ "core", "Core Tools" },
|
||
{ "vfx", "VFX & Shaders" },
|
||
{ "animation", "Animation" },
|
||
{ "ui", "UI Toolkit" },
|
||
{ "scripting_ext", "Scripting Extensions" },
|
||
{ "testing", "Testing" },
|
||
{ "probuilder", "ProBuilder — Experimental" },
|
||
{ "profiling", "Profiling & Frame Debugger" },
|
||
};
|
||
|
||
public VisualElement Root { get; }
|
||
|
||
public McpToolsSection(VisualElement root)
|
||
{
|
||
Root = root;
|
||
CacheUIElements();
|
||
RegisterCallbacks();
|
||
}
|
||
|
||
private void CacheUIElements()
|
||
{
|
||
projectScopedToolsToggle = Root.Q<Toggle>("project-scoped-tools-toggle");
|
||
summaryLabel = Root.Q<Label>("tools-summary");
|
||
noteLabel = Root.Q<Label>("tools-note");
|
||
enableAllButton = Root.Q<Button>("enable-all-button");
|
||
disableAllButton = Root.Q<Button>("disable-all-button");
|
||
rescanButton = Root.Q<Button>("rescan-button");
|
||
reconfigureButton = Root.Q<Button>("reconfigure-button");
|
||
categoryContainer = Root.Q<VisualElement>("tool-category-container");
|
||
}
|
||
|
||
private void RegisterCallbacks()
|
||
{
|
||
if (projectScopedToolsToggle != null)
|
||
{
|
||
projectScopedToolsToggle.value = EditorPrefs.GetBool(
|
||
EditorPrefKeys.ProjectScopedToolsLocalHttp,
|
||
false
|
||
);
|
||
projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local and stdio transports. Allows per-project tool customization.";
|
||
projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
|
||
{
|
||
EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);
|
||
});
|
||
}
|
||
|
||
if (enableAllButton != null)
|
||
{
|
||
enableAllButton.AddToClassList("tool-action-button");
|
||
enableAllButton.style.marginRight = 4;
|
||
enableAllButton.clicked += () => SetAllToolsState(true);
|
||
}
|
||
|
||
if (disableAllButton != null)
|
||
{
|
||
disableAllButton.AddToClassList("tool-action-button");
|
||
disableAllButton.style.marginRight = 4;
|
||
disableAllButton.clicked += () => SetAllToolsState(false);
|
||
}
|
||
|
||
if (rescanButton != null)
|
||
{
|
||
rescanButton.AddToClassList("tool-action-button");
|
||
rescanButton.clicked += () =>
|
||
{
|
||
McpLog.Info("Rescanning MCP tools from the editor window.");
|
||
MCPServiceLocator.ToolDiscovery.InvalidateCache();
|
||
Refresh();
|
||
};
|
||
}
|
||
|
||
if (reconfigureButton != null)
|
||
{
|
||
reconfigureButton.AddToClassList("tool-action-button");
|
||
reconfigureButton.clicked += OnReconfigureClientsClicked;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Rebuilds the tool list and synchronises toggle states.
|
||
/// Tools are displayed in group-based foldouts: core first, then other
|
||
/// groups alphabetically. Custom (non-built-in) tools appear in a
|
||
/// separate "Custom Tools" foldout at the bottom.
|
||
/// </summary>
|
||
public void Refresh()
|
||
{
|
||
toolToggleMap.Clear();
|
||
groupToggleMap.Clear();
|
||
foldoutEntries.Clear();
|
||
categoryContainer?.Clear();
|
||
|
||
var service = MCPServiceLocator.ToolDiscovery;
|
||
allTools = service.DiscoverAllTools()
|
||
.OrderBy(tool => tool.Name, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
bool hasTools = allTools.Count > 0;
|
||
enableAllButton?.SetEnabled(hasTools);
|
||
disableAllButton?.SetEnabled(hasTools);
|
||
|
||
if (noteLabel != null)
|
||
{
|
||
noteLabel.style.display = hasTools ? DisplayStyle.Flex : DisplayStyle.None;
|
||
if (hasTools)
|
||
{
|
||
bool isHttp = EditorConfigurationCache.Instance.UseHttpTransport;
|
||
noteLabel.text = isHttp
|
||
? "Changes apply after reconnecting or re-registering tools."
|
||
: "Stdio mode: toggles sync at startup. After changing toggles, ask the AI to run manage_tools with action 'sync' to refresh.";
|
||
}
|
||
}
|
||
|
||
if (!hasTools)
|
||
{
|
||
AddInfoLabel("No MCP tools found. Add classes decorated with [McpForUnityTool] to expose tools.");
|
||
UpdateSummary();
|
||
return;
|
||
}
|
||
|
||
// Partition into built-in and custom
|
||
var builtInTools = allTools.Where(IsBuiltIn).ToList();
|
||
var customTools = allTools.Where(tool => !IsBuiltIn(tool)).ToList();
|
||
|
||
// Group built-in tools by their Group property
|
||
var grouped = builtInTools
|
||
.GroupBy(t => t.Group ?? "core")
|
||
.ToDictionary(g => g.Key, g => g.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase).ToList());
|
||
|
||
// Render "core" first, then remaining groups alphabetically
|
||
if (grouped.TryGetValue("core", out var coreTools))
|
||
{
|
||
BuildCategory(GetGroupDisplayName("core"), "group-core", coreTools);
|
||
grouped.Remove("core");
|
||
}
|
||
|
||
foreach (var kvp in grouped.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
|
||
{
|
||
BuildCategory(GetGroupDisplayName(kvp.Key), $"group-{kvp.Key}", kvp.Value);
|
||
}
|
||
|
||
// Custom tools at the bottom
|
||
if (customTools.Count > 0)
|
||
{
|
||
BuildCategory("Custom Tools", "custom", customTools);
|
||
}
|
||
|
||
UpdateSummary();
|
||
}
|
||
|
||
private static string GetGroupDisplayName(string group)
|
||
{
|
||
if (GroupDisplayNames.TryGetValue(group, out var displayName))
|
||
return displayName;
|
||
// Fallback: capitalize first letter
|
||
return string.IsNullOrEmpty(group)
|
||
? "Other"
|
||
: char.ToUpper(group[0]) + group.Substring(1);
|
||
}
|
||
|
||
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ToolMetadata> tools)
|
||
{
|
||
var toolList = tools.ToList();
|
||
if (toolList.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool isExperimental = string.Equals(prefsSuffix, "group-probuilder", StringComparison.OrdinalIgnoreCase);
|
||
|
||
int enabledCount = toolList.Count(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||
|
||
// Default foldout state: core is open, others collapsed
|
||
bool defaultOpen = prefsSuffix == "group-core";
|
||
var foldout = new Foldout
|
||
{
|
||
text = $"{title} ({enabledCount}/{toolList.Count})",
|
||
value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, defaultOpen)
|
||
};
|
||
|
||
foldout.RegisterValueChangedCallback(evt =>
|
||
{
|
||
if (evt.target != foldout) return;
|
||
EditorPrefs.SetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, evt.newValue);
|
||
});
|
||
|
||
// Add a checkbox into the foldout header to toggle all tools in this group
|
||
bool allEnabled = enabledCount == toolList.Count;
|
||
var groupCheckbox = new Toggle { value = allEnabled };
|
||
groupCheckbox.AddToClassList("group-header-checkbox");
|
||
groupCheckbox.tooltip = $"Toggle all tools in \"{title}\" on or off.";
|
||
|
||
// Prevent the click from propagating to the foldout expand/collapse toggle
|
||
groupCheckbox.RegisterCallback<ClickEvent>(evt => evt.StopPropagation());
|
||
groupCheckbox.RegisterValueChangedCallback(evt =>
|
||
{
|
||
evt.StopPropagation();
|
||
SetGroupToolsState(toolList, evt.newValue, foldout, title);
|
||
});
|
||
|
||
// Insert the checkbox into the foldout's own header toggle element
|
||
foldout.Q<Toggle>()?.Add(groupCheckbox);
|
||
groupToggleMap[prefsSuffix] = groupCheckbox;
|
||
|
||
foreach (var tool in toolList)
|
||
{
|
||
foldout.Add(CreateToolRow(tool));
|
||
}
|
||
|
||
if (isExperimental)
|
||
{
|
||
var warning = new HelpBox(
|
||
"ProBuilder support is experimental. Mesh editing operations may produce " +
|
||
"unexpected results on complex topologies. Always save your scene before " +
|
||
"performing destructive operations.",
|
||
HelpBoxMessageType.Warning);
|
||
warning.style.marginTop = 4;
|
||
warning.style.marginBottom = 2;
|
||
foldout.Insert(0, warning);
|
||
}
|
||
|
||
foldoutEntries.Add((foldout, title, toolList));
|
||
categoryContainer?.Add(foldout);
|
||
}
|
||
|
||
private VisualElement CreateToolRow(ToolMetadata tool)
|
||
{
|
||
var row = new VisualElement();
|
||
row.AddToClassList("tool-item");
|
||
|
||
var header = new VisualElement();
|
||
header.AddToClassList("tool-item-header");
|
||
|
||
var toggle = new Toggle(tool.Name)
|
||
{
|
||
value = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name)
|
||
};
|
||
toggle.AddToClassList("tool-item-toggle");
|
||
toggle.tooltip = string.IsNullOrWhiteSpace(tool.Description) ? tool.Name : tool.Description;
|
||
|
||
toggle.RegisterValueChangedCallback(evt =>
|
||
{
|
||
HandleToggleChange(tool, evt.newValue);
|
||
});
|
||
|
||
toolToggleMap[tool.Name] = toggle;
|
||
header.Add(toggle);
|
||
|
||
var tagsContainer = new VisualElement();
|
||
tagsContainer.AddToClassList("tool-tags");
|
||
|
||
bool defaultEnabled = tool.AutoRegister || tool.IsBuiltIn;
|
||
tagsContainer.Add(CreateTag(defaultEnabled ? "On by default" : "Off by default"));
|
||
|
||
tagsContainer.Add(CreateTag(tool.StructuredOutput ? "Structured output" : "Free-form"));
|
||
|
||
if (tool.RequiresPolling)
|
||
{
|
||
tagsContainer.Add(CreateTag($"Polling: {tool.PollAction}"));
|
||
}
|
||
|
||
header.Add(tagsContainer);
|
||
row.Add(header);
|
||
|
||
// Skip auto-generated placeholder descriptions like "Tool: find_gameobjects"
|
||
if (!string.IsNullOrWhiteSpace(tool.Description)
|
||
&& !tool.Description.StartsWith("Tool: ", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var description = new Label(tool.Description);
|
||
description.AddToClassList("tool-item-description");
|
||
row.Add(description);
|
||
}
|
||
|
||
if (tool.Parameters != null && tool.Parameters.Count > 0)
|
||
{
|
||
var paramSummary = string.Join(", ", tool.Parameters.Select(p =>
|
||
$"{p.Name}{(p.Required ? string.Empty : " (optional)")}: {p.Type}"));
|
||
|
||
var parametersLabel = new Label(paramSummary);
|
||
parametersLabel.AddToClassList("tool-parameters");
|
||
row.Add(parametersLabel);
|
||
}
|
||
|
||
if (IsManageCameraTool(tool))
|
||
{
|
||
row.Add(CreateManageSceneActions());
|
||
}
|
||
|
||
if (IsBatchExecuteTool(tool))
|
||
{
|
||
row.Add(CreateBatchExecuteSettings());
|
||
}
|
||
|
||
return row;
|
||
}
|
||
|
||
private void HandleToggleChange(
|
||
ToolMetadata tool,
|
||
bool enabled,
|
||
bool updateSummary = true,
|
||
bool reregisterTools = true)
|
||
{
|
||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||
|
||
if (updateSummary)
|
||
{
|
||
UpdateSummary();
|
||
UpdateFoldoutHeaders();
|
||
SyncGroupToggles();
|
||
}
|
||
|
||
if (reregisterTools)
|
||
{
|
||
// Trigger tool reregistration with connected MCP server
|
||
ReregisterToolsAsync();
|
||
}
|
||
}
|
||
|
||
private void ReregisterToolsAsync()
|
||
{
|
||
// Fire and forget - don't block UI thread
|
||
var transportManager = MCPServiceLocator.TransportManager;
|
||
var client = transportManager.GetClient(TransportMode.Http);
|
||
if (client == null || !client.IsConnected)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_ = Task.Run(async () =>
|
||
{
|
||
try
|
||
{
|
||
await client.ReregisterToolsAsync().ConfigureAwait(false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
McpLog.Warn($"Failed to reregister tools: {ex}");
|
||
}
|
||
});
|
||
}
|
||
|
||
private void SetAllToolsState(bool enabled)
|
||
{
|
||
bool hasChanges = false;
|
||
|
||
foreach (var tool in allTools)
|
||
{
|
||
if (!toolToggleMap.TryGetValue(tool.Name, out var toggle))
|
||
{
|
||
bool currentEnabled = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name);
|
||
if (currentEnabled != enabled)
|
||
{
|
||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||
hasChanges = true;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (toggle.value == enabled)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
toggle.SetValueWithoutNotify(enabled);
|
||
HandleToggleChange(tool, enabled, updateSummary: false, reregisterTools: false);
|
||
hasChanges = true;
|
||
}
|
||
|
||
UpdateSummary();
|
||
UpdateFoldoutHeaders();
|
||
SyncGroupToggles();
|
||
|
||
if (hasChanges)
|
||
{
|
||
// Trigger a single reregistration after bulk change
|
||
ReregisterToolsAsync();
|
||
}
|
||
}
|
||
|
||
private void SetGroupToolsState(List<ToolMetadata> groupTools, bool enabled, Foldout foldout, string title)
|
||
{
|
||
bool hasChanges = false;
|
||
|
||
foreach (var tool in groupTools)
|
||
{
|
||
if (toolToggleMap.TryGetValue(tool.Name, out var toggle))
|
||
{
|
||
if (toggle.value != enabled)
|
||
{
|
||
toggle.SetValueWithoutNotify(enabled);
|
||
HandleToggleChange(tool, enabled, updateSummary: false, reregisterTools: false);
|
||
hasChanges = true;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
bool currentEnabled = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name);
|
||
if (currentEnabled != enabled)
|
||
{
|
||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||
hasChanges = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update the foldout header count
|
||
int enabledCount = groupTools.Count(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||
foldout.text = $"{title} ({enabledCount}/{groupTools.Count})";
|
||
|
||
// Sync global group toggles after group change
|
||
SyncGroupToggles();
|
||
|
||
UpdateSummary();
|
||
|
||
if (hasChanges)
|
||
{
|
||
ReregisterToolsAsync();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Synchronises group toggle checkmarks with actual tool states.
|
||
/// Called after individual tool toggles change so the group toggle
|
||
/// stays accurate.
|
||
/// </summary>
|
||
private void SyncGroupToggles()
|
||
{
|
||
// We need the grouped tool lists to check states.
|
||
var builtInTools = allTools.Where(IsBuiltIn).ToList();
|
||
var grouped = builtInTools
|
||
.GroupBy(t => t.Group ?? "core")
|
||
.ToDictionary(g => g.Key, g => g.ToList());
|
||
var customTools = allTools.Where(t => !IsBuiltIn(t)).ToList();
|
||
|
||
foreach (var kvp in groupToggleMap)
|
||
{
|
||
List<ToolMetadata> groupTools;
|
||
if (kvp.Key == "custom")
|
||
{
|
||
groupTools = customTools;
|
||
}
|
||
else
|
||
{
|
||
string groupKey = kvp.Key.StartsWith("group-") ? kvp.Key.Substring(6) : kvp.Key;
|
||
if (!grouped.TryGetValue(groupKey, out groupTools))
|
||
continue;
|
||
}
|
||
|
||
bool allEnabled = groupTools.All(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||
kvp.Value.SetValueWithoutNotify(allEnabled);
|
||
}
|
||
}
|
||
|
||
private void OnReconfigureClientsClicked()
|
||
{
|
||
try
|
||
{
|
||
// Re-register tools with the server (HTTP mode)
|
||
ReregisterToolsAsync();
|
||
|
||
// Reconfigure all already-configured clients.
|
||
// For CLI-based clients Configure() is a toggle (unregister if
|
||
// configured, register if not), so we call it twice: first to
|
||
// unregister, then to re-register with the updated tool set.
|
||
var clients = MCPServiceLocator.Client.GetAllClients();
|
||
int success = 0;
|
||
int skipped = 0;
|
||
var messages = new List<string>();
|
||
|
||
foreach (var client in clients)
|
||
{
|
||
try
|
||
{
|
||
client.CheckStatus(attemptAutoRewrite: false);
|
||
|
||
if (client.Status != McpStatus.Configured)
|
||
{
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
if (client is ClaudeCliMcpConfigurator)
|
||
{
|
||
// Toggle off (unregister), then toggle on (register)
|
||
MCPServiceLocator.Client.ConfigureClient(client);
|
||
MCPServiceLocator.Client.ConfigureClient(client);
|
||
}
|
||
else
|
||
{
|
||
// JSON-file clients: rewrite is idempotent
|
||
MCPServiceLocator.Client.ConfigureClient(client);
|
||
}
|
||
|
||
success++;
|
||
messages.Add($"✓ {client.DisplayName}: Reconfigured");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
messages.Add($"⚠ {client.DisplayName}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
string header = $"Reconfigured {success} client(s), skipped {skipped}.";
|
||
string body = messages.Count > 0
|
||
? header + "\n\n" + string.Join("\n", messages)
|
||
: header;
|
||
|
||
EditorUtility.DisplayDialog("Reconfigure Clients", body, "OK");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
EditorUtility.DisplayDialog("Reconfigure Failed", ex.Message, "OK");
|
||
McpLog.Error($"Reconfigure failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void UpdateSummary()
|
||
{
|
||
if (summaryLabel == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (allTools.Count == 0)
|
||
{
|
||
summaryLabel.text = "No MCP tools discovered.";
|
||
return;
|
||
}
|
||
|
||
int enabledCount = allTools.Count(tool => MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name));
|
||
summaryLabel.text = $"{enabledCount} of {allTools.Count} tools will register with connected clients.";
|
||
}
|
||
|
||
private void UpdateFoldoutHeaders()
|
||
{
|
||
foreach (var (foldout, title, tools) in foldoutEntries)
|
||
{
|
||
int enabledCount = tools.Count(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||
foldout.text = $"{title} ({enabledCount}/{tools.Count})";
|
||
}
|
||
}
|
||
|
||
private void AddInfoLabel(string message)
|
||
{
|
||
var label = new Label(message);
|
||
label.AddToClassList("help-text");
|
||
categoryContainer?.Add(label);
|
||
}
|
||
|
||
private VisualElement CreateManageSceneActions()
|
||
{
|
||
var actions = new VisualElement();
|
||
actions.AddToClassList("tool-item-actions");
|
||
|
||
var gameViewButton = new Button(OnManageSceneScreenshotClicked)
|
||
{
|
||
text = "Game View"
|
||
};
|
||
gameViewButton.AddToClassList("tool-action-button");
|
||
gameViewButton.style.marginTop = 4;
|
||
gameViewButton.tooltip = "Capture a game camera screenshot. Default: Assets/Screenshots (configurable in Advanced).";
|
||
|
||
var sceneViewButton = new Button(OnSceneViewScreenshotClicked)
|
||
{
|
||
text = "Scene View"
|
||
};
|
||
sceneViewButton.AddToClassList("tool-action-button");
|
||
sceneViewButton.style.marginTop = 4;
|
||
sceneViewButton.style.marginLeft = 4;
|
||
sceneViewButton.tooltip = "Capture the active Scene View viewport. Default: Assets/Screenshots (configurable in Advanced).";
|
||
|
||
var multiviewButton = new Button(OnManageSceneMultiviewClicked)
|
||
{
|
||
text = "Multiview"
|
||
};
|
||
multiviewButton.AddToClassList("tool-action-button");
|
||
multiviewButton.style.marginTop = 4;
|
||
multiviewButton.style.marginLeft = 4;
|
||
multiviewButton.tooltip = "Capture a 6-angle contact sheet around the scene centre. Default: Assets/Screenshots (configurable in Advanced).";
|
||
|
||
var captureLabel = new Label("Capture:");
|
||
captureLabel.style.marginTop = 6;
|
||
captureLabel.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Normal;
|
||
|
||
var row = new VisualElement();
|
||
row.style.flexDirection = FlexDirection.Row;
|
||
row.style.flexWrap = Wrap.Wrap;
|
||
row.Add(gameViewButton);
|
||
row.Add(sceneViewButton);
|
||
row.Add(multiviewButton);
|
||
|
||
actions.Add(captureLabel);
|
||
actions.Add(row);
|
||
return actions;
|
||
}
|
||
|
||
private VisualElement CreateBatchExecuteSettings()
|
||
{
|
||
var container = new VisualElement();
|
||
container.AddToClassList("tool-item-actions");
|
||
container.style.flexDirection = FlexDirection.Row;
|
||
container.style.alignItems = Align.Center;
|
||
container.style.marginTop = 4;
|
||
|
||
var label = new Label("Max commands per batch:");
|
||
label.style.marginRight = 8;
|
||
label.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Normal;
|
||
container.Add(label);
|
||
|
||
int currentValue = EditorPrefs.GetInt(
|
||
EditorPrefKeys.BatchExecuteMaxCommands,
|
||
BatchExecute.DefaultMaxCommandsPerBatch
|
||
);
|
||
|
||
var field = new IntegerField
|
||
{
|
||
value = Math.Clamp(currentValue, 1, BatchExecute.AbsoluteMaxCommandsPerBatch),
|
||
style = { width = 60 }
|
||
};
|
||
field.tooltip = $"Number of commands allowed per batch_execute call (1–{BatchExecute.AbsoluteMaxCommandsPerBatch}). Default: {BatchExecute.DefaultMaxCommandsPerBatch}.";
|
||
|
||
field.RegisterValueChangedCallback(evt =>
|
||
{
|
||
int clamped = Math.Clamp(evt.newValue, 1, BatchExecute.AbsoluteMaxCommandsPerBatch);
|
||
if (clamped != evt.newValue)
|
||
{
|
||
field.SetValueWithoutNotify(clamped);
|
||
}
|
||
EditorPrefs.SetInt(EditorPrefKeys.BatchExecuteMaxCommands, clamped);
|
||
});
|
||
|
||
container.Add(field);
|
||
|
||
var hint = new Label($"(max {BatchExecute.AbsoluteMaxCommandsPerBatch})");
|
||
hint.style.marginLeft = 4;
|
||
hint.style.color = new UnityEngine.Color(0.5f, 0.5f, 0.5f);
|
||
hint.style.fontSize = 10;
|
||
container.Add(hint);
|
||
|
||
return container;
|
||
}
|
||
|
||
private void OnManageSceneScreenshotClicked()
|
||
{
|
||
try
|
||
{
|
||
var response = ManageScene.ExecuteScreenshot();
|
||
if (response is SuccessResponse success && !string.IsNullOrWhiteSpace(success.Message))
|
||
{
|
||
McpLog.Info(success.Message);
|
||
}
|
||
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
|
||
{
|
||
McpLog.Error(error.Error);
|
||
}
|
||
else
|
||
{
|
||
McpLog.Info("Screenshot capture requested.");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
McpLog.Error($"Failed to capture screenshot: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void OnSceneViewScreenshotClicked()
|
||
{
|
||
try
|
||
{
|
||
var response = ManageScene.ExecuteSceneViewScreenshot();
|
||
if (response is SuccessResponse success && !string.IsNullOrWhiteSpace(success.Message))
|
||
{
|
||
McpLog.Info(success.Message);
|
||
}
|
||
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
|
||
{
|
||
McpLog.Error(error.Error);
|
||
}
|
||
else
|
||
{
|
||
McpLog.Info("Scene View screenshot capture requested.");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
McpLog.Error($"Failed to capture Scene View screenshot: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void OnManageSceneMultiviewClicked()
|
||
{
|
||
try
|
||
{
|
||
var response = ManageScene.ExecuteMultiviewScreenshot();
|
||
if (response is SuccessResponse success)
|
||
{
|
||
// The data object is an anonymous type with imageBase64 — serialize to extract it
|
||
var json = Newtonsoft.Json.Linq.JObject.FromObject(success.Data);
|
||
string base64 = json["imageBase64"]?.ToString();
|
||
if (!string.IsNullOrEmpty(base64))
|
||
{
|
||
string folderSpec = MCPForUnity.Editor.Helpers.ScreenshotPreferences.Resolve(null);
|
||
string folder;
|
||
try
|
||
{
|
||
folder = MCPForUnity.Runtime.Helpers.ScreenshotUtility.ResolveFolderAbsolute(folderSpec);
|
||
}
|
||
catch (System.InvalidOperationException ex)
|
||
{
|
||
McpLog.Error(ex.Message);
|
||
return;
|
||
}
|
||
if (!System.IO.Directory.Exists(folder))
|
||
System.IO.Directory.CreateDirectory(folder);
|
||
|
||
string fileName = $"Multiview_{System.DateTime.Now:yyyyMMdd_HHmmss}.png";
|
||
string filePath = System.IO.Path.Combine(folder, fileName);
|
||
System.IO.File.WriteAllBytes(filePath, Convert.FromBase64String(base64));
|
||
|
||
// Import only when the file landed under Assets/.
|
||
string projectRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(UnityEngine.Application.dataPath, "..")).Replace('\\', '/');
|
||
string normalizedFile = filePath.Replace('\\', '/');
|
||
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
|
||
string projectRelative = normalizedFile.StartsWith(normalizedRoot, System.StringComparison.OrdinalIgnoreCase)
|
||
? normalizedFile.Substring(normalizedRoot.Length)
|
||
: normalizedFile;
|
||
if (MCPForUnity.Runtime.Helpers.ScreenshotUtility.IsUnderAssets(projectRelative))
|
||
AssetDatabase.ImportAsset(projectRelative, ImportAssetOptions.ForceSynchronousImport);
|
||
|
||
McpLog.Info($"Multiview contact sheet saved to {projectRelative}");
|
||
}
|
||
else
|
||
{
|
||
McpLog.Info(success.Message ?? "Multiview capture completed.");
|
||
}
|
||
}
|
||
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
|
||
{
|
||
McpLog.Error(error.Error);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
McpLog.Error($"Failed to capture multiview: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private static Label CreateTag(string text)
|
||
{
|
||
var tag = new Label(text);
|
||
tag.AddToClassList("tool-tag");
|
||
return tag;
|
||
}
|
||
|
||
private static bool IsManageSceneTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_scene", StringComparison.OrdinalIgnoreCase);
|
||
|
||
private static bool IsManageCameraTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_camera", StringComparison.OrdinalIgnoreCase);
|
||
|
||
private static bool IsBatchExecuteTool(ToolMetadata tool) => string.Equals(tool?.Name, "batch_execute", StringComparison.OrdinalIgnoreCase);
|
||
|
||
private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false;
|
||
}
|
||
}
|