1199 lines
49 KiB
C#
1199 lines
49 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using UnityEditor;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using RuntimePlatform = UnityEngine.RuntimePlatform;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
public class SetupWizardWindow : EditorWindow
|
|
{
|
|
private const string WindowTitle = "Unity CLI Loop Setup";
|
|
private const string UXML_RELATIVE_PATH = "Editor/UI/Setup/SetupWizardWindow.uxml";
|
|
private const string USS_RELATIVE_PATH = "Editor/UI/Setup/SetupWizardWindow.uss";
|
|
private const string GITHUB_ICON_RELATIVE_PATH = "Editor/UI/Setup/GitHub_Invertocat_White.png";
|
|
private const int PreferredWrappedTextLineCount = 2;
|
|
private const bool ForceFlatSkillInstall = true;
|
|
private static readonly Vector2 MinimumWindowSize = new(360f, 380f);
|
|
|
|
[InitializeOnLoadMethod]
|
|
private static void InitializeOnLoad()
|
|
{
|
|
if (AssetDatabase.IsAssetImportWorkerProcess()) return;
|
|
if (Application.isBatchMode) return;
|
|
|
|
TryShowOnVersionChange();
|
|
}
|
|
|
|
[MenuItem("Window/Unity CLI Loop/Setup Wizard", priority = 3)]
|
|
public static void ShowWindow()
|
|
{
|
|
ShowWindowInternal(false);
|
|
}
|
|
|
|
internal static bool ShouldAutoShowForVersion(
|
|
string currentVersion,
|
|
string lastSeenVersion,
|
|
bool suppressAutoShow)
|
|
{
|
|
if (suppressAutoShow) return false;
|
|
|
|
return !string.Equals(currentVersion, lastSeenVersion, System.StringComparison.Ordinal);
|
|
}
|
|
|
|
internal static void MaybeRecordLastSeenVersion(bool shouldRecordVersion, string version)
|
|
{
|
|
if (!shouldRecordVersion) return;
|
|
|
|
Debug.Assert(!string.IsNullOrEmpty(version), "version must not be null or empty");
|
|
McpEditorSettings.SetLastSeenSetupWizardVersion(version);
|
|
}
|
|
|
|
internal static void MaybeRecordSuppressedVersion(bool suppressAutoShow, string version)
|
|
{
|
|
if (!suppressAutoShow) return;
|
|
|
|
Debug.Assert(!string.IsNullOrEmpty(version), "version must not be null or empty");
|
|
McpEditorSettings.SetLastSeenSetupWizardVersion(version);
|
|
}
|
|
|
|
private static void TryShowOnVersionChange()
|
|
{
|
|
string currentVersion = McpConstants.PackageInfo.version;
|
|
bool suppressAutoShow = McpEditorSettings.GetSuppressSetupWizardAutoShow();
|
|
MaybeRecordSuppressedVersion(suppressAutoShow, currentVersion);
|
|
string lastSeenVersion = McpEditorSettings.GetLastSeenSetupWizardVersion();
|
|
if (!ShouldAutoShowForVersion(currentVersion, lastSeenVersion, suppressAutoShow)) return;
|
|
|
|
EditorApplication.delayCall += ShowWindowOnVersionChange;
|
|
}
|
|
|
|
private static void ShowWindowOnVersionChange()
|
|
{
|
|
ShowWindowInternal(true);
|
|
}
|
|
|
|
private static void ShowWindowInternal(bool shouldRecordVersion)
|
|
{
|
|
string currentVersion = McpConstants.PackageInfo.version;
|
|
if (TryReuseOpenWindow(
|
|
HasOpenInstances<SetupWizardWindow>(),
|
|
shouldRecordVersion,
|
|
currentVersion,
|
|
FocusExistingWindow))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string lastSeenSetupWizardVersionBeforeOpen = McpEditorSettings.GetLastSeenSetupWizardVersion();
|
|
Rect windowPosition = CreateCenteredRect(EditorGUIUtility.GetMainWindowPosition(), MinimumWindowSize);
|
|
SetupWizardWindow window = CreateInstance<SetupWizardWindow>();
|
|
PrepareForOpen(window, WindowTitle, windowPosition, lastSeenSetupWizardVersionBeforeOpen);
|
|
window.ShowUtility();
|
|
window.ScheduleResizeToContent();
|
|
MaybeRecordLastSeenVersion(shouldRecordVersion, currentVersion);
|
|
}
|
|
|
|
internal static Rect WithContentSize(Rect currentRect, Vector2 contentSize, Vector2 frameSize)
|
|
{
|
|
Vector2 measuredSize = contentSize + frameSize;
|
|
Vector2 targetSize = new(
|
|
Mathf.Max(measuredSize.x, MinimumWindowSize.x),
|
|
Mathf.Max(measuredSize.y, MinimumWindowSize.y));
|
|
return CreateCenteredRect(currentRect, targetSize);
|
|
}
|
|
|
|
internal static Rect CreateCenteredRect(Rect bounds, Vector2 size)
|
|
{
|
|
Vector2 centeredPosition = bounds.center - (size * 0.5f);
|
|
return new Rect(centeredPosition, size);
|
|
}
|
|
|
|
internal static string GetGitHubRepositoryUrl()
|
|
{
|
|
return McpUIConstants.PROJECT_REPOSITORY_URL;
|
|
}
|
|
|
|
internal static bool TryReuseOpenWindow(
|
|
bool hasOpenWindow,
|
|
bool shouldRecordVersion,
|
|
string currentVersion,
|
|
System.Action focusExistingWindow)
|
|
{
|
|
if (!hasOpenWindow) return false;
|
|
|
|
Debug.Assert(focusExistingWindow != null, "focusExistingWindow must not be null");
|
|
Debug.Assert(!string.IsNullOrEmpty(currentVersion), "currentVersion must not be null or empty");
|
|
focusExistingWindow();
|
|
MaybeRecordLastSeenVersion(shouldRecordVersion, currentVersion);
|
|
return true;
|
|
}
|
|
|
|
internal static void PrepareForOpen(
|
|
SetupWizardWindow window,
|
|
string title,
|
|
Rect position,
|
|
string lastSeenSetupWizardVersionBeforeOpen)
|
|
{
|
|
Debug.Assert(window != null, "window must not be null");
|
|
Debug.Assert(!string.IsNullOrEmpty(title), "title must not be null or empty");
|
|
|
|
window.titleContent = new GUIContent(title);
|
|
window.position = position;
|
|
window._lastSeenSetupWizardVersionBeforeOpen =
|
|
lastSeenSetupWizardVersionBeforeOpen ?? string.Empty;
|
|
}
|
|
|
|
private static void FocusExistingWindow()
|
|
{
|
|
FocusWindowIfItsOpen<SetupWizardWindow>();
|
|
}
|
|
|
|
// Prerequisite
|
|
private VisualElement _nodejsWarning;
|
|
private VisualElement _nodejsOk;
|
|
private Button _refreshButton;
|
|
|
|
// Global CLI
|
|
private VisualElement _globalCliSection;
|
|
private VisualElement _cliStatusIcon;
|
|
private Label _cliStatusLabel;
|
|
private Button _installCliButton;
|
|
|
|
// Skills
|
|
private VisualElement _groupSkillsRow;
|
|
private VisualElement _projectCliVersionRow;
|
|
private VisualElement _skillsTargetRow;
|
|
private EnumField _skillsTargetField;
|
|
private Toggle _groupSkillsToggle;
|
|
private Label _groupSkillsLabel;
|
|
private Toggle _projectCliVersionToggle;
|
|
private VisualElement _skillsTargetList;
|
|
private VisualElement _skillsStatusDivider;
|
|
private Label _skillsStatusLabel;
|
|
private Button _installSkillsButton;
|
|
|
|
// Footer
|
|
private Toggle _suppressAutoShowToggle;
|
|
private Button _openSettingsButton;
|
|
private Button _closeButton;
|
|
private VisualElement _githubLinkRow;
|
|
private Label _githubLinkLabel;
|
|
private Image _githubLinkIcon;
|
|
private ScrollView _mainScrollView;
|
|
|
|
// State
|
|
private bool _isInstallingCli;
|
|
private bool _isInstallingSkills;
|
|
private bool _isApplyingContentSize;
|
|
private bool _isSkillsTargetFieldInitialized;
|
|
private bool _shouldUseFirstInstallSkillsUi;
|
|
private bool _installSkillsFlat;
|
|
[SerializeField]
|
|
private string _lastSeenSetupWizardVersionBeforeOpen = string.Empty;
|
|
private IVisualElementScheduledItem _initialRefreshScheduledItem;
|
|
private IVisualElementScheduledItem _resizeScheduledItem;
|
|
private CancellationTokenSource _skillInstallStateRefreshCts;
|
|
private SkillsTarget _skillsTarget = SkillsTarget.Claude;
|
|
|
|
private void CreateGUI()
|
|
{
|
|
InitializeFirstInstallSkillsUiState();
|
|
LoadLayout();
|
|
BindElements();
|
|
BindEvents();
|
|
BindSizeUpdates();
|
|
ApplyInitialCheckingState();
|
|
ScheduleInitialRefresh();
|
|
ScheduleResizeToContent();
|
|
}
|
|
|
|
private void InitializeFirstInstallSkillsUiState()
|
|
{
|
|
_shouldUseFirstInstallSkillsUi = ShouldUseFirstInstallSkillsUi(
|
|
_lastSeenSetupWizardVersionBeforeOpen);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
_initialRefreshScheduledItem?.Pause();
|
|
CancelSkillInstallStateRefresh();
|
|
}
|
|
|
|
private void LoadLayout()
|
|
{
|
|
string uxmlPath = $"{McpConstants.PackageAssetPath}/{UXML_RELATIVE_PATH}";
|
|
VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
|
|
Debug.Assert(visualTree != null, $"UXML not found at {uxmlPath}");
|
|
visualTree.CloneTree(rootVisualElement);
|
|
|
|
string ussPath = $"{McpConstants.PackageAssetPath}/{USS_RELATIVE_PATH}";
|
|
StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
|
|
Debug.Assert(styleSheet != null, $"USS not found at {ussPath}");
|
|
rootVisualElement.styleSheets.Add(styleSheet);
|
|
}
|
|
|
|
private void BindElements()
|
|
{
|
|
_nodejsWarning = rootVisualElement.Q<VisualElement>("nodejs-warning");
|
|
_nodejsOk = rootVisualElement.Q<VisualElement>("nodejs-ok");
|
|
_refreshButton = rootVisualElement.Q<Button>("refresh-button");
|
|
|
|
_globalCliSection = rootVisualElement.Q<VisualElement>("step-1");
|
|
_cliStatusIcon = rootVisualElement.Q<VisualElement>("cli-status-icon");
|
|
_cliStatusLabel = rootVisualElement.Q<Label>("cli-status-label");
|
|
_installCliButton = rootVisualElement.Q<Button>("install-cli-button");
|
|
|
|
_groupSkillsRow = rootVisualElement.Q<VisualElement>("group-skills-row");
|
|
_projectCliVersionRow = rootVisualElement.Q<VisualElement>("project-cli-version-row");
|
|
_skillsTargetRow = rootVisualElement.Q<VisualElement>("skills-target-row");
|
|
_skillsTargetField = rootVisualElement.Q<EnumField>("skills-target-field");
|
|
_groupSkillsToggle = rootVisualElement.Q<Toggle>("group-skills-toggle");
|
|
_groupSkillsLabel = rootVisualElement.Q<Label>("group-skills-label");
|
|
_projectCliVersionToggle = rootVisualElement.Q<Toggle>("project-cli-version-toggle");
|
|
_skillsTargetList = rootVisualElement.Q<VisualElement>("skills-target-list");
|
|
_skillsStatusDivider = rootVisualElement.Q<VisualElement>("skills-status-divider");
|
|
_skillsStatusLabel = rootVisualElement.Q<Label>("skills-status-label");
|
|
_installSkillsButton = rootVisualElement.Q<Button>("install-skills-button");
|
|
|
|
_suppressAutoShowToggle = rootVisualElement.Q<Toggle>("suppress-auto-show-toggle");
|
|
_openSettingsButton = rootVisualElement.Q<Button>("open-settings-button");
|
|
_closeButton = rootVisualElement.Q<Button>("close-button");
|
|
_githubLinkRow = rootVisualElement.Q<VisualElement>("github-link-row");
|
|
_githubLinkLabel = rootVisualElement.Q<Label>("github-link-label");
|
|
_githubLinkIcon = rootVisualElement.Q<Image>("github-link-icon");
|
|
_mainScrollView = rootVisualElement.Q<ScrollView>();
|
|
}
|
|
|
|
private void BindEvents()
|
|
{
|
|
_refreshButton.clicked += () => RefreshUI();
|
|
_installCliButton.clicked += HandleInstallCli;
|
|
_installSkillsButton.clicked += HandleInstallSkills;
|
|
InitializeSkillsTargetField();
|
|
InitializeGroupSkillsToggle();
|
|
InitializeProjectCliVersionToggle();
|
|
_suppressAutoShowToggle.RegisterValueChangedCallback(evt => HandleSuppressAutoShowChanged(evt.newValue));
|
|
_openSettingsButton.clicked += HandleOpenSettings;
|
|
_closeButton.clicked += HandleClose;
|
|
_githubLinkRow.RegisterCallback<ClickEvent>(_ => HandleOpenGitHub());
|
|
_githubLinkRow.RegisterCallback<MouseEnterEvent>(_ => HandleGitHubHoverChanged(true));
|
|
_githubLinkRow.RegisterCallback<MouseLeaveEvent>(_ => HandleGitHubHoverChanged(false));
|
|
ConfigureScrollView();
|
|
InitializeGitHubIcon();
|
|
}
|
|
|
|
private void ConfigureScrollView()
|
|
{
|
|
Debug.Assert(_mainScrollView != null, "mainScrollView must not be null");
|
|
_mainScrollView.verticalScrollerVisibility = ScrollerVisibility.Auto;
|
|
_mainScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
|
|
}
|
|
|
|
private void InitializeGitHubIcon()
|
|
{
|
|
string iconPath = $"{McpConstants.PackageAssetPath}/{GITHUB_ICON_RELATIVE_PATH}";
|
|
Texture2D iconTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(iconPath);
|
|
Debug.Assert(iconTexture != null, $"GitHub icon not found at {iconPath}");
|
|
_githubLinkIcon.image = iconTexture;
|
|
}
|
|
|
|
private void InitializeSkillsTargetField()
|
|
{
|
|
if (_isSkillsTargetFieldInitialized) return;
|
|
|
|
_skillsTargetField.Init(_skillsTarget);
|
|
_skillsTargetField.RegisterValueChangedCallback(evt =>
|
|
{
|
|
if (evt.newValue is SkillsTarget newTarget)
|
|
{
|
|
_skillsTarget = newTarget;
|
|
RefreshSkillsSection();
|
|
}
|
|
});
|
|
_isSkillsTargetFieldInitialized = true;
|
|
}
|
|
|
|
private void RefreshSkillsTargetField()
|
|
{
|
|
Debug.Assert(_isSkillsTargetFieldInitialized, "skills target field must be initialized");
|
|
_skillsTargetField.SetValueWithoutNotify(_skillsTarget);
|
|
}
|
|
|
|
private void InitializeGroupSkillsToggle()
|
|
{
|
|
ApplyFlatSkillInstallPreference();
|
|
ViewDataBinder.SetVisible(_groupSkillsRow, false);
|
|
_groupSkillsToggle.SetValueWithoutNotify(!_installSkillsFlat);
|
|
_groupSkillsToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
evt.StopPropagation();
|
|
ApplyFlatSkillInstallPreference();
|
|
RefreshSkillsSection();
|
|
});
|
|
_groupSkillsLabel.RegisterCallback<ClickEvent>(HandleGroupSkillsRowClicked);
|
|
}
|
|
|
|
private void InitializeProjectCliVersionToggle()
|
|
{
|
|
RefreshProjectCliVersionToggle();
|
|
_projectCliVersionToggle.RegisterValueChangedCallback(evt =>
|
|
{
|
|
evt.StopPropagation();
|
|
HandleProjectCliVersionChanged(evt.newValue);
|
|
});
|
|
_projectCliVersionRow.RegisterCallback<ClickEvent>(HandleProjectCliVersionRowClicked);
|
|
}
|
|
|
|
private void RefreshProjectCliVersionToggle()
|
|
{
|
|
_projectCliVersionToggle.SetValueWithoutNotify(IsProjectCliVersionEnabled());
|
|
}
|
|
|
|
private static bool IsProjectCliVersionEnabled()
|
|
{
|
|
return ToolSettings.GetSkillCliInvocation() == CliConstants.SKILL_CLI_INVOCATION_NPX;
|
|
}
|
|
|
|
private void UpdateGlobalCliSectionVisibility()
|
|
{
|
|
ViewDataBinder.SetVisible(_globalCliSection, !IsProjectCliVersionEnabled());
|
|
}
|
|
|
|
private void BindSizeUpdates()
|
|
{
|
|
rootVisualElement.RegisterCallback<GeometryChangedEvent>(_ =>
|
|
{
|
|
if (_isApplyingContentSize) return;
|
|
ScheduleResizeToContent();
|
|
});
|
|
}
|
|
|
|
private void RefreshAutoShowToggle()
|
|
{
|
|
_suppressAutoShowToggle.SetValueWithoutNotify(McpEditorSettings.GetSuppressSetupWizardAutoShow());
|
|
}
|
|
|
|
private void ApplyInitialCheckingState()
|
|
{
|
|
RefreshAutoShowToggle();
|
|
ViewDataBinder.SetVisible(_nodejsWarning, false);
|
|
ViewDataBinder.SetVisible(_nodejsOk, false);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--success", false);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--pending", true);
|
|
_cliStatusLabel.text = "Checking...";
|
|
_installCliButton.SetEnabled(false);
|
|
_installCliButton.text = "Checking...";
|
|
ViewDataBinder.SetVisible(_groupSkillsRow, false);
|
|
_groupSkillsToggle.SetEnabled(false);
|
|
RefreshProjectCliVersionToggle();
|
|
UpdateGlobalCliSectionVisibility();
|
|
_projectCliVersionToggle.SetEnabled(false);
|
|
UpdateSkillsStatusLabel("Checking installed skills...");
|
|
_installSkillsButton.SetEnabled(false);
|
|
_installSkillsButton.text = "Checking...";
|
|
ViewDataBinder.SetVisible(_skillsTargetRow, _shouldUseFirstInstallSkillsUi);
|
|
ViewDataBinder.SetVisible(_skillsTargetList, !_shouldUseFirstInstallSkillsUi);
|
|
_skillsTargetList.Clear();
|
|
}
|
|
|
|
private void UpdateSkillsStatusLabel(string text)
|
|
{
|
|
_skillsStatusLabel.text = text;
|
|
bool isVisible = !string.IsNullOrEmpty(text);
|
|
ViewDataBinder.SetVisible(_skillsStatusDivider, isVisible);
|
|
ViewDataBinder.SetVisible(_skillsStatusLabel, isVisible);
|
|
}
|
|
|
|
private void ScheduleInitialRefresh()
|
|
{
|
|
_initialRefreshScheduledItem?.Pause();
|
|
_initialRefreshScheduledItem = rootVisualElement.schedule.Execute(() => RefreshUI()).StartingIn(0);
|
|
}
|
|
|
|
private void RefreshSkillsSection()
|
|
{
|
|
string cachedCliVersion = CliInstallationDetector.GetCachedCliVersion();
|
|
bool cliInstalled = !string.IsNullOrEmpty(cachedCliVersion);
|
|
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> targets = DetectDisplayedSkillTargetsFast(projectRoot);
|
|
RefreshProjectCliVersionToggle();
|
|
UpdateGlobalCliSectionVisibility();
|
|
_projectCliVersionToggle.SetEnabled(!_isInstallingCli && !_isInstallingSkills);
|
|
bool canManageSkills = CanManageSkills(cliInstalled, IsProjectCliVersionEnabled()) && !_isInstallingCli;
|
|
UpdateSkillsStep(canManageSkills, targets);
|
|
BeginRefreshDisplayedSkillTargets(canManageSkills);
|
|
ScheduleResizeToContent();
|
|
}
|
|
|
|
private async void RefreshUI(bool refreshSkillsSection = true)
|
|
{
|
|
CancelSkillInstallStateRefresh();
|
|
RefreshAutoShowToggle();
|
|
ViewDataBinder.SetVisible(_nodejsWarning, false);
|
|
ViewDataBinder.SetVisible(_nodejsOk, false);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--success", false);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--pending", true);
|
|
_cliStatusLabel.text = "Checking...";
|
|
_installCliButton.SetEnabled(false);
|
|
_installCliButton.text = "Checking...";
|
|
if (refreshSkillsSection)
|
|
{
|
|
ViewDataBinder.SetVisible(_groupSkillsRow, false);
|
|
_groupSkillsToggle.SetEnabled(false);
|
|
RefreshProjectCliVersionToggle();
|
|
UpdateGlobalCliSectionVisibility();
|
|
_projectCliVersionToggle.SetEnabled(false);
|
|
UpdateSkillsStatusLabel("Checking installed skills...");
|
|
_installSkillsButton.SetEnabled(false);
|
|
_installSkillsButton.text = "Checking...";
|
|
ViewDataBinder.SetVisible(_skillsTargetRow, _shouldUseFirstInstallSkillsUi);
|
|
ViewDataBinder.SetVisible(_skillsTargetList, !_shouldUseFirstInstallSkillsUi);
|
|
_skillsTargetList.Clear();
|
|
}
|
|
|
|
await Task.Yield();
|
|
|
|
RuntimePlatform platform = Application.platform;
|
|
string nodePath = await Task.Run(() => NodeEnvironmentResolver.FindNodePathAtPlatform(platform));
|
|
bool nodeDetected = !string.IsNullOrEmpty(nodePath);
|
|
|
|
ViewDataBinder.SetVisible(_nodejsWarning, !nodeDetected);
|
|
ViewDataBinder.SetVisible(_nodejsOk, nodeDetected);
|
|
|
|
if (!nodeDetected)
|
|
{
|
|
_cliStatusLabel.text = "Requires Node.js";
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--success", false);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--pending", true);
|
|
_installCliButton.SetEnabled(false);
|
|
_installSkillsButton.SetEnabled(false);
|
|
_groupSkillsToggle.SetEnabled(false);
|
|
_projectCliVersionToggle.SetEnabled(false);
|
|
UpdateSkillsStatusLabel(string.Empty);
|
|
_skillsTargetList.Clear();
|
|
ScheduleResizeToContent();
|
|
return;
|
|
}
|
|
|
|
await CliInstallationDetector.ForceRefreshCliVersionAsync(CancellationToken.None);
|
|
string cliVersion = CliInstallationDetector.GetCachedCliVersion();
|
|
bool cliInstalled = cliVersion != null;
|
|
bool cliVersionMatched = IsCliVersionMatched(cliVersion);
|
|
|
|
UpdateCliStep(cliInstalled, cliVersion, cliVersionMatched);
|
|
RefreshProjectCliVersionToggle();
|
|
UpdateGlobalCliSectionVisibility();
|
|
_projectCliVersionToggle.SetEnabled(!_isInstallingCli && !_isInstallingSkills);
|
|
|
|
if (!refreshSkillsSection)
|
|
{
|
|
ScheduleResizeToContent();
|
|
return;
|
|
}
|
|
|
|
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> targets = DetectDisplayedSkillTargetsFast(projectRoot);
|
|
bool canManageSkills = CanManageSkills(cliInstalled, IsProjectCliVersionEnabled())
|
|
&& !_isInstallingCli;
|
|
UpdateSkillsStep(canManageSkills, targets);
|
|
BeginRefreshDisplayedSkillTargets(canManageSkills);
|
|
ScheduleResizeToContent();
|
|
}
|
|
|
|
private List<ToolSkillSynchronizer.SkillTargetInfo> DetectDisplayedSkillTargets(string projectRoot)
|
|
{
|
|
return ToolSkillSynchronizer.DetectTargetsForLayoutAtProjectRoot(projectRoot, !_installSkillsFlat);
|
|
}
|
|
|
|
private List<ToolSkillSynchronizer.SkillTargetInfo> DetectDisplayedSkillTargetsFast(string projectRoot)
|
|
{
|
|
return ToolSkillSynchronizer.DetectTargetsForLayoutFastAtProjectRoot(projectRoot, !_installSkillsFlat);
|
|
}
|
|
|
|
private void BeginRefreshDisplayedSkillTargets(bool canManageSkills)
|
|
{
|
|
CancelSkillInstallStateRefresh();
|
|
if (!canManageSkills || _isInstallingSkills)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CancellationTokenSource cts = new();
|
|
_skillInstallStateRefreshCts = cts;
|
|
RefreshDisplayedSkillTargetsAsync(cts.Token);
|
|
}
|
|
|
|
private async void RefreshDisplayedSkillTargetsAsync(CancellationToken ct)
|
|
{
|
|
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> targets =
|
|
await Task.Run(() => DetectDisplayedSkillTargets(projectRoot));
|
|
if (ct.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateSkillsStep(canManageSkills: true, targets);
|
|
ScheduleResizeToContent();
|
|
}
|
|
|
|
private void CancelSkillInstallStateRefresh()
|
|
{
|
|
if (_skillInstallStateRefreshCts == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_skillInstallStateRefreshCts.Cancel();
|
|
_skillInstallStateRefreshCts.Dispose();
|
|
_skillInstallStateRefreshCts = null;
|
|
}
|
|
|
|
internal static List<ToolSkillSynchronizer.SkillTargetInfo> FilterInstallableSkillTargets(
|
|
IEnumerable<ToolSkillSynchronizer.SkillTargetInfo> targets)
|
|
{
|
|
Debug.Assert(targets != null, "targets must not be null");
|
|
return targets
|
|
.Where(target => target.HasSkillsDirectory)
|
|
.ToList();
|
|
}
|
|
|
|
internal static bool ShouldUseFirstInstallSkillsUi(string lastSeenSetupWizardVersion)
|
|
{
|
|
return string.IsNullOrEmpty(lastSeenSetupWizardVersion);
|
|
}
|
|
|
|
internal static bool ShouldUseTargetSelectionSkillsUi(
|
|
bool shouldUseFirstInstallSkillsUi,
|
|
int installableTargetCount)
|
|
{
|
|
Debug.Assert(installableTargetCount >= 0, "installableTargetCount must not be negative");
|
|
return shouldUseFirstInstallSkillsUi || installableTargetCount == 0;
|
|
}
|
|
|
|
internal static bool CanManageSkills(bool cliInstalled, bool useProjectCliVersion)
|
|
{
|
|
return cliInstalled || useProjectCliVersion;
|
|
}
|
|
|
|
internal static ToolSkillSynchronizer.SkillTargetInfo CreateFirstInstallSkillTarget(
|
|
SkillsTarget target,
|
|
bool groupSkillsUnderUnityCliLoop)
|
|
{
|
|
SkillsTargetSelection selection = SkillsTargetSelectionResolver.Resolve(
|
|
target,
|
|
groupSkillsUnderUnityCliLoop);
|
|
return new(
|
|
selection.DisplayName,
|
|
selection.DirectoryName,
|
|
selection.InstallFlag,
|
|
hasSkillsDirectory: false,
|
|
hasExistingSkills: false);
|
|
}
|
|
|
|
internal static ToolSkillSynchronizer.SkillTargetInfo GetSelectedSkillTargetInfo(
|
|
IEnumerable<ToolSkillSynchronizer.SkillTargetInfo> targets,
|
|
SkillsTarget target,
|
|
bool groupSkillsUnderUnityCliLoop)
|
|
{
|
|
Debug.Assert(targets != null, "targets must not be null");
|
|
|
|
SkillsTargetSelection selection = SkillsTargetSelectionResolver.Resolve(
|
|
target,
|
|
groupSkillsUnderUnityCliLoop);
|
|
ToolSkillSynchronizer.SkillTargetInfo selectedTargetInfo = targets
|
|
.FirstOrDefault(info => info.DirName == selection.DirectoryName);
|
|
return string.IsNullOrEmpty(selectedTargetInfo.DirName)
|
|
? CreateFirstInstallSkillTarget(target, groupSkillsUnderUnityCliLoop)
|
|
: selectedTargetInfo;
|
|
}
|
|
|
|
internal static List<ToolSkillSynchronizer.SkillTargetInfo> GetFirstInstallableSkillTargets(
|
|
IEnumerable<ToolSkillSynchronizer.SkillTargetInfo> targets,
|
|
SkillsTarget target,
|
|
bool groupSkillsUnderUnityCliLoop)
|
|
{
|
|
ToolSkillSynchronizer.SkillTargetInfo selectedTargetInfo = GetSelectedSkillTargetInfo(
|
|
targets,
|
|
target,
|
|
groupSkillsUnderUnityCliLoop);
|
|
return selectedTargetInfo.InstallState == SkillInstallState.Installed
|
|
|| selectedTargetInfo.InstallState == SkillInstallState.Checking
|
|
? new List<ToolSkillSynchronizer.SkillTargetInfo>()
|
|
: new List<ToolSkillSynchronizer.SkillTargetInfo> { selectedTargetInfo };
|
|
}
|
|
|
|
internal static List<ToolSkillSynchronizer.SkillTargetInfo> GetSetupWizardInstallableSkillTargets(
|
|
IEnumerable<ToolSkillSynchronizer.SkillTargetInfo> targets,
|
|
SkillsTarget target,
|
|
bool groupSkillsUnderUnityCliLoop,
|
|
bool shouldUseFirstInstallSkillsUi)
|
|
{
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> installableTargets =
|
|
FilterInstallableSkillTargets(targets);
|
|
if (!ShouldUseTargetSelectionSkillsUi(
|
|
shouldUseFirstInstallSkillsUi,
|
|
installableTargets.Count))
|
|
{
|
|
return installableTargets;
|
|
}
|
|
|
|
return GetFirstInstallableSkillTargets(
|
|
targets,
|
|
target,
|
|
groupSkillsUnderUnityCliLoop);
|
|
}
|
|
|
|
private void UpdateCliStep(bool cliInstalled, string cliVersion, bool cliVersionMatched)
|
|
{
|
|
if (cliInstalled && cliVersionMatched)
|
|
{
|
|
_cliStatusLabel.text = $"v{cliVersion}";
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--success", true);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--pending", false);
|
|
_installCliButton.SetEnabled(false);
|
|
_installCliButton.text = "Installed";
|
|
return;
|
|
}
|
|
|
|
if (cliInstalled)
|
|
{
|
|
string requiredVersion = McpConstants.PackageInfo.version;
|
|
_cliStatusLabel.text = $"v{cliVersion} (requires v{requiredVersion})";
|
|
}
|
|
else
|
|
{
|
|
_cliStatusLabel.text = "Not installed";
|
|
}
|
|
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--success", false);
|
|
ViewDataBinder.ToggleClass(_cliStatusIcon, "setup-status-icon--pending", true);
|
|
_installCliButton.SetEnabled(!_isInstallingCli);
|
|
_installCliButton.text = _isInstallingCli ? "Installing..." : "Install CLI";
|
|
}
|
|
private static bool IsCliVersionMatched(string cliVersion)
|
|
{
|
|
return CliVersionComparison.IsSameVersion(cliVersion, McpConstants.PackageInfo.version);
|
|
}
|
|
|
|
private void UpdateSkillsStep(
|
|
bool canManageSkills,
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> targets)
|
|
{
|
|
_skillsTargetList.Clear();
|
|
|
|
if (!canManageSkills)
|
|
{
|
|
UpdateSkillsStatusLabel(string.Empty);
|
|
_installSkillsButton.SetEnabled(false);
|
|
_installSkillsButton.text = GetSkillsButtonTextForSetupWizard(
|
|
cliInstalled: false,
|
|
_isInstallingSkills,
|
|
hasOutdatedSkills: false);
|
|
_groupSkillsToggle.SetEnabled(false);
|
|
RefreshProjectCliVersionToggle();
|
|
_projectCliVersionToggle.SetEnabled(!_isInstallingCli && !_isInstallingSkills);
|
|
ViewDataBinder.SetVisible(_skillsTargetRow, false);
|
|
ViewDataBinder.SetVisible(_skillsTargetList, false);
|
|
return;
|
|
}
|
|
|
|
_groupSkillsToggle.SetEnabled(!_isInstallingSkills);
|
|
RefreshProjectCliVersionToggle();
|
|
_projectCliVersionToggle.SetEnabled(!_isInstallingSkills);
|
|
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> installableTargets = FilterInstallableSkillTargets(targets);
|
|
bool useTargetSelectionSkillsUi = ShouldUseTargetSelectionSkillsUi(
|
|
_shouldUseFirstInstallSkillsUi,
|
|
installableTargets.Count);
|
|
ViewDataBinder.SetVisible(_skillsTargetRow, useTargetSelectionSkillsUi);
|
|
ViewDataBinder.SetVisible(_skillsTargetList, !useTargetSelectionSkillsUi);
|
|
|
|
if (useTargetSelectionSkillsUi)
|
|
{
|
|
RefreshSkillsTargetField();
|
|
ToolSkillSynchronizer.SkillTargetInfo selectedTargetInfo = GetSelectedSkillTargetInfo(
|
|
targets,
|
|
_skillsTarget,
|
|
!_installSkillsFlat);
|
|
UpdateSkillsStatusLabel(string.Empty);
|
|
_installSkillsButton.text = CliSetupSection.GetInstallSkillsButtonText(
|
|
isCliInstalled: true,
|
|
_isInstallingSkills,
|
|
selectedTargetInfo.InstallState);
|
|
_installSkillsButton.SetEnabled(CliSetupSection.IsInstallSkillsButtonEnabled(
|
|
isCliInstalled: true,
|
|
_isInstallingSkills,
|
|
isChecking: false,
|
|
selectedTargetInfo.InstallState));
|
|
return;
|
|
}
|
|
|
|
foreach (ToolSkillSynchronizer.SkillTargetInfo target in installableTargets)
|
|
{
|
|
VisualElement item = new VisualElement();
|
|
item.AddToClassList("setup-target-item");
|
|
|
|
Label nameLabel = new Label($"{target.DisplayName} ({target.DirName}/)");
|
|
nameLabel.AddToClassList("setup-target-item__label");
|
|
item.Add(nameLabel);
|
|
|
|
Label statusLabel = new Label(GetSkillInstallStatusText(
|
|
target.InstallState,
|
|
target.HasDifferentLayoutSkills,
|
|
!_installSkillsFlat));
|
|
statusLabel.AddToClassList("setup-target-item__status");
|
|
statusLabel.AddToClassList(GetSkillInstallStatusClass(
|
|
target.InstallState,
|
|
target.HasDifferentLayoutSkills,
|
|
!_installSkillsFlat));
|
|
item.Add(statusLabel);
|
|
|
|
_skillsTargetList.Add(item);
|
|
}
|
|
|
|
bool isCheckingSkills = installableTargets.Any(
|
|
t => t.InstallState == SkillInstallState.Checking);
|
|
if (isCheckingSkills)
|
|
{
|
|
UpdateSkillsStatusLabel("Checking installed skills...");
|
|
_installSkillsButton.SetEnabled(false);
|
|
_installSkillsButton.text = "Checking...";
|
|
return;
|
|
}
|
|
|
|
bool allSkillsInstalled = installableTargets.All(
|
|
t => t.InstallState == SkillInstallState.Installed);
|
|
if (allSkillsInstalled)
|
|
{
|
|
UpdateSkillsStatusLabel($"Installed for {installableTargets.Count} targets");
|
|
_installSkillsButton.SetEnabled(false);
|
|
_installSkillsButton.text = "Installed";
|
|
}
|
|
else
|
|
{
|
|
bool hasOutdatedSkills = installableTargets.Any(
|
|
t => t.InstallState == SkillInstallState.Outdated);
|
|
UpdateSkillsStatusLabel(string.Empty);
|
|
_installSkillsButton.SetEnabled(!_isInstallingSkills);
|
|
_installSkillsButton.text = GetSkillsButtonTextForSetupWizard(
|
|
cliInstalled: true,
|
|
_isInstallingSkills,
|
|
hasOutdatedSkills);
|
|
}
|
|
}
|
|
|
|
internal static string GetSkillsButtonTextForSetupWizard(
|
|
bool cliInstalled,
|
|
bool isInstallingSkills,
|
|
bool hasOutdatedSkills)
|
|
{
|
|
return !cliInstalled
|
|
? "Install Skills"
|
|
: GetInstallSkillsButtonText(isInstallingSkills, hasOutdatedSkills);
|
|
}
|
|
|
|
internal static string GetInstallSkillsButtonText(
|
|
bool isInstallingSkills,
|
|
bool hasOutdatedSkills)
|
|
{
|
|
if (isInstallingSkills)
|
|
{
|
|
return "Installing...";
|
|
}
|
|
|
|
return hasOutdatedSkills ? "Update Skills" : "Install Skills";
|
|
}
|
|
|
|
internal static string GetSkillInstallStatusText(
|
|
SkillInstallState installState,
|
|
bool hasDifferentLayoutSkills,
|
|
bool groupSkillsUnderUnityCliLoop)
|
|
{
|
|
if (installState == SkillInstallState.Checking)
|
|
{
|
|
return "Checking...";
|
|
}
|
|
|
|
if (installState == SkillInstallState.Installed)
|
|
{
|
|
return "Installed";
|
|
}
|
|
|
|
if (installState == SkillInstallState.Outdated)
|
|
{
|
|
return "Outdated";
|
|
}
|
|
|
|
if (!hasDifferentLayoutSkills)
|
|
{
|
|
return "Missing";
|
|
}
|
|
|
|
return groupSkillsUnderUnityCliLoop ? "Not grouped" : "Grouped";
|
|
}
|
|
|
|
internal static string GetSkillInstallStatusClass(
|
|
SkillInstallState installState,
|
|
bool hasDifferentLayoutSkills,
|
|
bool groupSkillsUnderUnityCliLoop)
|
|
{
|
|
if (installState == SkillInstallState.Checking)
|
|
{
|
|
return "setup-target-item__status--checking";
|
|
}
|
|
|
|
if (installState == SkillInstallState.Installed)
|
|
{
|
|
return "setup-target-item__status--installed";
|
|
}
|
|
|
|
if (installState == SkillInstallState.Outdated)
|
|
{
|
|
return "setup-target-item__status--outdated";
|
|
}
|
|
|
|
if (!hasDifferentLayoutSkills)
|
|
{
|
|
return "setup-target-item__status--missing";
|
|
}
|
|
|
|
return groupSkillsUnderUnityCliLoop
|
|
? "setup-target-item__status--different-layout"
|
|
: "setup-target-item__status--different-layout";
|
|
}
|
|
|
|
private async void HandleInstallCli()
|
|
{
|
|
bool wasCliInstalledBeforeInstall = CliInstallationDetector.IsCliInstalled();
|
|
string npmPath = NodeEnvironmentResolver.FindNpmPath();
|
|
if (string.IsNullOrEmpty(npmPath))
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"npm Not Found",
|
|
"npm was not found on this system.\nPlease install Node.js first, then try again.",
|
|
"OK");
|
|
return;
|
|
}
|
|
|
|
string packageVersion = McpConstants.PackageInfo.version;
|
|
string installTarget = $"{CliConstants.NPM_PACKAGE_NAME}@{packageVersion}";
|
|
|
|
bool permissionOk = CliInstaller.CheckWindowsPermissions(
|
|
npmPath, installTarget, out string globalPrefix, out string manualCommand);
|
|
if (!permissionOk)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Permission Issue",
|
|
$"npm's global directory ({globalPrefix}) requires elevated permissions.\n\n"
|
|
+ NpmInstallDiagnostics.BuildPermissionSolutions(manualCommand),
|
|
"OK");
|
|
return;
|
|
}
|
|
|
|
_isInstallingCli = true;
|
|
CancelSkillInstallStateRefresh();
|
|
UpdateCliStep(false, null, false);
|
|
RefreshSkillsSection();
|
|
|
|
try
|
|
{
|
|
string nodePath = NodeEnvironmentResolver.FindNodePath();
|
|
CliInstallResult result = await CliInstaller.InstallAsync(npmPath, installTarget, nodePath);
|
|
|
|
if (!result.Success)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Installation Failed",
|
|
$"Failed to install uloop-cli.\n\n{result.ErrorOutput}\n\n"
|
|
+ $"You can install manually:\n npm install -g {installTarget}",
|
|
"OK");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_isInstallingCli = false;
|
|
RefreshUI(CliInstallRefreshPolicy.ShouldRefreshSkillsAfterCliInstall(
|
|
wasCliInstalledBeforeInstall));
|
|
}
|
|
}
|
|
|
|
private async void HandleInstallSkills()
|
|
{
|
|
CancelSkillInstallStateRefresh();
|
|
string projectRoot = UnityMcpPathResolver.GetProjectRoot();
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> targets = DetectDisplayedSkillTargets(projectRoot);
|
|
List<ToolSkillSynchronizer.SkillTargetInfo> installableTargets =
|
|
GetSetupWizardInstallableSkillTargets(
|
|
targets,
|
|
_skillsTarget,
|
|
!_installSkillsFlat,
|
|
_shouldUseFirstInstallSkillsUi);
|
|
if (installableTargets.Count == 0) return;
|
|
|
|
_isInstallingSkills = true;
|
|
UpdateSkillsStep(true, targets);
|
|
|
|
try
|
|
{
|
|
await ToolSkillSynchronizer.InstallSkillFiles(
|
|
installableTargets,
|
|
!_installSkillsFlat);
|
|
EditorDialogHelper.ShowSkillsInstalledDialog();
|
|
}
|
|
finally
|
|
{
|
|
_isInstallingSkills = false;
|
|
RefreshSkillsSection();
|
|
}
|
|
}
|
|
|
|
private void HandleOpenSettings()
|
|
{
|
|
McpEditorWindow.ShowWindow();
|
|
}
|
|
|
|
private void HandleSuppressAutoShowChanged(bool suppressAutoShow)
|
|
{
|
|
McpEditorSettings.SetSuppressSetupWizardAutoShow(suppressAutoShow);
|
|
MaybeRecordSuppressedVersion(suppressAutoShow, McpConstants.PackageInfo.version);
|
|
ScheduleResizeToContent();
|
|
}
|
|
|
|
private void HandleClose()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
private void HandleOpenGitHub()
|
|
{
|
|
Application.OpenURL(GetGitHubRepositoryUrl());
|
|
}
|
|
|
|
private void HandleGitHubHoverChanged(bool isHovered)
|
|
{
|
|
ViewDataBinder.ToggleClass(_githubLinkRow, "setup-footer__github-link--hover", isHovered);
|
|
ViewDataBinder.ToggleClass(_githubLinkLabel, "setup-footer__github-link-label--hover", isHovered);
|
|
ViewDataBinder.ToggleClass(_githubLinkIcon, "setup-footer__github-link-icon--hover", isHovered);
|
|
}
|
|
|
|
private void HandleGroupSkillsRowClicked(ClickEvent evt)
|
|
{
|
|
evt.StopPropagation();
|
|
if (!_groupSkillsToggle.enabledSelf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (evt.target is VisualElement targetElement && _groupSkillsToggle.Contains(targetElement))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool newValue = !_groupSkillsToggle.value;
|
|
_groupSkillsToggle.SetValueWithoutNotify(newValue);
|
|
ApplyFlatSkillInstallPreference();
|
|
RefreshSkillsSection();
|
|
}
|
|
|
|
private void HandleProjectCliVersionRowClicked(ClickEvent evt)
|
|
{
|
|
evt.StopPropagation();
|
|
if (!_projectCliVersionToggle.enabledSelf)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (evt.target is VisualElement targetElement && _projectCliVersionToggle.Contains(targetElement))
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool newValue = !_projectCliVersionToggle.value;
|
|
_projectCliVersionToggle.SetValueWithoutNotify(newValue);
|
|
HandleProjectCliVersionChanged(newValue);
|
|
}
|
|
|
|
private void HandleProjectCliVersionChanged(bool useProjectCliVersion)
|
|
{
|
|
string invocation = useProjectCliVersion
|
|
? CliConstants.SKILL_CLI_INVOCATION_NPX
|
|
: CliConstants.SKILL_CLI_INVOCATION_GLOBAL;
|
|
ToolSettings.SetSkillCliInvocation(invocation);
|
|
RefreshProjectCliVersionToggle();
|
|
UpdateGlobalCliSectionVisibility();
|
|
RefreshSkillsSection();
|
|
}
|
|
|
|
private void ApplyFlatSkillInstallPreference()
|
|
{
|
|
// Claude Code does not resolve nested skill folders, so setup keeps every editor target on the flat layout.
|
|
_installSkillsFlat = ForceFlatSkillInstall;
|
|
McpEditorSettings.SetInstallSkillsFlat(_installSkillsFlat);
|
|
}
|
|
|
|
private void ScheduleResizeToContent()
|
|
{
|
|
_resizeScheduledItem?.Pause();
|
|
_resizeScheduledItem = rootVisualElement.schedule.Execute(ResizeToContent).StartingIn(0);
|
|
}
|
|
|
|
private void ResizeToContent()
|
|
{
|
|
ScrollView mainContainer = rootVisualElement.Q<ScrollView>();
|
|
if (mainContainer == null) return;
|
|
if (rootVisualElement.layout.width <= 0f || rootVisualElement.layout.height <= 0f) return;
|
|
|
|
Vector2 contentSize = MeasureContentSize(mainContainer);
|
|
if (!HasFiniteSize(contentSize)) return;
|
|
if (contentSize.x <= 0f || contentSize.y <= 0f) return;
|
|
|
|
Vector2 frameSize = position.size - rootVisualElement.layout.size;
|
|
if (!HasFiniteSize(frameSize)) return;
|
|
Rect targetRect = WithContentSize(position, contentSize, frameSize);
|
|
if (!HasFiniteSize(targetRect.size)) return;
|
|
if (Approximately(position.size, targetRect.size))
|
|
{
|
|
minSize = targetRect.size;
|
|
maxSize = targetRect.size;
|
|
return;
|
|
}
|
|
|
|
_isApplyingContentSize = true;
|
|
minSize = targetRect.size;
|
|
maxSize = targetRect.size;
|
|
position = targetRect;
|
|
_isApplyingContentSize = false;
|
|
}
|
|
|
|
private static Vector2 MeasureContentSize(ScrollView mainContainer)
|
|
{
|
|
VisualElement contentContainer = mainContainer.contentContainer;
|
|
float width = MeasurePreferredContentWidth(mainContainer, contentContainer);
|
|
float height = MeasurePreferredContentHeight(mainContainer, contentContainer);
|
|
return new Vector2(width, height);
|
|
}
|
|
|
|
private static float MeasurePreferredContentWidth(VisualElement mainContainer, VisualElement contentContainer)
|
|
{
|
|
float maxRight = 0f;
|
|
foreach (TextElement textElement in contentContainer.Query<TextElement>().Build())
|
|
{
|
|
if (!textElement.visible) continue;
|
|
if (string.IsNullOrEmpty(textElement.text)) continue;
|
|
if (!HasFiniteRect(textElement.worldBound)) continue;
|
|
|
|
float left = textElement.worldBound.xMin - contentContainer.worldBound.xMin;
|
|
float horizontalChrome =
|
|
textElement.resolvedStyle.paddingLeft
|
|
+ textElement.resolvedStyle.paddingRight
|
|
+ textElement.resolvedStyle.borderLeftWidth
|
|
+ textElement.resolvedStyle.borderRightWidth;
|
|
float verticalChrome =
|
|
textElement.resolvedStyle.paddingTop
|
|
+ textElement.resolvedStyle.paddingBottom
|
|
+ textElement.resolvedStyle.borderTopWidth
|
|
+ textElement.resolvedStyle.borderBottomWidth;
|
|
float laidOutWidth = textElement.worldBound.width;
|
|
Vector2 measuredTextSize = textElement.MeasureTextSize(
|
|
textElement.text,
|
|
0f,
|
|
VisualElement.MeasureMode.Undefined,
|
|
0f,
|
|
VisualElement.MeasureMode.Undefined);
|
|
if (!IsFinite(left)) continue;
|
|
if (!IsFinite(horizontalChrome) || !IsFinite(verticalChrome)) continue;
|
|
if (!HasFiniteSize(measuredTextSize)) continue;
|
|
if (!IsFinite(laidOutWidth)) continue;
|
|
float measuredWidth = measuredTextSize.x + horizontalChrome;
|
|
int lineCount = EstimateWrappedLineCount(
|
|
textElement.worldBound.height - verticalChrome,
|
|
measuredTextSize.y);
|
|
float preferredWidth = SelectPreferredTextWidth(
|
|
laidOutWidth,
|
|
measuredWidth,
|
|
lineCount,
|
|
textElement.resolvedStyle.whiteSpace);
|
|
if (!IsFinite(preferredWidth)) continue;
|
|
float right = left + preferredWidth;
|
|
maxRight = Mathf.Max(maxRight, right);
|
|
}
|
|
|
|
float width =
|
|
mainContainer.resolvedStyle.paddingLeft
|
|
+ maxRight
|
|
+ mainContainer.resolvedStyle.paddingRight;
|
|
return IsFinite(width) ? Mathf.Ceil(width) : 0f;
|
|
}
|
|
|
|
internal static int EstimateWrappedLineCount(float laidOutTextHeight, float singleLineTextHeight)
|
|
{
|
|
if (singleLineTextHeight <= 0f) return 1;
|
|
|
|
return Mathf.Max(1, Mathf.RoundToInt(laidOutTextHeight / singleLineTextHeight));
|
|
}
|
|
|
|
internal static float SelectPreferredTextWidth(
|
|
float laidOutWidth,
|
|
float measuredWidth,
|
|
int lineCount,
|
|
WhiteSpace whiteSpace)
|
|
{
|
|
if (whiteSpace != WhiteSpace.Normal) return measuredWidth;
|
|
if (lineCount <= PreferredWrappedTextLineCount) return Mathf.Min(laidOutWidth, measuredWidth);
|
|
|
|
return Mathf.Max(laidOutWidth, measuredWidth / PreferredWrappedTextLineCount);
|
|
}
|
|
|
|
private static float MeasurePreferredContentHeight(VisualElement mainContainer, VisualElement contentContainer)
|
|
{
|
|
float maxBottom = 0f;
|
|
foreach (VisualElement child in contentContainer.Children())
|
|
{
|
|
if (!child.visible) continue;
|
|
if (!HasFiniteRect(child.worldBound)) continue;
|
|
float bottom = child.worldBound.yMax - contentContainer.worldBound.yMin;
|
|
if (!IsFinite(bottom)) continue;
|
|
maxBottom = Mathf.Max(maxBottom, bottom);
|
|
}
|
|
|
|
float height =
|
|
mainContainer.resolvedStyle.paddingTop
|
|
+ maxBottom
|
|
+ mainContainer.resolvedStyle.paddingBottom;
|
|
return IsFinite(height) ? Mathf.Ceil(height) : 0f;
|
|
}
|
|
|
|
private static bool Approximately(Vector2 left, Vector2 right)
|
|
{
|
|
const float Tolerance = 0.5f;
|
|
return Mathf.Abs(left.x - right.x) < Tolerance && Mathf.Abs(left.y - right.y) < Tolerance;
|
|
}
|
|
|
|
internal static bool HasFiniteSize(Vector2 size)
|
|
{
|
|
return IsFinite(size.x) && IsFinite(size.y);
|
|
}
|
|
|
|
private static bool HasFiniteRect(Rect rect)
|
|
{
|
|
return IsFinite(rect.xMin)
|
|
&& IsFinite(rect.xMax)
|
|
&& IsFinite(rect.yMin)
|
|
&& IsFinite(rect.yMax);
|
|
}
|
|
|
|
private static bool IsFinite(float value)
|
|
{
|
|
return !float.IsNaN(value) && !float.IsInfinity(value);
|
|
}
|
|
|
|
}
|
|
}
|