#pragma warning disable SA1512,SA1124 // Single-line comments should not be followed by blank line
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using NugetForUnity.Configuration;
using NugetForUnity.Helper;
using NugetForUnity.PackageSource;
using NugetForUnity.PluginSupport;
using UnityEditor;
using UnityEngine;
#region No ReShaper
// ReSharper disable All
// needed because 'JetBrains.Annotations.NotNull' and 'System.Diagnostics.CodeAnalysis.NotNull' collide if this file is compiled with a never version of Unity / C#
using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute;
// ReSharper restore All
#endregion
#pragma warning restore SA1512,SA1124 // Single-line comments should not be followed by blank line
namespace NugetForUnity.Ui
{
///
/// Handles the displaying, editing, and saving of the preferences for NuGet For Unity.
///
public class NugetPreferences : SettingsProvider
{
///
/// The current version of NuGet for Unity.
///
public const string NuGetForUnityVersion = "4.0.2";
private const float LabelPading = 5;
private readonly GUIContent deleteX = new GUIContent("\u2716");
private readonly GUIContent downArrow = new GUIContent("\u25bc");
private readonly Dictionary packageSourceAdvancedSettingsExpanded = new Dictionary();
private readonly List plugins;
private readonly GUIContent upArrow = new GUIContent("\u25b2");
private float? biggestAdvancedSettingsPackageSourceSectionLabelSize;
private float? biggestPackageSourceSectionLabelSize;
///
/// The current position of the scroll bar in the GUI for the list of plugins.
///
private Vector2 pluginsScrollPosition;
private GUIStyle redToggleStyle;
///
/// Indicates if the warning for packages.config file path should be shown in case it is outside of Assets folder.
///
private bool shouldShowPackagesConfigPathWarning;
///
/// The current position of the scroll bar in the GUI for the list of sources.
///
private Vector2 sourcesScrollPosition;
///
/// Initializes a new instance of the class.
/// Path of packages.config file is checked here as well in case it was manually changed.
///
private NugetPreferences()
: base("Preferences/NuGet For Unity", SettingsScope.User)
{
shouldShowPackagesConfigPathWarning = ConfigurationManager.NugetConfigFile.InstallLocation == PackageInstallLocation.CustomWithinAssets &&
!UnityPathHelper.IsPathInAssets(ConfigurationManager.NugetConfigFile.PackagesConfigDirectoryPath);
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var enabledPlugins = new HashSet(ConfigurationManager.NugetConfigFile.EnabledPlugins);
plugins = assemblies.Where(assembly => assembly.FullName.IndexOf("NugetForUnityPlugin", StringComparison.OrdinalIgnoreCase) >= 0)
.Select(assembly => new NugetPlugin(assembly, enabledPlugins.Remove(new NugetForUnityPluginId(assembly))))
.ToList();
// If there are any enabled plugins that are not found in the project add them to the list as invalid plugins so user can disable them.
plugins.AddRange(enabledPlugins.Select(pluginId => new NugetPlugin(pluginId.Name, pluginId.Path)));
}
///
/// Creates a instance of the NuGet for Unity settings provider.
///
/// The instance of the settings provider.
[SettingsProvider]
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used by unity.")]
public static SettingsProvider Create()
{
return new NugetPreferences();
}
///
/// Draws the preferences GUI inside the Unity preferences window in the Editor.
///
/// The search context for the preferences.
public override void OnGUI([CanBeNull] string searchContext)
{
redToggleStyle = redToggleStyle ??
new GUIStyle(GUI.skin.toggle) { onNormal = { textColor = Color.red }, onHover = { textColor = Color.red } };
var preferencesChangedThisFrame = false;
var sourcePathChangedThisFrame = false;
var needsAssetRefresh = false;
var biggestLabelSize = EditorStyles.label.CalcSize(new GUIContent("Request Timeout in seconds")).x;
EditorGUIUtility.labelWidth = biggestLabelSize;
EditorGUILayout.LabelField($"Version: {NuGetForUnityVersion}");
var installFromCache = EditorGUILayout.Toggle("Install From the Cache", ConfigurationManager.NugetConfigFile.InstallFromCache);
if (installFromCache != ConfigurationManager.NugetConfigFile.InstallFromCache)
{
preferencesChangedThisFrame = true;
ConfigurationManager.NugetConfigFile.InstallFromCache = installFromCache;
}
var readOnlyPackageFiles = EditorGUILayout.Toggle("Read-Only Package Files", ConfigurationManager.NugetConfigFile.ReadOnlyPackageFiles);
if (readOnlyPackageFiles != ConfigurationManager.NugetConfigFile.ReadOnlyPackageFiles)
{
preferencesChangedThisFrame = true;
ConfigurationManager.NugetConfigFile.ReadOnlyPackageFiles = readOnlyPackageFiles;
}
var verbose = EditorGUILayout.Toggle("Use Verbose Logging", ConfigurationManager.NugetConfigFile.Verbose);
if (verbose != ConfigurationManager.NugetConfigFile.Verbose)
{
preferencesChangedThisFrame = true;
ConfigurationManager.NugetConfigFile.Verbose = verbose;
}
var slimRestore = EditorGUILayout.Toggle(
new GUIContent(
"Slim Restore",
"Slim restore is a faster way of installing/restoring packages after opening the Unity project. " +
"To achieve this, the package installation step skips installing dependencies not listed inside the package.config, " +
"also it skips checking against Unity pre-imported libraries. If you have a complex project setup with multiple target " +
"platforms that include different packages, you might need to disable this feature. " +
"Manually restoring via menu option will ignore this setting."),
ConfigurationManager.NugetConfigFile.SlimRestore);
if (slimRestore != ConfigurationManager.NugetConfigFile.SlimRestore)
{
preferencesChangedThisFrame = true;
ConfigurationManager.NugetConfigFile.SlimRestore = slimRestore;
}
var newInstallLocation = (PackageInstallLocation)EditorGUILayout.EnumPopup(
"Placement:",
ConfigurationManager.NugetConfigFile.InstallLocation);
if (newInstallLocation != ConfigurationManager.NugetConfigFile.InstallLocation)
{
var oldRepoPath = ConfigurationManager.NugetConfigFile.RepositoryPath;
InstalledPackagesManager.UpdateInstalledPackages(); // Make sure it is initialized before we move files around
ConfigurationManager.MoveConfig(newInstallLocation);
var newRepoPath = ConfigurationManager.NugetConfigFile.RepositoryPath;
PackageContentManager.MoveInstalledPackages(oldRepoPath, newRepoPath);
if (newInstallLocation == PackageInstallLocation.CustomWithinAssets &&
Directory.Exists(UnityPathHelper.AbsoluteUnityPackagesNugetPath))
{
Directory.Delete(UnityPathHelper.AbsoluteUnityPackagesNugetPath, true);
}
preferencesChangedThisFrame = true;
needsAssetRefresh = true;
}
if (newInstallLocation == PackageInstallLocation.CustomWithinAssets)
{
using (new EditorGUILayout.HorizontalScope())
{
var repositoryPath = ConfigurationManager.NugetConfigFile.RepositoryPath;
GUILayout.Label(
new GUIContent("Packages Install path:", $"Absolute path: {repositoryPath}"),
GUILayout.Width(EditorGUIUtility.labelWidth));
GUILayout.Label(ConfigurationManager.NugetConfigFile.ConfiguredRepositoryPath);
if (GUILayout.Button("Browse", GUILayout.Width(100)))
{
var newPath = EditorUtility.OpenFolderPanel("Select folder where packages will be installed", repositoryPath, string.Empty);
if (!string.IsNullOrEmpty(newPath))
{
var newRelativePath = PathHelper.GetRelativePath(Application.dataPath, newPath);
// We need to make sure saved path is using forward slashes so it works on all systems
newRelativePath = newRelativePath.Replace('\\', '/');
if (newPath != repositoryPath && UnityPathHelper.IsValidInstallPath(newRelativePath))
{
PackageContentManager.MoveInstalledPackages(repositoryPath, newPath);
ConfigurationManager.NugetConfigFile.ConfiguredRepositoryPath = newRelativePath;
UnityPathHelper.EnsurePackageInstallDirectoryIsSetup();
preferencesChangedThisFrame = true;
needsAssetRefresh = true;
}
else if (newPath != repositoryPath)
{
Debug.LogError(
$"Packages install path {newPath} {newRelativePath} is not valid. It must be somewhere under Assets or Packages folder.");
}
}
}
}
using (new EditorGUILayout.HorizontalScope())
{
var packagesConfigPath = ConfigurationManager.NugetConfigFile.PackagesConfigDirectoryPath;
GUILayout.Label(
new GUIContent("Packages Config path:", $"Absolute path: {packagesConfigPath}"),
GUILayout.Width(EditorGUIUtility.labelWidth));
GUILayout.Label(ConfigurationManager.NugetConfigFile.RelativePackagesConfigDirectoryPath);
if (GUILayout.Button("Browse", GUILayout.Width(100)))
{
var newPath = EditorUtility.OpenFolderPanel("Select Folder", packagesConfigPath, string.Empty);
if (!string.IsNullOrEmpty(newPath) && newPath != packagesConfigPath)
{
// if the path root is different or it is not under Assets folder, we want to show a warning message
shouldShowPackagesConfigPathWarning = !UnityPathHelper.IsPathInAssets(newPath);
PackagesConfigFile.Move(newPath);
preferencesChangedThisFrame = true;
needsAssetRefresh = true;
}
}
}
if (shouldShowPackagesConfigPathWarning)
{
EditorGUILayout.HelpBox(
"The packages.config is placed outside of Assets folder, this disables the functionality of automatically restoring packages if the file is changed on the disk.",
MessageType.Warning);
}
}
var requestTimeout = EditorGUILayout.IntField(
new GUIContent(
"Request Timeout in seconds",
"Timeout used for web requests to the package source. A value of -1 can be used to disable timeout."),
ConfigurationManager.NugetConfigFile.RequestTimeoutSeconds);
if (requestTimeout != ConfigurationManager.NugetConfigFile.RequestTimeoutSeconds)
{
preferencesChangedThisFrame = true;
ConfigurationManager.NugetConfigFile.RequestTimeoutSeconds = requestTimeout;
}
EditorGUILayout.LabelField("Package Sources:");
using (var scrollView = new EditorGUILayout.ScrollViewScope(sourcesScrollPosition))
{
sourcesScrollPosition = scrollView.scrollPosition;
INugetPackageSource sourceToMoveUp = null;
INugetPackageSource sourceToMoveDown = null;
INugetPackageSource sourceToRemove = null;
var isFirstPackageSource = true;
foreach (var source in ConfigurationManager.NugetConfigFile.PackageSources)
{
if (!isFirstPackageSource)
{
EditorGUILayout.Separator();
}
isFirstPackageSource = false;
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Space(10);
var isEnabled = EditorGUILayout.Toggle(source.IsEnabled, GUILayout.Width(20));
if (isEnabled != source.IsEnabled)
{
preferencesChangedThisFrame = true;
source.IsEnabled = isEnabled;
}
var name = EditorGUILayout.TextField(source.Name, GUILayout.Width(140));
if (name != source.Name)
{
preferencesChangedThisFrame = true;
source.Name = name;
}
var savedPath = EditorGUILayout.TextField(source.SavedPath).Trim();
if (savedPath != source.SavedPath)
{
preferencesChangedThisFrame = true;
sourcePathChangedThisFrame = true;
source.SavedPath = savedPath;
}
if (GUILayout.Button(upArrow, GUILayout.Width(24)))
{
sourceToMoveUp = source;
}
if (GUILayout.Button(downArrow, GUILayout.Width(24)))
{
sourceToMoveDown = source;
}
if (GUILayout.Button(deleteX, GUILayout.Width(24)))
{
sourceToRemove = source;
}
}
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Space(29);
using (new EditorGUILayout.VerticalScope())
{
if (source is NugetPackageSourceV3 sourceV3)
{
packageSourceAdvancedSettingsExpanded.TryGetValue(sourceV3.SavedPath, out var advancedSettingsExpanded);
advancedSettingsExpanded = EditorGUILayout.Foldout(
advancedSettingsExpanded,
"Advanced Settings (maybe needed if using a custom non 'nuget.org' package source)",
true,
new GUIStyle(EditorStyles.foldout) { fontStyle = FontStyle.Bold });
packageSourceAdvancedSettingsExpanded[sourceV3.SavedPath] = advancedSettingsExpanded;
if (advancedSettingsExpanded)
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
biggestAdvancedSettingsPackageSourceSectionLabelSize = biggestAdvancedSettingsPackageSourceSectionLabelSize ??
EditorStyles.label.CalcSize(
new GUIContent(
"Package download URL template overwrite"))
.x;
EditorGUIUtility.labelWidth = biggestAdvancedSettingsPackageSourceSectionLabelSize.Value + LabelPading;
var supportsPackageIdSearchFilter = EditorGUILayout.Toggle(
new GUIContent(
"Supports 'packageId' query search filter",
"If the API supports querying a package by its id using the syntax 'packageid:{packageId}'. " +
"This syntax is used to easily search for package updates. " +
"If it is disabled package updates are searched by loading the information of all packages one by one. " +
"Currently we know that this setting need to be disabled when using a registry hosted by 'GitHub' or 'Azure Artifacts'."),
sourceV3.SupportsPackageIdSearchFilter);
if (supportsPackageIdSearchFilter != sourceV3.SupportsPackageIdSearchFilter)
{
preferencesChangedThisFrame = true;
sourceV3.SupportsPackageIdSearchFilter = supportsPackageIdSearchFilter;
}
if (sourceV3.SupportsPackageIdSearchFilter)
{
var updateSearchBatchSize = EditorGUILayout.IntField(
new GUIContent(
"Update search batch size",
"The size of each batch in witch avaiLabel updates are fetched. " +
"To prevent the search query string to exceed the URI length limit we fetch the updates in groups. " +
"Currently we know that 'Artifactory' requires the setting to be set to 1. Defaults to 20."),
sourceV3.UpdateSearchBatchSize);
if (updateSearchBatchSize != sourceV3.UpdateSearchBatchSize)
{
preferencesChangedThisFrame = true;
sourceV3.UpdateSearchBatchSize = updateSearchBatchSize;
}
}
var packageDownloadUrlTemplateOverwrite = EditorGUILayout.TextField(
new GUIContent(
"Package download URL template overwrite",
"Overwrite for the URL used to download packages '.nupkg' files. " +
"Normally this is not required. Currently we know that 'Artifactory' requires the setting. " +
"The template should contain the placeholder '{0}' for the package id and '{1}' for the package version. " +
"E.g. for nuget.org the template package download URL would be 'https://api.nuget.org/v3-flatcontainer/{0}/{1}/{0}.{1}.nupkg'"),
sourceV3.PackageDownloadUrlTemplateOverwrite);
packageDownloadUrlTemplateOverwrite = packageDownloadUrlTemplateOverwrite?.Trim();
if (string.IsNullOrEmpty(packageDownloadUrlTemplateOverwrite))
{
packageDownloadUrlTemplateOverwrite = null;
}
if (packageDownloadUrlTemplateOverwrite != sourceV3.PackageDownloadUrlTemplateOverwrite)
{
preferencesChangedThisFrame = true;
sourceV3.PackageDownloadUrlTemplateOverwrite = packageDownloadUrlTemplateOverwrite;
}
}
}
}
biggestPackageSourceSectionLabelSize = biggestPackageSourceSectionLabelSize ??
EditorStyles.label.CalcSize(new GUIContent("Force use API v3")).x;
EditorGUIUtility.labelWidth = biggestPackageSourceSectionLabelSize.Value + LabelPading;
var currentForceUseApiV3 = source.SavedProtocolVersion?.Equals("3", StringComparison.Ordinal) == true;
if ((source is NugetPackageSourceV3 && currentForceUseApiV3) || source is NugetPackageSourceV2)
{
var forceUseApiV3 = EditorGUILayout.Toggle(
new GUIContent(
"Force use API v3",
"Normally the API version is autodetect. " +
"It defaults to version \"2\" when not pointing to a package source URL ending in .json (e.g. https://api.nuget.org/v3/index.json). " +
"So if a package source uses API v3 but the URL doesn't end in .json you need to enable this setting (e.g. needed when using 'Aritfactory')."),
currentForceUseApiV3);
if (forceUseApiV3 != currentForceUseApiV3)
{
preferencesChangedThisFrame = true;
source.SavedProtocolVersion = forceUseApiV3 ? "3" : null;
sourcePathChangedThisFrame = true; // we need to fully reload to be able to switch from V2 to V3
}
}
var hasPassword = EditorGUILayout.Toggle("Credentials", source.HasPassword);
if (hasPassword != source.HasPassword)
{
preferencesChangedThisFrame = true;
source.HasPassword = hasPassword;
}
if (source.HasPassword)
{
var userName = EditorGUILayout.TextField("User Name", source.UserName);
if (userName != source.UserName)
{
preferencesChangedThisFrame = true;
source.UserName = userName;
}
var savedPassword = EditorGUILayout.PasswordField("Password", source.SavedPassword);
if (savedPassword != source.SavedPassword)
{
preferencesChangedThisFrame = true;
source.SavedPassword = savedPassword;
}
}
else
{
source.UserName = null;
}
EditorGUIUtility.labelWidth = 0;
}
}
}
if (sourceToMoveUp != null)
{
var index = ConfigurationManager.NugetConfigFile.PackageSources.IndexOf(sourceToMoveUp);
if (index > 0)
{
ConfigurationManager.NugetConfigFile.PackageSources[index] = ConfigurationManager.NugetConfigFile.PackageSources[index - 1];
ConfigurationManager.NugetConfigFile.PackageSources[index - 1] = sourceToMoveUp;
}
preferencesChangedThisFrame = true;
}
if (sourceToMoveDown != null)
{
var index = ConfigurationManager.NugetConfigFile.PackageSources.IndexOf(sourceToMoveDown);
if (index < ConfigurationManager.NugetConfigFile.PackageSources.Count - 1)
{
ConfigurationManager.NugetConfigFile.PackageSources[index] = ConfigurationManager.NugetConfigFile.PackageSources[index + 1];
ConfigurationManager.NugetConfigFile.PackageSources[index + 1] = sourceToMoveDown;
}
preferencesChangedThisFrame = true;
}
if (sourceToRemove != null)
{
ConfigurationManager.NugetConfigFile.PackageSources.Remove(sourceToRemove);
preferencesChangedThisFrame = true;
}
if (GUILayout.Button("Add New Source"))
{
ConfigurationManager.NugetConfigFile.PackageSources.Add(new NugetPackageSourceLocal("New Source", "source_path"));
preferencesChangedThisFrame = true;
}
}
if (plugins.Count > 0)
{
EditorGUILayout.LabelField("Plugins:");
using (var scrollView = new EditorGUILayout.ScrollViewScope(pluginsScrollPosition))
{
pluginsScrollPosition = scrollView.scrollPosition;
NugetPlugin pluginToRemove = null;
foreach (var plugin in plugins)
{
var valid = plugin.Assembly != null;
var style = valid ? GUI.skin.toggle : redToggleStyle;
var enabled = GUILayout.Toggle(plugin.Enabled, plugin.Name, style);
if (enabled == plugin.Enabled)
{
continue;
}
plugin.Enabled = enabled;
preferencesChangedThisFrame = true;
if (enabled)
{
ConfigurationManager.NugetConfigFile.EnabledPlugins.Add(new NugetForUnityPluginId(plugin.Name, plugin.Path));
}
else
{
ConfigurationManager.NugetConfigFile.EnabledPlugins.Remove(new NugetForUnityPluginId(plugin.Name, plugin.Path));
if (!valid)
{
pluginToRemove = plugin;
}
}
if (valid)
{
PluginRegistry.Reinitialize();
}
}
if (pluginToRemove != null)
{
plugins.Remove(pluginToRemove);
}
}
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("Reset To Default"))
{
NugetConfigFile.CreateDefaultFile(ConfigurationManager.NugetConfigFilePath);
ConfigurationManager.LoadNugetConfigFile();
preferencesChangedThisFrame = true;
}
if (!preferencesChangedThisFrame)
{
return;
}
ConfigurationManager.NugetConfigFile.Save(ConfigurationManager.NugetConfigFilePath);
if (sourcePathChangedThisFrame)
{
// we need to force reload as the changed 'url' can lead to a package source type change.
// e.g. the 'url' can be changed to a V3 nuget API url so we need to create a V3 package source.
ConfigurationManager.LoadNugetConfigFile();
}
if (needsAssetRefresh)
{
// AssetDatabase.Refresh(); doesn't work when we move the files from Assets to Packages so we use this instead:
EditorApplication.ExecuteMenuItem("Assets/Refresh");
}
}
private sealed class NugetPlugin
{
public NugetPlugin([NotNull] Assembly assembly, bool enabled)
{
Assembly = assembly;
Enabled = enabled;
Name = Assembly.GetName().Name;
Path = Assembly.Location;
}
public NugetPlugin(string name, string path)
{
Assembly = null;
Enabled = true;
Name = name;
Path = path;
}
[CanBeNull]
public Assembly Assembly { get; }
public string Name { get; }
public string Path { get; }
public bool Enabled { get; set; }
}
}
}