diff --git a/Assets/Tests/Editor/SettingsPresentationDecisionPolicyTests.cs b/Assets/Tests/Editor/SettingsPresentationDecisionPolicyTests.cs new file mode 100644 index 0000000000..ba7a41fc5a --- /dev/null +++ b/Assets/Tests/Editor/SettingsPresentationDecisionPolicyTests.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies UnityEditor-independent Settings decision policies extracted for Presentation Move Method. + /// + public class SettingsPresentationDecisionPolicyTests + { + [TestCase(false, true, true)] + [TestCase(false, false, false)] + [TestCase(true, true, false)] + [TestCase(true, false, false)] + public void CliPathSetupCheckPolicy_ShouldCheck_UsesWindowsAndOwnership( + bool isWindowsEditor, + bool hasPackageOwnedCurrentUserInstall, + bool expected) + { + // Verifies PATH checks stay off for Windows and non package-owned installs. + bool result = CliPathSetupCheckPolicy.ShouldCheck( + isWindowsEditor, + hasPackageOwnedCurrentUserInstall); + + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void SkillInstallDialogPolicy_ShouldShowForInstallableTargets_RequiresAllEligible() + { + // Verifies wizard dialog policy requires every target to be first-install eligible. + SkillSetupTargetInfo[] targets = + { + CreateSkillTarget(SkillInstallState.Missing, false), + CreateSkillTarget(SkillInstallState.Outdated, false) + }; + + bool result = SkillInstallDialogPolicy.ShouldShowForInstallableTargets(targets); + + Assert.That(result, Is.False); + } + + private static SkillSetupTargetInfo CreateSkillTarget( + SkillInstallState installState, + bool hasDifferentLayoutSkills) + { + return new SkillSetupTargetInfo( + "Claude Code", + ".claude", + "--claude", + hasSkillsDirectory: true, + hasExistingSkills: true, + hasDifferentLayoutSkills, + installState); + } + } +} diff --git a/Assets/Tests/Editor/SettingsPresentationDecisionPolicyTests.cs.meta b/Assets/Tests/Editor/SettingsPresentationDecisionPolicyTests.cs.meta new file mode 100644 index 0000000000..6266ff9fd2 --- /dev/null +++ b/Assets/Tests/Editor/SettingsPresentationDecisionPolicyTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fac85b7f95dfb4a3683922dfaea778f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/UnityCliLoopSettingsWindowCliActionTests.cs b/Assets/Tests/Editor/UnityCliLoopSettingsWindowCliActionTests.cs index d8b9b093c4..4f8cf14bc7 100644 --- a/Assets/Tests/Editor/UnityCliLoopSettingsWindowCliActionTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopSettingsWindowCliActionTests.cs @@ -27,7 +27,7 @@ public void ShouldUninstallCliFromPrimaryButton_ReturnsExpectedAction( bool expected) { // Verifies that package-owned installs route to uninstall only when the dispatcher minimum is satisfied. - bool result = UnityCliLoopSettingsWindow.ShouldUninstallCliFromPrimaryButton( + bool result = UnityCliLoopSettingsCliSetupPresenter.ShouldUninstallCliFromPrimaryButton( cliVersion, cliIsDispatcher, canUninstallCli, @@ -45,7 +45,7 @@ public void ShouldRepairCliPathFromPrimaryButton_ReturnsExpectedAction( bool expected) { // Verifies that stale terminal PATH state routes to repair only when CLI replacement is not needed. - bool result = UnityCliLoopSettingsWindow.ShouldRepairCliPathFromPrimaryButton( + bool result = UnityCliLoopSettingsCliSetupPresenter.ShouldRepairCliPathFromPrimaryButton( needsCliPathSetup, needsUpdate); @@ -70,7 +70,7 @@ public void ResolveCliPrimaryButtonAction_ReturnsClickedPrimaryAction( { // Verifies that the Settings window chooses repair only when dispatcher replacement is unnecessary. CliSetupPrimaryAction result = - UnityCliLoopSettingsWindow.ResolveCliPrimaryButtonAction( + UnityCliLoopSettingsCliSetupPresenter.ResolveCliPrimaryButtonAction( needsCliPathSetup, cliVersion, cliIsDispatcher, @@ -94,7 +94,7 @@ public void ResolveExecutableCliPrimaryButtonAction_IgnoresUnsafeStaleActions( { // Verifies that a refreshed Settings state cannot turn a stale click into a destructive action. CliSetupPrimaryAction result = - UnityCliLoopSettingsWindow.ResolveExecutableCliPrimaryButtonAction( + UnityCliLoopSettingsCliSetupPresenter.ResolveExecutableCliPrimaryButtonAction( ParseAction(clickedAction), ParseAction(refreshedAction)); @@ -111,7 +111,7 @@ public void ShouldCheckCliPathSetupForPlatform_RequiresPackageOwnedCli( bool expected) { // Verifies that PATH repair only runs for POSIX package-owned current-user installs. - bool result = UnityCliLoopSettingsWindow.ShouldCheckCliPathSetupForPlatform( + bool result = UnityCliLoopSettingsCliSetupPresenter.ShouldCheckCliPathSetupForPlatform( platform, hasPackageOwnedCurrentUserInstall); @@ -131,7 +131,7 @@ public void IsCliUpdateNeeded_UsesDispatcherMinimumVersion( bool expected) { // Verifies that the settings UI updates non-dispatcher or older dispatcher installs. - bool result = UnityCliLoopSettingsWindow.IsCliUpdateNeeded( + bool result = UnityCliLoopSettingsCliSetupPresenter.IsCliUpdateNeeded( cliVersion, cliIsDispatcher, TestMinimumDispatcherVersion); @@ -150,7 +150,7 @@ public void ShouldShowSkillsInstalledDialog_ReturnsExpectedValue( // Verifies that Settings keeps the success dialog for first install only. SkillSetupTargetInfo targetInfo = CreateSkillTarget(installState, hasDifferentLayoutSkills); - bool result = UnityCliLoopSettingsWindow.ShouldShowSkillsInstalledDialog(targetInfo); + bool result = SkillInstallDialogPolicy.ShouldShowForSelectedTarget(targetInfo); Assert.That(result, Is.EqualTo(expected)); } diff --git a/Packages/src/Editor/Domain/CliPathSetupCheckPolicy.cs b/Packages/src/Editor/Domain/CliPathSetupCheckPolicy.cs new file mode 100644 index 0000000000..0de9a24b31 --- /dev/null +++ b/Packages/src/Editor/Domain/CliPathSetupCheckPolicy.cs @@ -0,0 +1,18 @@ +namespace io.github.hatayama.UnityCliLoop.Domain +{ + /// + /// Decides when Settings should run CLI PATH visibility checks. + /// + public static class CliPathSetupCheckPolicy + { + /// + /// PATH repair checks run only for non-Windows package-owned current-user installs. + /// + public static bool ShouldCheck( + bool isWindowsEditor, + bool hasPackageOwnedCurrentUserInstall) + { + return !isWindowsEditor && hasPackageOwnedCurrentUserInstall; + } + } +} diff --git a/Packages/src/Editor/Domain/CliPathSetupCheckPolicy.cs.meta b/Packages/src/Editor/Domain/CliPathSetupCheckPolicy.cs.meta new file mode 100644 index 0000000000..c421e6e9a1 --- /dev/null +++ b/Packages/src/Editor/Domain/CliPathSetupCheckPolicy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dbe22adc4dab44338bd089a25d15c8fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Domain/SkillInstallDialogPolicy.cs b/Packages/src/Editor/Domain/SkillInstallDialogPolicy.cs new file mode 100644 index 0000000000..2ad5853342 --- /dev/null +++ b/Packages/src/Editor/Domain/SkillInstallDialogPolicy.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace io.github.hatayama.UnityCliLoop.Domain +{ + /// + /// Decides when the skills-installed success dialog should appear after an install attempt. + /// + public static class SkillInstallDialogPolicy + { + /// + /// Returns true when Settings should show the success dialog for a single selected target. + /// + public static bool ShouldShowForSelectedTarget(SkillSetupTargetInfo targetInfo) + { + return targetInfo.InstallState != SkillInstallState.Outdated + && !targetInfo.HasDifferentLayoutSkills; + } + + /// + /// Returns true when Setup Wizard should show the success dialog for all installable targets. + /// + public static bool ShouldShowForInstallableTargets(IEnumerable targets) + { + Debug.Assert(targets != null, "targets must not be null"); + return targets.All(ShouldShowForSelectedTarget); + } + } +} diff --git a/Packages/src/Editor/Domain/SkillInstallDialogPolicy.cs.meta b/Packages/src/Editor/Domain/SkillInstallDialogPolicy.cs.meta new file mode 100644 index 0000000000..1b99a4478b --- /dev/null +++ b/Packages/src/Editor/Domain/SkillInstallDialogPolicy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb88c5b20f2e34bab93527cb64cbcda7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Presentation/Setup/SetupWizardStartupFlow.cs b/Packages/src/Editor/Presentation/Setup/SetupWizardStartupFlow.cs index d0cf8aaeaa..68c372e24b 100644 --- a/Packages/src/Editor/Presentation/Setup/SetupWizardStartupFlow.cs +++ b/Packages/src/Editor/Presentation/Setup/SetupWizardStartupFlow.cs @@ -154,7 +154,7 @@ internal static bool HasSkillUpdateForSetupWizard(IEnumerable targets) { - Debug.Assert(targets != null, "targets must not be null"); - return targets.All(target => - target.InstallState != SkillInstallState.Outdated - && !target.HasDifferentLayoutSkills); + return SkillInstallDialogPolicy.ShouldShowForInstallableTargets(targets); } internal static bool ShouldUseFirstInstallSkillsUi(string lastSeenSetupWizardVersion) @@ -579,8 +576,9 @@ internal static bool ShouldCheckCliPathSetupForSetupWizard( RuntimePlatform platform, bool hasPackageOwnedCurrentUserInstall) { - return platform != RuntimePlatform.WindowsEditor - && hasPackageOwnedCurrentUserInstall; + return CliPathSetupCheckPolicy.ShouldCheck( + isWindowsEditor: platform == RuntimePlatform.WindowsEditor, + hasPackageOwnedCurrentUserInstall); } private static CliSetupCompatibilityState EvaluateCliSetupCompatibilityForSetupWizard( @@ -762,7 +760,8 @@ private async Task HandleInstallSkillsAsync(CancellationToken ct) : SetupWizardSkillsStepPresenter.FilterInstallableSkillTargets(targets); if (installableTargets.Count == 0) return; - bool shouldShowSkillsInstalledDialog = ShouldShowSkillsInstalledDialog(installableTargets); + bool shouldShowSkillsInstalledDialog = + SkillInstallDialogPolicy.ShouldShowForInstallableTargets(installableTargets); _isInstallingSkills = true; UpdateSkillsStep(true, targets); diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs index dc9a8142f9..fbd20b0aca 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardWindow.cs @@ -379,10 +379,10 @@ private void CreateGUI() InitializeApplicationServices(); _view = ThirdPartyToolMigrationWizardView.Create( rootVisualElement, - RefreshUI, - HandleMigrateThirdPartyTools, + () => RefreshUI().Forget(), + () => HandleMigrateThirdPartyTools().Forget(), HandleMigrationSkillTargetChanged, - HandleToggleMigrationSkill, + () => HandleToggleMigrationSkill().Forget(), Close); _resizer = new ThirdPartyToolMigrationWizardWindowResizer(this, _view.MainScrollView); _resizer.BindSizeUpdates(); @@ -425,10 +425,10 @@ private void ScheduleInitialRefresh(bool shouldStartInitialRefresh) return; } - rootVisualElement.schedule.Execute(RefreshUI).StartingIn(0); + rootVisualElement.schedule.Execute(() => RefreshUI().Forget()).StartingIn(0); } - private async void RefreshUI() + private async Task RefreshUI() { CancellationToken ct = BeginMigrationOperation(); ShowCheckingState(new ThirdPartyToolMigrationProgress(0, 0)); @@ -477,7 +477,7 @@ private async void RefreshUI() ShowMigrationTargetsState(preview.FilePaths); } - private async void HandleMigrateThirdPartyTools() + private async Task HandleMigrateThirdPartyTools() { if (!ConfirmMigrationApply( _pendingMigrationFilePaths.Length, @@ -546,7 +546,7 @@ private async void HandleMigrateThirdPartyTools() CompleteMigrationOperation(ct); if (shouldRefreshAfterInterruptedMigration) { - RefreshUI(); + await RefreshUI(); } } @@ -557,7 +557,7 @@ private async void HandleMigrateThirdPartyTools() if (ShouldRefreshAfterMigration(result)) { - RefreshUI(); + await RefreshUI(); return; } @@ -626,7 +626,7 @@ private void HandleMigrationSkillTargetChanged(SkillsTarget target) RefreshMigrationSkillState(); } - private async void HandleToggleMigrationSkill() + private async Task HandleToggleMigrationSkill() { CancellationToken ct = BeginMigrationSkillOperation(); string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); @@ -767,7 +767,7 @@ private void TryStartInitialRefresh() } ShowCheckingState(new ThirdPartyToolMigrationProgress(0, 0)); - rootVisualElement.schedule.Execute(RefreshUI).StartingIn(0); + rootVisualElement.schedule.Execute(() => RefreshUI().Forget()).StartingIn(0); } private void CompleteMigrationOperation(CancellationToken ct) diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsCliSetupPresenter.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsCliSetupPresenter.cs index ae666dc9da..f1a0057cfe 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsCliSetupPresenter.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsCliSetupPresenter.cs @@ -7,7 +7,7 @@ namespace io.github.hatayama.UnityCliLoop.Presentation { /// - /// Presents the CLI setup section in the Unity CLI Loop settings window. + /// Presents the CLI setup section and owns Settings primary-action mediation decisions. /// internal sealed class UnityCliLoopSettingsCliSetupPresenter { @@ -50,6 +50,89 @@ internal void Update( _view.UpdateCliSetup(cliData); } + internal CliSetupPrimaryAction ResolveCurrentPrimaryButtonAction(bool needsCliPathSetup) + { + string cliVersion = _cliSetupApplicationService.GetCachedCliVersion(); + bool cliIsDispatcher = _cliSetupApplicationService.GetCachedCliIsDispatcher(); + string cliExecutablePath = _cliSetupApplicationService.GetCachedCliExecutablePath(); + bool canUninstallCli = _cliSetupApplicationService.IsPackageOwnedCurrentUserInstallPath( + cliExecutablePath, + UnityEngine.Application.platform); + string requiredCliVersion = _cliSetupApplicationService.GetMinimumRequiredCliVersion(); + return ResolveCliPrimaryButtonAction( + needsCliPathSetup, + cliVersion, + cliIsDispatcher, + canUninstallCli, + requiredCliVersion); + } + + internal static bool ShouldUninstallCliFromPrimaryButton( + string cliVersion, + bool cliIsDispatcher, + bool canUninstallCli, + string requiredCliVersion) + { + bool isCliInstalled = cliVersion != null; + bool needsUpdate = IsCliUpdateNeeded(cliVersion, cliIsDispatcher, requiredCliVersion); + return CliSetupPrimaryActionPolicy.ShouldUninstallCli( + isCliInstalled, + needsUpdate, + canUninstallCli); + } + + internal static CliSetupPrimaryAction ResolveCliPrimaryButtonAction( + bool needsCliPathSetup, + string cliVersion, + bool cliIsDispatcher, + bool canUninstallCli, + string requiredCliVersion) + { + bool needsUpdate = IsCliUpdateNeeded(cliVersion, cliIsDispatcher, requiredCliVersion); + bool isCliInstalled = cliVersion != null; + return CliSetupPrimaryActionPolicy.ResolveSettingsPrimaryAction( + needsCliPathSetup, + needsUpdate, + isCliInstalled, + canUninstallCli); + } + + internal static CliSetupPrimaryAction ResolveExecutableCliPrimaryButtonAction( + CliSetupPrimaryAction clickedAction, + CliSetupPrimaryAction refreshedAction) + { + return CliSetupPrimaryActionPolicy.ResolveExecutableSettingsAction( + clickedAction, + refreshedAction); + } + + internal static bool ShouldRepairCliPathFromPrimaryButton( + bool needsCliPathSetup, + bool needsUpdate) + { + return CliSetupPrimaryActionPolicy.ShouldRepairCliPath(needsCliPathSetup, needsUpdate); + } + + internal static bool ShouldCheckCliPathSetupForPlatform( + RuntimePlatform platform, + bool hasPackageOwnedCurrentUserInstall) + { + return CliPathSetupCheckPolicy.ShouldCheck( + isWindowsEditor: platform == RuntimePlatform.WindowsEditor, + hasPackageOwnedCurrentUserInstall); + } + + internal static bool IsCliUpdateNeeded( + string cliVersion, + bool cliIsDispatcher, + string requiredCliVersion) + { + return CliSetupCompatibility.Evaluate( + cliVersion, + cliIsDispatcher, + requiredCliVersion).NeedsUpdate; + } + private CliSetupData CreateCliSetupData( bool needsCliPathSetup, bool isInstallingCli, diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsToolSettingsPresenter.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsToolSettingsPresenter.cs index 477f89d5a3..929b81640b 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsToolSettingsPresenter.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsToolSettingsPresenter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using UnityEditor; using UnityEngine; using io.github.hatayama.UnityCliLoop.Application; @@ -7,13 +8,24 @@ namespace io.github.hatayama.UnityCliLoop.Presentation { /// - /// Presents the Tool Settings section in the Unity CLI Loop settings window. + /// Presents the Tool Settings section and owns catalog cache / registry warmup. /// internal sealed class UnityCliLoopSettingsToolSettingsPresenter { + private const double RegistryWarmupInitialDelaySeconds = 0.05; + private const double RegistryWarmupMaxDelaySeconds = 0.8; + private const int RegistryWarmupMaxAttempts = 5; + private readonly UnityCliLoopSettingsWindowUI _view; private readonly ToolSettingsUseCase _toolSettingsUseCase; + private bool _isCatalogDirty = true; + private bool _isRegistryWarmupScheduled; + private double _registryWarmupDueTime; + private int _registryWarmupAttemptCount; + private bool _showToolSettings = true; + private bool _isViewReady; + internal UnityCliLoopSettingsToolSettingsPresenter( UnityCliLoopSettingsWindowUI view, ToolSettingsUseCase toolSettingsUseCase) @@ -26,25 +38,138 @@ internal UnityCliLoopSettingsToolSettingsPresenter( ?? throw new ArgumentNullException(nameof(toolSettingsUseCase)); } + internal void SetViewReady(bool isViewReady) + { + _isViewReady = isViewReady; + if (!isViewReady) + { + CancelRegistryWarmup(); + } + } + + internal void InvalidateCatalog() + { + _isCatalogDirty = true; + } + internal void UpdateHeader(bool showToolSettings) { + _showToolSettings = showToolSettings; ToolSettingsSectionData toolSettingsData = CreateToolSettingsHeaderData(showToolSettings); _view.UpdateToolSettings(toolSettingsData); } - internal ToolSettingsSectionData UpdateCatalog(bool showToolSettings) + internal void RefreshCatalogIfNeeded(bool showToolSettings) + { + _showToolSettings = showToolSettings; + if (!showToolSettings || !_isCatalogDirty || !_isViewReady) + { + return; + } + + RefreshCatalog(showToolSettings); + } + + internal void HandleShowToolSettingsChanged(bool show) + { + UpdateHeader(show); + if (!show) + { + _isCatalogDirty = true; + CancelRegistryWarmup(); + ResetRegistryWarmupAttemptCount(); + return; + } + + RefreshCatalogIfNeeded(show); + } + + internal void CancelRegistryWarmup() + { + if (!_isRegistryWarmupScheduled) + { + return; + } + + EditorApplication.update -= RunRegistryWarmupWhenDue; + _isRegistryWarmupScheduled = false; + } + + internal void ResetRegistryWarmupAttemptCount() + { + _registryWarmupAttemptCount = 0; + } + + private void RefreshCatalog(bool showToolSettings) { ToolSettingsSectionData toolSettingsData = CreateToolSettingsData(showToolSettings); _view.UpdateToolSettings(toolSettingsData); - return toolSettingsData; + + if (UnityCliLoopSettingsWindowRefreshPolicy.ShouldKeepToolSettingsCatalogDirty(toolSettingsData)) + { + if (ScheduleRegistryWarmup()) + { + _isCatalogDirty = true; + return; + } + + _isCatalogDirty = false; + return; + } + + CancelRegistryWarmup(); + ResetRegistryWarmupAttemptCount(); + _isCatalogDirty = false; + } + + private bool ScheduleRegistryWarmup() + { + if (UnityCliLoopSettingsWindowRefreshPolicy.ShouldStartToolSettingsRegistryWarmup( + _isRegistryWarmupScheduled, + _registryWarmupAttemptCount, + RegistryWarmupMaxAttempts)) + { + double delaySeconds = UnityCliLoopSettingsWindowRefreshPolicy.CalculateToolSettingsRegistryWarmupDelaySeconds( + RegistryWarmupInitialDelaySeconds, + RegistryWarmupMaxDelaySeconds, + _registryWarmupAttemptCount); + + _isRegistryWarmupScheduled = true; + _registryWarmupDueTime = EditorApplication.timeSinceStartup + delaySeconds; + _registryWarmupAttemptCount++; + EditorApplication.update += RunRegistryWarmupWhenDue; + return true; + } + + return _isRegistryWarmupScheduled; + } + + private void RunRegistryWarmupWhenDue() + { + if (EditorApplication.timeSinceStartup < _registryWarmupDueTime) + { + return; + } + + CancelRegistryWarmup(); + + if (!_isViewReady || !_showToolSettings) + { + ResetRegistryWarmupAttemptCount(); + return; + } + + _toolSettingsUseCase.WarmupRegistry(); + InvalidateCatalog(); + RefreshCatalogIfNeeded(_showToolSettings); } private static ToolSettingsSectionData CreateToolSettingsHeaderData(bool showToolSettings) { return new ToolSettingsSectionData( showToolSettings, - System.Array.Empty(), - System.Array.Empty(), + Array.Empty(), + Array.Empty(), true, false); } @@ -58,8 +183,8 @@ private ToolSettingsSectionData CreateToolSettingsData(bool showToolSettings) { return new ToolSettingsSectionData( showToolSettings, - System.Array.Empty(), - System.Array.Empty(), + Array.Empty(), + Array.Empty(), false, true); } diff --git a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs index 0b4069b1c1..0fec5b81c6 100644 --- a/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs +++ b/Packages/src/Editor/Presentation/UnityCliLoopSettingsWindow.cs @@ -19,9 +19,6 @@ public class UnityCliLoopSettingsWindow : EditorWindow { private const bool ForceFlatSkillInstall = true; private const double DeferredInitialRefreshDelaySeconds = 0.05; - private const double ToolSettingsRegistryWarmupInitialDelaySeconds = 0.05; - private const double ToolSettingsRegistryWarmupMaxDelaySeconds = 0.8; - private const int ToolSettingsRegistryWarmupMaxAttempts = 5; private static IUnityCliLoopEditorSettingsPort RegisteredEditorSettingsPort; private static ISessionFlagsRepository RegisteredSessionFlagsRepository; @@ -49,13 +46,9 @@ public class UnityCliLoopSettingsWindow : EditorWindow private bool _isRefreshingVersion; private bool _isRefreshingCliPathSetup; private bool _needsCliPathSetup; - private bool _isToolSettingsCatalogDirty = true; private bool _isDeferredInitialRefreshScheduled; private bool _hasCompletedDeferredInitialRefresh; private double _deferredInitialRefreshDueTime; - private bool _isToolSettingsRegistryWarmupScheduled; - private double _toolSettingsRegistryWarmupDueTime; - private int _toolSettingsRegistryWarmupAttemptCount; private SkillInstallState _selectedTargetInstallState = SkillInstallState.Missing; private CancellationTokenSource _skillInstallStateRefreshCts; @@ -107,9 +100,10 @@ private void OnEnable() private void OnDestroy() { CancelDeferredInitialRefresh(); - CancelToolSettingsRegistryWarmup(); - ResetToolSettingsRegistryWarmupAttemptCount(); + _toolSettingsPresenter?.CancelRegistryWarmup(); + _toolSettingsPresenter?.ResetRegistryWarmupAttemptCount(); CancelSkillInstallStateRefresh(); + _toolSettingsPresenter?.SetViewReady(false); _view?.Dispose(); _view = null; _cliSetupPresenter = null; @@ -129,8 +123,8 @@ private void InitializeAll() InitializeModel(); InitializeEventHandler(); LoadSavedSettings(); - RestoreSessionState(); - HandlePostCompileMode(); + _model.LoadFromSessionState(); + HandlePostCompileMode().Forget(); } private void InitializeModel() @@ -159,14 +153,15 @@ private void InitializeView() _toolSettingsPresenter = new UnityCliLoopSettingsToolSettingsPresenter( _view, _toolSettingsUseCase); + _toolSettingsPresenter.SetViewReady(true); SetupViewCallbacks(); } private void SetupViewCallbacks() { - _view.OnRefreshCliVersion += HandleRefreshCliVersion; - _view.OnInstallCli += HandleInstallCli; - _view.OnInstallSkills += HandleInstallSkills; + _view.OnRefreshCliVersion += () => HandleRefreshCliVersion().Forget(); + _view.OnInstallCli += () => HandleInstallCli().Forget(); + _view.OnInstallSkills += () => HandleInstallSkills().Forget(); _view.OnRefreshSkillsState += HandleRefreshSkillsState; _view.OnSkillsTargetChanged += value => { @@ -196,12 +191,7 @@ private void LoadSavedSettings() _installSkillsFlat = ForceFlatSkillInstall; } - private void RestoreSessionState() - { - _model.LoadFromSessionState(); - } - - private async void HandlePostCompileMode() + private async Task HandlePostCompileMode() { _model.EnablePostCompileMode(); _sessionFlagsRepository.SetShowReconnectingUI(false); @@ -226,11 +216,12 @@ private async void HandlePostCompileMode() private void OnDisable() { CancelDeferredInitialRefresh(); - CancelToolSettingsRegistryWarmup(); - ResetToolSettingsRegistryWarmupAttemptCount(); + _toolSettingsPresenter?.CancelRegistryWarmup(); + _toolSettingsPresenter?.ResetRegistryWarmupAttemptCount(); CancelSkillInstallStateRefresh(); CleanupEventHandler(); - SaveSessionState(); + _model?.SaveToSessionState(); + _toolSettingsPresenter?.SetViewReady(false); _view?.Dispose(); _view = null; _cliSetupPresenter = null; @@ -242,11 +233,6 @@ private void CleanupEventHandler() _eventHandler?.Cleanup(); } - private void SaveSessionState() - { - _model.SaveToSessionState(); - } - private void ScheduleDeferredInitialRefresh() { if (!UnityCliLoopSettingsWindowRefreshPolicy.ShouldScheduleDeferredInitialRefresh( @@ -324,8 +310,8 @@ internal void RefreshAllSections( if (runExpensiveChecks) { - RefreshCliVersionInBackground(); - RefreshCliPathSetupInBackground(); + RefreshCliVersionInBackground().Forget(); + RefreshCliPathSetupInBackground().Forget(); if (refreshSkillInstallState) { RefreshSelectedTargetInstallStateInBackground(); @@ -336,11 +322,11 @@ internal void RefreshAllSections( RefreshToolSettingsHeader(); if (runExpensiveChecks) { - RefreshToolSettingsCatalogIfNeeded(); + _toolSettingsPresenter?.RefreshCatalogIfNeeded(_model.UI.ShowToolSettings); } } - private async void RefreshCliVersionInBackground() + private async Task RefreshCliVersionInBackground() { if (_cliSetupApplicationService.IsCliCheckCompleted()) { @@ -348,12 +334,12 @@ private async void RefreshCliVersionInBackground() } await _cliSetupApplicationService.RefreshCliVersionAsync(CancellationToken.None); - RefreshCliPathSetupInBackground(); + RefreshCliPathSetupInBackground().Forget(); RefreshCliSetupSection(); RefreshSelectedTargetInstallStateInBackground(); } - private async void RefreshCliPathSetupInBackground() + private async Task RefreshCliPathSetupInBackground() { if (_isRefreshingCliPathSetup) { @@ -383,7 +369,7 @@ private async void RefreshCliPathSetupInBackground() } } - private async void HandleRefreshCliVersion() + private async Task HandleRefreshCliVersion() { if (_isRefreshingVersion) { @@ -398,7 +384,7 @@ private async void HandleRefreshCliVersion() Task forceRefresh = _cliSetupApplicationService.ForceRefreshCliVersionAsync(CancellationToken.None); Task minimumDelay = Task.Delay(500); await Task.WhenAll(forceRefresh, minimumDelay); - RefreshCliPathSetupInBackground(); + RefreshCliPathSetupInBackground().Forget(); } finally { @@ -409,7 +395,7 @@ private async void HandleRefreshCliVersion() public void InvalidateToolSettingsCatalog() { - _isToolSettingsCatalogDirty = true; + _toolSettingsPresenter?.InvalidateCatalog(); } private void RefreshToolSettingsHeader() @@ -422,120 +408,10 @@ private void RefreshToolSettingsHeader() _toolSettingsPresenter.UpdateHeader(_model.UI.ShowToolSettings); } - private void RefreshToolSettingsCatalog() - { - if (_toolSettingsPresenter == null) - { - return; - } - - ToolSettingsSectionData toolSettingsData = - _toolSettingsPresenter.UpdateCatalog(_model.UI.ShowToolSettings); - - if (UnityCliLoopSettingsWindowRefreshPolicy.ShouldKeepToolSettingsCatalogDirty(toolSettingsData)) - { - if (ScheduleToolSettingsRegistryWarmup()) - { - _isToolSettingsCatalogDirty = true; - return; - } - - _isToolSettingsCatalogDirty = false; - return; - } - - CancelToolSettingsRegistryWarmup(); - ResetToolSettingsRegistryWarmupAttemptCount(); - _isToolSettingsCatalogDirty = false; - } - - private void RefreshToolSettingsCatalogIfNeeded() - { - if (!_model.UI.ShowToolSettings || !_isToolSettingsCatalogDirty) - { - return; - } - - if (_view == null) - { - return; - } - - RefreshToolSettingsCatalog(); - } - private void UpdateShowToolSettings(bool show) { _model.UpdateShowToolSettings(show); - RefreshToolSettingsHeader(); - - if (!show) - { - _isToolSettingsCatalogDirty = true; - CancelToolSettingsRegistryWarmup(); - ResetToolSettingsRegistryWarmupAttemptCount(); - return; - } - - RefreshToolSettingsCatalogIfNeeded(); - } - - private bool ScheduleToolSettingsRegistryWarmup() - { - if (UnityCliLoopSettingsWindowRefreshPolicy.ShouldStartToolSettingsRegistryWarmup( - _isToolSettingsRegistryWarmupScheduled, - _toolSettingsRegistryWarmupAttemptCount, - ToolSettingsRegistryWarmupMaxAttempts)) - { - double delaySeconds = UnityCliLoopSettingsWindowRefreshPolicy.CalculateToolSettingsRegistryWarmupDelaySeconds( - ToolSettingsRegistryWarmupInitialDelaySeconds, - ToolSettingsRegistryWarmupMaxDelaySeconds, - _toolSettingsRegistryWarmupAttemptCount); - - _isToolSettingsRegistryWarmupScheduled = true; - _toolSettingsRegistryWarmupDueTime = EditorApplication.timeSinceStartup + delaySeconds; - _toolSettingsRegistryWarmupAttemptCount++; - EditorApplication.update += RunToolSettingsRegistryWarmupWhenDue; - return true; - } - - return _isToolSettingsRegistryWarmupScheduled; - } - - private void RunToolSettingsRegistryWarmupWhenDue() - { - if (EditorApplication.timeSinceStartup < _toolSettingsRegistryWarmupDueTime) - { - return; - } - - CancelToolSettingsRegistryWarmup(); - - if (_view == null || !_model.UI.ShowToolSettings) - { - ResetToolSettingsRegistryWarmupAttemptCount(); - return; - } - - _toolSettingsUseCase.WarmupRegistry(); - InvalidateToolSettingsCatalog(); - RefreshToolSettingsCatalogIfNeeded(); - } - - private void CancelToolSettingsRegistryWarmup() - { - if (!_isToolSettingsRegistryWarmupScheduled) - { - return; - } - - EditorApplication.update -= RunToolSettingsRegistryWarmupWhenDue; - _isToolSettingsRegistryWarmupScheduled = false; - } - - private void ResetToolSettingsRegistryWarmupAttemptCount() - { - _toolSettingsRegistryWarmupAttemptCount = 0; + _toolSettingsPresenter?.HandleShowToolSettingsChanged(show); } private void HandleToolToggled(string toolName, bool enabled) @@ -544,10 +420,10 @@ private void HandleToolToggled(string toolName, bool enabled) _view?.UpdateSingleToolToggle(toolName, enabled); // Skill synchronization can touch many files, so defer it to keep UI input responsive. - EditorApplication.delayCall += () => ApplyToolToggleSideEffects(toolName, enabled); + EditorApplication.delayCall += () => ApplyToolToggleSideEffects(toolName, enabled).Forget(); } - private async void ApplyToolToggleSideEffects(string toolName, bool enabled) + private async Task ApplyToolToggleSideEffects(string toolName, bool enabled) { if (!enabled) { @@ -597,18 +473,11 @@ private void RefreshCliSetupSection(bool includeSkillDirectoryChecks = true) private bool ShouldCheckCliPathSetup() { - return ShouldCheckCliPathSetupForPlatform( + return UnityCliLoopSettingsCliSetupPresenter.ShouldCheckCliPathSetupForPlatform( UnityEngine.Application.platform, _cliSetupApplicationService.HasPackageOwnedCurrentUserInstall(UnityEngine.Application.platform)); } - internal static bool ShouldCheckCliPathSetupForPlatform( - RuntimePlatform platform, - bool hasPackageOwnedCurrentUserInstall) - { - return platform != RuntimePlatform.WindowsEditor && hasPackageOwnedCurrentUserInstall; - } - private void RefreshSelectedTargetInstallStateFast() { if (!_cliSetupApplicationService.IsCliInstalled()) @@ -646,10 +515,10 @@ private void RefreshSelectedTargetInstallStateInBackground(bool allowDuringCliRe CancellationTokenSource cts = new(); _skillInstallStateRefreshCts = cts; - RefreshSelectedTargetInstallStateAsync(cts.Token); + RefreshSelectedTargetInstallStateAsync(cts.Token).Forget(); } - private async void RefreshSelectedTargetInstallStateAsync(CancellationToken ct) + private async Task RefreshSelectedTargetInstallStateAsync(CancellationToken ct) { string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); SkillInstallState installState = await Task.Run( @@ -707,15 +576,16 @@ private void CancelSkillInstallStateRefresh() _skillInstallStateRefreshCts = null; } - private async void HandleInstallCli() + private async Task HandleInstallCli() { - CliSetupPrimaryAction clickedAction = GetCurrentCliPrimaryButtonAction(); + CliSetupPrimaryAction clickedAction = _cliSetupPresenter.ResolveCurrentPrimaryButtonAction(_needsCliPathSetup); await RefreshCliPrimaryActionStateAsync(CancellationToken.None); - CliSetupPrimaryAction refreshedAction = GetCurrentCliPrimaryButtonAction(); - CliSetupPrimaryAction executableAction = ResolveExecutableCliPrimaryButtonAction( - clickedAction, - refreshedAction); + CliSetupPrimaryAction refreshedAction = _cliSetupPresenter.ResolveCurrentPrimaryButtonAction(_needsCliPathSetup); + CliSetupPrimaryAction executableAction = + UnityCliLoopSettingsCliSetupPresenter.ResolveExecutableCliPrimaryButtonAction( + clickedAction, + refreshedAction); if (executableAction == CliSetupPrimaryAction.None) { return; @@ -771,69 +641,6 @@ await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync( } } - private CliSetupPrimaryAction GetCurrentCliPrimaryButtonAction() - { - string cliVersion = _cliSetupApplicationService.GetCachedCliVersion(); - bool cliIsDispatcher = _cliSetupApplicationService.GetCachedCliIsDispatcher(); - string cliExecutablePath = _cliSetupApplicationService.GetCachedCliExecutablePath(); - bool canUninstallCli = _cliSetupApplicationService.IsPackageOwnedCurrentUserInstallPath( - cliExecutablePath, - UnityEngine.Application.platform); - string requiredCliVersion = _cliSetupApplicationService.GetMinimumRequiredCliVersion(); - return ResolveCliPrimaryButtonAction( - _needsCliPathSetup, - cliVersion, - cliIsDispatcher, - canUninstallCli, - requiredCliVersion); - } - - internal static bool ShouldUninstallCliFromPrimaryButton( - string cliVersion, - bool cliIsDispatcher, - bool canUninstallCli, - string requiredCliVersion) - { - bool isCliInstalled = cliVersion != null; - bool needsUpdate = IsCliUpdateNeeded(cliVersion, cliIsDispatcher, requiredCliVersion); - return CliSetupPrimaryActionPolicy.ShouldUninstallCli( - isCliInstalled, - needsUpdate, - canUninstallCli); - } - - internal static CliSetupPrimaryAction ResolveCliPrimaryButtonAction( - bool needsCliPathSetup, - string cliVersion, - bool cliIsDispatcher, - bool canUninstallCli, - string requiredCliVersion) - { - bool needsUpdate = IsCliUpdateNeeded(cliVersion, cliIsDispatcher, requiredCliVersion); - bool isCliInstalled = cliVersion != null; - return CliSetupPrimaryActionPolicy.ResolveSettingsPrimaryAction( - needsCliPathSetup, - needsUpdate, - isCliInstalled, - canUninstallCli); - } - - internal static CliSetupPrimaryAction ResolveExecutableCliPrimaryButtonAction( - CliSetupPrimaryAction clickedAction, - CliSetupPrimaryAction refreshedAction) - { - return CliSetupPrimaryActionPolicy.ResolveExecutableSettingsAction( - clickedAction, - refreshedAction); - } - - internal static bool ShouldRepairCliPathFromPrimaryButton( - bool needsCliPathSetup, - bool needsUpdate) - { - return CliSetupPrimaryActionPolicy.ShouldRepairCliPath(needsCliPathSetup, needsUpdate); - } - private async Task RefreshCliPrimaryActionStateAsync(CancellationToken ct) { _isRefreshingVersion = true; @@ -885,34 +692,6 @@ await CliPathSetupPrompt.EnsureVisibleAndShowResultAsync( } } - internal static bool IsCliUpdateNeeded( - string cliVersion, - bool cliIsDispatcher, - string requiredCliVersion) - { - return EvaluateCliSetupCompatibility( - cliVersion, - cliIsDispatcher, - requiredCliVersion).NeedsUpdate; - } - - internal static bool ShouldShowSkillsInstalledDialog(SkillSetupTargetInfo targetInfo) - { - return targetInfo.InstallState != SkillInstallState.Outdated - && !targetInfo.HasDifferentLayoutSkills; - } - - private static CliSetupCompatibilityState EvaluateCliSetupCompatibility( - string cliVersion, - bool cliIsDispatcher, - string requiredCliVersion) - { - return CliSetupCompatibility.Evaluate( - cliVersion, - cliIsDispatcher, - requiredCliVersion); - } - private async Task HandleUninstallCli() { if (!CliUninstallPrompt.ConfirmUninstall()) @@ -944,7 +723,7 @@ private async Task HandleUninstallCli() } } - private async void HandleInstallSkills() + private async Task HandleInstallSkills() { if (!_cliSetupApplicationService.IsCliInstalled()) { @@ -958,7 +737,8 @@ private async void HandleInstallSkills() string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); SkillSetupTargetInfo selectedTargetInfo = GetSelectedTargetInfo(projectRoot, includeFreshnessCheck: true); - bool shouldShowSkillsInstalledDialog = ShouldShowSkillsInstalledDialog(selectedTargetInfo); + bool shouldShowSkillsInstalledDialog = + SkillInstallDialogPolicy.ShouldShowForSelectedTarget(selectedTargetInfo); CancelSkillInstallStateRefresh(); _isInstallingSkills = true; RefreshCliSetupSection();