diff --git a/Editor/TextureBrowser.uss b/Editor/TextureBrowser.uss index c72c24d..e77ecb5 100644 --- a/Editor/TextureBrowser.uss +++ b/Editor/TextureBrowser.uss @@ -2,6 +2,12 @@ height: 30px; } +.toolbar-button { + min-width: 30px; + width: 30px; + padding: 4px; +} + .texture-list-container { flex-grow: 1; } @@ -20,9 +26,8 @@ } .tex-parent { - background-size: contain; - background-repeat: no-repeat; margin: 2px; + margin-bottom: 19px; flex-direction: column-reverse; border-color: rgba(0,0,0,.7); border-width: 1px; @@ -30,32 +35,56 @@ .tex-parent:hover { border-color: white; - border-width: 1px; } .tex-mat-button { - width: 30px; - height: 30px; + width: 24px; + height: 24px; position: absolute; top: 2px; - right: 2px; + right: 0px; + padding-top: 1px; + padding-left: 3px; + padding-right: 1px; + opacity: 0.2; display: none; } +.tex-mat-button:hover { + opacity: 1; +} + .tex-parent:hover .tex-mat-button { display: initial; } +.tex-image { + position: absolute; + width: 100%; + height: 100%; + background-size: contain; + background-repeat: repeat; + background-size: 32px; + --unity-image-size: scale-and-crop; +} + .tex-fav-button { - width: 30px; - height: 30px; + width: 24px; + height: 24px; position: absolute; top: 2px; - left: 2px; + left: 0px; + padding-left: 1px; + padding-right: 1px; + opacity: 0.2; display: none; } -.tex-parent:hover .tex-fav-button{ +.tex-fav-button:hover { + opacity: 1; +} + +.tex-parent:hover .tex-fav-button { display: initial; } @@ -65,26 +94,56 @@ } .tex-label { - background-color: rgba(0,0,0,.7); + height: 16px; + background-color: rgba(0,0,0,.4); + bottom: -17px; + margin-left: -1px; + margin-right: -1px; + padding-left: 17px; + padding-right: 1px; + overflow: hidden; + text-overflow: ellipsis; +} + +.tex-label-icon { + width: 16px; + left: -16px; + --unity-image-tint-color: #6BB6D3; +} + +.tex-favourite .tex-label-icon { + --unity-image-tint-color: #BF8600; } .info-label { padding: 3px; } -.folder-selection{ +.path-label { + padding: 3px; + width: 10px; flex-grow: 1; - min-width: 500px; + overflow: hidden; + text-overflow: ellipsis; + -unity-text-overflow-position: start; } -.size-slider{ - flex-grow: 1; - min-width: 300px; +.preferences-button { + min-width: 26px; +} + +.size-slider { + width: 50px; } -.search-field{ +.search-field { flex-grow: 1; - min-width: 300px; + width: 100px; +} + +.unity-search-field-base__cancel-button { + width: 20px; + height: 19px; } /* saved search */ @@ -100,7 +159,7 @@ padding: 4px; } -.saved-search-button{ +.saved-search-button { height: 24px; flex-direction: row; padding-left: 10px; @@ -145,4 +204,3 @@ -unity-font-style: bold; margin-bottom: 10px; } - diff --git a/Editor/TextureBrowserWindow.cs b/Editor/TextureBrowserWindow.cs index 58a3338..6ec5371 100644 --- a/Editor/TextureBrowserWindow.cs +++ b/Editor/TextureBrowserWindow.cs @@ -16,7 +16,7 @@ public class TextureBrowserWindow : EditorWindow { [SerializeField] private StyleSheet m_styleSheet; - [MenuItem("LEG/Texture Browser")] + [MenuItem("Window/Texture Browser")] private static void Open() { var window = GetWindow(); @@ -27,19 +27,24 @@ private static void Open() } private static StyleSheet _styleSheet; // static copy for settings provider + private const int TexParentBorderWidth = 1; // can't fetch this at the start unfortunately so need to keep it here // window state - private Vector2 m_scroll; private TextureInfo m_draggedTexture; private Vector2 m_dragStartPosition; private string m_searchString = ""; private Texture2D m_materialIcon; + private Texture2D m_textureIcon; + private Texture2D m_generateMaterialIcon; private Texture2D m_favouriteIcon; private Texture2D m_usedIcon; + private Texture2D m_plusIcon; + private Texture2D m_refreshIcon; + private Texture2D m_settingsIcon; private VisualElement m_textureListView; private VisualElement m_quickSearchView; - private FilterFlags m_filter = FilterFlags.None; + private FilterFlags m_filter = FilterFlags.Materials | FilterFlags.Textures; // data @@ -49,7 +54,8 @@ private static void Open() private readonly List m_savedSearches = new(); private ToolbarSearchField m_searchField; private Label m_infoLabel; - private EditorCoroutine m_loadRoutine; + private Label m_resolutionLabel; + private Label m_pathLabel; private ToolbarButton m_refreshButton; private int m_loadTaskId; @@ -59,46 +65,73 @@ private enum FilterFlags None = 1 << 0, Favourites = 1 << 1, Used = 1 << 2, + Textures = 1 << 3, + Materials = 1 << 4, } - + private class TextureInfo : IComparable { - public string Name { get; } + public string Name { get; private set; } public Texture2D Texture { get; } - public Material Material { get; set; } + public string Path { get; private set; } + private Material _material; + public Material Material + { + get => _material; + set + { + if (_material == value) return; + _material = value; + + if (_material == null) + { + Name = Texture.name; + Path = AssetDatabase.GetAssetPath(Texture); + return; + } + + Name = _material.name; + Path = AssetDatabase.GetAssetPath(_material); + } + } + public bool IsMaterial { get => Material != null; } public bool IsFavourite { get; set; } - public VisualElement Element { get; set; } + public VisualElement UIElement { get; set; } - public TextureInfo(string name, Texture2D tex) + public TextureInfo(Texture2D tex) { - if (name.StartsWith("mat.")) - name = name.Remove(0, "mat.".Length); - Name = name; + Name = tex.name; Texture = tex; + Path = AssetDatabase.GetAssetPath(tex); } public int CompareTo(TextureInfo other) { if (ReferenceEquals(this, other)) return 0; if (other is null) return 1; - return string.Compare(Name, other.Name, StringComparison.Ordinal); + return string.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase); } } private void OnEnable() { _styleSheet = m_styleSheet; // for settings provider, hacky but whatever - - m_materialIcon = EditorGUIUtility.IconContent("Material Icon").image as Texture2D; - m_favouriteIcon = EditorGUIUtility.IconContent("Favorite").image as Texture2D; - m_usedIcon = EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolSplat On").image as Texture2D; + + m_materialIcon = EditorGUIUtility.IconContent("d_Material On Icon").image as Texture2D; + m_textureIcon = EditorGUIUtility.IconContent("d_RawImage Icon").image as Texture2D; + m_generateMaterialIcon = EditorGUIUtility.IconContent("d_ProceduralMaterial Icon").image as Texture2D; + m_favouriteIcon = EditorGUIUtility.IconContent("d_Favorite On Icon").image as Texture2D; + m_usedIcon = EditorGUIUtility.IconContent("LightProbeGroup Gizmo").image as Texture2D; + m_plusIcon = EditorGUIUtility.IconContent("Toolbar Plus").image as Texture2D; + m_refreshIcon = EditorGUIUtility.IconContent("d_Refresh").image as Texture2D; + m_settingsIcon = EditorGUIUtility.IconContent("Settings").image as Texture2D; } private void OnDisable() { // cleanup statics _styleSheet = null; - + m_data.Clear(); m_materialCache.Clear(); m_favourites.Clear(); @@ -123,8 +156,8 @@ private void CreateGUI() var saveSearchButton = new ToolbarButton(SaveCurrentSearchTerm) { tooltip = "Save Search", - iconImage = EditorGUIUtility.IconContent("Toolbar Plus").image as Texture2D - }.WithClasses("save-search-button").AddTo(toolbar); + iconImage = m_plusIcon + }.WithClasses("save-search-button", "toolbar-button").AddTo(toolbar); saveSearchButton.SetEnabled(false); @@ -137,11 +170,58 @@ private void CreateGUI() new ToolbarSpacer().AddTo(toolbar); + var texToggle = new ToolbarToggle() + { + value = m_filter.HasFlag(FilterFlags.Textures), + tooltip = "Show raw Textures without associated Materials" + } + .WithClasses("toolbar-button") + .AddTo(toolbar); + + texToggle.Add(new Image() { image = m_textureIcon }); + texToggle.RegisterValueChangedCallback(e => + { + if (e.newValue) + { + m_filter |= FilterFlags.Textures; + } + else + { + m_filter &= ~FilterFlags.Textures; + } + + PopulateList(); + }); + + var matToggle = new ToolbarToggle() + { + value = m_filter.HasFlag(FilterFlags.Materials), + tooltip = "Show Textures with Materials" + } + .WithClasses("toolbar-button") + .AddTo(toolbar); + + matToggle.Add(new Image() { image = m_materialIcon }); + matToggle.RegisterValueChangedCallback(e => + { + if (e.newValue) + { + m_filter |= FilterFlags.Materials; + } + else + { + m_filter &= ~FilterFlags.Materials; + } + + PopulateList(); + }); + var faveToggle = new ToolbarToggle() { value = m_filter.HasFlag(FilterFlags.Favourites), tooltip = "Show only Favourites" } + .WithClasses("toolbar-button") .AddTo(toolbar); faveToggle.Add(new Image() { image = m_favouriteIcon }); @@ -164,6 +244,7 @@ private void CreateGUI() value = m_filter.HasFlag(FilterFlags.Favourites), tooltip = "Show only textures used in open scenes" } + .WithClasses("toolbar-button") .AddTo(toolbar); usedToggle.Add(new Image() { image = m_usedIcon }); @@ -183,29 +264,30 @@ private void CreateGUI() m_refreshButton = new ToolbarButton(TryRefresh) { - iconImage = EditorGUIUtility.IconContent("Refresh").image as Texture2D, + iconImage = m_refreshIcon, tooltip = "Refresh content" } + .WithClasses("toolbar-button") .AddTo(toolbar); - + m_refreshButton.SetEnabled(false); // saved search list - + m_quickSearchView = new VisualElement().WithClasses("saved-search-list").AddTo(root); - + // texture grid m_textureListView = new ScrollView(ScrollViewMode.Vertical).AddTo(root); m_textureListView.AddToClassList("texture-list-container"); m_textureListView.contentContainer.AddToClassList("texture-list"); - + // footer var footer = new Toolbar().AddTo(root).WithClasses("footer"); - + m_infoLabel = new Label().AddTo(footer).WithClasses("info-label"); - + new SliderInt { value = ThumbnailSize, lowValue = 1, highValue = 4 } .WithClasses("size-slider") .AddTo(footer) @@ -215,31 +297,35 @@ private void CreateGUI() foreach (var e in m_data) { - if (e.Element != null) + if (e.UIElement != null) { - e.Element.style.width = ThumbnailSize * 64; - e.Element.style.height = ThumbnailSize * 64; + e.UIElement.style.minWidth = ThumbnailSize * 64 + TexParentBorderWidth * 2; + e.UIElement.style.minHeight = ThumbnailSize * 64 + TexParentBorderWidth * 2; } } }); new ToolbarButton(() => { SettingsService.OpenUserPreferences("Preferences/Texture Browser"); }) { - iconImage = EditorGUIUtility.IconContent("Settings").image as Texture2D, + iconImage = m_settingsIcon, tooltip = "Open preferences" } + .WithClasses("preferences-button") .AddTo(footer); + m_resolutionLabel = new Label().AddTo(footer).WithClasses("info-label"); + m_pathLabel = new Label().AddTo(footer).WithClasses("path-label"); + TryRefresh(); return; void TryRefresh() { - if (Progress.Exists(m_loadTaskId)) + if (m_loadTaskId != 0 && Progress.Exists(m_loadTaskId)) { return; } - + m_refreshButton.SetEnabled(false); EditorCoroutineUtility.StartCoroutine(FullRefresh(), this); } @@ -248,16 +334,16 @@ void TryRefresh() private IEnumerator FullRefresh() { m_loadTaskId = Progress.Start("Refreshing Textures"); - + LoadQuickSearch(); LoadFavourites(); LoadContent(); PopulateList(); yield return null; - + Progress.Remove(m_loadTaskId); - + m_refreshButton.SetEnabled(true); } @@ -277,11 +363,15 @@ private void RefreshQuickSearchContent() foreach (var search in m_savedSearches) { + if (string.IsNullOrEmpty(search)) + { + continue; + } + var button = new Button().WithClasses("saved-search-button"); button.RegisterCallback(evt => { LoadSearchTerm(search, evt.modifiers == EventModifiers.Shift); - }); button.Add(new Label { name = "label", text = search }); button.Add(new Button(() => DeleteSearchTerm(search)) { name = "delete", tooltip = "Remove", text = "X"}); @@ -295,11 +385,11 @@ private void SaveCurrentSearchTerm() { return; } - + m_savedSearches.Add(m_searchString); - + EditorPrefs.SetString(SavedSearchesKey, string.Join("|", m_savedSearches)); - + RefreshQuickSearchContent(); } @@ -314,59 +404,116 @@ private void LoadSearchTerm(string searchTerm, bool additive) m_searchField.value = searchTerm; } } - + private void DeleteSearchTerm(string searchTerm) { m_savedSearches.Remove(searchTerm); - + EditorPrefs.SetString(SavedSearchesKey, string.Join("|", m_savedSearches)); - + RefreshQuickSearchContent(); } - + #endregion - + private VisualElement CreateElement(TextureInfo texInfo) { - var texElement = new VisualElement().WithClasses("tex-parent"); - - texElement.name = texInfo.Texture.name; // texture name helps us ID this later - + var texParent = new VisualElement().WithClasses("tex-parent"); + + texParent.name = texInfo.Texture.name; // texture name helps us ID this later + + bool hoveringTexture = false; + float widthAspect = (float)texInfo.Texture.width / texInfo.Texture.height; + float heightAspect = (float)texInfo.Texture.height / texInfo.Texture.width; + bool textureAspectEven = widthAspect == heightAspect; + if (texInfo.IsFavourite) { - texElement.WithClasses("tex-favourite"); + texParent.WithClasses("tex-favourite"); } - - texElement.style.backgroundImage = texInfo.Texture; - texElement.style.width = ThumbnailSize * 64; - texElement.style.height = ThumbnailSize * 64; - - new Label { text = texInfo.Name }.WithClasses("tex-label").AddTo(texElement); - + + texParent.style.minWidth = ThumbnailSize * 64 + TexParentBorderWidth * 2; + texParent.style.minHeight = ThumbnailSize * 64 + TexParentBorderWidth * 2; + + var texImage = new Image { image = texInfo.Texture }.WithClasses("tex-image").AddTo(texParent); + texImage.style.backgroundImage = AssetDatabase.GetBuiltinExtraResource("Default-Checker-Gray.png"); + + if (ColorUtility.TryParseHtmlString(CheckerBGTint, out Color checkerBGTintColor)) + { + texImage.style.unityBackgroundImageTintColor = checkerBGTintColor; + } + else + { + texImage.style.unityBackgroundImageTintColor = new Color(1, 1, 1, 0.25f); + } + + if (!textureAspectEven) MatchImageAspectToTexture(texImage); + + var texNameLabel = new Label { text = texInfo.Name }.WithClasses("tex-label").AddTo(texParent); + var texLabelIcon = new Image { image = m_materialIcon }.WithClasses("tex-label-icon").AddTo(texNameLabel); + // button to ping material - - new Button(() => + var pingMaterialButton = new Button(() => { var material = FindOrCreateMaterial(texInfo); EditorGUIUtility.PingObject(material); - - }){ iconImage = m_materialIcon, tooltip = "Locate Material In Project"} + + }){ iconImage = m_materialIcon, tooltip = "Locate Material in Project" } .WithClasses("tex-mat-button") - .AddTo(texElement); - + .AddTo(texParent); + + if (!texInfo.IsMaterial) + { + pingMaterialButton.visible = false; + texLabelIcon.image = m_textureIcon; + + // button to generate a new material from the texture + new Button(() => + { + var material = FindOrCreateMaterial(texInfo); + EditorGUIUtility.PingObject(material); + ChangeTextureEntryToMaterial(material); + + }){ name = "generate-mat-button", iconImage = m_generateMaterialIcon, tooltip = "Generate new Material for Texture" } + .WithClasses("tex-mat-button") + .AddTo(texParent); + } + // button to toggle favourite - + new Button(() => { ToggleFavourite(texInfo); - }){ iconImage = m_favouriteIcon, tooltip = "Favourite"} + }){ iconImage = m_favouriteIcon, tooltip = "Favourite" } .WithClasses("tex-fav-button") - .AddTo(texElement); - + .AddTo(texParent); + + // enter and leave events for hovering + + texParent.RegisterCallback(evt => + { + hoveringTexture = true; + m_resolutionLabel.text = $"[{texInfo.Texture.width} x {texInfo.Texture.height}]"; + m_pathLabel.text = texInfo.Path; + texImage.style.backgroundSize = new BackgroundSize(new Length(0, LengthUnit.Pixel), new Length(0, LengthUnit.Pixel)); + + if (ZoomNonSquare) ResetImageAspect(texImage); + }); + + texParent.RegisterCallback(evt => + { + hoveringTexture = false; + m_resolutionLabel.text = string.Empty; + m_pathLabel.text = string.Empty; + texImage.style.backgroundSize = new BackgroundSize(new Length(32, LengthUnit.Pixel), new Length(32, LengthUnit.Pixel)); + + if (ZoomNonSquare) MatchImageAspectToTexture(texImage); + }); + // start drag on mouse down - texElement.RegisterCallback(evt => + texParent.RegisterCallback(evt => { if (evt.button == 0) // Left mouse button { @@ -374,59 +521,138 @@ private VisualElement CreateElement(TextureInfo texInfo) m_dragStartPosition = evt.localMousePosition; } }); - + // rmb context - texElement.RegisterCallback(_ => + texParent.RegisterCallback(_ => { var menu = new GenericMenu(); - menu.AddItem(new GUIContent("Find in Project"), false, () => + menu.AddItem(new GUIContent("Locate Texture in Project"), false, () => { EditorGUIUtility.PingObject(texInfo.Texture); }); menu.ShowAsContext(); }); - texElement.RegisterCallback(evt => + texParent.RegisterCallback(evt => { // start drag after a small movement - + if (m_draggedTexture != null && Vector2.Distance(evt.localMousePosition, m_dragStartPosition) > 5) { var material = FindOrCreateMaterial(m_draggedTexture); + if (!texInfo.IsMaterial) ChangeTextureEntryToMaterial(material); + DragAndDrop.PrepareStartDrag(); DragAndDrop.objectReferences = new Object[] { material }; DragAndDrop.StartDrag(material.name); m_draggedTexture = null; // Reset dragged texture } + + // pan a zoomed texture relative to the mouse position + + if (!textureAspectEven && ZoomNonSquare && hoveringTexture) + { + float borderSafeZone = 0.2f; // percentage in 0 to 1, adds an invisible border around the texture where it doesn't pan + float thumbnailSafeZoneSize = ThumbnailSize * 64 * borderSafeZone; + float thumbnailSizeMinusSafeZone = (ThumbnailSize * 64) - (thumbnailSafeZoneSize * 2); + float localMousePosX = Mathf.Clamp01((evt.mousePosition.x - (texImage.worldBound.x + thumbnailSafeZoneSize)) / thumbnailSizeMinusSafeZone) * -1; + float localMousePosY = Mathf.Clamp01((evt.mousePosition.y - (texImage.worldBound.y + thumbnailSafeZoneSize)) / thumbnailSizeMinusSafeZone); + float posU = 0; + float posV = 0; + + if (heightAspect < 1) + { + posU = (localMousePosX + 0.5f) * (1 - heightAspect); + } + if (widthAspect < 1) + { + posV = (localMousePosY - 0.5f) * (1 - widthAspect); + } + + texImage.uv = new Rect(posU, posV, 1, 1); + texImage.MarkDirtyRepaint(); + } }); - + // reset dragged texture on mouse up - texElement.RegisterCallback(_ => + texParent.RegisterCallback(_ => { - m_draggedTexture = null; + m_draggedTexture = null; }); - return texElement; + return texParent; + + void ChangeTextureEntryToMaterial(Material material) + { + texInfo.Material = material; + texNameLabel.text = texInfo.Name; + texLabelIcon.image = m_materialIcon; + pingMaterialButton.visible = true; + texParent.Q