// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Text; using FlaxEditor.Content.GUI; using FlaxEditor.GUI.Drag; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; namespace FlaxEditor.Content { /// /// Content item types. /// [HideInEditor] public enum ContentItemType { /// /// The binary or text asset. /// Asset, /// /// The directory. /// Folder, /// /// The script file. /// Script, /// /// The scene file. /// Scene, /// /// The other type. /// Other, } /// /// Content item filter types used for searching. /// [HideInEditor] public enum ContentItemSearchFilter { /// /// The model. /// Model, /// /// The skinned model. /// SkinnedModel, /// /// The material. /// Material, /// /// The texture. /// Texture, /// /// The scene. /// Scene, /// /// The prefab. /// Prefab, /// /// The script. /// Script, /// /// The audio. /// Audio, /// /// The animation. /// Animation, /// /// The json. /// Json, /// /// The particles. /// Particles, /// /// The shader source files. /// Shader, /// /// The other. /// Other, } /// /// Interface for objects that can reference the content items in order to receive events from them. /// [HideInEditor] public interface IContentItemOwner { /// /// Called when referenced item gets deleted (asset unloaded, file deleted, etc.). /// Item should not be used after that. /// /// The item. void OnItemDeleted(ContentItem item); /// /// Called when referenced item gets renamed (filename change, path change, etc.) /// /// The item. void OnItemRenamed(ContentItem item); /// /// Called when item gets reimported or reloaded. /// /// The item. void OnItemReimported(ContentItem item); /// /// Called when referenced item gets disposed (editor closing, database internal changes, etc.). /// Item should not be used after that. /// /// The item. void OnItemDispose(ContentItem item); } /// /// Base class for all content items. /// Item parent GUI control is always or null if not in a view. /// /// [HideInEditor] public abstract class ContentItem : Control { /// /// The default margin size. /// public const int DefaultMarginSize = 4; /// /// The default text height. /// public const int DefaultTextHeight = 42; /// /// The default thumbnail size. /// public const int DefaultThumbnailSize = PreviewsCache.AssetIconSize; /// /// The default width. /// public const int DefaultWidth = (DefaultThumbnailSize + 2 * DefaultMarginSize); /// /// The default height. /// public const int DefaultHeight = (DefaultThumbnailSize + 2 * DefaultMarginSize + DefaultTextHeight); private ContentFolder _parentFolder; private bool _isMouseDown; private Float2 _mouseDownStartPos; private readonly List _references = new List(4); private SpriteHandle _thumbnail; private SpriteHandle _shadowIcon; /// /// Gets the type of the item. /// public abstract ContentItemType ItemType { get; } /// /// Gets the type of the item searching filter to use. /// public abstract ContentItemSearchFilter SearchFilter { get; } /// /// Gets a value indicating whether this instance is asset. /// public bool IsAsset => ItemType == ContentItemType.Asset; /// /// Gets a value indicating whether this instance is folder. /// public bool IsFolder => ItemType == ContentItemType.Folder; /// /// Gets a value indicating whether this instance can have children. /// public bool CanHaveChildren => ItemType == ContentItemType.Folder; /// /// Determines whether this item can be renamed. /// public virtual bool CanRename => true; /// /// Gets a value indicating whether this item can be dragged and dropped. /// public virtual bool CanDrag => Root != null; /// /// Gets a value indicating whether this exists on drive. /// public virtual bool Exists => System.IO.File.Exists(Path); /// /// Gets the parent folder. /// public ContentFolder ParentFolder { get => _parentFolder; set { if (_parentFolder == value) return; // Remove from old _parentFolder?.Children.Remove(this); // Link _parentFolder = value; // Add to new _parentFolder?.Children.Add(this); OnParentFolderChanged(); } } /// /// Gets the path to the item. /// public string Path { get; private set; } /// /// Gets the item file name (filename with extension). /// public string FileName { get; internal set; } /// /// Gets the item short name (filename without extension). /// public string ShortName { get; internal set; } /// /// Gets the asset name relative to the project root folder (without asset file extension) /// public string NamePath => FlaxEditor.Utilities.Utils.GetAssetNamePath(Path); /// /// Gets the content item type description (for UI). /// public abstract string TypeDescription { get; } /// /// Gets the default name of the content item thumbnail. Returns null if not used. /// public virtual SpriteHandle DefaultThumbnail => SpriteHandle.Invalid; /// /// Gets a value indicating whether this item has default thumbnail. /// public bool HasDefaultThumbnail => DefaultThumbnail.IsValid; /// /// Gets or sets the item thumbnail. Warning, thumbnail may not be available if item has no references (). /// public SpriteHandle Thumbnail { get => _thumbnail; set => _thumbnail = value; } /// /// True if force show file extension. /// public bool ShowFileExtension; /// /// Initializes a new instance of the class. /// /// The path to the item. protected ContentItem(string path) : base(0, 0, DefaultWidth, DefaultHeight) { // Set path Path = path; FileName = System.IO.Path.GetFileName(path); ShortName = System.IO.Path.GetFileNameWithoutExtension(path); } /// /// Updates the item path. Use with caution or even don't use it. It's dangerous. /// /// The new path. internal virtual void UpdatePath(string value) { Assert.AreNotEqual(Path, value); // Set path Path = StringUtils.NormalizePath(value); FileName = System.IO.Path.GetFileName(value); ShortName = System.IO.Path.GetFileNameWithoutExtension(value); // Fire event OnPathChanged(); for (int i = 0; i < _references.Count; i++) { _references[i].OnItemRenamed(this); } } /// /// Refreshes the item thumbnail. /// public virtual void RefreshThumbnail() { // Skip if item has default thumbnail if (HasDefaultThumbnail) return; var thumbnails = Editor.Instance.Thumbnails; // Delete old thumbnail and remove it from the cache thumbnails.DeletePreview(this); // Request new one (if need to) if (_references.Count > 0) { thumbnails.RequestPreview(this); } } /// /// Updates the tooltip text text. /// public virtual void UpdateTooltipText() { var sb = new StringBuilder(); OnBuildTooltipText(sb); if (sb.Length != 0 && sb[sb.Length - 1] == '\n') { // Remove new-line from end int sub = 1; if (sb.Length != 1 && sb[sb.Length - 2] == '\r') sub = 2; sb.Length -= sub; } TooltipText = sb.ToString(); } /// /// Called when building tooltip text. /// /// The output string builder. protected virtual void OnBuildTooltipText(StringBuilder sb) { sb.Append("Type: ").Append(TypeDescription).AppendLine(); if (File.Exists(Path)) sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine(); } /// /// Tries to find the item at the specified path. /// /// The path. /// Found item or null if missing. public virtual ContentItem Find(string path) { return Path == path ? this : null; } /// /// Tries to find a specified item in the assets tree. /// /// The item. /// True if has been found, otherwise false. public virtual bool Find(ContentItem item) { return this == item; } /// /// Tries to find the item with the specified id. /// /// The id. /// Found item or null if missing. public virtual ContentItem Find(Guid id) { return null; } /// /// Tries to find script with the given name. /// /// Name of the script. /// Found script or null if missing. public virtual ScriptItem FindScriptWitScriptName(string scriptName) { return null; } /// /// Gets a value indicating whether draw item shadow. /// protected virtual bool DrawShadow => false; /// /// Gets the local space rectangle for element name text area. /// public Rectangle TextRectangle { get { // Skip when hidden if (!Visible) return Rectangle.Empty; var view = Parent as ContentView; var size = Size; switch (view?.ViewType ?? ContentViewType.Tiles) { case ContentViewType.Tiles: { var textHeight = DefaultTextHeight * size.X / DefaultWidth; return new Rectangle(0, size.Y - textHeight, size.X, textHeight); } case ContentViewType.List: { var thumbnailSize = size.Y - 2 * DefaultMarginSize; var textHeight = Mathf.Min(size.Y, 24.0f); return new Rectangle(thumbnailSize + DefaultMarginSize * 2, (size.Y - textHeight) * 0.5f, size.X - textHeight - DefaultMarginSize * 3.0f, textHeight); } default: throw new ArgumentOutOfRangeException(); } } } /// /// Draws the item thumbnail. /// /// The thumbnail rectangle. public void DrawThumbnail(ref Rectangle rectangle) { // Draw shadow if (DrawShadow) { const float thumbnailInShadowSize = 50.0f; var shadowRect = rectangle.MakeExpanded((DefaultThumbnailSize - thumbnailInShadowSize) * rectangle.Width / DefaultThumbnailSize * 1.3f); if (!_shadowIcon.IsValid) _shadowIcon = Editor.Instance.Icons.AssetShadow128; Render2D.DrawSprite(_shadowIcon, shadowRect); } // Draw thumbnail if (_thumbnail.IsValid) Render2D.DrawSprite(_thumbnail, rectangle); else Render2D.FillRectangle(rectangle, Color.Black); } /// /// Draws the item thumbnail. /// /// The thumbnail rectangle. /// /// Whether or not to draw the shadow. Overrides DrawShadow. public void DrawThumbnail(ref Rectangle rectangle, bool shadow) { // Draw shadow if (shadow) { const float thumbnailInShadowSize = 50.0f; var shadowRect = rectangle.MakeExpanded((DefaultThumbnailSize - thumbnailInShadowSize) * rectangle.Width / DefaultThumbnailSize * 1.3f); if (!_shadowIcon.IsValid) _shadowIcon = Editor.Instance.Icons.AssetShadow128; Render2D.DrawSprite(_shadowIcon, shadowRect); } // Draw thumbnail if (_thumbnail.IsValid) Render2D.DrawSprite(_thumbnail, rectangle); else Render2D.FillRectangle(rectangle, Color.Black); } /// /// Gets the amount of references to that item. /// public int ReferencesCount => _references.Count; /// /// Adds the reference to the item. /// /// The object. public void AddReference(IContentItemOwner obj) { Assert.IsNotNull(obj); Assert.IsFalse(_references.Contains(obj)); _references.Add(obj); // Check if need to generate preview if (_references.Count == 1 && !_thumbnail.IsValid) { RequestThumbnail(); } } /// /// Removes the reference from the item. /// /// The object. public void RemoveReference(IContentItemOwner obj) { if (_references.Remove(obj)) { // Check if need to release the preview if (_references.Count == 0 && _thumbnail.IsValid) { ReleaseThumbnail(); } } } /// /// Called when context menu is being prepared to show. Can be used to add custom options. /// /// The menu. public virtual void OnContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu) { } /// /// Called when item gets renamed or location gets changed (path modification). /// public virtual void OnPathChanged() { } /// /// Called when content item gets removed (by the user or externally). /// public virtual void OnDelete() { // Fire event while (_references.Count > 0) { var reference = _references[0]; reference.OnItemDeleted(this); RemoveReference(reference); } // Release thumbnail if (_thumbnail.IsValid) { ReleaseThumbnail(); } } /// /// Called when item parent folder gets changed. /// protected virtual void OnParentFolderChanged() { } /// /// Requests the thumbnail. /// protected void RequestThumbnail() { Editor.Instance.Thumbnails.RequestPreview(this); } /// /// Releases the thumbnail. /// protected void ReleaseThumbnail() { // Simply unlink sprite _thumbnail = SpriteHandle.Invalid; } /// /// Called when item gets reimported or reloaded. /// protected virtual void OnReimport() { for (int i = 0; i < _references.Count; i++) _references[i].OnItemReimported(this); RefreshThumbnail(); } /// /// Does the drag and drop operation with this asset. /// protected virtual void DoDrag() { if (!CanDrag) return; DragData data; // Check if is selected if (Parent is ContentView view && view.IsSelected(this)) { // Drag selected item data = DragItems.GetDragData(view.Selection); } else { // Drag single item data = DragItems.GetDragData(this); } // Start drag operation DoDragDrop(data); } /// protected override bool ShowTooltip => true; /// public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) { UpdateTooltipText(); var result = base.OnShowTooltip(out text, out _, out area); location = Size * new Float2(0.9f, 0.5f); return result; } /// public override void NavigationFocus() { base.NavigationFocus(); if (IsFocused) (Parent as ContentView)?.Select(this); } /// public override void Draw() { var size = Size; var style = Style.Current; var view = Parent as ContentView; var isSelected = view.IsSelected(this); var clientRect = new Rectangle(Float2.Zero, size); var textRect = TextRectangle; Rectangle thumbnailRect; TextAlignment nameAlignment; switch (view.ViewType) { case ContentViewType.Tiles: { var thumbnailSize = size.X; thumbnailRect = new Rectangle(0, 0, thumbnailSize, thumbnailSize); nameAlignment = TextAlignment.Center; if (this is ContentFolder) { // Small shadow var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1); var color = Color.Black.AlphaMultiplied(0.2f); Render2D.FillRectangle(shadowRect, color); Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f)); if (isSelected) Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); else if (IsMouseOver) Render2D.FillRectangle(clientRect, style.BackgroundHighlighted); DrawThumbnail(ref thumbnailRect, false); } else { // Small shadow var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1); var color = Color.Black.AlphaMultiplied(0.2f); Render2D.FillRectangle(shadowRect, color); Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f)); Render2D.FillRectangle(TextRectangle, style.LightBackground); var accentHeight = 2 * view.ViewScale; var barRect = new Rectangle(0, thumbnailRect.Height - accentHeight, clientRect.Width, accentHeight); Render2D.FillRectangle(barRect, Color.DimGray); DrawThumbnail(ref thumbnailRect, false); if (isSelected) { Render2D.FillRectangle(textRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); Render2D.DrawRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); } else if (IsMouseOver) { Render2D.FillRectangle(textRect, style.BackgroundHighlighted); Render2D.DrawRectangle(clientRect, style.BackgroundHighlighted); } } break; } case ContentViewType.List: { var thumbnailSize = size.Y - 2 * DefaultMarginSize; thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize); nameAlignment = TextAlignment.Near; if (isSelected) Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); else if (IsMouseOver) Render2D.FillRectangle(clientRect, style.BackgroundHighlighted); DrawThumbnail(ref thumbnailRect); break; } default: throw new ArgumentOutOfRangeException(); } // Draw short name Render2D.PushClip(ref textRect); Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 1f, 0.95f); Render2D.PopClip(); } /// public override bool OnMouseDown(Float2 location, MouseButton button) { Focus(); if (button == MouseButton.Left) { // Cache data _isMouseDown = true; _mouseDownStartPos = location; } return true; } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (button == MouseButton.Left && _isMouseDown) { // Clear flag _isMouseDown = false; // Fire event (Parent as ContentView).OnItemClick(this); } return base.OnMouseUp(location, button); } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { Focus(); // Open (Parent as ContentView).OnItemDoubleClick(this); return true; } /// public override void OnMouseMove(Float2 location) { // Check if start drag and drop if (_isMouseDown && Float2.Distance(_mouseDownStartPos, location) > 10.0f) { // Clear flag _isMouseDown = false; // Start drag drop DoDrag(); } } /// public override void OnMouseLeave() { // Check if start drag and drop if (_isMouseDown) { // Clear flag _isMouseDown = false; // Start drag drop DoDrag(); } base.OnMouseLeave(); } /// public override void OnSubmit() { // Open (Parent as ContentView).OnItemDoubleClick(this); base.OnSubmit(); } /// public override int Compare(Control other) { if (other is ContentItem otherItem) { if (otherItem.IsFolder) return 1; return string.Compare(ShortName, otherItem.ShortName, StringComparison.InvariantCulture); } return base.Compare(other); } /// public override void OnDestroy() { // Fire event while (_references.Count > 0) { var reference = _references[0]; reference.OnItemDispose(this); RemoveReference(reference); } // Release thumbnail if (_thumbnail.IsValid) { ReleaseThumbnail(); } base.OnDestroy(); } /// public override string ToString() { return Path; } } }