303 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
		
		
			
		
	
	
			303 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
|  | using System; | ||
|  | using System.Collections.Generic; | ||
|  | using System.IO; | ||
|  | using System.Linq; | ||
|  | using UnityEngine; | ||
|  | using UnityEditor.PackageManager; | ||
|  | using UnityEditor.PackageManager.Requests; | ||
|  | using UnityEditor.PackageManager.UI; | ||
|  | 
 | ||
|  | namespace UnityEditor.Timeline | ||
|  | { | ||
|  |     [InitializeOnLoad] | ||
|  |     class SampleDependencyImporter : IPackageManagerExtension | ||
|  |     { | ||
|  |         static readonly string k_DependenciesDialogTitle = L10n.Tr("Import Sample Package Dependencies"); | ||
|  |         static readonly string k_DependenciesDialogMessage = L10n.Tr("These samples contain package dependencies that your project does not have: "); | ||
|  |         static readonly string k_DependenciesDialogOK = L10n.Tr("Import samples and their dependencies"); | ||
|  |         static readonly string k_DependenciesDialogCancel = L10n.Tr("Import samples without their dependencies"); | ||
|  |         static readonly string k_NewLine = "\n"; | ||
|  | 
 | ||
|  |         const string k_TimelinePackageName = "com.unity.timeline"; | ||
|  | 
 | ||
|  |         PackageManager.PackageInfo m_PackageInfo; | ||
|  |         IEnumerable<Sample> m_Samples; | ||
|  |         SampleConfiguration m_SampleConfiguration; | ||
|  |         PackageChecker m_PackageChecker = new PackageChecker(); | ||
|  | 
 | ||
|  |         static SampleDependencyImporter() => PackageManagerExtensions.RegisterExtension(new SampleDependencyImporter()); | ||
|  |         UnityEngine.UIElements.VisualElement IPackageManagerExtension.CreateExtensionUI() => default; | ||
|  | 
 | ||
|  |         public void OnPackageAddedOrUpdated(PackageManager.PackageInfo packageInfo) => m_PackageChecker.RefreshPackageCache(); | ||
|  |         public void OnPackageRemoved(PackageManager.PackageInfo packageInfo) => m_PackageChecker.RefreshPackageCache(); | ||
|  | 
 | ||
|  |         /// <summary> | ||
|  |         /// Called when the package selection changes in the Package Manager window. | ||
|  |         /// It loads Timeline package info and configuration, when Timeline is selected. | ||
|  |         /// </summary> | ||
|  |         public void OnPackageSelectionChange(PackageManager.PackageInfo packageInfo) | ||
|  |         { | ||
|  |             bool timelinePackageInfo = packageInfo != null && packageInfo.name.StartsWith(k_TimelinePackageName); | ||
|  |             if (m_PackageInfo == null && timelinePackageInfo) | ||
|  |             { | ||
|  |                 m_PackageInfo = packageInfo; | ||
|  |                 m_Samples = Sample.FindByPackage(packageInfo.name, packageInfo.version); | ||
|  |                 if (TryLoadSampleConfiguration(m_PackageInfo, out m_SampleConfiguration)) | ||
|  |                     SamplePostprocessor.AssetImported += LoadAssetDependencies; | ||
|  |             } | ||
|  |             else if (!timelinePackageInfo) | ||
|  |             { | ||
|  |                 m_PackageInfo = null; | ||
|  |                 SamplePostprocessor.AssetImported -= LoadAssetDependencies; | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary>Load the sample configuration for the specified package, if one is available.</summary> | ||
|  |         static bool TryLoadSampleConfiguration(PackageManager.PackageInfo packageInfo, out SampleConfiguration configuration) | ||
|  |         { | ||
|  |             var configurationPath = $"{packageInfo.assetPath}/Samples~/sampleDependencies.json"; | ||
|  |             if (File.Exists(configurationPath)) | ||
|  |             { | ||
|  |                 var configurationText = File.ReadAllText(configurationPath); | ||
|  |                 configuration = JsonUtility.FromJson<SampleConfiguration>(configurationText); | ||
|  |                 return true; | ||
|  |             } | ||
|  |             configuration = null; | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         AddRequest m_PackageAddRequest; | ||
|  |         int m_PackageDependencyIndex; | ||
|  |         List<string> m_PackageDependencies = new List<string>(); | ||
|  | 
 | ||
|  |         void LoadAssetDependencies(string assetPath) | ||
|  |         { | ||
|  |             if (!PackageManagerUIOpen()) | ||
|  |             { | ||
|  |                 SamplePostprocessor.AssetImported -= LoadAssetDependencies; | ||
|  |                 return; | ||
|  |             } | ||
|  | 
 | ||
|  |             if (m_SampleConfiguration != null) | ||
|  |             { | ||
|  |                 var assetsImported = false; | ||
|  |                 foreach (var sample in m_Samples) | ||
|  |                 { | ||
|  |                     if (assetPath.EndsWith(sample.displayName)) | ||
|  |                     { | ||
|  |                         var sampleEntry = m_SampleConfiguration.GetEntry(sample); | ||
|  |                         if (sampleEntry != null) | ||
|  |                         { | ||
|  |                             // Import common asset dependencies | ||
|  |                             assetsImported = ImportAssetDependencies( | ||
|  |                                 m_PackageInfo, m_SampleConfiguration.SharedAssetDependencies, out var sharedDestinations); | ||
|  | 
 | ||
|  |                             // Import sample-specific asset dependencies | ||
|  |                             assetsImported |= ImportAssetDependencies( | ||
|  |                                 m_PackageInfo, sampleEntry.AssetDependencies, out var localDestinations); | ||
|  | 
 | ||
|  | 
 | ||
|  |                             // Import common amd sample specific package dependencies | ||
|  |                             m_PackageDependencyIndex = 0; | ||
|  |                             m_PackageDependencies = new List<string>(m_SampleConfiguration.SharedPackageDependencies); | ||
|  |                             m_PackageDependencies.AddRange(sampleEntry.PackageDependencies); | ||
|  | 
 | ||
|  |                             if (m_PackageDependencies.Count != 0 && | ||
|  |                                 DoDependenciesNeedToBeImported(out var dependenciesToImport)) | ||
|  |                             { | ||
|  |                                 if (PromptUserImportDependencyConfirmation(dependenciesToImport)) | ||
|  |                                 { | ||
|  |                                     // only import dependencies that are missing | ||
|  |                                     m_PackageDependencies = dependenciesToImport; | ||
|  |                                     // Import package dependencies using the editor update loop, because | ||
|  |                                     // adding packages need to be done in sequence one after the other | ||
|  |                                     EditorApplication.update += ImportPackageDependencies; | ||
|  |                                 } | ||
|  |                             } | ||
|  |                         } | ||
|  |                         break; | ||
|  |                     } | ||
|  |                 } | ||
|  | 
 | ||
|  |                 if (assetsImported) | ||
|  |                     AssetDatabase.Refresh(); | ||
|  |             } | ||
|  | 
 | ||
|  |             // local functions | ||
|  |             bool DoDependenciesNeedToBeImported(out List<string> packagesToImport) | ||
|  |             { | ||
|  |                 packagesToImport = new List<string>(); | ||
|  |                 foreach (var packageName in m_PackageDependencies) | ||
|  |                 { | ||
|  |                     if (!m_PackageChecker.ContainsPackage(packageName)) | ||
|  |                         packagesToImport.Add(packageName); | ||
|  |                 } | ||
|  | 
 | ||
|  |                 return packagesToImport.Count != 0; | ||
|  |             } | ||
|  | 
 | ||
|  |             void ImportPackageDependencies() | ||
|  |             { | ||
|  |                 if (m_PackageAddRequest != null && !m_PackageAddRequest.IsCompleted) | ||
|  |                     return; // wait while we have a request pending | ||
|  | 
 | ||
|  |                 if (m_PackageDependencyIndex < m_PackageDependencies.Count) | ||
|  |                     m_PackageAddRequest = Client.Add(m_PackageDependencies[m_PackageDependencyIndex++]); | ||
|  |                 else | ||
|  |                 { | ||
|  |                     m_PackageDependencies.Clear(); | ||
|  |                     m_PackageAddRequest = null; | ||
|  |                     EditorApplication.update -= ImportPackageDependencies; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             static bool ImportAssetDependencies(PackageManager.PackageInfo packageInfo, string[] paths, out List<string> destinations) | ||
|  |             { | ||
|  |                 destinations = new List<string>(); | ||
|  |                 if (paths == null) | ||
|  |                     return false; | ||
|  | 
 | ||
|  |                 var assetsImported = false; | ||
|  |                 foreach (var path in paths) | ||
|  |                 { | ||
|  |                     var dependencyPath = Path.GetFullPath($"Packages/{packageInfo.name}/Samples~/{path}"); | ||
|  |                     if (Directory.Exists(dependencyPath)) | ||
|  |                     { | ||
|  |                         var copyTo = | ||
|  |                             $"{Application.dataPath}/Samples/{packageInfo.displayName}/{packageInfo.version}/{path}"; | ||
|  |                         CopyDirectory(dependencyPath, copyTo); | ||
|  |                         destinations.Add(copyTo); | ||
|  |                         assetsImported = true; | ||
|  |                     } | ||
|  |                 } | ||
|  | 
 | ||
|  |                 return assetsImported; | ||
|  |             } | ||
|  | 
 | ||
|  |             static bool PromptUserImportDependencyConfirmation(List<string> dependencies) | ||
|  |             { | ||
|  |                 var aggregate = dependencies.Aggregate(string.Empty, (current, dependency) => current + (dependency + k_NewLine)); | ||
|  |                 return EditorUtility.DisplayDialog( | ||
|  |                     k_DependenciesDialogTitle, | ||
|  |                     $"{k_DependenciesDialogMessage}{k_NewLine}{aggregate}", | ||
|  |                     k_DependenciesDialogOK, | ||
|  |                     k_DependenciesDialogCancel); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         static bool PackageManagerUIOpen() => PackageManagerWindow.instance != null; | ||
|  | 
 | ||
|  |         /// <summary>Copies a directory from the source to target path. Overwrites existing directories.</summary> | ||
|  |         static void CopyDirectory(string sourcePath, string targetPath) | ||
|  |         { | ||
|  |             // Verify source directory | ||
|  |             var source = new DirectoryInfo(sourcePath); | ||
|  |             if (!source.Exists) | ||
|  |                 throw new DirectoryNotFoundException($"{sourcePath}  directory not found"); | ||
|  | 
 | ||
|  |             // Delete pre-existing directory at target path | ||
|  |             var target = new DirectoryInfo(targetPath); | ||
|  |             if (target.Exists) | ||
|  |                 target.Delete(true); | ||
|  | 
 | ||
|  |             Directory.CreateDirectory(targetPath); | ||
|  | 
 | ||
|  |             // Copy all files to target path | ||
|  |             foreach (FileInfo file in source.GetFiles()) | ||
|  |             { | ||
|  |                 var newFilePath = Path.Combine(targetPath, file.Name); | ||
|  |                 file.CopyTo(newFilePath); | ||
|  |             } | ||
|  | 
 | ||
|  |             // Recursively copy all subdirectories | ||
|  |             foreach (DirectoryInfo child in source.GetDirectories()) | ||
|  |             { | ||
|  |                 var newDirectoryPath = Path.Combine(targetPath, child.Name); | ||
|  |                 CopyDirectory(child.FullName, newDirectoryPath); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary>An AssetPostProcessor which will raise an event when a new asset is imported.</summary> | ||
|  |         class SamplePostprocessor : AssetPostprocessor | ||
|  |         { | ||
|  |             public static event Action<string> AssetImported; | ||
|  | 
 | ||
|  |             static void OnPostprocessAllAssets(string[] importedAssets, | ||
|  |                 string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) | ||
|  |             { | ||
|  |                 if (AssetImported == null) | ||
|  |                     return; | ||
|  | 
 | ||
|  |                 foreach (var importedAsset in importedAssets) | ||
|  |                     AssetImported.Invoke(importedAsset); | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         /// <summary>A configuration class defining information related to samples for the package.</summary> | ||
|  |         [Serializable] | ||
|  |         class SampleConfiguration | ||
|  |         { | ||
|  |             /// <summary>This class defines the path and dependencies for a specific sample.</summary> | ||
|  |             [Serializable] | ||
|  |             public class SampleEntry | ||
|  |             { | ||
|  |                 public string Path; | ||
|  |                 public string[] AssetDependencies; | ||
|  |                 public string[] PackageDependencies; | ||
|  |             } | ||
|  | 
 | ||
|  |             public string[] SharedAssetDependencies; | ||
|  |             public string[] SharedPackageDependencies; | ||
|  |             public SampleEntry[] SampleEntries; | ||
|  | 
 | ||
|  |             public SampleEntry GetEntry(Sample sample) => | ||
|  |                 SampleEntries?.FirstOrDefault(t => sample.resolvedPath.EndsWith(t.Path)); | ||
|  |         } | ||
|  | 
 | ||
|  |         class PackageChecker | ||
|  |         { | ||
|  |             ListRequest m_Request; | ||
|  |             PackageCollection m_Packages; | ||
|  | 
 | ||
|  |             public PackageChecker() | ||
|  |             { | ||
|  |                 RefreshPackageCache(); | ||
|  |             } | ||
|  | 
 | ||
|  |             public void RefreshPackageCache() | ||
|  |             { | ||
|  |                 if (m_Request != null && !m_Request.IsCompleted) | ||
|  |                     return; // need to wait for previous request to finish | ||
|  | 
 | ||
|  |                 m_Request = Client.List(true); | ||
|  |                 EditorApplication.update += WaitForRequestToComplete; | ||
|  |             } | ||
|  | 
 | ||
|  |             void WaitForRequestToComplete() | ||
|  |             { | ||
|  |                 if (m_Request.IsCompleted) | ||
|  |                 { | ||
|  |                     if (m_Request.Status == StatusCode.Success) | ||
|  |                         m_Packages = m_Request.Result; | ||
|  |                     EditorApplication.update -= WaitForRequestToComplete; | ||
|  |                 } | ||
|  |             } | ||
|  | 
 | ||
|  |             public bool ContainsPackage(string packageName) | ||
|  |             { | ||
|  |                 // Check each package and package dependency for packageName | ||
|  |                 foreach (var package in m_Packages) | ||
|  |                 { | ||
|  |                     if (string.Compare(package.name, packageName) == 0) | ||
|  |                         return true; | ||
|  | 
 | ||
|  |                     if (package.dependencies.Any(dependencyInfo => string.Compare(dependencyInfo.name, packageName) == 0)) | ||
|  |                         return true; | ||
|  |                 } | ||
|  | 
 | ||
|  |                 return false; | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  | } |