diff --git a/CHANGELOG.md b/CHANGELOG.md index fad6802c2..285f1fd1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Built with Unity 2021.2. ### Added +- Asset Browser ([#412](https://github.com/freezy/VisualPinball.Engine/pull/412)) - Trigger meshes can now be easily scaled ([#374](https://github.com/freezy/VisualPinball.Engine/pull/374)) - We got a new game item called *Metal Wire Guide* (thanks @Cupiii, [#366](https://github.com/freezy/VisualPinball.Engine/pull/366)) - A *Collision Switch* component ([#344](https://github.com/freezy/VisualPinball.Engine/pull/344), [Documentation](https://docs.visualpinball.org/creators-guide/manual/mechanisms/collision-switches.html)). @@ -36,6 +37,7 @@ Built with Unity 2021.2. - Native trough component ([#229](https://github.com/freezy/VisualPinball.Engine/pull/229), [#248](https://github.com/freezy/VisualPinball.Engine/pull/248), [#256](https://github.com/freezy/VisualPinball.Engine/pull/256), [Documentation](https://docs.visualpinball.org/creators-guide/manual/mechanisms/troughs.html)). ### Changed +- Removed internal ID in gamelogic engine API ([#408](https://github.com/freezy/VisualPinball.Engine/pull/408)) - When importing, meshes are now saved as easily editable `.fbx` files instead of Unity's internal format ([#387](https://github.com/freezy/VisualPinball.Engine/pull/387)). - Revised rubber mesh generation ([#384](https://github.com/freezy/VisualPinball.Engine/pull/384)). - APIs for RGB lamps and Visual Scripting ([#382](https://github.com/freezy/VisualPinball.Engine/pull/382)). diff --git a/VisualPinball.Unity/Assets/Editor/Icons/large_gray/asset_library.png b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/asset_library.png new file mode 100644 index 000000000..ae4e037b6 Binary files /dev/null and b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/asset_library.png differ diff --git a/VisualPinball.Unity/Assets/Editor/Icons/large_gray/asset_library.png.meta b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/asset_library.png.meta new file mode 100644 index 000000000..2257dbe31 --- /dev/null +++ b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/asset_library.png.meta @@ -0,0 +1,122 @@ +fileFormatVersion: 2 +guid: 8c50266a2f53dc348adc049e274bc88e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 1 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 512 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Assets/Editor/Icons/large_gray/calendar.png b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/calendar.png new file mode 100644 index 000000000..afba2ab67 Binary files /dev/null and b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/calendar.png differ diff --git a/VisualPinball.Unity/Assets/Editor/Icons/large_gray/calendar.png.meta b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/calendar.png.meta new file mode 100644 index 000000000..afa9536d7 --- /dev/null +++ b/VisualPinball.Unity/Assets/Editor/Icons/large_gray/calendar.png.meta @@ -0,0 +1,122 @@ +fileFormatVersion: 2 +guid: 3091f9ca1ca324e41bcd564e610e4c68 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 512 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Assets/Editor/Icons/small_gray/asset_library.png b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/asset_library.png new file mode 100644 index 000000000..6e650529b Binary files /dev/null and b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/asset_library.png differ diff --git a/VisualPinball.Unity/Assets/Editor/Icons/small_gray/asset_library.png.meta b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/asset_library.png.meta new file mode 100644 index 000000000..35c11e137 --- /dev/null +++ b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/asset_library.png.meta @@ -0,0 +1,122 @@ +fileFormatVersion: 2 +guid: 27a2eddcb7f17fb46b2757a26cdd28d4 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Assets/Editor/Icons/small_gray/calendar.png b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/calendar.png new file mode 100644 index 000000000..aeaa20649 Binary files /dev/null and b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/calendar.png differ diff --git a/VisualPinball.Unity/Assets/Editor/Icons/small_gray/calendar.png.meta b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/calendar.png.meta new file mode 100644 index 000000000..0376b80b1 --- /dev/null +++ b/VisualPinball.Unity/Assets/Editor/Icons/small_gray/calendar.png.meta @@ -0,0 +1,122 @@ +fileFormatVersion: 2 +guid: cf7326f171ba0fc4c900f98980dc421a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 2 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 64 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Server + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser.meta new file mode 100644 index 000000000..3d46f7101 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72b6e71ef81848578f7a444569738193 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.cs new file mode 100644 index 000000000..5c016e9e2 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.cs @@ -0,0 +1,494 @@ +// Visual Pinball Engine +// Copyright (C) 2022 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable InconsistentNaming + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace VisualPinball.Unity.Editor +{ + public partial class AssetBrowserX : EditorWindow, IDragHandler + { + [SerializeField] + private int _thumbnailSize = 150; + + [SerializeField] + public string ActiveLibraryForCategories; + + [NonSerialized] + public List Libraries; + + [SerializeField] + private List _selectedLibraries; + + [NonSerialized] + private List _assets; + + [NonSerialized] + private readonly HashSet _previewLoadingAssets = new(); + + [NonSerialized] + public AssetQuery Query; + + private AssetResult LastSelectedAsset { + set => _detailsElement.Asset = value; + } + + private AssetResult _firstSelectedAsset; + private readonly HashSet _selectedAssets = new(); + + private readonly Dictionary _elementByAsset = new(); + private readonly Dictionary _assetsByElement = new(); + + [MenuItem("Visual Pinball/Asset Browser")] + public static void ShowWindow() + { + var wnd = GetWindow(); + wnd.titleContent = new GUIContent("Asset Browser", Icons.AssetLibrary(IconSize.Small)); + + // Limit size of the window + wnd.minSize = new Vector2(640, 240); + } + + #region Data + + private void Refresh() + { + RefreshLibraries(); + RefreshCategories(); + RefreshAssets(); + } + + private void RefreshLibraries() + { + var selectedLibraries = _selectedLibraries == null ? null : new HashSet(_selectedLibraries); + + if (Libraries != null) { + foreach (var lib in Libraries) { + lib.OnChange -= OnLibraryChanged; + } + } + + // find library assets + Libraries = AssetDatabase.FindAssets($"t:{typeof(AssetLibrary)}") + .Select(AssetDatabase.GUIDToAssetPath) + .Select(AssetDatabase.LoadAssetAtPath) + .Where(asset => asset != null).ToList(); + + // toggle label + if (Libraries.Count == 0) { + _noLibrariesLabel.RemoveFromClassList("hidden"); + } else { + _noLibrariesLabel.AddToClassList("hidden"); + } + + // setup query + Query = new AssetQuery(Libraries.Where(lib => lib.IsActive).ToList()); + Query.OnQueryUpdated += OnQueryUpdated; + + // update left column and subscribe + _libraryList.Clear(); + foreach (var lib in Libraries) { + lib.IsActive = selectedLibraries?.Contains(lib.Id) ?? true; + _libraryList.Add(NewAssetLibrary(lib)); + lib.OnChange += OnLibraryChanged; + } + } + + private void OnLibraryChanged(object sender, EventArgs e) + { + RefreshLibraries(); + RefreshCategories(); + _detailsElement.UpdateDetails(); + } + + public void FilterByAttribute(string attributeKey, string value, bool remove = false) + { + var queryString = attributeKey.Contains(" ") ? $"\"{attributeKey}\":" : $"{attributeKey}:"; + queryString += value.Contains(" ") ? $"\"{value}\"" : value; + + if (remove) { + _queryInput.value = _queryInput.value.Replace(queryString, "").Trim(); + + } else { + _queryInput.value = $"{_queryInput.value} {queryString}".Trim(); + } + } + + public void FilterByTag(string tag, bool remove = false) + { + if (remove) { + _queryInput.value = _queryInput.value.Replace($"[{tag}]", "").Trim(); + + } else { + _queryInput.value = $"{_queryInput.value} [{tag}]".Trim(); + } + } + + + private void RefreshCategories() + { + _categoryView.Refresh(this); + } + + private void RefreshAssets() + { + Query.Search(_queryInput.value); + } + + private void OnQueryUpdated(object sender, AssetQueryResult e) + { + UpdateQueryResults(e.Rows); + } + + private void UpdateQueryResults(List assets) + { + _statusLabel.text = $"Found {assets.Count} asset" + (assets.Count == 1 ? "" : "s") + "."; + _assets = assets; + _gridContent.Clear(); + _elementByAsset.Clear(); + _assetsByElement.Clear(); + _selectedAssets.Clear(); + _previewLoadingAssets.Clear(); + + LastSelectedAsset = null; + foreach (var row in assets) { + var element = NewItem(row); + _elementByAsset[row.Asset] = element; + _assetsByElement[element] = row; + _gridContent.Add(_elementByAsset[row.Asset]); + if (row.IsLoadingAssetPreview) { + _previewLoadingAssets.Add(row); + } + } + + if (!assets.Contains(_firstSelectedAsset)) { + _firstSelectedAsset = null; + } else { + SelectOnly(_firstSelectedAsset); + } + } + + private void AddAssetContextMenu(ContextualMenuPopulateEvent evt) + { + if (evt.target is VisualElement ve && _assetsByElement.ContainsKey(ve)) { + var clickedAsset = _assetsByElement[ve]; + var lib = _assetsByElement[ve].Library; + if (!lib.IsLocked) { + evt.menu.AppendAction("Remove from Library", _ => { + if (!_selectedAssets.Contains(clickedAsset)) { + _selectedAssets.Add(clickedAsset); + ToggleSelectionClass(_elementByAsset[clickedAsset.Asset]); + } + var numRemovedAssets = 0; + foreach (var asset in _selectedAssets.Where(a => !a.Library.IsLocked).ToList()) { + _selectedAssets.Remove(asset); + asset.Library.RemoveAsset(asset.Asset); + numRemovedAssets++; + } + + RefreshCategories(); + RefreshAssets(); + _statusLabel.text = $"Removed {numRemovedAssets} assets from library."; + }); + } + } + } + + private void OnEmptyClicked(PointerUpEvent evt) + { + SelectNone(); + } + + private void OnAssetClicked(IMouseEvent evt, VisualElement element) + { + // only interested in left click here + if (evt.button != 0) { + return; + } + var clickedAsset = _assetsByElement[element]; + + // no modifier pressed + if (!evt.shiftKey && !evt.ctrlKey) { + // already selected? + if (_selectedAssets.Contains(clickedAsset)) { + if (_selectedAssets.Count != 1) { + SelectOnly(clickedAsset); + } // if count is 1, and user clicks on it, do nothing. + } else { + SelectOnly(clickedAsset); + } + } + + // only CTRL pressed + if (!evt.shiftKey && evt.ctrlKey) { + // already selected? + if (_selectedAssets.Contains(clickedAsset)) { + UnSelect(clickedAsset); + } else { + Select(clickedAsset); + } + } + + // only SHIFT pressed + if (evt.shiftKey && !evt.ctrlKey) { + var startIndex = _firstSelectedAsset != null ? _assets.IndexOf(_firstSelectedAsset) : 0; + var endIndex = _assets.IndexOf(clickedAsset); + LastSelectedAsset = clickedAsset; + SelectRange(startIndex, endIndex); + } + + + // both SHIFT and CTRL pressed + if (evt.shiftKey && evt.ctrlKey) { + // todo + } + } + + public void OnCategoriesUpdated(Dictionary> categories) => Query.Filter(categories); + private void OnSearchQueryChanged(ChangeEvent evt) => Query.Search(evt.newValue); + private void OnLibraryToggled(AssetLibrary lib, bool enabled) + { + lib.IsActive = enabled; + Query.Toggle(lib); + _selectedLibraries = Libraries.Where(l => l.IsActive).Select(l => l.Id).ToList(); + RefreshCategories(); + } + + public AssetLibrary GetLibraryByPath(string pathToCheck) + { + pathToCheck = pathToCheck.Replace('\\', '/'); + return Libraries.FirstOrDefault(assetLibrary => { + var libraryPath = assetLibrary.LibraryRoot.Replace('\\', '/'); + return pathToCheck.StartsWith(libraryPath); + }); + } + + public void AddAssets(IEnumerable paths, Func getCategory) + { + var numAdded = 0; + var numUpdated = 0; + AssetLibrary updatedLibrary = null; + foreach (var path in paths) { + var assetLibrary = GetLibraryByPath(path); + if (assetLibrary == null) { + continue; + } + var obj = AssetDatabase.LoadAssetAtPath(path, AssetDatabase.GetMainAssetTypeAtPath(path)); + var category = getCategory(assetLibrary); + + if (assetLibrary.AddAsset(obj, category)) { + Debug.Log($"{Path.GetFileName(path)} added to library {assetLibrary.Name}."); + numAdded++; + } else { + Debug.Log($"{Path.GetFileName(path)} updated in library {assetLibrary.Name}."); + numUpdated++; + } + updatedLibrary = assetLibrary; + } + + _categoryView.Refresh(this); + + if (numAdded > 0 && numUpdated == 0) { + _statusLabel.text = $"{numAdded} asset" + (numAdded == 1 ? "" : "s") + $" added to library {updatedLibrary!.Name}."; + } else if (numAdded == 0 && numUpdated > 0) { + _statusLabel.text = $"{numUpdated} asset" + (numUpdated == 1 ? "" : "s") + $" updated in library {updatedLibrary!.Name}."; + } else if (numAdded > 0 && numUpdated > 0) { + _statusLabel.text = $"{numAdded} asset" + (numAdded == 1 ? "" : "s") + $" added and {numUpdated} asset" + (numUpdated == 1 ? "" : "s") + $" updated in library {updatedLibrary!.Name}."; + } else { + _statusLabel.text = "No assets added to library."; + } + } + + #endregion + + #region Selection + + private void SelectRange(int start, int end) + { + if (start > end) { + (start, end) = (end, start); + } + for (var i = 0; i < _assets.Count; i++) { + var asset = _assets[i]; + if (i >= start && i <= end) { + if (!_selectedAssets.Contains(asset)) { + _selectedAssets.Add(asset); + ToggleSelectionClass(_elementByAsset[asset.Asset]); + } + } else if (_selectedAssets.Contains(asset)) { + _selectedAssets.Remove(asset); + ToggleSelectionClass(_elementByAsset[asset.Asset]); + } + } + } + + private void SelectNone() + { + foreach (var selectedAsset in _selectedAssets) { + ToggleSelectionClass(_elementByAsset[selectedAsset.Asset]); + } + _selectedAssets.Clear(); + _firstSelectedAsset = null; + LastSelectedAsset = null; + } + + private void SelectOnly(AssetResult asset) + { + var wasAlreadySelected = false; + foreach (var selectedAsset in _selectedAssets) { + if (selectedAsset != asset) { + ToggleSelectionClass(_elementByAsset[selectedAsset.Asset]); + } else { + wasAlreadySelected = true; + } + } + _selectedAssets.Clear(); + _selectedAssets.Add(asset); + if (!wasAlreadySelected) { + ToggleSelectionClass(_elementByAsset[asset.Asset]); + } + _firstSelectedAsset = asset; + LastSelectedAsset = asset; + } + + private void UnSelect(AssetResult asset) + { + _selectedAssets.Remove(asset); + ToggleSelectionClass(_elementByAsset[asset.Asset]); + _firstSelectedAsset = _selectedAssets.Count > 0 ? _selectedAssets.FirstOrDefault() : null; + LastSelectedAsset = _selectedAssets.Count > 0 ? _selectedAssets.LastOrDefault() : null; + } + + + private void Select(AssetResult asset) + { + _selectedAssets.Add(asset); + ToggleSelectionClass(_elementByAsset[asset.Asset]); + LastSelectedAsset = asset; + } + + private static void ToggleSelectionClass(VisualElement element) => element.ToggleInClassList("selected"); + + #endregion Selection + + #region Drag and Drop + + private static void StartDraggingAssets(HashSet data) => DragAndDrop.SetGenericData("assets", data); + public static void StopDraggingAssets() => DragAndDrop.SetGenericData("assets", null); + + public static bool IsDraggingExistingAssets => DragAndDrop.GetGenericData("assets") is HashSet; + + public static bool IsDraggingNewAssets => DragAndDrop.paths is { Length: > 0 }; + + private void OnDragEnterEvent(DragEnterEvent evt) + { + DragError = null; + + if (!IsDraggingNewAssets) { + return; + } + + if (_categoryView.NumCategories == 0) { + DragError = "Unknown category. Seems there are no categories in the database, so you'll need to create one first."; + return; + } + + if (_categoryView.NumSelectedCategories != 1) { + DragError = "Unknown category. You have to filter by one single category when dragging into the grid view. But you can also drag onto the category directly on the left directly."; + return; + } + + foreach (var path in DragAndDrop.paths) { + var assetLibrary = GetLibraryByPath(path); + + if (assetLibrary == null) { + DragError = "Unknown library. Your assets must be under the root of a library, and at least one of the assets you're dragging is not."; + break; + } + + if (assetLibrary.IsLocked) { + DragError = "Access Error. The library you're trying to add assets to is locked."; + break; + } + } + } + + private static void OnDragUpdatedEvent(DragUpdatedEvent evt) + { + DragAndDrop.visualMode = IsDraggingNewAssets ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected; + } + + private void OnDragLeaveEvent(DragLeaveEvent evt) + { + // hide error panel + DragError = null; + } + + private void OnDragPerformEvent(DragPerformEvent evt) + { + if (DragError != null) { + DragError = null; + return; + } + + DragAndDrop.AcceptDrag(); + AddAssets(DragAndDrop.paths, assetLibrary => _categoryView.GetOrCreateSelected(assetLibrary)); + } + + #endregion + + private void Update() + { + foreach (var asset in _previewLoadingAssets) { + if (_elementByAsset.ContainsKey(asset.Asset)) { + asset.RefreshPreviewImage(_elementByAsset[asset.Asset]); + } + } + _previewLoadingAssets.RemoveWhere(asset => !asset.IsLoadingAssetPreview); + } + + private void OnThumbSizeChanged(ChangeEvent evt) + { + _thumbnailSize = (int)evt.newValue; + foreach (var e in _elementByAsset.Values) { + e.style.width = _thumbnailSize; + e.style.height = _thumbnailSize; + } + } + + public void AttachData(AssetResult clickedAsset) + { + if (!_selectedAssets.Contains(clickedAsset)) { + _selectedAssets.Add(clickedAsset); + } + DragAndDrop.objectReferences = _selectedAssets.Select(result => result.Asset.Object).ToArray(); + StartDraggingAssets(_selectedAssets); + } + } + + public interface IDragHandler + { + void AttachData(AssetResult clickedAsset); + } + +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Common/UnitPropertyDrawer.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.cs.meta similarity index 83% rename from VisualPinball.Unity/VisualPinball.Unity.Editor/Common/UnitPropertyDrawer.cs.meta rename to VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.cs.meta index a922a58e7..5771d2a01 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Common/UnitPropertyDrawer.cs.meta +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1accad5d5db00244284c9de6406ebaff +guid: 681ba8a5561dfea4497a01e5b5e1b801 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss new file mode 100644 index 000000000..f9ddf6429 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss @@ -0,0 +1,120 @@ +/* generic stuff */ +/* ------------- */ +.hidden { + display: none; +} + +/* left panel */ +/* ---------- */ + +#leftPane { + padding: 5px; +} + +.left-title { + -unity-font-style: bold; + margin-top: 10px; + margin-bottom: 3px; +} + +.left-empty { + margin: 16px; + white-space: normal; + -unity-font-style: italic; + -unity-text-align: middle-center; +} + +AssetDetailsElement > TemplateContainer { + flex-grow: 1; +} + +#libraryList .library-item { + margin-left: 8px; +} +#libraryList .library-item Image { + width: 16px; + height: 16px; +} + +LibraryCategoryView { + flex-grow: 1; +} + +#dragErrorContainerLeft { + margin: -5px; +} + +/* "grid" */ +/* ------ */ +#gridContent { + flex-grow: 1; + background-color: #333333; /* sampled from project window */ +} + +#gridContent .unity-scroll-view__content-container { + flex-direction: row; + flex-wrap: wrap; + padding: 10px; +} + +#gridContent .unity-image { + width: 150px; + height: 150px; + margin: 10px 10px 5px 10px; +} + +#dragErrorContainer, #dragErrorContainerLeft { + background-color: darkred; + padding: 2px 0 0 5px; + flex-wrap: wrap; +} + +#dragErrorContainer Label, #dragErrorContainerLeft Label { + white-space: normal; +} + +/* thumb item */ +/* ---------- */ +.thumb-item { + align-items: center; + overflow: scroll; +} +.thumb-item #thumbnail { + flex: 1; +} +.thumb-item #label { + margin-top: 0; + margin-bottom: 5px; + font-size: 80%; +} +.selected .thumb-item { + background-color: #2C5D87; /* picked from list view */ +} + +/* bottom toolbar */ +/* -------------- */ +#bottomToolbar { + justify-content: space-between; + border-top-width: 1px; + border-bottom-width: 0; + padding: 0 5px; + background-color: #383838; /* sampled from background */ +} + +#bottomLabel { margin-top: 2px; } +#sizeSlider { width: 75px; } + + +/* fixes and overridden colors */ +/* --------------------------- */ +ListView { + flex-grow: 1; +} + +.unity-two-pane-split-view { + flex-grow: 1; +} + +.unity-two-pane-split-view__dragline-anchor { + background-color: #1d1d1d; +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss.meta new file mode 100644 index 000000000..ed016fa45 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 98b5afa4f7306e04fb160371138d6662 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml new file mode 100644 index 000000000..f317c4df7 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml.meta new file mode 100644 index 000000000..f50e51df0 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 466b5f13746a85b4ba3ed622cfc5198c +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX_Init.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX_Init.cs new file mode 100644 index 000000000..8d185d408 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX_Init.cs @@ -0,0 +1,178 @@ +// Visual Pinball Engine +// Copyright (C) 2022 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System.IO; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine.UIElements; + +namespace VisualPinball.Unity.Editor +{ + public partial class AssetBrowserX + { + private ToolbarButton _refreshButton; + private ToolbarSearchField _queryInput; + + private LibraryCategoryView _categoryView; + private VisualElement _libraryList; + private Label _noLibrariesLabel; + + private VisualElement _gridContent; + private Label _dragErrorLabelLeft; + private VisualElement _dragErrorContainerLeft; + private Label _dragErrorLabel; + private VisualElement _dragErrorContainer; + private AssetDetailsElement _detailsElement; + private Label _statusLabel; + private Slider _sizeSlider; + + private VisualTreeAsset _assetTree; + + public string DragErrorLeft { + get => _dragErrorContainerLeft.ClassListContains("hidden") ? null : _dragErrorLabelLeft.text; + set { + if (value == null) { + if (!_dragErrorContainerLeft.ClassListContains("hidden")) { + _dragErrorContainerLeft.AddToClassList("hidden"); + } + return; + } + + _dragErrorContainerLeft.RemoveFromClassList("hidden"); + _dragErrorLabelLeft.text = value; + } + } + + private string DragError { + get => _dragErrorContainer.ClassListContains("hidden") ? null : _dragErrorLabel.text; + set { + if (value == null) { + if (!_dragErrorContainer.ClassListContains("hidden")) { + _dragErrorContainer.AddToClassList("hidden"); + } + return; + } + + _dragErrorContainer.RemoveFromClassList("hidden"); + _dragErrorLabel.text = value; + } + } + + /// + /// Setup the UI. Data is already set up at this point. We'll just trigger a refresh once the UI is set up. + /// + public void CreateGUI() + { + // import UXML + var visualTree = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uxml"); + visualTree.CloneTree(rootVisualElement); + _assetTree = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/LibraryAssetElement.uxml"); + + var ui = rootVisualElement; + + // import style sheet + var styleSheet = AssetDatabase.LoadAssetAtPath("Packages/org.visualpinball.engine.unity/VisualPinball.Unity/VisualPinball.Unity.Editor/AssetBrowser/AssetBrowserX.uss"); + ui.styleSheets.Add(styleSheet); + + // libraries + _libraryList = ui.Q("libraryList"); + _noLibrariesLabel = ui.Q