using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Helper; using NugetForUnity.Models; using NugetForUnity.PluginAPI; using NugetForUnity.PluginSupport; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; namespace NugetForUnity.Ui { /// /// Represents the NuGet Package Manager Window in the Unity Editor. /// public class NugetWindow : EditorWindow, ISerializationCallbackReceiver { private readonly StringBuilder cachedStringBuilder = new StringBuilder(); private readonly Dictionary foldouts = new Dictionary(); /// /// The number of packages to get from the request to the server. /// private readonly int numberToGet = 15; /// /// Used to keep track of which packages the user has opened the clone window on. /// private readonly HashSet openCloneWindows = new HashSet(new NugetPackageIdEqualityComparer()); /// /// Used to keep track of which packages are selected for downgrading. /// 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. /// private readonly HashSet selectedPackageUninstalls = new HashSet(new NugetPackageIdEqualityComparer()); /// /// Used to keep track of which packages are selected for updating. /// private readonly HashSet selectedPackageUpdates = new HashSet(new NugetPackageIdEqualityComparer()); private readonly GUIContent showDowngradesContent = new GUIContent("Show Downgrades"); private readonly GUIContent showPrereleaseContent = new GUIContent("Show Prerelease"); /// /// The titles of the tabs in the window. /// private readonly GUIContent[] tabTitles = { new GUIContent("Online"), new GUIContent("Installed"), new GUIContent("Updates") }; /// /// For each package this contains the currently selected version / the state of the version drop-down. /// private readonly Dictionary versionDropdownDataPerPackage = new Dictionary(); /// /// The list of NugetPackages available to install. /// private List availablePackages = new List(); /// /// The currently selected tab in the window. /// private NugetWindowTab currentTab; /// /// The default icon to display for packages. /// [CanBeNull] [SerializeField] private Texture2D defaultIcon; [CanBeNull] private List filteredInstalledPackages; /// /// True when the NugetWindow has initialized. This is used to skip time-consuming reloading operations when the assembly is reloaded. /// [SerializeField] private bool hasRefreshed; /// /// The search term to search the installed packages for. /// private string installedSearchTerm; [CanBeNull] private string lastInstalledSearchTerm; /// /// The number of packages to skip when requesting a list of packages from the server. This is used to get a new group of packages. /// [SerializeField] private int numberToSkip; /// /// The search term to search the online packages for. /// private string onlineSearchTerm; /// /// The current position of the scroll bar in the GUI. /// private Vector2 scrollPosition; [CanBeNull] [SerializeField] private List serializableAvailablePackages; [CanBeNull] [SerializeField] private List serializableToInstallPackages; [CanBeNull] [SerializeField] private List serializableUpdatePackages; /// /// True to show downgrades of package version instead of updates. /// private bool showDowngrades; private bool showImplicitlyInstalled; private bool showInstalled = true; private bool showOnlinePackages = true; /// /// True to show beta and alpha package versions. False to only show stable versions. /// 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; /// /// True to show beta and alpha package versions. False to only show stable versions. /// private bool showPrereleaseUpdates; /// /// The list of package updates available, based on the already installed packages. /// private List updatePackages = new List(); /// /// The search term to search the update packages for. /// private string updatesSearchTerm; /// /// Gets the filtered list of package updates available. /// private List FilteredUpdatePackages { get { IEnumerable result; if (string.IsNullOrWhiteSpace(updatesSearchTerm)) { result = updatePackages; } else { result = updatePackages.Where( package => package.Id.IndexOf(updatesSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0 || package.Title?.IndexOf(updatesSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); } var installedPackages = InstalledPackagesManager.InstalledPackages; // filter not updatable / not downgradable packages return result.Where( package => { var installed = installedPackages.FirstOrDefault(p => p.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase)); if (installed == null || package.Versions.Count == 0) { // normally shouldn't happen but for now include it in the result return true; } // we do not want to show packages that have no updates if we're showing updates // similarly, we do not show packages that are on the lowest possible version if we're showing downgrades return (showDowngrades && installed.PackageVersion > package.Versions[package.Versions.Count - 1]) || (!showDowngrades && installed.PackageVersion < package.Versions[0]); }) .ToList(); } } private List FilteredInstalledPackages { get { if (filteredInstalledPackages != null && lastInstalledSearchTerm == installedSearchTerm) { return filteredInstalledPackages; } lastInstalledSearchTerm = installedSearchTerm; if (string.IsNullOrWhiteSpace(installedSearchTerm)) { filteredInstalledPackages = InstalledPackagesManager.InstalledPackages.ToList(); } else { filteredInstalledPackages = InstalledPackagesManager.InstalledPackages.Where( package => package.Id.IndexOf(installedSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0 || package.Title?.IndexOf(installedSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) .ToList(); } filteredInstalledPackages.Sort( (p1, p2) => { var cmp = p2.IsManuallyInstalled.CompareTo(p1.IsManuallyInstalled); return cmp != 0 ? cmp : string.Compare(p1.Id, p2.Id, StringComparison.Ordinal); }); return filteredInstalledPackages; } } private HashSet SelectedPackages { get { switch (currentTab) { case NugetWindowTab.UpdatesTab: return showDowngrades ? selectedPackageDowngrades : selectedPackageUpdates; case NugetWindowTab.InstalledTab: return selectedPackageUninstalls; case NugetWindowTab.OnlineTab: return selectedPackageInstalls; default: throw new ArgumentOutOfRangeException(); } } } /// 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)); } /// public void OnAfterDeserialize() { if (serializableAvailablePackages != null) { availablePackages = serializableAvailablePackages.ConvertAll(package => package.Interfaced); 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); serializableUpdatePackages = null; } } /// /// Opens the NuGet Package Manager Window. /// [MenuItem("NuGet/Manage NuGet Packages", false, 0)] protected static void DisplayNugetWindow() { GetWindow(); } /// /// Restores all packages defined in packages.config. /// [MenuItem("NuGet/Restore Packages", false, 1)] protected static void RestorePackages() { PackageRestorer.Restore(false); foreach (var nugetWindow in Resources.FindObjectsOfTypeAll()) { nugetWindow.ClearViewCache(); } } /// /// Opens the preferences window. /// [MenuItem("NuGet/Preferences", false, 9)] protected static void DisplayPreferences() { SettingsService.OpenUserPreferences("Preferences/NuGet For Unity"); } /// /// Automatically called by Unity to draw the GUI. /// protected void OnGUI() { EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); var selectedTab = (NugetWindowTab)GUILayout.Toolbar( (int)currentTab, tabTitles, null, GUI.ToolbarButtonSize.FitToContents, GUILayout.Height(25f)); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); if (selectedTab != currentTab) { OnTabChanged(); } currentTab = selectedTab; switch (currentTab) { case NugetWindowTab.OnlineTab: DrawOnline(); break; case NugetWindowTab.InstalledTab: DrawInstalled(); break; case NugetWindowTab.UpdatesTab: DrawUpdates(); break; } } private static void GUILayoutLink(string url) { var rect = EditorGUILayout.GetControlRect(); rect.yMin -= 2f; rect.xMin += 30f; if (GUI.Button(rect, url, Styles.LinkLabelStyle)) { Application.OpenURL(url); } } private static void DrawNoDataAvailableInfo(string message) { EditorGUILayout.LabelField(message, Styles.BoldLabelStyle, GUILayout.Height(20)); } /// /// Called when enabling the window. /// private void OnEnable() { name = "NuGetForUnity"; titleContent = new GUIContent("NuGet For Unity"); Refresh(false); UnityPathHelper.EnsurePackageInstallDirectoryIsSetup(); } private void ClearViewCache() { filteredInstalledPackages = null; } private void Refresh(bool forceFullRefresh) { var stopwatch = new Stopwatch(); stopwatch.Start(); try { if (forceFullRefresh) { CredentialProviderHelper.ClearCachedCredentials(); } // reload the NuGet.config file, in case it was changed after Unity opened, but before the manager window opened (now) ConfigurationManager.LoadNugetConfigFile(); // if we are entering playmode, don't do anything if (EditorApplication.isPlayingOrWillChangePlaymode) { return; } NugetLogger.LogVerbose(hasRefreshed ? "NugetWindow reloading config" : "NugetWindow reloading config and updating packages"); if (!hasRefreshed || forceFullRefresh) { // reset the number to skip numberToSkip = 0; // TODO: Do we even need to load ALL of the data, or can we just get the Online tab packages? EditorUtility.DisplayProgressBar("Opening NuGet", "Fetching packages from server...", 0.3f); UpdateOnlinePackages(); EditorUtility.DisplayProgressBar("Opening NuGet", "Getting installed packages...", 0.6f); UpdateInstalledPackages(); EditorUtility.DisplayProgressBar("Opening NuGet", "Getting available updates...", 0.9f); UpdateUpdatePackages(); versionDropdownDataPerPackage.Clear(); // load the default icon from the Resources folder defaultIcon = (Texture2D)Resources.Load("defaultIcon", typeof(Texture2D)); } hasRefreshed = true; } catch (Exception e) { Debug.LogErrorFormat("Error while refreshing NuGet packages list: {0}", e); } finally { ClearViewCache(); EditorUtility.ClearProgressBar(); NugetLogger.LogVerbose("NugetWindow reloading took {0} ms", stopwatch.ElapsedMilliseconds); } } /// /// Updates the list of available packages by running a search with the server using the currently set parameters (# to get, # to skip, etc). /// private void UpdateOnlinePackages() { var stopwatch = new Stopwatch(); stopwatch.Start(); // we just block the main thread availablePackages = Task.Run(() => ConfigurationManager.SearchAsync(onlineSearchTerm, showOnlinePrerelease, numberToGet, numberToSkip)) .GetAwaiter() .GetResult(); NugetLogger.LogVerbose( "Searching '{0}' in all active package sources returned: {1} packages after {2} ms", onlineSearchTerm, 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); } /// /// Updates the list of update packages. /// private void UpdateUpdatePackages() { // get any available updates for the installed packages updatePackages = ConfigurationManager.GetUpdates(InstalledPackagesManager.InstalledPackages, showPrereleaseUpdates); } private void OnTabChanged() { openCloneWindows.Clear(); foreach (var dropdownData in versionDropdownDataPerPackage.Values) { // reset to latest package version else the update tab will show the same version as the installed tab dropdownData.SelectedIndex = 0; } ResetScrollPosition(); } private void ResetScrollPosition() { scrollPosition.y = 0f; } /// /// Draws the list of installed packages that have updates available. /// private void DrawUpdates() { DrawUpdatesHeader(); // display all of the installed packages scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.BeginVertical(); var filteredUpdatePackages = FilteredUpdatePackages; if (filteredUpdatePackages.Count > 0) { DrawPackagesSplittedByManuallyInstalled(filteredUpdatePackages); } else { DrawNoDataAvailableInfo("There are no updates available!"); } EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } /// /// Draws the list of installed packages. /// private void DrawInstalled() { DrawInstalledHeader(); // display all of the installed packages scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.BeginVertical(); var installedPackages = FilteredInstalledPackages; if (installedPackages.Count > 0) { DrawPackagesSplittedByManuallyInstalled(installedPackages); } else { DrawNoDataAvailableInfo("There are no packages installed!"); } EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } private void DrawPackagesSplittedByManuallyInstalled(List packages) { var foldoutRect = EditorGUILayout.GetControlRect(true, 20f).AddY(-1f); EditorGUI.DrawRect(foldoutRect.Expand(2f), Styles.FoldoutHeaderColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(-2f).SetHeight(1f), Styles.LineColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(foldoutRect.height + 1f).SetHeight(1f), Styles.LineColor); showInstalled = EditorGUI.Foldout(foldoutRect, showInstalled, "Installed packages", true); if (showInstalled) { if (packages.Exists(package => package.IsManuallyInstalled)) { DrawPackages(packages.Where(package => package.IsManuallyInstalled), true); } else { DrawNoDataAvailableInfo("There are no explicitly installed packages."); } } foldoutRect = EditorGUILayout.GetControlRect(true, 20f); EditorGUI.DrawRect(foldoutRect.Expand(2f), Styles.FoldoutHeaderColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(-2f).SetHeight(1f), Styles.LineColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(foldoutRect.height + 1f).SetHeight(1f), Styles.LineColor); showImplicitlyInstalled = EditorGUI.Foldout(foldoutRect, showImplicitlyInstalled, "Implicitly installed packages", true); if (showImplicitlyInstalled) { DrawPackages(packages.Where(package => !package.IsManuallyInstalled), true); } } /// /// Draws the current list of available online packages. /// private void DrawOnline() { DrawOnlineHeader(); // display all of the packages scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.BeginVertical(); if (selectedPackageInstalls.Count > 0) { DrawSelectedForInstallationHeader(); if (showPackagesToInstall) { DrawPackages(availablePackages.Where(p => selectedPackageInstalls.Contains(p)), true); } } // If user deselected all the packages revert to showing available packages if (selectedPackageInstalls.Count == 0) { showOnlinePackages = true; } if (selectedPackageInstalls.Count > 0) { var foldoutRect = EditorGUILayout.GetControlRect(false, 22f); EditorGUI.DrawRect(foldoutRect.Expand(2f), Styles.FoldoutHeaderColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(-2f).SetHeight(1f), Styles.LineColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(foldoutRect.height + 1f).SetHeight(1f), Styles.LineColor); showOnlinePackages = EditorGUI.Foldout(foldoutRect, showOnlinePackages, "Online packages", true); } if (showOnlinePackages) { DrawPackages(availablePackages.Where(p => !selectedPackageInstalls.Contains(p)), true); } GUILayout.Space(3f); // allow the user to display more results using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); if (showOnlinePackages && GUILayout.Button("Show More", GUILayout.Width(120))) { numberToSkip += numberToGet; availablePackages.AddRange( Task.Run(() => ConfigurationManager.SearchAsync(onlineSearchTerm, showOnlinePrerelease, numberToGet, numberToSkip)) .GetAwaiter() .GetResult()); } GUILayout.FlexibleSpace(); } GUILayout.Space(4f); EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } private void DrawSelectedForInstallationHeader() { var foldoutRect = EditorGUILayout.GetControlRect(false, 22f).AddY(-1f); EditorGUI.DrawRect(foldoutRect.Expand(2f), Styles.FoldoutHeaderColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(-2f).SetHeight(1f), Styles.LineColor); EditorGUI.DrawRect(foldoutRect.ExpandX(2f).AddY(foldoutRect.height + 1f).SetHeight(1f), Styles.LineColor); foldoutRect.width -= 150f; showPackagesToInstall = EditorGUI.Foldout( foldoutRect, showPackagesToInstall, $"Selected for installation: {selectedPackageInstalls.Count}", true); foldoutRect.x += foldoutRect.width; foldoutRect.width = 148f; foldoutRect.y += 2f; foldoutRect.height -= 4f; if (GUI.Button(foldoutRect, "Install All Selected")) { foreach (var package in selectedPackageInstalls) { package.IsManuallyInstalled = true; NugetPackageInstaller.InstallIdentifier(package, false); } selectedPackageInstalls.Clear(); AssetDatabase.Refresh(); UpdateInstalledPackages(); UpdateUpdatePackages(); } } private void DrawPackages(IEnumerable packages, bool canBeSelected = false) { var backgroundStyle = Styles.BackgroundStyle; foreach (var package in packages) { using (new EditorGUILayout.VerticalScope(backgroundStyle)) { DrawPackage(package, backgroundStyle, canBeSelected); } } } /// /// Draws the header which allows filtering the online list of packages. /// private void DrawOnlineHeader() { using (new EditorGUILayout.VerticalScope(Styles.BackgroundStyle)) { using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { var showPrereleaseTemp = GUILayout.Toggle( showOnlinePrerelease, showPrereleaseContent, EditorStyles.toolbarButton, GUILayout.Width(120f)); if (showPrereleaseTemp != showOnlinePrerelease) { showOnlinePrerelease = showPrereleaseTemp; UpdateOnlinePackages(); } GUILayout.FlexibleSpace(); DrawSelectFromClipboardButton(); DrawMandatoryButtons(); } using (new EditorGUILayout.HorizontalScope(Styles.ToolbarStyle)) { var enterPressed = Event.current.Equals(Event.KeyboardEvent("return")); // draw search field onlineSearchTerm = EditorGUILayout.TextField(onlineSearchTerm, Styles.SearchFieldStyle, GUILayout.Height(20)); if (GUILayout.Button("Search", GUILayout.Width(100), GUILayout.Height(20))) { // the search button emulates the Enter key enterPressed = true; } // search only if the enter key is pressed if (enterPressed) { // reset the number to skip numberToSkip = 0; UpdateOnlinePackages(); } } } } private void DrawSelectFromClipboardButton() { if (GUILayout.Button("Select all from clipboard", EditorStyles.toolbarButton, 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. /// private void DrawInstalledHeader() { using (new EditorGUILayout.VerticalScope(Styles.HeaderStyle)) { using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { GUILayout.FlexibleSpace(); if (InstalledPackagesManager.InstalledPackages.Any()) { if (GUILayout.Button("Uninstall All", EditorStyles.toolbarButton, GUILayout.Width(100))) { NugetPackageUninstaller.UninstallAll(); UpdateInstalledPackages(); UpdateUpdatePackages(); } } if (selectedPackageUninstalls.Count > 0) { if (GUILayout.Button("Uninstall Selected", EditorStyles.toolbarButton, GUILayout.Width(120))) { NugetPackageUninstaller.UninstallAll(selectedPackageUninstalls.ToList()); UpdateInstalledPackages(); UpdateUpdatePackages(); } } DrawMandatoryButtons(); } using (new EditorGUILayout.HorizontalScope(Styles.ToolbarStyle)) { installedSearchTerm = EditorGUILayout.TextField(installedSearchTerm, Styles.SearchFieldStyle, GUILayout.Height(20)); } } } /// /// Draws the header for the Updates tab. /// private void DrawUpdatesHeader() { using (new EditorGUILayout.VerticalScope(Styles.HeaderStyle)) { using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { var showPrereleaseTemp = GUILayout.Toggle( showPrereleaseUpdates, showPrereleaseContent, EditorStyles.toolbarButton, GUILayout.Width(120f)); if (showPrereleaseTemp != showPrereleaseUpdates) { showPrereleaseUpdates = showPrereleaseTemp; UpdateUpdatePackages(); } var showDowngradesTemp = GUILayout.Toggle( showDowngrades, showDowngradesContent, EditorStyles.toolbarButton, GUILayout.Width(120f)); if (showDowngradesTemp != showDowngrades) { versionDropdownDataPerPackage.Clear(); showDowngrades = showDowngradesTemp; } GUILayout.FlexibleSpace(); if (updatePackages.Count > 0) { if (!showDowngrades && GUILayout.Button("Update All", EditorStyles.toolbarButton, GUILayout.Width(100))) { NugetPackageUpdater.UpdateAll(updatePackages, InstalledPackagesManager.InstalledPackages); UpdateInstalledPackages(); UpdateUpdatePackages(); } var workingSelections = SelectedPackages; if (workingSelections.Count > 0) { if (GUILayout.Button( showDowngrades ? "Downgrade Selected" : "Update Selected", EditorStyles.toolbarButton, GUILayout.Width(120))) { NugetPackageUpdater.UpdateAll(workingSelections, InstalledPackagesManager.InstalledPackages); UpdateInstalledPackages(); UpdateUpdatePackages(); } } } DrawMandatoryButtons(); } using (new EditorGUILayout.HorizontalScope(Styles.ToolbarStyle)) { updatesSearchTerm = EditorGUILayout.TextField(updatesSearchTerm, Styles.SearchFieldStyle, GUILayout.Height(20)); } } } /// /// Draws the "Refresh" and "Preferences" buttons in the upper right corner, which are visible in every tab. /// private void DrawMandatoryButtons() { if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60))) { Refresh(true); } if (GUILayout.Button("Preferences", EditorStyles.toolbarButton, GUILayout.Width(80))) { SettingsService.OpenUserPreferences("Preferences/NuGet For Unity"); GetWindow().Close(); } } /// /// Draws the given . /// /// The to draw. /// The normal style of the package section. /// If a check-box should be shown. private void DrawPackage(INugetPackage package, GUIStyle backgroundStyle, bool canBeSelected = false) { var installedPackages = InstalledPackagesManager.InstalledPackages; var installed = installedPackages.FirstOrDefault(p => p.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase)); var isAlreadyImportedInEngine = UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false); GUILayout.Space(7f); using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(7f); // The Unity GUI system (in the Editor) is terrible. This probably requires some explanation. // Every time you use a Horizontal block, Unity appears to divide the space evenly. // (i.e. 2 components have half of the window width, 3 components have a third of the window width, etc) // GUILayoutUtility.GetRect is SUPPOSED to return a rect with the given height and width, but in the GUI layout. It doesn't. // We have to use GUILayoutUtility to get SOME rect properties, but then manually calculate others. using (new EditorGUILayout.HorizontalScope()) { const int iconSize = 32; var paddingX = 5f; var rect = GUILayoutUtility.GetRect(0, iconSize); rect.y += Math.Max(EditorStyles.label.padding.vertical, 3); if (canBeSelected) { const int toggleSize = 18; rect.x += toggleSize; var workingSelections = SelectedPackages; var isSelected = workingSelections.Contains(package); var alreadyInstalled = installed != null || isAlreadyImportedInEngine; using (new EditorGUI.DisabledScope(currentTab == NugetWindowTab.OnlineTab && alreadyInstalled)) { var shouldBeSelected = EditorGUILayout.Toggle(isSelected, GUILayout.Height(iconSize)); if (shouldBeSelected != isSelected) { if (shouldBeSelected) { workingSelections.Add(package); } else { workingSelections.Remove(package); } } } } rect.x += paddingX; rect.width = iconSize; rect.height = iconSize; var icon = defaultIcon; if (package.IconTask != null && package.IconTask.IsCompleted && package.IconTask.Result != null) { // as this is called every frame we don't need to wait for the task we can just use the image if it is available icon = package.IconTask.Result; } else if (installed != null && installed.IconTask != null && installed.IconTask.IsCompleted && installed.IconTask.Result != null) { // fallback to the icon of the already installed package (somehow there are cases where the update package has no icon URL) // as this is called every frame we don't need to wait for the task we can just use the image if it is available icon = installed.IconTask.Result; } if (icon != null) { GUI.DrawTexture(rect, icon, ScaleMode.StretchToFill); rect.x += iconSize + paddingX; } // text is allowed to get the half of the available space rest is for buttons and version label rect.width = (position.width - rect.x) / 2; var labelStyle = Styles.PackageNameLabelStyle; var idSize = labelStyle.CalcSize(new GUIContent(package.Id)); GUI.Label(rect, package.Id, labelStyle); rect.x += Mathf.Min(idSize.x, rect.width) + paddingX; } GUILayout.FlexibleSpace(); // Show the installed version of the package if it exists if (installed != null) { GUILayout.Label($"Installed Version: {installed.PackageVersion.FullVersion}"); } // Show the version selection dropdown only on Updates tab OR on Online tab if the package is not installed and not already in Unity if (currentTab == NugetWindowTab.UpdatesTab || (currentTab == NugetWindowTab.OnlineTab && installed == null && !isAlreadyImportedInEngine)) { if (package.Versions.Count <= 1) { GUILayout.Label($"Version {package.PackageVersion.FullVersion}"); } else { if (!versionDropdownDataPerPackage.TryGetValue(package.Id, out var versionDropdownData)) { EditorStyles.popup.CalcMinMaxWidth( new GUIContent( package.Versions.Select(version => version.FullVersion).OrderByDescending(version => version.Length).First()), out _, out var maxWidth); var sortedVersions = package.Versions; if (installed != null) { sortedVersions = showDowngrades ? package.Versions.FindAll(version => version < installed.PackageVersion) : package.Versions.FindAll(version => version > installed.PackageVersion); } versionDropdownData = new VersionDropdownData { SortedVersions = sortedVersions, CalculatedMaxWith = maxWidth + 5 }; versionDropdownData.DropdownOptions = versionDropdownData.SortedVersions.Select(version => version.FullVersion).ToArray(); // Show the highest available update/downgrade first versionDropdownData.SelectedIndex = 0; versionDropdownDataPerPackage.Add(package.Id, versionDropdownData); } if (versionDropdownData.SortedVersions.Count > 0) { GUILayout.Label("Version"); versionDropdownData.SelectedIndex = EditorGUILayout.Popup( versionDropdownData.SelectedIndex, versionDropdownData.DropdownOptions, GUILayout.Width(versionDropdownData.CalculatedMaxWith)); package.PackageVersion = versionDropdownData.SortedVersions[versionDropdownData.SelectedIndex]; } } } var existsInUnity = installed == null && isAlreadyImportedInEngine; PluginRegistry.Instance.DrawButtons(package, installed, existsInUnity); if (installed != null) { if (currentTab == NugetWindowTab.InstalledTab && !installed.IsManuallyInstalled) { if (GUILayout.Button("Add as explicit")) { InstalledPackagesManager.SetManuallyInstalledFlag(installed); ClearViewCache(); } } if (currentTab == NugetWindowTab.UpdatesTab) { if (showDowngrades) { // Showing only downgrades if (GUILayout.Button("Downgrade")) { NugetPackageUpdater.Update(installed, package); UpdateInstalledPackages(); UpdateUpdatePackages(); } } else { // Showing only updates if (GUILayout.Button("Update")) { NugetPackageUpdater.Update(installed, package); UpdateInstalledPackages(); UpdateUpdatePackages(); } } } if (currentTab == NugetWindowTab.InstalledTab && GUILayout.Button("Uninstall")) { NugetPackageUninstaller.Uninstall(installed, PackageUninstallReason.IndividualUninstall); UpdateInstalledPackages(); UpdateUpdatePackages(); } } else { using (new EditorGUI.DisabledScope(existsInUnity)) { if (GUILayout.Button(new GUIContent("Install", null, existsInUnity ? "Already imported by Unity" : null))) { package.IsManuallyInstalled = true; NugetPackageInstaller.InstallIdentifier(package); UpdateInstalledPackages(); UpdateUpdatePackages(); } } } } // authors { var rect = EditorGUILayout.GetControlRect().AddX(10f); var labelStyle = Styles.AuthorsLabelStyle; rect.y += labelStyle.fontSize / 2f; if (package.Authors.Count > 0) { var authorLabel = $"by {string.Join(", ", package.Authors)}"; var size = labelStyle.CalcSize(new GUIContent(authorLabel)); GUI.Label(rect, authorLabel, labelStyle); rect.x += size.x; } if (package.TotalDownloads > 0) { var downloadLabel = $"{package.TotalDownloads:#,#} downloads"; GUI.Label(rect, downloadLabel, labelStyle); } } GUILayout.Space(7f); using (new EditorGUILayout.HorizontalScope()) { using (new EditorGUILayout.VerticalScope()) { // Show the package details var labelStyle = Styles.DescriptionLabelStyle; var summary = package.Summary; if (string.IsNullOrEmpty(summary)) { summary = package.Description; } if (!string.IsNullOrEmpty(package.Title) && !package.Title.Equals(package.Id, StringComparison.OrdinalIgnoreCase)) { summary = $"{package.Title} - {summary}"; } if (summary != null && summary.Length >= 240) { summary = $"{summary.Substring(0, 237)}..."; } var summaryRect = EditorGUILayout.GetControlRect( true, labelStyle.CalcHeight(new GUIContent(summary), EditorGUIUtility.currentViewWidth - 20f) + 5f) .AddX(10f); EditorGUI.LabelField(summaryRect, summary, labelStyle); var detailsFoldoutId = $"{package.Id}.Details"; if (!foldouts.TryGetValue(detailsFoldoutId, out var detailsFoldout)) { foldouts[detailsFoldoutId] = detailsFoldout; } GUILayout.Space(2f); var detailsFoldoutRect = EditorGUILayout.GetControlRect().AddX(10f); detailsFoldout = EditorGUI.Foldout(detailsFoldoutRect, detailsFoldout, "Details", true); foldouts[detailsFoldoutId] = detailsFoldout; if (detailsFoldout) { EditorGUI.indentLevel += 2; if (!string.IsNullOrEmpty(package.Description)) { EditorGUILayout.LabelField("Description", EditorStyles.boldLabel); var descriptionContent = new GUIContent(package.Description); var descriptionRect = EditorGUILayout.GetControlRect( true, labelStyle.CalcHeight(descriptionContent, EditorGUIUtility.currentViewWidth - 20f) + 12f); EditorGUI.LabelField(descriptionRect, descriptionContent, labelStyle); GUILayout.Space(4f); } if (!string.IsNullOrEmpty(package.ReleaseNotes)) { EditorGUILayout.LabelField("Release Notes", EditorStyles.boldLabel); EditorGUILayout.LabelField(package.ReleaseNotes); GUILayout.Space(4f); } // Show project URL link if (!string.IsNullOrEmpty(package.ProjectUrl)) { EditorGUILayout.LabelField("Project URL", EditorStyles.boldLabel); GUILayoutLink(package.ProjectUrl); GUILayout.Space(4f); } // Show the dependencies if (package.GetDependenciesAsync().IsCompleted) { var frameworkDependencies = package.CurrentFrameworkDependencies; if (frameworkDependencies.Count > 0) { EditorGUILayout.LabelField("Dependencies", EditorStyles.boldLabel); labelStyle.fontStyle = FontStyle.Italic; cachedStringBuilder.Clear(); foreach (var dependency in frameworkDependencies) { cachedStringBuilder.AppendLine($"{dependency.Id} {dependency.Version}"); } EditorGUILayout.LabelField(cachedStringBuilder.ToString(), labelStyle); labelStyle.fontStyle = FontStyle.Normal; } } else { EditorGUILayout.LabelField("Loading dependencies..."); } // Create the style for putting a box around the 'Clone' button var cloneButtonBoxStyle = new GUIStyle("box") { stretchWidth = false, margin = { top = 0, bottom = 0 }, padding = { bottom = 4 } }; var normalButtonBoxStyle = new GUIStyle(cloneButtonBoxStyle) { normal = { background = backgroundStyle.normal.background } }; var showCloneWindow = openCloneWindows.Contains(package); cloneButtonBoxStyle.normal.background = backgroundStyle.normal.background; // Create a similar style for the 'Clone' window var cloneWindowStyle = new GUIStyle(cloneButtonBoxStyle) { padding = new RectOffset(6, 6, 2, 6) }; if (package.RepositoryType == RepositoryType.Git || package.RepositoryType == RepositoryType.TfsGit) { if (!string.IsNullOrEmpty(package.RepositoryUrl)) { var buttonRect = EditorGUILayout.GetControlRect(); buttonRect.width = 116f; buttonRect.xMin += 31f; // Show the clone button if (GUI.Button(buttonRect, "Clone")) { showCloneWindow = !showCloneWindow; } if (showCloneWindow) { openCloneWindows.Add(package); } else { openCloneWindows.Remove(package); } } } if (!string.IsNullOrEmpty(package.LicenseUrl) && package.LicenseUrl != "http://your_license_url_here") { var buttonRect = EditorGUILayout.GetControlRect(); buttonRect.width = 116f; buttonRect.xMin += 31f; // Show the license button if (GUI.Button(buttonRect, "View License")) { Application.OpenURL(package.LicenseUrl); } } if (showCloneWindow) { var oldIndent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; using (new EditorGUILayout.HorizontalScope()) { GUILayout.Space(23f); using (new EditorGUILayout.VerticalScope(cloneWindowStyle)) { // Clone latest label EditorGUILayout.LabelField("clone latest"); // Clone latest row using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Copy", GUILayout.ExpandWidth(false))) { GUI.FocusControl(package.Id + package.Version + "repoUrl"); GUIUtility.systemCopyBuffer = package.RepositoryUrl; } GUI.SetNextControlName(package.Id + package.Version + "repoUrl"); EditorGUILayout.TextField(package.RepositoryUrl); } // Clone @ commit label GUILayout.Space(4f); EditorGUILayout.LabelField("clone @ commit"); // Clone @ commit row using (new EditorGUILayout.HorizontalScope()) { // Create the three commands a user will need to run to get the repo @ the commit. Intentionally leave off the last newline for better UI appearance var commands = string.Format( CultureInfo.InvariantCulture, "git clone {0} {1} --no-checkout{2}cd {1}{2}git checkout {3}", package.RepositoryUrl, package.Id, Environment.NewLine, package.RepositoryCommit); if (GUILayout.Button("Copy", GUILayout.ExpandWidth(false))) { GUI.FocusControl(package.Id + package.Version + "commands"); // Add a newline so the last command will execute when pasted to the CL GUIUtility.systemCopyBuffer = commands + Environment.NewLine; } using (new EditorGUILayout.VerticalScope()) { GUI.SetNextControlName(package.Id + package.Version + "commands"); EditorGUILayout.TextArea(commands); } } } } EditorGUI.indentLevel = oldIndent; } EditorGUI.indentLevel -= 2; } EditorGUILayout.Space(); } } EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1f), Styles.LineColor); } private sealed class VersionDropdownData { public int SelectedIndex { get; set; } public List SortedVersions { get; set; } public float CalculatedMaxWith { get; set; } public string[] DropdownOptions { get; set; } } } }