diff --git a/README.md b/README.md index 85f54abd..1c102f38 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,11 @@ Click the **Install** to install the package version specified in the dropdown l Note: If the package is already installed, the currently installed version will be displayed in the upper right corner instead. If the **Install** button is disabled, it means the package is already imported by Unity. +You can also select multiple packages for installation and install them all at once. + +In addition to manual selection you can also copy a list of packageIds that you need to install, separated by new line or comma, and simply click the "Select all from clipboard" button at the top right to add them all to the selection. +Note that if the package is already installed or available in Unity it will not show up in the selected list. + The **Installed** tabs shows the packages already installed in the current Unity project. Installed Packages Tap diff --git a/docs/screenshots/online.png b/docs/screenshots/online.png index 587b172a..2e0c6bda 100644 Binary files a/docs/screenshots/online.png and b/docs/screenshots/online.png differ diff --git a/src/NuGetForUnity.Cli/Fakes/UnityPreImportedLibraryResolver.cs b/src/NuGetForUnity.Cli/Fakes/UnityPreImportedLibraryResolver.cs index 31cbe943..14c756e1 100644 --- a/src/NuGetForUnity.Cli/Fakes/UnityPreImportedLibraryResolver.cs +++ b/src/NuGetForUnity.Cli/Fakes/UnityPreImportedLibraryResolver.cs @@ -1,7 +1,5 @@ #nullable enable -using NugetForUnity.Models; - namespace NugetForUnity { /// @@ -12,10 +10,10 @@ internal static class UnityPreImportedLibraryResolver /// /// Check if a package is already imported in the Unity project e.g. is a part of Unity. /// - /// The package of witch the identifier is checked. + /// The package of witch the identifier is checked. /// Whether to log a message with the result of the check. /// If it is included in Unity. - public static bool IsAlreadyImportedInEngine(INugetPackageIdentifier package, bool log = true) + public static bool IsAlreadyImportedInEngine(string packageId, bool log = true) { // the CLI is running outside of Unity so we can't easily detect what libraries are imported by the Unity Engine. return false; diff --git a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetForUnity.Editor.Tests.asmdef b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetForUnity.Editor.Tests.asmdef index a1244f74..aa897f12 100644 --- a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetForUnity.Editor.Tests.asmdef +++ b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetForUnity.Editor.Tests.asmdef @@ -1,10 +1,10 @@ { "name": "NuGetForUnity.Editor.Tests", - "rootNamespace": "NugetForUnity.Tests", "references": [ - "NuGetForUnity", - "UnityEngine.TestRunner", - "UnityEditor.TestRunner" + "NuGetForUnity" + ], + "optionalUnityReferences": [ + "TestAssemblies" ], "includePlatforms": [ "Editor" @@ -12,13 +12,7 @@ "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, - "precompiledReferences": [ - "nunit.framework.dll" - ], - "autoReferenced": false, - "defineConstraints": [ - "UNITY_INCLUDE_TESTS" - ], - "versionDefines": [], - "noEngineReferences": false + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [] } \ No newline at end of file diff --git a/src/NuGetForUnity/Editor/InstalledPackagesManager.cs b/src/NuGetForUnity/Editor/InstalledPackagesManager.cs index ac54dca9..ebce0096 100644 --- a/src/NuGetForUnity/Editor/InstalledPackagesManager.cs +++ b/src/NuGetForUnity/Editor/InstalledPackagesManager.cs @@ -304,7 +304,7 @@ internal static bool RemoveUnnecessaryPackages() /// True if the given package is installed. False if it is not. internal static bool IsInstalled([NotNull] INugetPackageIdentifier package, bool checkIsAlreadyImportedInEngine) { - if (checkIsAlreadyImportedInEngine && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package)) + if (checkIsAlreadyImportedInEngine && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id)) { return true; } @@ -318,6 +318,22 @@ internal static bool IsInstalled([NotNull] INugetPackageIdentifier package, bool return isInstalled; } + /// + /// Checks if any version of the given package Id is installed. + /// + /// The package to check if is installed. + /// Determine if it should check if the package is already imported by unity itself. + /// True if the given package is installed. False if it is not. + internal static bool IsInstalled([NotNull] string packageId, bool checkIsAlreadyImportedInEngine) + { + if (checkIsAlreadyImportedInEngine && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(packageId)) + { + return true; + } + + return InstalledPackagesDictionary.ContainsKey(packageId); + } + /// /// Gets a list of all root packages that are installed in the project. /// Root packages are packages that are not depended on by any other package. diff --git a/src/NuGetForUnity/Editor/NugetPackageInstaller.cs b/src/NuGetForUnity/Editor/NugetPackageInstaller.cs index f3abfc92..d9014c3b 100644 --- a/src/NuGetForUnity/Editor/NugetPackageInstaller.cs +++ b/src/NuGetForUnity/Editor/NugetPackageInstaller.cs @@ -27,7 +27,7 @@ public static class NugetPackageInstaller /// True if the package was installed successfully, otherwise false. public static bool InstallIdentifier([NotNull] INugetPackageIdentifier package, bool refreshAssets = true, bool isSlimRestoreInstall = false) { - if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package, false)) + if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { NugetLogger.LogVerbose("Package {0} is already imported in engine, skipping install.", package); return true; @@ -54,7 +54,7 @@ public static bool InstallIdentifier([NotNull] INugetPackageIdentifier package, /// True if the package was installed successfully, otherwise false. private static bool Install([NotNull] INugetPackage package, bool refreshAssets, bool isSlimRestoreInstall) { - if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package, false)) + if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { NugetLogger.LogVerbose("Package {0} is already imported in engine, skipping install.", package); return true; diff --git a/src/NuGetForUnity/Editor/NugetPackageUninstaller.cs b/src/NuGetForUnity/Editor/NugetPackageUninstaller.cs index 4b6a6585..826bffb7 100644 --- a/src/NuGetForUnity/Editor/NugetPackageUninstaller.cs +++ b/src/NuGetForUnity/Editor/NugetPackageUninstaller.cs @@ -24,7 +24,7 @@ public static void Uninstall([NotNull] INugetPackageIdentifier package, PackageU { // Checking for pre-imported packages also ensures that the pre-imported package list is up-to-date before we uninstall packages. // Without this the pre-imported package list can contain the package as we delete the .dll before we call 'AssetDatabase.Refresh()'. - if (UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package, false)) + if (UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { Debug.LogWarning($"Uninstalling {package} makes no sense because it is a package that is 'pre-imported' by Unity."); } @@ -73,7 +73,7 @@ public static void Uninstall([NotNull] INugetPackageIdentifier package, PackageU } /// - /// Uninstalls all of the currently installed packages. + /// Uninstalls all given installed packages. /// /// The list of packages to uninstall. public static void UninstallAll([NotNull] [ItemNotNull] List packagesToUninstall) @@ -87,5 +87,13 @@ public static void UninstallAll([NotNull] [ItemNotNull] List pack AssetDatabase.Refresh(); } + + /// + /// Uninstalls all of the currently installed packages. + /// + public static void UninstallAll() + { + UninstallAll(InstalledPackagesManager.InstalledPackages.ToList()); + } } } diff --git a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs index b743d36c..d0db9011 100644 --- a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs +++ b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs @@ -22,6 +22,10 @@ namespace NugetForUnity.Ui /// public class NugetWindow : EditorWindow, ISerializationCallbackReceiver { + private const string ArrowTipUp = "\u2227"; + + private const string ArrowTipDown = "\u2228"; + [CanBeNull] private static GUIStyle cachedHeaderStyle; @@ -51,6 +55,11 @@ public class NugetWindow : EditorWindow, ISerializationCallbackReceiver /// private readonly HashSet selectedPackageDowngrades = new HashSet(new NugetPackageIdEqualityComparer()); + /// + /// Used to keep track of which packages are selected for installing. + /// + private readonly HashSet selectedPackageInstalls = new HashSet(new NugetPackageIdEqualityComparer()); + /// /// Used to keep track of which packages are selected for uninstalling. /// @@ -125,6 +134,10 @@ public class NugetWindow : EditorWindow, ISerializationCallbackReceiver [SerializeField] private List serializableAvailablePackages; + [CanBeNull] + [SerializeField] + private List serializableToInstallPackages; + [CanBeNull] [SerializeField] private List serializableUpdatePackages; @@ -141,11 +154,21 @@ public class NugetWindow : EditorWindow, ISerializationCallbackReceiver /// private bool showOnlinePrerelease; + /// + /// True if packages selected for install should be displayed on Online tab, false if availablePackages should be displayed. + /// + private bool showPackagesToInstall; + /// /// True to show beta and alpha package versions. False to only show stable versions. /// private bool showPrereleaseUpdates; + /// + /// The current position of the scroll bar for packages selected for installation. + /// + private Vector2 toInstallScrollPosition; + /// /// The list of package updates available, based on the already installed packages. /// @@ -235,12 +258,17 @@ private HashSet SelectedPackages { get { - if (currentTab == NugetWindowTab.UpdatesTab) + switch (currentTab) { - return showDowngrades ? selectedPackageDowngrades : selectedPackageUpdates; + case NugetWindowTab.UpdatesTab: + return showDowngrades ? selectedPackageDowngrades : selectedPackageUpdates; + case NugetWindowTab.InstalledTab: + return selectedPackageUninstalls; + case NugetWindowTab.OnlineTab: + return selectedPackageInstalls; + default: + throw new ArgumentOutOfRangeException(); } - - return selectedPackageUninstalls; } } @@ -248,6 +276,7 @@ private HashSet SelectedPackages public void OnBeforeSerialize() { serializableAvailablePackages = availablePackages.ConvertAll(package => new SerializableNugetPackage(package)); + serializableToInstallPackages = selectedPackageInstalls.Select(package => new SerializableNugetPackage(package)).ToList(); serializableUpdatePackages = updatePackages.ConvertAll(package => new SerializableNugetPackage(package)); } @@ -260,6 +289,13 @@ public void OnAfterDeserialize() serializableAvailablePackages = null; } + if (serializableToInstallPackages != null) + { + selectedPackageInstalls.Clear(); + selectedPackageInstalls.UnionWith(serializableToInstallPackages.Select(package => package.Interfaced)); + serializableToInstallPackages = null; + } + if (serializableUpdatePackages != null) { updatePackages = serializableUpdatePackages.ConvertAll(package => package.Interfaced); @@ -553,12 +589,19 @@ private void UpdateOnlinePackages() searchTerm, availablePackages.Count, stopwatch.ElapsedMilliseconds); + + // We need to make sure previously selected packages remain in the list + foreach (var selectedPackage in selectedPackageInstalls.Where(selectedPackage => !availablePackages.Contains(selectedPackage))) + { + availablePackages.Add(selectedPackage); + } } private void UpdateInstalledPackages() { InstalledPackagesManager.UpdateInstalledPackages(); ClearViewCache(); + selectedPackageInstalls.ExceptWith(InstalledPackagesManager.InstalledPackages); } /// @@ -668,18 +711,50 @@ private void DrawPackagesSplittedByManuallyInstalled(List package private void DrawOnline() { DrawOnlineHeader(); + var headerStyle = GetHeaderStyle(); + + if (selectedPackageInstalls.Count > 0) + { + DrawSelectedForInstallationHeader(headerStyle); + } // display all of the packages scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.BeginVertical(); - DrawPackages(availablePackages); + IEnumerable packagesToShow; + + if (selectedPackageInstalls.Count > 0 && showPackagesToInstall) + { + packagesToShow = availablePackages.Where(p => selectedPackageInstalls.Contains(p)); + } + else + { + packagesToShow = availablePackages.Where(p => !selectedPackageInstalls.Contains(p)); + } + + DrawPackages(packagesToShow, true); + + // If user deselected all the packages revert to showing available packages + if (selectedPackageInstalls.Count == 0) + { + showPackagesToInstall = false; + } var showMoreStyle = GetHeaderStyle(); EditorGUILayout.BeginVertical(showMoreStyle); + if (showPackagesToInstall) + { + var arrow = !showPackagesToInstall ? ArrowTipUp : ArrowTipDown; + if (GUILayout.Button($" {arrow} Online packages", headerStyle, GUILayout.Height(25f))) + { + showPackagesToInstall = !showPackagesToInstall; + } + } + // allow the user to display more results - if (GUILayout.Button("Show More", GUILayout.Width(120))) + if (!showPackagesToInstall && GUILayout.Button("Show More", GUILayout.Width(120))) { numberToSkip += numberToGet; availablePackages.AddRange( @@ -699,6 +774,49 @@ private void DrawOnline() EditorGUILayout.EndScrollView(); } + private void DrawSelectedForInstallationHeader(GUIStyle headerStyle) + { + var rectangle = GUILayoutUtility.GetRect(GUIContent.none, headerStyle, GUILayout.Height(25f)); + + EditorGUI.LabelField(rectangle, string.Empty, headerStyle); + + var arrow = showPackagesToInstall ? ArrowTipUp : ArrowTipDown; + rectangle.width -= 150f; + if (GUI.Button(rectangle, $" {arrow} Selected for installation: {selectedPackageInstalls.Count}", headerStyle)) + { + showPackagesToInstall = !showPackagesToInstall; + } + + rectangle.x += rectangle.width; + rectangle.width = 148f; + rectangle.y += 2f; + rectangle.height -= 6f; + + if (GUI.Button(rectangle, "Install All Selected")) + { + foreach (var package in selectedPackageInstalls) + { + package.IsManuallyInstalled = true; + NugetPackageInstaller.InstallIdentifier(package, false); + } + + selectedPackageInstalls.Clear(); + + AssetDatabase.Refresh(); + UpdateInstalledPackages(); + UpdateUpdatePackages(); + } + + if (!showPackagesToInstall) + { + arrow = !showPackagesToInstall ? ArrowTipUp : ArrowTipDown; + if (GUILayout.Button($" {arrow} Online packages", headerStyle, GUILayout.Height(25f))) + { + showPackagesToInstall = !showPackagesToInstall; + } + } + } + private void DrawPackages(IEnumerable packages, bool canBeSelected = false) { var backgroundStyle = GetBackgroundStyle(); @@ -734,6 +852,7 @@ private void DrawOnlineHeader() UpdateOnlinePackages(); } + DrawSelectFromClipboardButton(); DrawMandatoryButtons(); } @@ -770,6 +889,46 @@ private void DrawOnlineHeader() EditorGUILayout.EndVertical(); } + private void DrawSelectFromClipboardButton() + { + if (GUILayout.Button("Select all from clipboard", GUILayout.Width(170f))) + { + var packageIds = GUIUtility.systemCopyBuffer.Split('\n', ',').Select(p => p.Trim()).ToList(); + try + { + for (var i = 0; i < packageIds.Count; i++) + { + var packageId = packageIds[i]; + if (InstalledPackagesManager.IsInstalled(packageId, true)) + { + continue; + } + + var alreadyAvailablePackage = availablePackages.Find(package => package.Id == packageId); + if (alreadyAvailablePackage != null) + { + selectedPackageInstalls.Add(alreadyAvailablePackage); + continue; + } + + EditorUtility.DisplayProgressBar("Searching", "Searching for packages", (float)i / packageIds.Count); + var packages = Task.Run(() => ConfigurationManager.SearchAsync(packageId, showOnlinePrerelease, numberToGet, numberToSkip)) + .GetAwaiter() + .GetResult(); + if (packages.Count > 0) + { + selectedPackageInstalls.Add(packages[0]); + availablePackages.Add(packages[0]); + } + } + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + } + /// /// Draws the header which allows filtering the installed list of packages. /// @@ -787,7 +946,7 @@ private void DrawInstalledHeader() { if (GUILayout.Button("Uninstall All", GUILayout.Width(100))) { - NugetPackageUninstaller.UninstallAll(InstalledPackagesManager.InstalledPackages.ToList()); + NugetPackageUninstaller.UninstallAll(); UpdateInstalledPackages(); UpdateUpdatePackages(); } @@ -922,6 +1081,7 @@ private void DrawPackage(INugetPackage package, GUIStyle packageStyle, GUIStyle { var installedPackages = InstalledPackagesManager.InstalledPackages; var installed = installedPackages.FirstOrDefault(p => p.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase)); + var isAlreadyImportedInEngine = UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false); using (new EditorGUILayout.HorizontalScope()) { @@ -942,16 +1102,20 @@ private void DrawPackage(INugetPackage package, GUIStyle packageStyle, GUIStyle rect.x += toggleSize; var workingSelections = SelectedPackages; var isSelected = workingSelections.Contains(package); - var shouldBeSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Height(iconSize)); - if (shouldBeSelected != isSelected) + var alreadyInstalled = installed != null || isAlreadyImportedInEngine; + using (new EditorGUI.DisabledScope(currentTab == NugetWindowTab.OnlineTab && alreadyInstalled)) { - if (shouldBeSelected) - { - workingSelections.Add(package); - } - else + var shouldBeSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Height(iconSize)); + if (shouldBeSelected != isSelected) { - workingSelections.Remove(package); + if (shouldBeSelected) + { + workingSelections.Add(package); + } + else + { + workingSelections.Remove(package); + } } } } @@ -1020,7 +1184,7 @@ private void DrawPackage(INugetPackage package, GUIStyle packageStyle, GUIStyle if (currentTab == NugetWindowTab.UpdatesTab || (currentTab == NugetWindowTab.OnlineTab && installed == null && - !UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package, false))) + !isAlreadyImportedInEngine)) { if (package.Versions.Count <= 1) { @@ -1065,7 +1229,7 @@ private void DrawPackage(INugetPackage package, GUIStyle packageStyle, GUIStyle } } - var existsInUnity = installed == null && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package, false); + var existsInUnity = installed == null && isAlreadyImportedInEngine; PluginRegistry.Instance.DrawButtons(package, installed, existsInUnity); diff --git a/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs b/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs index f6940737..f67a082c 100644 --- a/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs +++ b/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs @@ -3,7 +3,6 @@ using System.Linq; using JetBrains.Annotations; using NugetForUnity.Configuration; -using NugetForUnity.Models; using UnityEditor; using UnityEditor.Compilation; using UnityEngine; @@ -43,13 +42,13 @@ internal static HashSet GetAlreadyImportedEditorOnlyLibraries() /// The package of witch the identifier is checked. /// Whether to log a message with the result of the check. /// If it is included in Unity. - internal static bool IsAlreadyImportedInEngine([NotNull] INugetPackageIdentifier package, bool log = true) + internal static bool IsAlreadyImportedInEngine([NotNull] string packageId, bool log = true) { var alreadyImported = GetAlreadyImportedLibs(); - var isAlreadyImported = alreadyImported.Contains(package.Id); + var isAlreadyImported = alreadyImported.Contains(packageId); if (log) { - NugetLogger.LogVerbose("Is package '{0}' already imported? {1}", package.Id, isAlreadyImported); + NugetLogger.LogVerbose("Is package '{0}' already imported? {1}", packageId, isAlreadyImported); } return isAlreadyImported;