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(), shouldRecordVersion, currentVersion, FocusExistingWindow)) { return; } string lastSeenSetupWizardVersionBeforeOpen = McpEditorSettings.GetLastSeenSetupWizardVersion(); Rect windowPosition = CreateCenteredRect(EditorGUIUtility.GetMainWindowPosition(), MinimumWindowSize); SetupWizardWindow window = CreateInstance(); 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(); } // 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(uxmlPath); Debug.Assert(visualTree != null, $"UXML not found at {uxmlPath}"); visualTree.CloneTree(rootVisualElement); string ussPath = $"{McpConstants.PackageAssetPath}/{USS_RELATIVE_PATH}"; StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath(ussPath); Debug.Assert(styleSheet != null, $"USS not found at {ussPath}"); rootVisualElement.styleSheets.Add(styleSheet); } private void BindElements() { _nodejsWarning = rootVisualElement.Q("nodejs-warning"); _nodejsOk = rootVisualElement.Q("nodejs-ok"); _refreshButton = rootVisualElement.Q