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 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(); /// /// Called when the package selection changes in the Package Manager window. /// It loads Timeline package info and configuration, when Timeline is selected. /// 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; } } /// Load the sample configuration for the specified package, if one is available. 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(configurationText); return true; } configuration = null; return false; } AddRequest m_PackageAddRequest; int m_PackageDependencyIndex; List m_PackageDependencies = new List(); 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(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 packagesToImport) { packagesToImport = new List(); 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 destinations) { destinations = new List(); 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 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; /// Copies a directory from the source to target path. Overwrites existing directories. 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); } } /// An AssetPostProcessor which will raise an event when a new asset is imported. class SamplePostprocessor : AssetPostprocessor { public static event Action AssetImported; static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { if (AssetImported == null) return; foreach (var importedAsset in importedAssets) AssetImported.Invoke(importedAsset); } } /// A configuration class defining information related to samples for the package. [Serializable] class SampleConfiguration { /// This class defines the path and dependencies for a specific sample. [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; } } } }