681 lines
29 KiB
C#
681 lines
29 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using MCPForUnity.Editor.Constants;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using MCPForUnity.Editor.Services;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace MCPForUnity.Editor.Windows.Components.Advanced
|
|
{
|
|
/// <summary>
|
|
/// Controller for the Advanced Settings section.
|
|
/// Handles path overrides, server source configuration, dev mode, and package deployment.
|
|
/// </summary>
|
|
public class McpAdvancedSection
|
|
{
|
|
// UI Elements
|
|
private TextField uvxPathOverride;
|
|
private Button browseUvxButton;
|
|
private Button clearUvxButton;
|
|
private VisualElement uvxPathStatus;
|
|
private TextField gitUrlOverride;
|
|
private Button browseGitUrlButton;
|
|
private Button clearGitUrlButton;
|
|
private Toggle autoStartOnLoadToggle;
|
|
private Toggle debugLogsToggle;
|
|
private Toggle logRecordToggle;
|
|
private Toggle devModeForceRefreshToggle;
|
|
private Toggle allowLanHttpBindToggle;
|
|
private Toggle allowInsecureRemoteHttpToggle;
|
|
private TextField screenshotsFolderOverride;
|
|
private Button browseScreenshotsFolderButton;
|
|
private Button clearScreenshotsFolderButton;
|
|
private TextField deploySourcePath;
|
|
private Button browseDeploySourceButton;
|
|
private Button clearDeploySourceButton;
|
|
private Button deployButton;
|
|
private Button deployRestoreButton;
|
|
private Label deployTargetLabel;
|
|
private Label deployBackupLabel;
|
|
private Label deployStatusLabel;
|
|
private VisualElement healthIndicator;
|
|
private Label healthStatus;
|
|
private Button testConnectionButton;
|
|
|
|
// Events
|
|
public event Action OnGitUrlChanged;
|
|
public event Action OnHttpServerCommandUpdateRequested;
|
|
public event Action OnTestConnectionRequested;
|
|
public event Action OnPackageDeployed;
|
|
|
|
public VisualElement Root { get; private set; }
|
|
|
|
public McpAdvancedSection(VisualElement root)
|
|
{
|
|
Root = root;
|
|
CacheUIElements();
|
|
InitializeUI();
|
|
RegisterCallbacks();
|
|
}
|
|
|
|
private void CacheUIElements()
|
|
{
|
|
uvxPathOverride = Root.Q<TextField>("uv-path-override");
|
|
browseUvxButton = Root.Q<Button>("browse-uv-button");
|
|
clearUvxButton = Root.Q<Button>("clear-uv-button");
|
|
uvxPathStatus = Root.Q<VisualElement>("uv-path-status");
|
|
gitUrlOverride = Root.Q<TextField>("git-url-override");
|
|
browseGitUrlButton = Root.Q<Button>("browse-git-url-button");
|
|
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
|
|
autoStartOnLoadToggle = Root.Q<Toggle>("auto-start-on-load-toggle");
|
|
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
|
|
logRecordToggle = Root.Q<Toggle>("log-record-toggle");
|
|
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
|
|
allowLanHttpBindToggle = Root.Q<Toggle>("allow-lan-http-bind-toggle");
|
|
allowInsecureRemoteHttpToggle = Root.Q<Toggle>("allow-insecure-remote-http-toggle");
|
|
screenshotsFolderOverride = Root.Q<TextField>("screenshots-folder-override");
|
|
browseScreenshotsFolderButton = Root.Q<Button>("browse-screenshots-folder-button");
|
|
clearScreenshotsFolderButton = Root.Q<Button>("clear-screenshots-folder-button");
|
|
deploySourcePath = Root.Q<TextField>("deploy-source-path");
|
|
browseDeploySourceButton = Root.Q<Button>("browse-deploy-source-button");
|
|
clearDeploySourceButton = Root.Q<Button>("clear-deploy-source-button");
|
|
deployButton = Root.Q<Button>("deploy-button");
|
|
deployRestoreButton = Root.Q<Button>("deploy-restore-button");
|
|
deployTargetLabel = Root.Q<Label>("deploy-target-label");
|
|
deployBackupLabel = Root.Q<Label>("deploy-backup-label");
|
|
deployStatusLabel = Root.Q<Label>("deploy-status-label");
|
|
healthIndicator = Root.Q<VisualElement>("health-indicator");
|
|
healthStatus = Root.Q<Label>("health-status");
|
|
testConnectionButton = Root.Q<Button>("test-connection-button");
|
|
}
|
|
|
|
private void InitializeUI()
|
|
{
|
|
// Set tooltips for fields
|
|
if (uvxPathOverride != null)
|
|
uvxPathOverride.tooltip = "Override path to uvx executable. Leave empty for auto-detection.";
|
|
if (gitUrlOverride != null)
|
|
gitUrlOverride.tooltip = "Override server source for uvx --from. Leave empty to use default PyPI package. Example local dev: /path/to/unity-mcp/Server";
|
|
if (debugLogsToggle != null)
|
|
{
|
|
debugLogsToggle.tooltip = "Enable verbose debug logging to the Unity Console.";
|
|
var debugLabel = debugLogsToggle?.parent?.Q<Label>();
|
|
if (debugLabel != null)
|
|
debugLabel.tooltip = debugLogsToggle.tooltip;
|
|
}
|
|
if (logRecordToggle != null)
|
|
{
|
|
logRecordToggle.tooltip = "Log every MCP tool execution (tool, action, status, duration) to Assets/UnityMCP/Log/mcp.log.";
|
|
var logRecordLabel = logRecordToggle?.parent?.Q<Label>();
|
|
if (logRecordLabel != null)
|
|
logRecordLabel.tooltip = logRecordToggle.tooltip;
|
|
}
|
|
if (devModeForceRefreshToggle != null)
|
|
{
|
|
devModeForceRefreshToggle.tooltip = "When enabled, generated uvx commands add '--no-cache --refresh' before launching (slower startup, but avoids stale cached builds while iterating on the Server).";
|
|
var forceRefreshLabel = devModeForceRefreshToggle?.parent?.Q<Label>();
|
|
if (forceRefreshLabel != null)
|
|
forceRefreshLabel.tooltip = devModeForceRefreshToggle.tooltip;
|
|
}
|
|
if (allowLanHttpBindToggle != null)
|
|
{
|
|
allowLanHttpBindToggle.tooltip = "Allow HTTP Local to bind on all interfaces (0.0.0.0 / ::). Disabled by default because devices on your LAN may reach MCP tools.";
|
|
var lanBindLabel = allowLanHttpBindToggle?.parent?.Q<Label>();
|
|
if (lanBindLabel != null)
|
|
lanBindLabel.tooltip = allowLanHttpBindToggle.tooltip;
|
|
}
|
|
if (allowInsecureRemoteHttpToggle != null)
|
|
{
|
|
allowInsecureRemoteHttpToggle.tooltip = "Allow HTTP Remote over plaintext http/ws. Disabled by default to require HTTPS/WSS.";
|
|
var insecureRemoteLabel = allowInsecureRemoteHttpToggle?.parent?.Q<Label>();
|
|
if (insecureRemoteLabel != null)
|
|
insecureRemoteLabel.tooltip = allowInsecureRemoteHttpToggle.tooltip;
|
|
}
|
|
if (testConnectionButton != null)
|
|
testConnectionButton.tooltip = "Test the connection between Unity and the MCP server.";
|
|
if (screenshotsFolderOverride != null)
|
|
{
|
|
screenshotsFolderOverride.tooltip = "Default folder for screenshots from manage_camera / manage_ui. " +
|
|
"Project-relative (e.g. 'Assets/Screenshots' or 'Captures'). Empty = built-in default (Assets/Screenshots). " +
|
|
"Per-call 'output_folder' parameters always override this.";
|
|
screenshotsFolderOverride.SetValueWithoutNotify(ScreenshotPreferences.DefaultFolder);
|
|
}
|
|
if (browseScreenshotsFolderButton != null)
|
|
browseScreenshotsFolderButton.tooltip = "Pick a folder inside the project; the path is stored project-relative.";
|
|
if (clearScreenshotsFolderButton != null)
|
|
clearScreenshotsFolderButton.tooltip = "Clear override and use the built-in default (Assets/Screenshots).";
|
|
if (deploySourcePath != null)
|
|
deploySourcePath.tooltip = "Copy a MCPForUnity folder into this project's package location.";
|
|
|
|
// Set tooltips for buttons
|
|
if (browseUvxButton != null)
|
|
browseUvxButton.tooltip = "Browse for uvx executable";
|
|
if (clearUvxButton != null)
|
|
clearUvxButton.tooltip = "Clear override and use auto-detection";
|
|
if (browseGitUrlButton != null)
|
|
browseGitUrlButton.tooltip = "Select local server source folder";
|
|
if (clearGitUrlButton != null)
|
|
clearGitUrlButton.tooltip = "Clear override and use default PyPI package";
|
|
if (browseDeploySourceButton != null)
|
|
browseDeploySourceButton.tooltip = "Select MCPForUnity source folder";
|
|
if (clearDeploySourceButton != null)
|
|
clearDeploySourceButton.tooltip = "Clear deployment source path";
|
|
if (deployButton != null)
|
|
deployButton.tooltip = "Copy MCPForUnity to this project's package location";
|
|
if (deployRestoreButton != null)
|
|
deployRestoreButton.tooltip = "Restore the last backup before deployment";
|
|
|
|
if (autoStartOnLoadToggle != null)
|
|
{
|
|
autoStartOnLoadToggle.tooltip = "Automatically start the local HTTP server and connect the MCP bridge when the Unity Editor opens. Only applies to HTTP transport (stdio always auto-starts).";
|
|
var autoStartLabel = autoStartOnLoadToggle.parent?.Q<Label>();
|
|
if (autoStartLabel != null)
|
|
autoStartLabel.tooltip = autoStartOnLoadToggle.tooltip;
|
|
autoStartOnLoadToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false));
|
|
}
|
|
|
|
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
|
|
|
bool debugEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
|
debugLogsToggle.value = debugEnabled;
|
|
McpLog.SetDebugLoggingEnabled(debugEnabled);
|
|
|
|
if (logRecordToggle != null)
|
|
logRecordToggle.value = McpLogRecord.IsEnabled;
|
|
|
|
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
|
if (allowLanHttpBindToggle != null)
|
|
{
|
|
allowLanHttpBindToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AllowLanHttpBind, false));
|
|
}
|
|
if (allowInsecureRemoteHttpToggle != null)
|
|
{
|
|
allowInsecureRemoteHttpToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AllowInsecureRemoteHttp, false));
|
|
}
|
|
UpdatePathOverrides();
|
|
UpdateDeploymentSection();
|
|
}
|
|
|
|
private void RegisterCallbacks()
|
|
{
|
|
browseUvxButton.clicked += OnBrowseUvxClicked;
|
|
clearUvxButton.clicked += OnClearUvxClicked;
|
|
browseGitUrlButton.clicked += OnBrowseGitUrlClicked;
|
|
|
|
gitUrlOverride.RegisterValueChangedCallback(evt =>
|
|
{
|
|
string url = evt.newValue?.Trim();
|
|
if (string.IsNullOrEmpty(url))
|
|
{
|
|
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
|
|
}
|
|
else
|
|
{
|
|
url = ResolveServerPath(url);
|
|
// Update the text field if the path was auto-corrected, without re-triggering the callback
|
|
if (url != evt.newValue?.Trim())
|
|
{
|
|
gitUrlOverride.SetValueWithoutNotify(url);
|
|
}
|
|
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, url);
|
|
}
|
|
OnGitUrlChanged?.Invoke();
|
|
OnHttpServerCommandUpdateRequested?.Invoke();
|
|
});
|
|
|
|
clearGitUrlButton.clicked += () =>
|
|
{
|
|
gitUrlOverride.value = string.Empty;
|
|
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
|
|
OnGitUrlChanged?.Invoke();
|
|
OnHttpServerCommandUpdateRequested?.Invoke();
|
|
};
|
|
|
|
debugLogsToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
McpLog.SetDebugLoggingEnabled(evt.newValue);
|
|
});
|
|
|
|
if (logRecordToggle != null)
|
|
{
|
|
logRecordToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
McpLogRecord.IsEnabled = evt.newValue;
|
|
});
|
|
}
|
|
|
|
if (autoStartOnLoadToggle != null)
|
|
{
|
|
autoStartOnLoadToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
EditorPrefs.SetBool(EditorPrefKeys.AutoStartOnLoad, evt.newValue);
|
|
});
|
|
}
|
|
|
|
devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
|
|
OnHttpServerCommandUpdateRequested?.Invoke();
|
|
});
|
|
|
|
if (allowLanHttpBindToggle != null)
|
|
{
|
|
allowLanHttpBindToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, evt.newValue);
|
|
OnHttpServerCommandUpdateRequested?.Invoke();
|
|
});
|
|
}
|
|
|
|
if (allowInsecureRemoteHttpToggle != null)
|
|
{
|
|
allowInsecureRemoteHttpToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
EditorPrefs.SetBool(EditorPrefKeys.AllowInsecureRemoteHttp, evt.newValue);
|
|
OnHttpServerCommandUpdateRequested?.Invoke();
|
|
});
|
|
}
|
|
|
|
deploySourcePath.RegisterValueChangedCallback(evt =>
|
|
{
|
|
string path = evt.newValue?.Trim();
|
|
if (string.IsNullOrEmpty(path) || path == "Not set")
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
MCPServiceLocator.Deployment.SetStoredSourcePath(path);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EditorUtility.DisplayDialog("Invalid Source", ex.Message, "OK");
|
|
UpdateDeploymentSection();
|
|
}
|
|
});
|
|
|
|
if (screenshotsFolderOverride != null)
|
|
{
|
|
screenshotsFolderOverride.RegisterValueChangedCallback(evt =>
|
|
{
|
|
ScreenshotPreferences.DefaultFolder = evt.newValue;
|
|
});
|
|
}
|
|
if (browseScreenshotsFolderButton != null)
|
|
{
|
|
browseScreenshotsFolderButton.clicked += OnBrowseScreenshotsFolderClicked;
|
|
}
|
|
if (clearScreenshotsFolderButton != null)
|
|
{
|
|
clearScreenshotsFolderButton.clicked += () =>
|
|
{
|
|
ScreenshotPreferences.DefaultFolder = string.Empty;
|
|
screenshotsFolderOverride?.SetValueWithoutNotify(string.Empty);
|
|
};
|
|
}
|
|
|
|
browseDeploySourceButton.clicked += OnBrowseDeploySourceClicked;
|
|
clearDeploySourceButton.clicked += OnClearDeploySourceClicked;
|
|
deployButton.clicked += OnDeployClicked;
|
|
deployRestoreButton.clicked += OnRestoreBackupClicked;
|
|
testConnectionButton.clicked += () => OnTestConnectionRequested?.Invoke();
|
|
}
|
|
|
|
public void UpdatePathOverrides()
|
|
{
|
|
var pathService = MCPServiceLocator.Paths;
|
|
|
|
bool hasOverride = pathService.HasUvxPathOverride;
|
|
bool hasFallback = pathService.HasUvxPathFallback;
|
|
string uvxPath = hasOverride ? pathService.GetUvxPath() : null;
|
|
|
|
// Determine display text based on override and fallback status
|
|
if (hasOverride)
|
|
{
|
|
if (hasFallback)
|
|
{
|
|
// Override path invalid, using system fallback
|
|
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
|
uvxPathOverride.value = $"Invalid override path: {overridePath} (fallback to uvx path) {uvxPath}";
|
|
}
|
|
else if (!string.IsNullOrEmpty(uvxPath))
|
|
{
|
|
// Override path valid
|
|
uvxPathOverride.value = uvxPath;
|
|
}
|
|
else
|
|
{
|
|
// Override set but invalid, no fallback available
|
|
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
|
uvxPathOverride.value = $"Invalid override path: {overridePath}, no uv found";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uvxPathOverride.value = "uvx (uses PATH)";
|
|
}
|
|
|
|
uvxPathStatus.RemoveFromClassList("valid");
|
|
uvxPathStatus.RemoveFromClassList("invalid");
|
|
uvxPathStatus.RemoveFromClassList("warning");
|
|
|
|
if (hasOverride)
|
|
{
|
|
if (hasFallback)
|
|
{
|
|
// Using fallback - show as warning (yellow)
|
|
uvxPathStatus.AddToClassList("warning");
|
|
}
|
|
else
|
|
{
|
|
// Override mode: validate the override path
|
|
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
|
if (pathService.TryValidateUvxExecutable(overridePath, out _))
|
|
{
|
|
uvxPathStatus.AddToClassList("valid");
|
|
}
|
|
else
|
|
{
|
|
uvxPathStatus.AddToClassList("invalid");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// PATH mode: validate system uvx
|
|
string systemUvxPath = pathService.GetUvxPath();
|
|
if (!string.IsNullOrEmpty(systemUvxPath) && pathService.TryValidateUvxExecutable(systemUvxPath, out _))
|
|
{
|
|
uvxPathStatus.AddToClassList("valid");
|
|
}
|
|
else
|
|
{
|
|
uvxPathStatus.AddToClassList("invalid");
|
|
}
|
|
}
|
|
|
|
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
|
if (autoStartOnLoadToggle != null)
|
|
autoStartOnLoadToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false);
|
|
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
|
if (logRecordToggle != null)
|
|
logRecordToggle.value = McpLogRecord.IsEnabled;
|
|
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
|
if (allowLanHttpBindToggle != null)
|
|
{
|
|
allowLanHttpBindToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AllowLanHttpBind, false);
|
|
}
|
|
if (allowInsecureRemoteHttpToggle != null)
|
|
{
|
|
allowInsecureRemoteHttpToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AllowInsecureRemoteHttp, false);
|
|
}
|
|
UpdateDeploymentSection();
|
|
}
|
|
|
|
private void OnBrowseUvxClicked()
|
|
{
|
|
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
|
? "/opt/homebrew/bin"
|
|
: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
string picked = EditorUtility.OpenFilePanel("Select uv Executable", suggested, "");
|
|
if (!string.IsNullOrEmpty(picked))
|
|
{
|
|
try
|
|
{
|
|
MCPServiceLocator.Paths.SetUvxPathOverride(picked);
|
|
UpdatePathOverrides();
|
|
McpLog.Info($"uv path override set to: {picked}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnClearUvxClicked()
|
|
{
|
|
MCPServiceLocator.Paths.ClearUvxPathOverride();
|
|
UpdatePathOverrides();
|
|
McpLog.Info("uv path override cleared");
|
|
}
|
|
|
|
private void OnBrowseGitUrlClicked()
|
|
{
|
|
string picked = EditorUtility.OpenFolderPanel("Select Server folder (containing pyproject.toml)", string.Empty, string.Empty);
|
|
if (!string.IsNullOrEmpty(picked))
|
|
{
|
|
picked = ResolveServerPath(picked);
|
|
gitUrlOverride.value = picked;
|
|
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, picked);
|
|
OnGitUrlChanged?.Invoke();
|
|
OnHttpServerCommandUpdateRequested?.Invoke();
|
|
McpLog.Info($"Server source override set to: {picked}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates and auto-corrects a local server path to ensure it points to the directory
|
|
/// containing pyproject.toml (the Python package root). If the user selects a parent
|
|
/// directory (e.g. the repo root), this checks for a "Server" subdirectory with
|
|
/// pyproject.toml and returns that instead.
|
|
/// </summary>
|
|
private static string ResolveServerPath(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return path;
|
|
|
|
// If path is not a local filesystem path, return as-is (git URLs, PyPI refs, etc.)
|
|
if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
|
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
|
path.StartsWith("git+", StringComparison.OrdinalIgnoreCase) ||
|
|
path.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
// Strip file:// prefix for filesystem checks, but preserve it for the return value
|
|
string checkPath = path;
|
|
string prefix = string.Empty;
|
|
if (checkPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
prefix = "file://";
|
|
checkPath = checkPath.Substring(7);
|
|
}
|
|
|
|
// Already points to a directory with pyproject.toml — correct path
|
|
if (File.Exists(Path.Combine(checkPath, "pyproject.toml")))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
// Check if "Server" subdirectory contains pyproject.toml (common repo structure)
|
|
string serverSubDir = Path.Combine(checkPath, "Server");
|
|
if (File.Exists(Path.Combine(serverSubDir, "pyproject.toml")))
|
|
{
|
|
string corrected = prefix + serverSubDir;
|
|
McpLog.Info($"Auto-corrected server path to 'Server' subdirectory: {corrected}");
|
|
return corrected;
|
|
}
|
|
|
|
// Return as-is; uvx will report the error if the path is invalid
|
|
return path;
|
|
}
|
|
|
|
private void UpdateDeploymentSection()
|
|
{
|
|
var deployService = MCPServiceLocator.Deployment;
|
|
|
|
string sourcePath = deployService.GetStoredSourcePath();
|
|
deploySourcePath.value = sourcePath ?? string.Empty;
|
|
|
|
deployTargetLabel.text = $"Target: {deployService.GetTargetDisplayPath()}";
|
|
|
|
string backupPath = deployService.GetLastBackupPath();
|
|
if (deployService.HasBackup())
|
|
{
|
|
// Use forward slashes to avoid backslash escape sequence issues in UI text
|
|
deployBackupLabel.text = $"Last backup: {backupPath?.Replace('\\', '/')}";
|
|
}
|
|
else
|
|
{
|
|
deployBackupLabel.text = "Last backup: none";
|
|
}
|
|
|
|
deployRestoreButton?.SetEnabled(deployService.HasBackup());
|
|
}
|
|
|
|
private void OnBrowseScreenshotsFolderClicked()
|
|
{
|
|
// Start the picker at the project's Assets/ since that's the most common target.
|
|
string startDir = UnityEngine.Application.dataPath;
|
|
string picked = EditorUtility.OpenFolderPanel("Select Screenshots Folder (inside this project)", startDir, string.Empty);
|
|
if (string.IsNullOrEmpty(picked))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string projectRoot = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "..")).Replace('\\', '/');
|
|
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
|
|
string normalizedPicked = picked.Replace('\\', '/');
|
|
|
|
if (normalizedPicked.Equals(projectRoot, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Storing "" would wipe the EditorPrefs key (= "unset"), so reject the project
|
|
// root rather than silently revert the override the user just chose.
|
|
EditorUtility.DisplayDialog(
|
|
"Pick a Subfolder",
|
|
"Please pick a subfolder of the project (for example 'Assets/Screenshots' or 'Captures'). " +
|
|
"Selecting the project root would mix screenshots in with your project files.",
|
|
"OK");
|
|
return;
|
|
}
|
|
|
|
if (!normalizedPicked.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Folder Outside Project",
|
|
$"The selected folder is outside the Unity project root.\n\nPicked: {normalizedPicked}\nProject: {projectRoot}\n\nPlease pick a folder inside the project.",
|
|
"OK");
|
|
return;
|
|
}
|
|
|
|
string projectRelative = normalizedPicked.Substring(normalizedRoot.Length);
|
|
|
|
ScreenshotPreferences.DefaultFolder = projectRelative;
|
|
screenshotsFolderOverride?.SetValueWithoutNotify(projectRelative);
|
|
McpLog.Info($"Default screenshots folder set to '{projectRelative}'.");
|
|
}
|
|
|
|
private void OnBrowseDeploySourceClicked()
|
|
{
|
|
string picked = EditorUtility.OpenFolderPanel("Select MCPForUnity folder", string.Empty, string.Empty);
|
|
if (string.IsNullOrEmpty(picked))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
MCPServiceLocator.Deployment.SetStoredSourcePath(picked);
|
|
SetDeployStatus($"Source set: {picked}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EditorUtility.DisplayDialog("Invalid Source", ex.Message, "OK");
|
|
SetDeployStatus("Source selection failed");
|
|
}
|
|
|
|
UpdateDeploymentSection();
|
|
}
|
|
|
|
private void OnClearDeploySourceClicked()
|
|
{
|
|
MCPServiceLocator.Deployment.ClearStoredSourcePath();
|
|
UpdateDeploymentSection();
|
|
SetDeployStatus("Source cleared");
|
|
}
|
|
|
|
private void OnDeployClicked()
|
|
{
|
|
var result = MCPServiceLocator.Deployment.DeployFromStoredSource();
|
|
SetDeployStatus(result.Message, !result.Success);
|
|
|
|
if (!result.Success)
|
|
{
|
|
EditorUtility.DisplayDialog("Deployment Failed", result.Message, "OK");
|
|
}
|
|
else
|
|
{
|
|
EditorUtility.DisplayDialog("Deployment Complete", result.Message + (string.IsNullOrEmpty(result.BackupPath) ? string.Empty : $"\nBackup: {result.BackupPath}"), "OK");
|
|
OnPackageDeployed?.Invoke();
|
|
}
|
|
|
|
UpdateDeploymentSection();
|
|
}
|
|
|
|
private void OnRestoreBackupClicked()
|
|
{
|
|
var result = MCPServiceLocator.Deployment.RestoreLastBackup();
|
|
SetDeployStatus(result.Message, !result.Success);
|
|
|
|
if (!result.Success)
|
|
{
|
|
EditorUtility.DisplayDialog("Restore Failed", result.Message, "OK");
|
|
}
|
|
else
|
|
{
|
|
EditorUtility.DisplayDialog("Restore Complete", result.Message, "OK");
|
|
OnPackageDeployed?.Invoke();
|
|
}
|
|
|
|
UpdateDeploymentSection();
|
|
}
|
|
|
|
private void SetDeployStatus(string message, bool isError = false)
|
|
{
|
|
if (deployStatusLabel == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
deployStatusLabel.text = message;
|
|
deployStatusLabel.style.color = isError
|
|
? new StyleColor(new Color(0.85f, 0.2f, 0.2f))
|
|
: StyleKeyword.Null;
|
|
}
|
|
|
|
public void UpdateHealthStatus(bool isHealthy, string statusText)
|
|
{
|
|
if (healthStatus != null)
|
|
{
|
|
healthStatus.text = statusText;
|
|
}
|
|
|
|
if (healthIndicator != null)
|
|
{
|
|
healthIndicator.RemoveFromClassList("healthy");
|
|
healthIndicator.RemoveFromClassList("disconnected");
|
|
healthIndicator.RemoveFromClassList("unknown");
|
|
|
|
if (isHealthy)
|
|
{
|
|
healthIndicator.AddToClassList("healthy");
|
|
}
|
|
else if (statusText == HealthStatus.Unknown)
|
|
{
|
|
healthIndicator.AddToClassList("unknown");
|
|
}
|
|
else
|
|
{
|
|
healthIndicator.AddToClassList("disconnected");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|