diff --git a/Source/Editor/Content/GUI/ContentNavigation.cs b/Source/Editor/Content/GUI/ContentNavigation.cs
index a8581142b..c84b98746 100644
--- a/Source/Editor/Content/GUI/ContentNavigation.cs
+++ b/Source/Editor/Content/GUI/ContentNavigation.cs
@@ -19,7 +19,7 @@ namespace FlaxEditor.Content.GUI
///
/// Gets the target node.
///
- public ContentTreeNode TargetNode { get; }
+ public ContentFolderTreeNode TargetNode { get; }
///
/// Initializes a new instance of the class.
@@ -28,7 +28,7 @@ namespace FlaxEditor.Content.GUI
/// The x position.
/// The y position.
/// The height.
- public ContentNavigationButton(ContentTreeNode targetNode, float x, float y, float height)
+ public ContentNavigationButton(ContentFolderTreeNode targetNode, float x, float y, float height)
: base(x, y, height)
{
TargetNode = targetNode;
@@ -147,7 +147,7 @@ namespace FlaxEditor.Content.GUI
ClearItems();
foreach (var child in Target.TargetNode.Children)
{
- if (child is ContentTreeNode node)
+ if (child is ContentFolderTreeNode node)
{
if (node.Folder.VisibleInHierarchy) // Respect the filter set by ContentFilterConfig.Filter(...)
AddItem(node.Folder.ShortName);
@@ -180,7 +180,7 @@ namespace FlaxEditor.Content.GUI
var item = _items[index];
foreach (var child in Target.TargetNode.Children)
{
- if (child is ContentTreeNode node && node.Folder.ShortName == item)
+ if (child is ContentFolderTreeNode node && node.Folder.ShortName == item)
{
Editor.Instance.Windows.ContentWin.Navigate(node);
return;
diff --git a/Source/Editor/Content/Items/ContentFolder.cs b/Source/Editor/Content/Items/ContentFolder.cs
index 2bc4c03b3..463dd8341 100644
--- a/Source/Editor/Content/Items/ContentFolder.cs
+++ b/Source/Editor/Content/Items/ContentFolder.cs
@@ -59,7 +59,7 @@ namespace FlaxEditor.Content
///
/// Gets the content node.
///
- public ContentTreeNode Node { get; }
+ public ContentFolderTreeNode Node { get; }
///
/// The subitems of this folder.
@@ -72,7 +72,7 @@ namespace FlaxEditor.Content
/// The folder type.
/// The path to the item.
/// The folder parent node.
- internal ContentFolder(ContentFolderType type, string path, ContentTreeNode node)
+ internal ContentFolder(ContentFolderType type, string path, ContentFolderTreeNode node)
: base(path)
{
FolderType = type;
@@ -118,7 +118,7 @@ namespace FlaxEditor.Content
get
{
var hasParentFolder = ParentFolder != null;
- var isContentFolder = Node is MainContentTreeNode;
+ var isContentFolder = Node is MainContentFolderTreeNode;
return hasParentFolder && !isContentFolder;
}
}
diff --git a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs
new file mode 100644
index 000000000..6741910cf
--- /dev/null
+++ b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs
@@ -0,0 +1,433 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using FlaxEditor.GUI;
+using FlaxEditor.GUI.Drag;
+using FlaxEditor.GUI.Tree;
+using FlaxEditor.SceneGraph;
+using FlaxEditor.Utilities;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.Content;
+
+///
+/// Content folder tree node.
+///
+///
+public class ContentFolderTreeNode : TreeNode
+{
+ private DragItems _dragOverItems;
+ private DragActors _dragActors;
+ private List _highlights;
+
+ ///
+ /// The folder.
+ ///
+ protected ContentFolder _folder;
+
+ ///
+ /// Whether this node can be deleted.
+ ///
+ public virtual bool CanDelete => true;
+
+ ///
+ /// Whether this node can be duplicated.
+ ///
+ public virtual bool CanDuplicate => true;
+
+ ///
+ /// Gets the content folder item.
+ ///
+ public ContentFolder Folder => _folder;
+
+ ///
+ /// Gets the type of the folder.
+ ///
+ public ContentFolderType FolderType => _folder.FolderType;
+
+ ///
+ /// Returns true if that folder can import/manage scripts.
+ ///
+ public bool CanHaveScripts => _folder.CanHaveScripts;
+
+ ///
+ /// Returns true if that folder can import/manage assets.
+ ///
+ /// True if can contain assets for project, otherwise false
+ public bool CanHaveAssets => _folder.CanHaveAssets;
+
+ ///
+ /// Gets the parent node.
+ ///
+ public ContentFolderTreeNode ParentNode => Parent as ContentFolderTreeNode;
+
+ ///
+ /// Gets the folder path.
+ ///
+ public string Path => _folder.Path;
+
+ ///
+ /// Gets the navigation button label.
+ ///
+ public virtual string NavButtonLabel => _folder.ShortName;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent node.
+ /// The folder path.
+ public ContentFolderTreeNode(ContentFolderTreeNode parent, string path)
+ : this(parent, parent?.FolderType ?? ContentFolderType.Other, path)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent node.
+ /// The folder type.
+ /// The folder path.
+ protected ContentFolderTreeNode(ContentFolderTreeNode parent, ContentFolderType type, string path)
+ : base(false, Editor.Instance.Icons.FolderClosed32, Editor.Instance.Icons.FolderOpen32)
+ {
+ _folder = new ContentFolder(type, path, this);
+ Text = _folder.ShortName;
+ if (parent != null)
+ {
+ Folder.ParentFolder = parent.Folder;
+ Parent = parent;
+ }
+ IconColor = Color.Transparent; // Hide default icon, we draw scaled icon manually
+ UpdateCustomArrowRect();
+ Editor.Instance?.Windows?.ContentWin?.TryAutoExpandContentNode(this);
+ }
+
+ ///
+ /// Updates the custom arrow rectangle so it stays aligned with the current layout.
+ ///
+ private void UpdateCustomArrowRect()
+ {
+ var contentWindow = Editor.Instance?.Windows?.ContentWin;
+ var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f;
+ var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f);
+ var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
+
+ // Use the current text layout, not just cached values.
+ var textRect = TextRect;
+ var iconLeft = textRect.Left - iconSize - 2.0f;
+ var x = Mathf.Max(iconLeft - arrowSize - 2.0f, 0.0f);
+ var y = Mathf.Max((HeaderHeight - arrowSize) * 0.5f, 0.0f);
+
+ CustomArrowRect = new Rectangle(x, y, arrowSize, arrowSize);
+ }
+
+ ///
+ public override void PerformLayout(bool force = false)
+ {
+ base.PerformLayout(force);
+ UpdateCustomArrowRect();
+ }
+
+ ///
+ /// Shows the rename popup for the item.
+ ///
+ public void StartRenaming()
+ {
+ if (!_folder.CanRename)
+ return;
+
+ // Start renaming the folder
+ Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(false);
+ var dialog = RenamePopup.Show(this, TextRect, _folder.ShortName, false);
+ dialog.Tag = _folder;
+ dialog.Renamed += popup =>
+ {
+ Editor.Instance.Windows.ContentWin.Rename((ContentFolder)popup.Tag, popup.Text);
+ Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true);
+ };
+ dialog.Closed += popup => { Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); };
+ }
+
+ ///
+ /// Updates the query search filter.
+ ///
+ /// The filter text.
+ public void UpdateFilter(string filterText)
+ {
+ bool noFilter = string.IsNullOrWhiteSpace(filterText);
+
+ // Update itself
+ bool isThisVisible;
+ if (noFilter)
+ {
+ // Clear filter
+ _highlights?.Clear();
+ isThisVisible = true;
+ }
+ else
+ {
+ var text = Text;
+ if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
+ {
+ // Update highlights
+ if (_highlights == null)
+ _highlights = new List(ranges.Length);
+ else
+ _highlights.Clear();
+ var style = Style.Current;
+ var font = style.FontSmall;
+ var textRect = TextRect;
+ for (int i = 0; i < ranges.Length; i++)
+ {
+ var start = font.GetCharPosition(text, ranges[i].StartIndex);
+ var end = font.GetCharPosition(text, ranges[i].EndIndex);
+ _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height));
+ }
+ isThisVisible = true;
+ }
+ else
+ {
+ // Hide
+ _highlights?.Clear();
+ isThisVisible = false;
+ }
+ }
+
+ // Update children
+ bool isAnyChildVisible = false;
+ for (int i = 0; i < _children.Count; i++)
+ {
+ if (_children[i] is ContentFolderTreeNode child)
+ {
+ child.UpdateFilter(filterText);
+ isAnyChildVisible |= child.Visible;
+ }
+ else if (_children[i] is ContentItemTreeNode itemNode)
+ {
+ itemNode.UpdateFilter(filterText);
+ isAnyChildVisible |= itemNode.Visible;
+ }
+ }
+
+ if (!noFilter)
+ {
+ bool isExpanded = isAnyChildVisible;
+ if (isExpanded)
+ Expand(true);
+ else
+ Collapse(true);
+ }
+
+ Visible = isThisVisible | isAnyChildVisible;
+ }
+
+ ///
+ public override int Compare(Control other)
+ {
+ if (other is ContentItemTreeNode)
+ return -1;
+ if (other is ContentFolderTreeNode otherNode)
+ return string.Compare(Text, otherNode.Text, StringComparison.Ordinal);
+ return base.Compare(other);
+ }
+
+ ///
+ public override void Draw()
+ {
+ base.Draw();
+
+ // Draw all highlights
+ if (_highlights != null)
+ {
+ var style = Style.Current;
+ var color = style.ProgressNormal * 0.6f;
+ for (int i = 0; i < _highlights.Count; i++)
+ Render2D.FillRectangle(_highlights[i], color);
+ }
+
+ var contentWindow = Editor.Instance.Windows.ContentWin;
+ var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f;
+ var icon = IsExpanded ? Editor.Instance.Icons.FolderOpen32 : Editor.Instance.Icons.FolderClosed32;
+ var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
+ var iconRect = new Rectangle(TextRect.Left - iconSize - 2.0f, (HeaderHeight - iconSize) * 0.5f, iconSize, iconSize);
+ Render2D.DrawSprite(icon, iconRect);
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ // Delete folder item
+ _folder.Dispose();
+
+ base.OnDestroy();
+ }
+
+ ///
+ protected override void OnExpandedChanged()
+ {
+ base.OnExpandedChanged();
+
+ Editor.Instance?.Windows?.ContentWin?.OnContentTreeNodeExpandedChanged(this, IsExpanded);
+ }
+
+ private DragDropEffect GetDragEffect(DragData data)
+ {
+ if (_dragActors != null && _dragActors.HasValidDrag)
+ return DragDropEffect.Move;
+ if (data is DragDataFiles)
+ {
+ if (_folder.CanHaveAssets)
+ return DragDropEffect.Copy;
+ }
+ else
+ {
+ if (_dragOverItems != null && _dragOverItems.HasValidDrag)
+ return DragDropEffect.Move;
+ }
+
+ return DragDropEffect.None;
+ }
+
+ private bool ValidateDragItem(ContentItem item)
+ {
+ // Reject itself and any parent
+ return item != _folder && !item.Find(_folder);
+ }
+
+ private bool ValidateDragActors(ActorNode actor)
+ {
+ return actor.CanCreatePrefab && _folder.CanHaveAssets;
+ }
+
+ private void ImportActors(DragActors actors)
+ {
+ Select();
+ foreach (var actorNode in actors.Objects)
+ {
+ var actor = actorNode.Actor;
+ if (actors.Objects.Contains(actorNode.ParentNode as ActorNode))
+ continue;
+
+ Editor.Instance.Prefabs.CreatePrefab(actor, false);
+ }
+ Editor.Instance.Windows.ContentWin.RefreshView();
+ }
+
+ ///
+ protected override DragDropEffect OnDragEnterHeader(DragData data)
+ {
+ if (data is DragDataFiles)
+ return _folder.CanHaveAssets ? DragDropEffect.Copy : DragDropEffect.None;
+
+ if (_dragActors == null)
+ _dragActors = new DragActors(ValidateDragActors);
+ if (_dragActors.OnDragEnter(data))
+ return DragDropEffect.Move;
+
+ if (_dragOverItems == null)
+ _dragOverItems = new DragItems(ValidateDragItem);
+
+ _dragOverItems.OnDragEnter(data);
+ return GetDragEffect(data);
+ }
+
+ ///
+ protected override DragDropEffect OnDragMoveHeader(DragData data)
+ {
+ if (data is DragDataFiles)
+ return _folder.CanHaveAssets ? DragDropEffect.Copy : DragDropEffect.None;
+ if (_dragActors != null && _dragActors.HasValidDrag)
+ return DragDropEffect.Move;
+ return GetDragEffect(data);
+ }
+
+ ///
+ protected override void OnDragLeaveHeader()
+ {
+ _dragOverItems?.OnDragLeave();
+ _dragActors?.OnDragLeave();
+ base.OnDragLeaveHeader();
+ }
+
+ ///
+ protected override DragDropEffect OnDragDropHeader(DragData data)
+ {
+ var result = DragDropEffect.None;
+
+ // Check if drop element or files
+ if (data is DragDataFiles files)
+ {
+ // Import files
+ Editor.Instance.ContentImporting.Import(files.Files, _folder);
+ result = DragDropEffect.Copy;
+
+ Expand();
+ }
+ else if (_dragActors != null && _dragActors.HasValidDrag)
+ {
+ ImportActors(_dragActors);
+ _dragActors.OnDragDrop();
+ result = DragDropEffect.Move;
+
+ Expand();
+ }
+ else if (_dragOverItems != null && _dragOverItems.HasValidDrag)
+ {
+ // Move items
+ Editor.Instance.ContentDatabase.Move(_dragOverItems.Objects, _folder);
+ result = DragDropEffect.Move;
+
+ Expand();
+ }
+
+ _dragOverItems?.OnDragDrop();
+
+ return result;
+ }
+
+ ///
+ protected override void DoDragDrop()
+ {
+ DoDragDrop(DragItems.GetDragData(_folder));
+ }
+
+ ///
+ protected override void OnLongPress()
+ {
+ Select();
+
+ StartRenaming();
+ }
+
+ ///
+ public override bool OnKeyDown(KeyboardKeys key)
+ {
+ if (IsFocused)
+ {
+ switch (key)
+ {
+ case KeyboardKeys.F2:
+ StartRenaming();
+ return true;
+ case KeyboardKeys.Delete:
+ if (Folder.Exists && CanDelete)
+ Editor.Instance.Windows.ContentWin.Delete(Folder);
+ return true;
+ }
+ if (RootWindow.GetKey(KeyboardKeys.Control))
+ {
+ switch (key)
+ {
+ case KeyboardKeys.D:
+ if (Folder.Exists && CanDuplicate)
+ Editor.Instance.Windows.ContentWin.Duplicate(Folder);
+ return true;
+ }
+ }
+ }
+
+ return base.OnKeyDown(key);
+ }
+}
diff --git a/Source/Editor/Content/Tree/ContentItemTreeNode.cs b/Source/Editor/Content/Tree/ContentItemTreeNode.cs
new file mode 100644
index 000000000..43dbe278a
--- /dev/null
+++ b/Source/Editor/Content/Tree/ContentItemTreeNode.cs
@@ -0,0 +1,221 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using FlaxEditor.Content.GUI;
+using FlaxEditor.GUI.Drag;
+using FlaxEditor.GUI.Tree;
+using FlaxEditor.Utilities;
+using FlaxEngine;
+using FlaxEngine.GUI;
+
+namespace FlaxEditor.Content;
+
+///
+/// Tree node for non-folder content items.
+///
+public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner
+{
+ private List _highlights;
+
+ ///
+ /// The content item.
+ ///
+ public ContentItem Item { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content item.
+ public ContentItemTreeNode(ContentItem item)
+ : base(false, Editor.Instance.Icons.Document128, Editor.Instance.Icons.Document128)
+ {
+ Item = item ?? throw new ArgumentNullException(nameof(item));
+ UpdateDisplayedName();
+ IconColor = Color.Transparent; // Reserve icon space but draw custom thumbnail.
+ Item.AddReference(this);
+ }
+
+ private static SpriteHandle GetIcon(ContentItem item)
+ {
+ if (item == null)
+ return SpriteHandle.Invalid;
+ var icon = item.Thumbnail;
+ if (!icon.IsValid)
+ icon = item.DefaultThumbnail;
+ if (!icon.IsValid)
+ icon = Editor.Instance.Icons.Document128;
+ return icon;
+ }
+
+ ///
+ /// Updates the query search filter.
+ ///
+ /// The filter text.
+ public void UpdateFilter(string filterText)
+ {
+ bool noFilter = string.IsNullOrWhiteSpace(filterText);
+ bool isVisible;
+ if (noFilter)
+ {
+ _highlights?.Clear();
+ isVisible = true;
+ }
+ else
+ {
+ var text = Text;
+ if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
+ {
+ if (_highlights == null)
+ _highlights = new List(ranges.Length);
+ else
+ _highlights.Clear();
+ var style = Style.Current;
+ var font = style.FontSmall;
+ var textRect = TextRect;
+ for (int i = 0; i < ranges.Length; i++)
+ {
+ var start = font.GetCharPosition(text, ranges[i].StartIndex);
+ var end = font.GetCharPosition(text, ranges[i].EndIndex);
+ _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height));
+ }
+ isVisible = true;
+ }
+ else
+ {
+ _highlights?.Clear();
+ isVisible = false;
+ }
+ }
+
+ Visible = isVisible;
+ }
+
+ ///
+ public override void Draw()
+ {
+ base.Draw();
+
+ var icon = GetIcon(Item);
+ if (icon.IsValid)
+ {
+ var contentWindow = Editor.Instance.Windows.ContentWin;
+ var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f;
+ var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
+ var textRect = TextRect;
+ var iconRect = new Rectangle(textRect.Left - iconSize - 2.0f, (HeaderHeight - iconSize) * 0.5f, iconSize, iconSize);
+ Render2D.DrawSprite(icon, iconRect);
+ }
+
+ if (_highlights != null)
+ {
+ var style = Style.Current;
+ var color = style.ProgressNormal * 0.6f;
+ for (int i = 0; i < _highlights.Count; i++)
+ Render2D.FillRectangle(_highlights[i], color);
+ }
+ }
+
+ ///
+ protected override bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button)
+ {
+ if (button == MouseButton.Left)
+ {
+ Editor.Instance.Windows.ContentWin.Open(Item);
+ return true;
+ }
+
+ return base.OnMouseDoubleClickHeader(ref location, button);
+ }
+
+ ///
+ public override bool OnKeyDown(KeyboardKeys key)
+ {
+ if (IsFocused)
+ {
+ switch (key)
+ {
+ case KeyboardKeys.Return:
+ Editor.Instance.Windows.ContentWin.Open(Item);
+ return true;
+ case KeyboardKeys.F2:
+ Editor.Instance.Windows.ContentWin.Rename(Item);
+ return true;
+ case KeyboardKeys.Delete:
+ Editor.Instance.Windows.ContentWin.Delete(Item);
+ return true;
+ }
+ }
+
+ return base.OnKeyDown(key);
+ }
+
+ ///
+ protected override void DoDragDrop()
+ {
+ DoDragDrop(DragItems.GetDragData(Item));
+ }
+
+ ///
+ public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area)
+ {
+ Item.UpdateTooltipText();
+ TooltipText = Item.TooltipText;
+ return base.OnShowTooltip(out text, out location, out area);
+ }
+
+ ///
+ void IContentItemOwner.OnItemDeleted(ContentItem item)
+ {
+ }
+
+ ///
+ void IContentItemOwner.OnItemRenamed(ContentItem item)
+ {
+ UpdateDisplayedName();
+ }
+
+ ///
+ void IContentItemOwner.OnItemReimported(ContentItem item)
+ {
+ }
+
+ ///
+ void IContentItemOwner.OnItemDispose(ContentItem item)
+ {
+ }
+
+ ///
+ public override int Compare(Control other)
+ {
+ if (other is ContentFolderTreeNode)
+ return 1;
+ if (other is ContentItemTreeNode otherItem)
+ return ApplySortOrder(string.Compare(Text, otherItem.Text, StringComparison.InvariantCulture));
+ return base.Compare(other);
+ }
+
+ ///
+ public override void OnDestroy()
+ {
+ Item.RemoveReference(this);
+ base.OnDestroy();
+ }
+
+ public void UpdateDisplayedName()
+ {
+ var contentWindow = Editor.Instance?.Windows?.ContentWin;
+ var showExtensions = contentWindow?.View?.ShowFileExtensions ?? true;
+ Text = Item.ShowFileExtension || showExtensions ? Item.FileName : Item.ShortName;
+ }
+
+ private static SortType GetSortType()
+ {
+ return Editor.Instance?.Windows?.ContentWin?.CurrentSortType ?? SortType.AlphabeticOrder;
+ }
+
+ private static int ApplySortOrder(int result)
+ {
+ return GetSortType() == SortType.AlphabeticReverse ? -result : result;
+ }
+}
diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs
deleted file mode 100644
index ca629000d..000000000
--- a/Source/Editor/Content/Tree/ContentTreeNode.cs
+++ /dev/null
@@ -1,331 +0,0 @@
-// Copyright (c) Wojciech Figat. All rights reserved.
-
-using System.Collections.Generic;
-using FlaxEditor.GUI;
-using FlaxEditor.GUI.Drag;
-using FlaxEditor.GUI.Tree;
-using FlaxEditor.Utilities;
-using FlaxEngine;
-using FlaxEngine.GUI;
-
-namespace FlaxEditor.Content
-{
- ///
- /// Content folder tree node.
- ///
- ///
- public class ContentTreeNode : TreeNode
- {
- private DragItems _dragOverItems;
- private List _highlights;
-
- ///
- /// The folder.
- ///
- protected ContentFolder _folder;
-
- ///
- /// Whether this node can be deleted.
- ///
- public virtual bool CanDelete => true;
-
- ///
- /// Whether this node can be duplicated.
- ///
- public virtual bool CanDuplicate => true;
-
- ///
- /// Gets the content folder item.
- ///
- public ContentFolder Folder => _folder;
-
- ///
- /// Gets the type of the folder.
- ///
- public ContentFolderType FolderType => _folder.FolderType;
-
- ///
- /// Returns true if that folder can import/manage scripts.
- ///
- public bool CanHaveScripts => _folder.CanHaveScripts;
-
- ///
- /// Returns true if that folder can import/manage assets.
- ///
- /// True if can contain assets for project, otherwise false
- public bool CanHaveAssets => _folder.CanHaveAssets;
-
- ///
- /// Gets the parent node.
- ///
- public ContentTreeNode ParentNode => Parent as ContentTreeNode;
-
- ///
- /// Gets the folder path.
- ///
- public string Path => _folder.Path;
-
- ///
- /// Gets the navigation button label.
- ///
- public virtual string NavButtonLabel => _folder.ShortName;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The parent node.
- /// The folder path.
- public ContentTreeNode(ContentTreeNode parent, string path)
- : this(parent, parent?.FolderType ?? ContentFolderType.Other, path)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The parent node.
- /// The folder type.
- /// The folder path.
- protected ContentTreeNode(ContentTreeNode parent, ContentFolderType type, string path)
- : base(false, Editor.Instance.Icons.FolderClosed32, Editor.Instance.Icons.FolderOpen32)
- {
- _folder = new ContentFolder(type, path, this);
- Text = _folder.ShortName;
- if (parent != null)
- {
- Folder.ParentFolder = parent.Folder;
- Parent = parent;
- }
- IconColor = Style.Current.Foreground;
- }
-
- ///
- /// Shows the rename popup for the item.
- ///
- public void StartRenaming()
- {
- if (!_folder.CanRename)
- return;
-
- // Start renaming the folder
- Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(false);
- var dialog = RenamePopup.Show(this, TextRect, _folder.ShortName, false);
- dialog.Tag = _folder;
- dialog.Renamed += popup =>
- {
- Editor.Instance.Windows.ContentWin.Rename((ContentFolder)popup.Tag, popup.Text);
- Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true);
- };
- dialog.Closed += popup => { Editor.Instance.Windows.ContentWin.ScrollingOnTreeView(true); };
- }
-
- ///
- /// Updates the query search filter.
- ///
- /// The filter text.
- public void UpdateFilter(string filterText)
- {
- bool noFilter = string.IsNullOrWhiteSpace(filterText);
-
- // Update itself
- bool isThisVisible;
- if (noFilter)
- {
- // Clear filter
- _highlights?.Clear();
- isThisVisible = true;
- }
- else
- {
- var text = Text;
- if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
- {
- // Update highlights
- if (_highlights == null)
- _highlights = new List(ranges.Length);
- else
- _highlights.Clear();
- var style = Style.Current;
- var font = style.FontSmall;
- var textRect = TextRect;
- for (int i = 0; i < ranges.Length; i++)
- {
- var start = font.GetCharPosition(text, ranges[i].StartIndex);
- var end = font.GetCharPosition(text, ranges[i].EndIndex);
- _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height));
- }
- isThisVisible = true;
- }
- else
- {
- // Hide
- _highlights?.Clear();
- isThisVisible = false;
- }
- }
-
- // Update children
- bool isAnyChildVisible = false;
- for (int i = 0; i < _children.Count; i++)
- {
- if (_children[i] is ContentTreeNode child)
- {
- child.UpdateFilter(filterText);
- isAnyChildVisible |= child.Visible;
- }
- }
-
- if (!noFilter)
- {
- bool isExpanded = isAnyChildVisible;
- if (isExpanded)
- Expand(true);
- else
- Collapse(true);
- }
-
- Visible = isThisVisible | isAnyChildVisible;
- }
-
- ///
- public override void Draw()
- {
- base.Draw();
-
- // Draw all highlights
- if (_highlights != null)
- {
- var style = Style.Current;
- var color = style.ProgressNormal * 0.6f;
- for (int i = 0; i < _highlights.Count; i++)
- Render2D.FillRectangle(_highlights[i], color);
- }
- }
-
- ///
- public override void OnDestroy()
- {
- // Delete folder item
- _folder.Dispose();
-
- base.OnDestroy();
- }
-
- private DragDropEffect GetDragEffect(DragData data)
- {
- if (data is DragDataFiles)
- {
- if (_folder.CanHaveAssets)
- return DragDropEffect.Copy;
- }
- else
- {
- if (_dragOverItems.HasValidDrag)
- return DragDropEffect.Move;
- }
-
- return DragDropEffect.None;
- }
-
- private bool ValidateDragItem(ContentItem item)
- {
- // Reject itself and any parent
- return item != _folder && !item.Find(_folder);
- }
-
- ///
- protected override DragDropEffect OnDragEnterHeader(DragData data)
- {
- if (_dragOverItems == null)
- _dragOverItems = new DragItems(ValidateDragItem);
-
- _dragOverItems.OnDragEnter(data);
- return GetDragEffect(data);
- }
-
- ///
- protected override DragDropEffect OnDragMoveHeader(DragData data)
- {
- return GetDragEffect(data);
- }
-
- ///
- protected override void OnDragLeaveHeader()
- {
- _dragOverItems.OnDragLeave();
- base.OnDragLeaveHeader();
- }
-
- ///
- protected override DragDropEffect OnDragDropHeader(DragData data)
- {
- var result = DragDropEffect.None;
-
- // Check if drop element or files
- if (data is DragDataFiles files)
- {
- // Import files
- Editor.Instance.ContentImporting.Import(files.Files, _folder);
- result = DragDropEffect.Copy;
-
- Expand();
- }
- else if (_dragOverItems.HasValidDrag)
- {
- // Move items
- Editor.Instance.ContentDatabase.Move(_dragOverItems.Objects, _folder);
- result = DragDropEffect.Move;
-
- Expand();
- }
-
- _dragOverItems.OnDragDrop();
-
- return result;
- }
-
- ///
- protected override void DoDragDrop()
- {
- DoDragDrop(DragItems.GetDragData(_folder));
- }
-
- ///
- protected override void OnLongPress()
- {
- Select();
-
- StartRenaming();
- }
-
- ///
- public override bool OnKeyDown(KeyboardKeys key)
- {
- if (IsFocused)
- {
- switch (key)
- {
- case KeyboardKeys.F2:
- StartRenaming();
- return true;
- case KeyboardKeys.Delete:
- if (Folder.Exists && CanDelete)
- Editor.Instance.Windows.ContentWin.Delete(Folder);
- return true;
- }
- if (RootWindow.GetKey(KeyboardKeys.Control))
- {
- switch (key)
- {
- case KeyboardKeys.D:
- if (Folder.Exists && CanDuplicate)
- Editor.Instance.Windows.ContentWin.Duplicate(Folder);
- return true;
- }
- }
- }
-
- return base.OnKeyDown(key);
- }
- }
-}
diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs
index fc147f5c9..aaeed0eda 100644
--- a/Source/Editor/Content/Tree/MainContentTreeNode.cs
+++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs
@@ -7,8 +7,8 @@ namespace FlaxEditor.Content
///
/// Content tree node used for main directories.
///
- ///
- public class MainContentTreeNode : ContentTreeNode
+ ///
+ public class MainContentFolderTreeNode : ContentFolderTreeNode
{
private FileSystemWatcher _watcher;
@@ -19,12 +19,12 @@ namespace FlaxEditor.Content
public override bool CanDuplicate => false;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The parent project.
/// The folder type.
/// The folder path.
- public MainContentTreeNode(ProjectTreeNode parent, ContentFolderType type, string path)
+ public MainContentFolderTreeNode(ProjectFolderTreeNode parent, ContentFolderType type, string path)
: base(parent, type, path)
{
_watcher = new FileSystemWatcher(path)
diff --git a/Source/Editor/Content/Tree/ProjectTreeNode.cs b/Source/Editor/Content/Tree/ProjectTreeNode.cs
index 61002d4fa..4f921e371 100644
--- a/Source/Editor/Content/Tree/ProjectTreeNode.cs
+++ b/Source/Editor/Content/Tree/ProjectTreeNode.cs
@@ -1,5 +1,6 @@
// Copyright (c) Wojciech Figat. All rights reserved.
+using System;
using FlaxEngine.GUI;
namespace FlaxEditor.Content
@@ -7,8 +8,8 @@ namespace FlaxEditor.Content
///
/// Root tree node for the project workspace.
///
- ///
- public sealed class ProjectTreeNode : ContentTreeNode
+ ///
+ public sealed class ProjectFolderTreeNode : ContentFolderTreeNode
{
///
/// The project/
@@ -18,18 +19,18 @@ namespace FlaxEditor.Content
///
/// The project content directory.
///
- public MainContentTreeNode Content;
+ public MainContentFolderTreeNode Content;
///
/// The project source code directory.
///
- public MainContentTreeNode Source;
+ public MainContentFolderTreeNode Source;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The project.
- public ProjectTreeNode(ProjectInfo project)
+ public ProjectFolderTreeNode(ProjectInfo project)
: base(null, project.ProjectFolderPath)
{
Project = project;
@@ -48,9 +49,29 @@ namespace FlaxEditor.Content
///
public override int Compare(Control other)
{
- // Move the main game project to the top
- if (Project.Name == Editor.Instance.GameProject.Name)
- return -1;
+ if (other is ProjectFolderTreeNode otherProject)
+ {
+ var gameProject = Editor.Instance.GameProject;
+ var engineProject = Editor.Instance.EngineProject;
+ bool isGame = Project == gameProject;
+ bool isEngine = Project == engineProject;
+ bool otherIsGame = otherProject.Project == gameProject;
+ bool otherIsEngine = otherProject.Project == engineProject;
+
+ // Main game project at the top
+ if (isGame && !otherIsGame)
+ return -1;
+ if (!isGame && otherIsGame)
+ return 1;
+
+ // Engine project at the bottom (when distinct)
+ if (isEngine && !otherIsEngine)
+ return 1;
+ if (!isEngine && otherIsEngine)
+ return -1;
+
+ return string.CompareOrdinal(Project.Name, otherProject.Project.Name);
+ }
return base.Compare(other);
}
}
diff --git a/Source/Editor/Content/Tree/RootContentTreeNode.cs b/Source/Editor/Content/Tree/RootContentTreeNode.cs
index d9c1ba761..d1f816c14 100644
--- a/Source/Editor/Content/Tree/RootContentTreeNode.cs
+++ b/Source/Editor/Content/Tree/RootContentTreeNode.cs
@@ -5,13 +5,13 @@ namespace FlaxEditor.Content
///
/// Root tree node for the content workspace.
///
- ///
- public sealed class RootContentTreeNode : ContentTreeNode
+ ///
+ public sealed class RootContentFolderTreeNode : ContentFolderTreeNode
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- public RootContentTreeNode()
+ public RootContentFolderTreeNode()
: base(null, string.Empty)
{
}
diff --git a/Source/Editor/GUI/NavigationBar.cs b/Source/Editor/GUI/NavigationBar.cs
index eb87eaafc..7f43d09ac 100644
--- a/Source/Editor/GUI/NavigationBar.cs
+++ b/Source/Editor/GUI/NavigationBar.cs
@@ -76,9 +76,23 @@ namespace FlaxEditor.GUI
toolstrip.IsLayoutLocked = toolstripLocked;
toolstrip.ItemsMargin = toolstripMargin;
- var lastToolstripButton = toolstrip.LastButton;
+ var margin = toolstrip.ItemsMargin;
+ float xOffset = margin.Left;
+ bool hadChild = false;
+ for (int i = 0; i < toolstrip.ChildrenCount; i++)
+ {
+ var child = toolstrip.GetChild(i);
+ if (child == this || !child.Visible)
+ continue;
+ hadChild = true;
+ xOffset += child.Width + margin.Width;
+ }
+
+ var right = hadChild ? xOffset - margin.Width : margin.Left;
var parentSize = Parent.Size;
- Bounds = new Rectangle(lastToolstripButton.Right + 8.0f, 0, parentSize.X - X - 8.0f, toolstrip.Height);
+ var x = right + 8.0f;
+ var width = Mathf.Max(parentSize.X - x - 8.0f, 0.0f);
+ Bounds = new Rectangle(x, 0, width, toolstrip.Height);
}
///
diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs
index 53e45ff25..db2bdd7e6 100644
--- a/Source/Editor/Modules/ContentDatabaseModule.cs
+++ b/Source/Editor/Modules/ContentDatabaseModule.cs
@@ -24,22 +24,22 @@ namespace FlaxEditor.Modules
private bool _rebuildInitFlag;
private int _itemsCreated;
private int _itemsDeleted;
- private readonly HashSet _dirtyNodes = new HashSet();
+ private readonly HashSet _dirtyNodes = new HashSet();
///
/// The project directory.
///
- public ProjectTreeNode Game { get; private set; }
+ public ProjectFolderTreeNode Game { get; private set; }
///
/// The engine directory.
///
- public ProjectTreeNode Engine { get; private set; }
+ public ProjectFolderTreeNode Engine { get; private set; }
///
/// The list of all projects workspace directories (including game, engine and plugins projects).
///
- public readonly List Projects = new List();
+ public readonly List Projects = new List();
///
/// The list with all content items proxy objects. Use and to modify this or to refresh database when adding new item proxy types.
@@ -116,7 +116,7 @@ namespace FlaxEditor.Modules
///
/// The project.
/// The project workspace or null if not loaded into database.
- public ProjectTreeNode GetProjectWorkspace(ProjectInfo project)
+ public ProjectFolderTreeNode GetProjectWorkspace(ProjectInfo project)
{
return Projects.FirstOrDefault(x => x.Project == project);
}
@@ -874,7 +874,7 @@ namespace FlaxEditor.Modules
}
}
- private void LoadFolder(ContentTreeNode node, bool checkSubDirs)
+ private void LoadFolder(ContentFolderTreeNode node, bool checkSubDirs)
{
if (node == null)
return;
@@ -953,7 +953,7 @@ namespace FlaxEditor.Modules
if (childFolderNode == null)
{
// Create node
- ContentTreeNode n = new ContentTreeNode(node, childPath);
+ ContentFolderTreeNode n = new ContentFolderTreeNode(node, childPath);
if (!_isDuringFastSetup)
sortChildren = true;
@@ -978,7 +978,7 @@ namespace FlaxEditor.Modules
node.SortChildren();
// Ignore some special folders
- if (node is MainContentTreeNode mainNode && mainNode.Folder.ShortName == "Source")
+ if (node is MainContentFolderTreeNode mainNode && mainNode.Folder.ShortName == "Source")
{
var mainNodeChild = mainNode.Folder.Find(StringUtils.CombinePaths(mainNode.Path, "obj")) as ContentFolder;
if (mainNodeChild != null)
@@ -995,7 +995,7 @@ namespace FlaxEditor.Modules
}
}
- private void LoadScripts(ContentTreeNode parent, string[] files)
+ private void LoadScripts(ContentFolderTreeNode parent, string[] files)
{
for (int i = 0; i < files.Length; i++)
{
@@ -1041,7 +1041,7 @@ namespace FlaxEditor.Modules
}
}
- private void LoadAssets(ContentTreeNode parent, string[] files)
+ private void LoadAssets(ContentFolderTreeNode parent, string[] files)
{
for (int i = 0; i < files.Length; i++)
{
@@ -1093,20 +1093,20 @@ namespace FlaxEditor.Modules
var workspace = GetProjectWorkspace(project);
if (workspace == null)
{
- workspace = new ProjectTreeNode(project);
+ workspace = new ProjectFolderTreeNode(project);
Projects.Add(workspace);
var contentFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Content");
if (Directory.Exists(contentFolder))
{
- workspace.Content = new MainContentTreeNode(workspace, ContentFolderType.Content, contentFolder);
+ workspace.Content = new MainContentFolderTreeNode(workspace, ContentFolderType.Content, contentFolder);
workspace.Content.Folder.ParentFolder = workspace.Folder;
}
var sourceFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Source");
if (Directory.Exists(sourceFolder))
{
- workspace.Source = new MainContentTreeNode(workspace, ContentFolderType.Source, sourceFolder);
+ workspace.Source = new MainContentFolderTreeNode(workspace, ContentFolderType.Source, sourceFolder);
workspace.Source.Folder.ParentFolder = workspace.Folder;
}
}
@@ -1213,16 +1213,16 @@ namespace FlaxEditor.Modules
Proxy.Add(new GenericJsonAssetProxy());
// Create content folders nodes
- Engine = new ProjectTreeNode(Editor.EngineProject)
+ Engine = new ProjectFolderTreeNode(Editor.EngineProject)
{
- Content = new MainContentTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder),
+ Content = new MainContentFolderTreeNode(Engine, ContentFolderType.Content, Globals.EngineContentFolder),
};
if (Editor.GameProject != Editor.EngineProject)
{
- Game = new ProjectTreeNode(Editor.GameProject)
+ Game = new ProjectFolderTreeNode(Editor.GameProject)
{
- Content = new MainContentTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder),
- Source = new MainContentTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder),
+ Content = new MainContentFolderTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder),
+ Source = new MainContentFolderTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder),
};
// TODO: why it's required? the code above should work for linking the nodes hierarchy
Game.Content.Folder.ParentFolder = Game.Folder;
@@ -1302,7 +1302,7 @@ namespace FlaxEditor.Modules
}
}
- internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e)
+ internal void OnDirectoryEvent(MainContentFolderTreeNode node, FileSystemEventArgs e)
{
// Ensure to be ready for external events
if (_isDuringFastSetup)
diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs
index 40dd9131b..ad5a8caf2 100644
--- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs
+++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs
@@ -58,7 +58,7 @@ namespace FlaxEditor.Windows
cm.AddSeparator();
}
- if (item is ContentFolder contentFolder && contentFolder.Node is ProjectTreeNode)
+ if (item is ContentFolder contentFolder && contentFolder.Node is ProjectFolderTreeNode)
{
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path));
}
@@ -76,8 +76,8 @@ namespace FlaxEditor.Windows
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path)));
- if (!String.IsNullOrEmpty(Editor.Instance.Windows.ContentWin._itemsSearchBox.Text))
- {
+ if (!_showAllContentInTree && !String.IsNullOrEmpty(Editor.Instance.Windows.ContentWin._itemsSearchBox.Text))
+ {
cm.AddButton("Show in Content Panel", () =>
{
Editor.Instance.Windows.ContentWin.ClearItemsSearch();
@@ -129,7 +129,7 @@ namespace FlaxEditor.Windows
}
}
- if (isFolder && folder.Node is MainContentTreeNode)
+ if (isFolder && folder.Node is MainContentFolderTreeNode)
{
cm.AddSeparator();
}
@@ -145,7 +145,7 @@ namespace FlaxEditor.Windows
b = cm.AddButton("Paste", _view.Paste);
b.Enabled = _view.CanPaste();
- if (isFolder && folder.Node is MainContentTreeNode)
+ if (isFolder && folder.Node is MainContentFolderTreeNode)
{
// Do nothing
}
@@ -201,7 +201,7 @@ namespace FlaxEditor.Windows
private void CreateNewModuleMenu(ContextMenu menu, ContentFolder folder, bool disableUncreatable = false)
{
// Check if is source folder to add new module
- if (folder?.ParentFolder?.Node is ProjectTreeNode parentFolderNode && folder.Node == parentFolderNode.Source)
+ if (folder?.ParentFolder?.Node is ProjectFolderTreeNode parentFolderNode && folder.Node == parentFolderNode.Source)
{
var button = menu.AddButton("New module");
button.CloseMenuOnClick = false;
@@ -216,7 +216,7 @@ namespace FlaxEditor.Windows
private bool CanCreateFolder(ContentItem item = null)
{
- bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode);
+ bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode);
return canCreateFolder;
}
diff --git a/Source/Editor/Windows/ContentWindow.Navigation.cs b/Source/Editor/Windows/ContentWindow.Navigation.cs
index 53dd6447b..cc9429d47 100644
--- a/Source/Editor/Windows/ContentWindow.Navigation.cs
+++ b/Source/Editor/Windows/ContentWindow.Navigation.cs
@@ -10,15 +10,41 @@ namespace FlaxEditor.Windows
{
public partial class ContentWindow
{
- private static readonly List NavUpdateCache = new List(8);
+ private static readonly List NavUpdateCache = new List(8);
private void OnTreeSelectionChanged(List from, List to)
{
+ if (!_showAllContentInTree && to.Count > 1)
+ {
+ _tree.Select(to[^1]);
+ return;
+ }
+ if (_showAllContentInTree && to.Count > 1)
+ {
+ var activeNode = GetActiveTreeSelection(to);
+ if (activeNode is ContentItemTreeNode itemNode)
+ SaveLastViewedFolder(itemNode.Item?.ParentFolder?.Node);
+ else
+ SaveLastViewedFolder(activeNode as ContentFolderTreeNode);
+ UpdateUI();
+ return;
+ }
+
// Navigate
- var source = from.Count > 0 ? from[0] as ContentTreeNode : null;
- var target = to.Count > 0 ? to[0] as ContentTreeNode : null;
+ var source = from.Count > 0 ? from[0] as ContentFolderTreeNode : null;
+ var targetNode = GetActiveTreeSelection(to);
+ if (targetNode is ContentItemTreeNode itemNode2)
+ {
+ SaveLastViewedFolder(itemNode2.Item?.ParentFolder?.Node);
+ UpdateUI();
+ itemNode2.Focus();
+ return;
+ }
+
+ var target = targetNode as ContentFolderTreeNode;
Navigate(source, target);
+ SaveLastViewedFolder(target);
target?.Focus();
}
@@ -26,12 +52,12 @@ namespace FlaxEditor.Windows
/// Navigates to the specified target content location.
///
/// The target.
- public void Navigate(ContentTreeNode target)
+ public void Navigate(ContentFolderTreeNode target)
{
Navigate(SelectedNode, target);
}
- private void Navigate(ContentTreeNode source, ContentTreeNode target)
+ private void Navigate(ContentFolderTreeNode source, ContentFolderTreeNode target)
{
if (target == null)
target = _root;
@@ -50,7 +76,8 @@ namespace FlaxEditor.Windows
}
// Show folder contents and select tree node
- RefreshView(target);
+ if (!_showAllContentInTree)
+ RefreshView(target);
_tree.Select(target);
target.ExpandAllParents();
@@ -62,7 +89,8 @@ namespace FlaxEditor.Windows
//UndoList.SetSize(32);
// Update search
- UpdateItemsSearch();
+ if (!_showAllContentInTree)
+ UpdateItemsSearch();
// Unlock navigation
_navigationUnlocked = true;
@@ -81,7 +109,7 @@ namespace FlaxEditor.Windows
if (_navigationUnlocked && _navigationUndo.Count > 0)
{
// Pop node
- ContentTreeNode node = _navigationUndo.Pop();
+ ContentFolderTreeNode node = _navigationUndo.Pop();
// Lock navigation
_navigationUnlocked = false;
@@ -90,7 +118,8 @@ namespace FlaxEditor.Windows
_navigationRedo.Push(SelectedNode);
// Select node
- RefreshView(node);
+ if (!_showAllContentInTree)
+ RefreshView(node);
_tree.Select(node);
node.ExpandAllParents();
@@ -99,14 +128,16 @@ namespace FlaxEditor.Windows
//UndoList.SetSize(32);
// Update search
- UpdateItemsSearch();
+ if (!_showAllContentInTree)
+ UpdateItemsSearch();
// Unlock navigation
_navigationUnlocked = true;
// Update UI
UpdateUI();
- _view.SelectFirstItem();
+ if (!_showAllContentInTree)
+ _view.SelectFirstItem();
}
}
@@ -119,7 +150,7 @@ namespace FlaxEditor.Windows
if (_navigationUnlocked && _navigationRedo.Count > 0)
{
// Pop node
- ContentTreeNode node = _navigationRedo.Pop();
+ ContentFolderTreeNode node = _navigationRedo.Pop();
// Lock navigation
_navigationUnlocked = false;
@@ -128,7 +159,8 @@ namespace FlaxEditor.Windows
_navigationUndo.Push(SelectedNode);
// Select node
- RefreshView(node);
+ if (!_showAllContentInTree)
+ RefreshView(node);
_tree.Select(node);
node.ExpandAllParents();
@@ -137,14 +169,16 @@ namespace FlaxEditor.Windows
//UndoList.SetSize(32);
// Update search
- UpdateItemsSearch();
+ if (!_showAllContentInTree)
+ UpdateItemsSearch();
// Unlock navigation
_navigationUnlocked = true;
// Update UI
UpdateUI();
- _view.SelectFirstItem();
+ if (!_showAllContentInTree)
+ _view.SelectFirstItem();
}
}
@@ -153,8 +187,8 @@ namespace FlaxEditor.Windows
///
public void NavigateUp()
{
- ContentTreeNode target = _root;
- ContentTreeNode current = SelectedNode;
+ ContentFolderTreeNode target = _root;
+ ContentFolderTreeNode current = SelectedNode;
if (current?.Folder.ParentFolder != null)
{
@@ -188,7 +222,7 @@ namespace FlaxEditor.Windows
// Spawn buttons
var nodes = NavUpdateCache;
nodes.Clear();
- ContentTreeNode node = SelectedNode;
+ ContentFolderTreeNode node = SelectedNode;
while (node != null)
{
nodes.Add(node);
@@ -222,13 +256,31 @@ namespace FlaxEditor.Windows
///
/// Gets the selected tree node.
///
- public ContentTreeNode SelectedNode => _tree.SelectedNode as ContentTreeNode;
+ public ContentFolderTreeNode SelectedNode
+ {
+ get
+ {
+ var selected = GetActiveTreeSelection(_tree.Selection);
+ if (selected is ContentItemTreeNode itemNode)
+ return itemNode.Parent as ContentFolderTreeNode;
+ return selected as ContentFolderTreeNode;
+ }
+ }
///
/// Gets the current view folder.
///
public ContentFolder CurrentViewFolder => SelectedNode?.Folder;
+ private TreeNode GetActiveTreeSelection(List selection)
+ {
+ if (selection == null || selection.Count == 0)
+ return null;
+ return _showAllContentInTree && selection.Count > 1
+ ? selection[^1]
+ : selection[0];
+ }
+
///
/// Shows the root folder.
///
@@ -236,5 +288,10 @@ namespace FlaxEditor.Windows
{
_tree.Select(_root);
}
+
+ private void SaveLastViewedFolder(ContentFolderTreeNode node)
+ {
+ Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, node?.Path ?? string.Empty);
+ }
}
}
diff --git a/Source/Editor/Windows/ContentWindow.Search.cs b/Source/Editor/Windows/ContentWindow.Search.cs
index 5a0ed63aa..af6cc67ef 100644
--- a/Source/Editor/Windows/ContentWindow.Search.cs
+++ b/Source/Editor/Windows/ContentWindow.Search.cs
@@ -115,12 +115,14 @@ namespace FlaxEditor.Windows
var root = _root;
root.LockChildrenRecursive();
+ _suppressExpandedStateSave = true;
PerformLayout();
// Update tree
var query = _foldersSearchBox.Text;
root.UpdateFilter(query);
+ _suppressExpandedStateSave = false;
root.UnlockChildrenRecursive();
PerformLayout();
PerformLayout();
@@ -161,6 +163,11 @@ namespace FlaxEditor.Windows
// Skip events during setup or init stuff
if (IsLayoutLocked)
return;
+ if (_showAllContentInTree)
+ {
+ RefreshTreeItems();
+ return;
+ }
// Check if clear filters
if (_itemsSearchBox.TextLength == 0 && !_viewDropdown.HasSelection)
@@ -200,7 +207,7 @@ namespace FlaxEditor.Windows
// Special case for root folder
for (int i = 0; i < _root.ChildrenCount; i++)
{
- if (_root.GetChild(i) is ContentTreeNode node)
+ if (_root.GetChild(i) is ContentFolderTreeNode node)
UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles);
}
}
@@ -222,7 +229,7 @@ namespace FlaxEditor.Windows
// Special case for root folder
for (int i = 0; i < _root.ChildrenCount; i++)
{
- if (_root.GetChild(i) is ContentTreeNode node)
+ if (_root.GetChild(i) is ContentFolderTreeNode node)
UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles, query);
}
}
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index 6313a91b0..f61dfa903 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -27,10 +27,15 @@ namespace FlaxEditor.Windows
public sealed partial class ContentWindow : EditorWindow
{
private const string ProjectDataLastViewedFolder = "LastViewedFolder";
+ private const string ProjectDataExpandedFolders = "ExpandedFolders";
private bool _isWorkspaceDirty;
private string _workspaceRebuildLocation;
private string _lastViewedFolderBeforeReload;
private SplitPanel _split;
+ private Panel _treeOnlyPanel;
+ private ContainerControl _treePanelRoot;
+ private ContainerControl _treeHeaderPanel;
+ private Panel _contentItemsSearchPanel;
private Panel _contentViewPanel;
private Panel _contentTreePanel;
private ContentView _view;
@@ -43,18 +48,23 @@ namespace FlaxEditor.Windows
private readonly ToolStripButton _navigateUpButton;
private NavigationBar _navigationBar;
+ private Panel _viewDropdownPanel;
private Tree _tree;
private TextBox _foldersSearchBox;
private TextBox _itemsSearchBox;
private ViewDropdown _viewDropdown;
private SortType _sortType;
private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true, _showGeneratedFiles = false;
+ private bool _showAllContentInTree;
+ private bool _suppressExpandedStateSave;
+ private readonly HashSet _expandedFolderPaths = new HashSet(StringComparer.OrdinalIgnoreCase);
+ private bool _renameInTree;
- private RootContentTreeNode _root;
+ private RootContentFolderTreeNode _root;
private bool _navigationUnlocked;
- private readonly Stack _navigationUndo = new Stack(32);
- private readonly Stack _navigationRedo = new Stack(32);
+ private readonly Stack _navigationUndo = new Stack(32);
+ private readonly Stack _navigationRedo = new Stack(32);
private NewItem _newElement;
@@ -134,6 +144,9 @@ namespace FlaxEditor.Windows
}
}
+ internal bool IsTreeOnlyMode => _showAllContentInTree;
+ internal SortType CurrentSortType => _sortType;
+
///
/// Initializes a new instance of the class.
///
@@ -163,14 +176,6 @@ namespace FlaxEditor.Windows
_navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up.");
_toolStrip.AddSeparator();
- // Navigation bar
- _navigationBar = new NavigationBar
- {
- Parent = _toolStrip,
- ScrollbarTrackColor = style.Background,
- ScrollbarThumbColor = style.ForegroundGrey,
- };
-
// Split panel
_split = new SplitPanel(options.Options.Interface.ContentWindowOrientation, ScrollBars.None, ScrollBars.None)
{
@@ -180,19 +185,38 @@ namespace FlaxEditor.Windows
Parent = this,
};
+ // Tree-only panel (used when showing all content in the tree)
+ _treeOnlyPanel = new Panel(ScrollBars.None)
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Offsets = new Margin(0, 0, _toolStrip.Bottom, 0),
+ Visible = false,
+ Parent = this,
+ };
+
+ // Tree host panel
+ _treePanelRoot = new ContainerControl
+ {
+ AnchorPreset = AnchorPresets.StretchAll,
+ Offsets = Margin.Zero,
+ Parent = _split.Panel1,
+ };
+
// Content structure tree searching query input box
- var headerPanel = new ContainerControl
+ _treeHeaderPanel = new ContainerControl
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
BackgroundColor = style.Background,
IsScrollable = false,
Offsets = new Margin(0, 0, 0, 18 + 6),
+ Parent = _treePanelRoot,
};
+
_foldersSearchBox = new SearchBox
{
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
- Parent = headerPanel,
- Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18),
+ Parent = _treeHeaderPanel,
+ Bounds = new Rectangle(4, 4, _treeHeaderPanel.Width - 8, 18),
};
_foldersSearchBox.TextChanged += OnFoldersSearchBoxTextChanged;
@@ -200,55 +224,74 @@ namespace FlaxEditor.Windows
_contentTreePanel = new Panel
{
AnchorPreset = AnchorPresets.StretchAll,
- Offsets = new Margin(0, 0, headerPanel.Bottom, 0),
+ Offsets = new Margin(0, 0, _treeHeaderPanel.Bottom, 0),
IsScrollable = true,
ScrollBars = ScrollBars.Both,
- Parent = _split.Panel1,
+ Parent = _treePanelRoot,
};
// Content structure tree
- _tree = new Tree(false)
+ _tree = new Tree(true)
{
DrawRootTreeLine = false,
Parent = _contentTreePanel,
};
_tree.SelectedChanged += OnTreeSelectionChanged;
- headerPanel.Parent = _split.Panel1;
// Content items searching query input box and filters selector
- var contentItemsSearchPanel = new ContainerControl
+ _contentItemsSearchPanel = new Panel
{
AnchorPreset = AnchorPresets.HorizontalStretchTop,
IsScrollable = true,
Offsets = new Margin(0, 0, 0, 18 + 8),
Parent = _split.Panel2,
};
- const float viewDropdownWidth = 50.0f;
+
_itemsSearchBox = new SearchBox
{
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
- Parent = contentItemsSearchPanel,
- Bounds = new Rectangle(viewDropdownWidth + 8, 4, contentItemsSearchPanel.Width - 12 - viewDropdownWidth, 18),
+ Parent = _contentItemsSearchPanel,
+ Bounds = new Rectangle(4, 4, _contentItemsSearchPanel.Width - 8, 18),
};
_itemsSearchBox.TextChanged += UpdateItemsSearch;
+
+ _viewDropdownPanel = new Panel
+ {
+ Width = 50.0f,
+ Parent = this,
+ AnchorPreset = AnchorPresets.TopLeft,
+ BackgroundColor = Color.Transparent,
+ };
+
_viewDropdown = new ViewDropdown
{
- AnchorPreset = AnchorPresets.MiddleLeft,
SupportMultiSelect = true,
TooltipText = "Change content view and filter options",
- Parent = contentItemsSearchPanel,
- Offsets = new Margin(4, viewDropdownWidth, -9, 18),
+ Offsets = Margin.Zero,
+ Width = 46.0f,
+ Height = 18.0f,
+ Parent = _viewDropdownPanel,
};
+ _viewDropdown.LocalX += 2.0f;
+ _viewDropdown.LocalY += _toolStrip.ItemsHeight * 0.5f - 9.0f;
_viewDropdown.SelectedIndexChanged += e => UpdateItemsSearch();
for (int i = 0; i <= (int)ContentItemSearchFilter.Other; i++)
_viewDropdown.Items.Add(((ContentItemSearchFilter)i).ToString());
_viewDropdown.PopupCreate += OnViewDropdownPopupCreate;
+ // Navigation bar (after view dropdown so layout order stays stable)
+ _navigationBar = new NavigationBar
+ {
+ Parent = _toolStrip,
+ ScrollbarTrackColor = style.Background,
+ ScrollbarThumbColor = style.ForegroundGrey,
+ };
+
// Content view panel
_contentViewPanel = new Panel
{
AnchorPreset = AnchorPresets.StretchAll,
- Offsets = new Margin(0, 0, contentItemsSearchPanel.Bottom + 4, 0),
+ Offsets = new Margin(0, 0, _contentItemsSearchPanel.Bottom + 4, 0),
IsScrollable = true,
ScrollBars = ScrollBars.Vertical,
Parent = _split.Panel2,
@@ -268,9 +311,14 @@ namespace FlaxEditor.Windows
_view.OnDelete += Delete;
_view.OnDuplicate += Duplicate;
_view.OnPaste += Paste;
+ _view.ViewScaleChanged += ApplyTreeViewScale;
_view.InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
+
+ LoadExpandedFolders();
+ UpdateViewDropdownBounds();
+ ApplyTreeViewScale();
}
private void OnCreateNewItemButtonClicked()
@@ -325,6 +373,7 @@ namespace FlaxEditor.Windows
var viewType = menu.AddChildMenu("View Type");
viewType.ContextMenu.AddButton("Tiles", OnViewTypeButtonClicked).Tag = ContentViewType.Tiles;
viewType.ContextMenu.AddButton("List", OnViewTypeButtonClicked).Tag = ContentViewType.List;
+ viewType.ContextMenu.AddButton("Tree View", OnViewTypeButtonClicked).Tag = "Tree";
viewType.ContextMenu.VisibleChanged += control =>
{
if (!control.Visible)
@@ -332,13 +381,23 @@ namespace FlaxEditor.Windows
foreach (var item in ((ContextMenu)control).Items)
{
if (item is ContextMenuButton button)
- button.Checked = View.ViewType == (ContentViewType)button.Tag;
+ {
+ if (button.Tag is ContentViewType type)
+ button.Checked = View.ViewType == type && !_showAllContentInTree;
+ else
+ button.Checked = _showAllContentInTree;
+ }
}
};
var show = menu.AddChildMenu("Show");
{
- var b = show.ContextMenu.AddButton("File extensions", () => View.ShowFileExtensions = !View.ShowFileExtensions);
+ var b = show.ContextMenu.AddButton("File extensions", () =>
+ {
+ View.ShowFileExtensions = !View.ShowFileExtensions;
+ if (_showAllContentInTree)
+ UpdateTreeItemNames(_root);
+ });
b.TooltipText = "Shows all files with extensions";
b.Checked = View.ShowFileExtensions;
b.CloseMenuOnClick = false;
@@ -419,9 +478,63 @@ namespace FlaxEditor.Windows
RefreshView();
}
+ private void SetShowAllContentInTree(bool value)
+ {
+ if (_showAllContentInTree == value)
+ return;
+
+ _showAllContentInTree = value;
+ ApplyTreeViewMode();
+ }
+
+ private void ApplyTreeViewMode()
+ {
+ if (_treeOnlyPanel == null || _split == null || _treePanelRoot == null)
+ return;
+
+ if (_showAllContentInTree)
+ {
+ _split.Visible = false;
+ _treeOnlyPanel.Visible = true;
+ _treePanelRoot.Parent = _treeOnlyPanel;
+ _treePanelRoot.Offsets = Margin.Zero;
+ _contentItemsSearchPanel.Visible = false;
+ _itemsSearchBox.Visible = false;
+ _contentViewPanel.Visible = false;
+ RefreshTreeItems();
+ }
+ else
+ {
+ _treeOnlyPanel.Visible = false;
+ _split.Visible = true;
+ _treePanelRoot.Parent = _split.Panel1;
+ _treePanelRoot.Offsets = Margin.Zero;
+ _contentItemsSearchPanel.Visible = true;
+ _itemsSearchBox.Visible = true;
+ _contentViewPanel.Visible = true;
+ if (_tree.SelectedNode is ContentItemTreeNode itemNode && itemNode.Parent is TreeNode parentNode)
+ _tree.Select(parentNode);
+ if (_root != null)
+ RemoveTreeAssetNodes(_root);
+ RefreshView(SelectedNode);
+ }
+
+ PerformLayout();
+ ApplyTreeViewScale();
+ _tree.PerformLayout();
+ }
+
private void OnViewTypeButtonClicked(ContextMenuButton button)
{
- View.ViewType = (ContentViewType)button.Tag;
+ if (button.Tag is ContentViewType viewType)
+ {
+ SetShowAllContentInTree(false);
+ View.ViewType = viewType;
+ }
+ else
+ {
+ SetShowAllContentInTree(true);
+ }
}
private void OnFilterClicked(ContextMenuButton filterButton)
@@ -481,15 +594,58 @@ namespace FlaxEditor.Windows
// Show element in the view
Select(item, true);
- // Disable scrolling in content view
- if (_contentViewPanel.VScrollBar != null)
- _contentViewPanel.VScrollBar.ThumbEnabled = false;
- if (_contentViewPanel.HScrollBar != null)
- _contentViewPanel.HScrollBar.ThumbEnabled = false;
- ScrollingOnContentView(false);
+ // Disable scrolling in proper view
+ _renameInTree = _showAllContentInTree;
+ if (_renameInTree)
+ {
+ if (_contentTreePanel.VScrollBar != null)
+ _contentTreePanel.VScrollBar.ThumbEnabled = false;
+ if (_contentTreePanel.HScrollBar != null)
+ _contentTreePanel.HScrollBar.ThumbEnabled = false;
+ ScrollingOnTreeView(false);
+ }
+ else
+ {
+ if (_contentViewPanel.VScrollBar != null)
+ _contentViewPanel.VScrollBar.ThumbEnabled = false;
+ if (_contentViewPanel.HScrollBar != null)
+ _contentViewPanel.HScrollBar.ThumbEnabled = false;
+ ScrollingOnContentView(false);
+ }
// Show rename popup
- var popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true);
+ RenamePopup popup;
+ if (_renameInTree)
+ {
+ TreeNode node = null;
+ if (item is ContentFolder folder)
+ node = folder.Node;
+ else if (item.ParentFolder != null)
+ node = FindTreeItemNode(item.ParentFolder.Node, item);
+ if (node == null)
+ {
+ // Fallback to content view rename
+ popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true);
+ }
+ else
+ {
+ var area = node.TextRect;
+ const float minRenameWidth = 220.0f;
+ if (area.Width < minRenameWidth)
+ {
+ float expand = minRenameWidth - area.Width;
+ area.X -= expand * 0.5f;
+ area.Width = minRenameWidth;
+ }
+ area.Y -= 2;
+ area.Height += 4.0f;
+ popup = RenamePopup.Show(node, area, item.ShortName, true);
+ }
+ }
+ else
+ {
+ popup = RenamePopup.Show(item, item.TextRectangle, item.ShortName, true);
+ }
popup.Tag = item;
popup.Validate += OnRenameValidate;
popup.Renamed += renamePopup => Rename((ContentItem)renamePopup.Tag, renamePopup.Text);
@@ -509,12 +665,24 @@ namespace FlaxEditor.Windows
private void OnRenameClosed(RenamePopup popup)
{
- // Restore scrolling in content view
- if (_contentViewPanel.VScrollBar != null)
- _contentViewPanel.VScrollBar.ThumbEnabled = true;
- if (_contentViewPanel.HScrollBar != null)
- _contentViewPanel.HScrollBar.ThumbEnabled = true;
- ScrollingOnContentView(true);
+ // Restore scrolling in proper view
+ if (_renameInTree)
+ {
+ if (_contentTreePanel.VScrollBar != null)
+ _contentTreePanel.VScrollBar.ThumbEnabled = true;
+ if (_contentTreePanel.HScrollBar != null)
+ _contentTreePanel.HScrollBar.ThumbEnabled = true;
+ ScrollingOnTreeView(true);
+ }
+ else
+ {
+ if (_contentViewPanel.VScrollBar != null)
+ _contentViewPanel.VScrollBar.ThumbEnabled = true;
+ if (_contentViewPanel.HScrollBar != null)
+ _contentViewPanel.HScrollBar.ThumbEnabled = true;
+ ScrollingOnContentView(true);
+ }
+ _renameInTree = false;
// Check if was creating new element
if (_newElement != null)
@@ -927,6 +1095,16 @@ namespace FlaxEditor.Windows
}
}
+ private void OnContentDatabaseItemAdded(ContentItem contentItem)
+ {
+ if (contentItem is ContentFolder folder && _expandedFolderPaths.Contains(StringUtils.NormalizePath(folder.Path)))
+ {
+ _suppressExpandedStateSave = true;
+ folder.Node?.Expand(true);
+ _suppressExpandedStateSave = false;
+ }
+ }
+
///
/// Opens the specified content item.
///
@@ -943,7 +1121,8 @@ namespace FlaxEditor.Windows
var folder = (ContentFolder)item;
folder.Node.Expand();
_tree.Select(folder.Node);
- _view.SelectFirstItem();
+ if (!_showAllContentInTree)
+ _view.SelectFirstItem();
return;
}
@@ -984,6 +1163,36 @@ namespace FlaxEditor.Windows
// Ensure that window is visible
FocusOrShow();
+ if (_showAllContentInTree)
+ {
+ var targetNode = item is ContentFolder folder ? folder.Node : parent.Node;
+ if (targetNode != null)
+ {
+ targetNode.ExpandAllParents();
+ if (item is ContentFolder)
+ {
+ _tree.Select(targetNode);
+ _contentTreePanel.ScrollViewTo(targetNode, fastScroll);
+ targetNode.Focus();
+ }
+ else
+ {
+ var itemNode = FindTreeItemNode(targetNode, item);
+ if (itemNode != null)
+ {
+ _tree.Select(itemNode);
+ _contentTreePanel.ScrollViewTo(itemNode, fastScroll);
+ itemNode.Focus();
+ }
+ else
+ {
+ _tree.Select(targetNode);
+ }
+ }
+ }
+ return;
+ }
+
// Navigate to the parent directory
Navigate(parent.Node);
@@ -995,23 +1204,45 @@ namespace FlaxEditor.Windows
_view.Focus();
}
+ private ContentItemTreeNode FindTreeItemNode(ContentFolderTreeNode parentNode, ContentItem item)
+ {
+ if (parentNode == null || item == null)
+ return null;
+ for (int i = 0; i < parentNode.ChildrenCount; i++)
+ {
+ if (parentNode.GetChild(i) is ContentItemTreeNode itemNode && itemNode.Item == item)
+ return itemNode;
+ }
+ return null;
+ }
+
///
/// Refreshes the current view items collection.
///
public void RefreshView()
{
- if (_view.IsSearching)
+ if (_showAllContentInTree)
+ RefreshTreeItems();
+ else if (_view.IsSearching)
UpdateItemsSearch();
else
RefreshView(SelectedNode);
+
+ return;
}
///
/// Refreshes the view.
///
/// The target location.
- public void RefreshView(ContentTreeNode target)
+ public void RefreshView(ContentFolderTreeNode target)
{
+ if (_showAllContentInTree)
+ {
+ RefreshTreeItems();
+ return;
+ }
+
_view.IsSearching = false;
if (target == _root)
{
@@ -1019,7 +1250,7 @@ namespace FlaxEditor.Windows
var items = new List(8);
for (int i = 0; i < _root.ChildrenCount; i++)
{
- if (_root.GetChild(i) is ContentTreeNode node)
+ if (_root.GetChild(i) is ContentFolderTreeNode node)
{
items.Add(node.Folder);
}
@@ -1038,12 +1269,263 @@ namespace FlaxEditor.Windows
}
}
+ private void RefreshTreeItems()
+ {
+ if (!_showAllContentInTree || _root == null)
+ return;
+
+ _root.LockChildrenRecursive();
+ RemoveTreeAssetNodes(_root);
+ AddTreeAssetNodes(_root);
+ _root.UnlockChildrenRecursive();
+ _tree.PerformLayout();
+ }
+
+ private void UpdateTreeItemNames(ContentFolderTreeNode node)
+ {
+ if (node == null)
+ return;
+
+ for (int i = 0; i < node.ChildrenCount; i++)
+ {
+ if (node.GetChild(i) is ContentFolderTreeNode childFolder)
+ {
+ UpdateTreeItemNames(childFolder);
+ }
+ else if (node.GetChild(i) is ContentItemTreeNode itemNode)
+ {
+ itemNode.UpdateDisplayedName();
+ }
+ }
+ }
+
+ internal void OnContentTreeNodeExpandedChanged(ContentFolderTreeNode node, bool isExpanded)
+ {
+ if (_suppressExpandedStateSave || node == null || node == _root)
+ return;
+
+ var path = node.Path;
+ if (string.IsNullOrEmpty(path))
+ return;
+ path = StringUtils.NormalizePath(path);
+
+ if (isExpanded)
+ _expandedFolderPaths.Add(path);
+ else
+ // Remove all sub paths if parent folder is closed.
+ _expandedFolderPaths.RemoveWhere(x => x.Contains(path));
+
+ SaveExpandedFolders();
+ }
+
+ internal void TryAutoExpandContentNode(ContentFolderTreeNode node)
+ {
+ if (node == null || node == _root)
+ return;
+
+ var path = node.Path;
+ if (string.IsNullOrEmpty(path))
+ return;
+ path = StringUtils.NormalizePath(path);
+
+ if (!_expandedFolderPaths.Contains(path))
+ return;
+
+ _suppressExpandedStateSave = true;
+ node.Expand(true);
+ _suppressExpandedStateSave = false;
+ }
+
+ private void LoadExpandedFolders()
+ {
+ _expandedFolderPaths.Clear();
+ if (Editor.ProjectCache.TryGetCustomData(ProjectDataExpandedFolders, out string data) && !string.IsNullOrWhiteSpace(data))
+ {
+ var entries = data.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ for (int i = 0; i < entries.Length; i++)
+ {
+ var path = entries[i].Trim();
+ if (path.Length == 0)
+ continue;
+ _expandedFolderPaths.Add(StringUtils.NormalizePath(path));
+ }
+ }
+ }
+
+ private void SaveExpandedFolders()
+ {
+ if (_expandedFolderPaths.Count == 0)
+ {
+ Editor.ProjectCache.RemoveCustomData(ProjectDataExpandedFolders);
+ return;
+ }
+
+ var data = string.Join("\n", _expandedFolderPaths);
+ Editor.ProjectCache.SetCustomData(ProjectDataExpandedFolders, data);
+ }
+
+ private void ApplyExpandedFolders()
+ {
+ if (_root == null || _expandedFolderPaths.Count == 0)
+ return;
+
+ _suppressExpandedStateSave = true;
+ foreach (var path in _expandedFolderPaths)
+ {
+ if (Editor.ContentDatabase.Find(path) is ContentFolder folder)
+ {
+ folder.Node.ExpandAllParents(true);
+ folder.Node.Expand(true);
+ }
+ }
+ _suppressExpandedStateSave = false;
+ }
+
+ private void RemoveTreeAssetNodes(ContentFolderTreeNode node)
+ {
+ for (int i = node.ChildrenCount - 1; i >= 0; i--)
+ {
+ if (node.GetChild(i) is ContentItemTreeNode itemNode)
+ {
+ node.RemoveChild(itemNode);
+ itemNode.Dispose();
+ }
+ else if (node.GetChild(i) is ContentFolderTreeNode childFolder)
+ {
+ RemoveTreeAssetNodes(childFolder);
+ }
+ }
+ }
+
+ private void AddTreeAssetNodes(ContentFolderTreeNode node)
+ {
+ if (node.Folder != null)
+ {
+ var children = node.Folder.Children;
+ for (int i = 0; i < children.Count; i++)
+ {
+ var child = children[i];
+ if (child is ContentFolder)
+ continue;
+ if (!ShouldShowTreeItem(child))
+ continue;
+
+ var itemNode = new ContentItemTreeNode(child)
+ {
+ Parent = node,
+ };
+ }
+ }
+
+ for (int i = 0; i < node.ChildrenCount; i++)
+ {
+ if (node.GetChild(i) is ContentFolderTreeNode childFolder)
+ {
+ AddTreeAssetNodes(childFolder);
+ }
+ }
+
+ node.SortChildren();
+ }
+
+ private bool ShouldShowTreeItem(ContentItem item)
+ {
+ if (item == null || !item.Visible)
+ return false;
+ if (_viewDropdown != null && _viewDropdown.HasSelection)
+ {
+ var filterIndex = (int)item.SearchFilter;
+ if (!_viewDropdown.Selection.Contains(filterIndex))
+ return false;
+ }
+ if (!_showAllFiles && item is FileItem)
+ return false;
+ if (!_showGeneratedFiles && IsGeneratedFile(item.Path))
+ return false;
+ return true;
+ }
+
+ private static bool IsGeneratedFile(string path)
+ {
+ return path.EndsWith(".Gen.cs", StringComparison.Ordinal) ||
+ path.EndsWith(".Gen.h", StringComparison.Ordinal) ||
+ path.EndsWith(".Gen.cpp", StringComparison.Ordinal) ||
+ path.EndsWith(".csproj", StringComparison.Ordinal) ||
+ path.Contains(".CSharp");
+ }
+
private void UpdateUI()
{
UpdateToolstrip();
UpdateNavigationBar();
}
+ private void ApplyTreeViewScale()
+ {
+ if (_tree == null)
+ return;
+
+ var scale = _showAllContentInTree ? View.ViewScale : 1.0f;
+ var headerHeight = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
+ var style = Style.Current;
+ var fontSize = Mathf.Clamp(style.FontSmall.Size * scale, 8.0f, 28.0f);
+ var fontRef = new FontReference(style.FontSmall.Asset, fontSize);
+ var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
+ var textMarginLeft = 2.0f + Mathf.Max(0.0f, iconSize - 16.0f);
+ ApplyTreeNodeScale(_root, headerHeight, fontRef, textMarginLeft);
+ _root?.PerformLayout(true);
+ _tree.PerformLayout();
+ }
+
+ private void ApplyTreeNodeScale(ContentFolderTreeNode node, float headerHeight, FontReference fontRef, float textMarginLeft)
+ {
+ if (node == null)
+ return;
+
+ var margin = node.TextMargin;
+ margin.Left = textMarginLeft;
+ margin.Top = 2.0f;
+ margin.Right = 2.0f;
+ margin.Bottom = 2.0f;
+ node.TextMargin = margin;
+ node.CustomArrowRect = GetTreeArrowRect(node, headerHeight);
+ node.HeaderHeight = headerHeight;
+ node.TextFont = fontRef;
+ for (int i = 0; i < node.ChildrenCount; i++)
+ {
+ if (node.GetChild(i) is ContentFolderTreeNode child)
+ ApplyTreeNodeScale(child, headerHeight, fontRef, textMarginLeft);
+ else if (node.GetChild(i) is ContentItemTreeNode itemNode)
+ {
+ var itemMargin = itemNode.TextMargin;
+ itemMargin.Left = textMarginLeft;
+ itemMargin.Top = 2.0f;
+ itemMargin.Right = 2.0f;
+ itemMargin.Bottom = 2.0f;
+ itemNode.TextMargin = itemMargin;
+ itemNode.HeaderHeight = headerHeight;
+ itemNode.TextFont = fontRef;
+ }
+ }
+ }
+
+ private static Rectangle GetTreeArrowRect(ContentFolderTreeNode node, float headerHeight)
+ {
+ if (node == null)
+ return Rectangle.Empty;
+
+ var scale = Editor.Instance?.Windows?.ContentWin?.IsTreeOnlyMode == true
+ ? Editor.Instance.Windows.ContentWin.View.ViewScale
+ : 1.0f;
+ var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f);
+ var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
+ var textRect = node.TextRect;
+ var iconLeft = textRect.Left - iconSize - 2.0f;
+ var x = iconLeft - arrowSize - 2.0f;
+ var y = (headerHeight - arrowSize) * 0.5f;
+ return new Rectangle(Mathf.Max(x, 0.0f), Mathf.Max(y, 0.0f), arrowSize, arrowSize);
+ }
+
private void UpdateToolstrip()
{
if (_toolStrip == null)
@@ -1063,20 +1545,42 @@ namespace FlaxEditor.Windows
{
var bottomPrev = _toolStrip.Bottom;
_navigationBar.UpdateBounds(_toolStrip);
+ if (_viewDropdownPanel != null && _viewDropdownPanel.Visible)
+ {
+ var reserved = _viewDropdownPanel.Width + 8.0f;
+ _navigationBar.Width = Mathf.Max(_navigationBar.Width - reserved, 0.0f);
+ }
if (bottomPrev != _toolStrip.Bottom)
{
// Navigation bar changed toolstrip height
_split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
+ if (_treeOnlyPanel != null)
+ _treeOnlyPanel.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
PerformLayout();
}
+ UpdateViewDropdownBounds();
}
}
+ private void UpdateViewDropdownBounds()
+ {
+ if (_viewDropdownPanel == null || _toolStrip == null)
+ return;
+
+ var margin = _toolStrip.ItemsMargin;
+ var height = _toolStrip.ItemsHeight;
+ var y = _toolStrip.Y + (_toolStrip.Height - height) * 0.5f;
+ var width = _viewDropdownPanel.Width;
+ var x = _toolStrip.Right - width - margin.Right;
+ _viewDropdownPanel.Bounds = new Rectangle(x, y, width, height);
+ }
+
///
public override void OnInit()
{
// Content database events
Editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true;
+ Editor.ContentDatabase.ItemAdded += OnContentDatabaseItemAdded;
Editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved;
Editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; };
Editor.ContentDatabase.WorkspaceRebuilt += () =>
@@ -1095,6 +1599,7 @@ namespace FlaxEditor.Windows
ShowRoot();
};
+ LoadExpandedFolders();
Refresh();
// Load last viewed folder
@@ -1110,7 +1615,7 @@ namespace FlaxEditor.Windows
private void OnScriptsReloadBegin()
{
- var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null;
+ var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentFolderTreeNode : null;
_lastViewedFolderBeforeReload = lastViewedFolder?.Path ?? string.Empty;
_tree.RemoveChild(_root);
@@ -1133,7 +1638,7 @@ namespace FlaxEditor.Windows
private void Refresh()
{
// Setup content root node
- _root = new RootContentTreeNode
+ _root = new RootContentFolderTreeNode
{
ChildrenIndent = 0
};
@@ -1156,7 +1661,7 @@ namespace FlaxEditor.Windows
_root.AddChild(Editor.ContentDatabase.Engine);
Editor.ContentDatabase.Game?.Expand(true);
- _tree.Margin = new Margin(0.0f, 0.0f, -16.0f, 2.0f); // Hide root node
+ _tree.Margin = new Margin(0.0f, 0.0f, -16.0f, ScrollBar.DefaultSize + 2); // Hide root node
_tree.AddChild(_root);
// Setup navigation
@@ -1167,6 +1672,8 @@ namespace FlaxEditor.Windows
// Update UI layout
_isLayoutLocked = false;
PerformLayout();
+ ApplyExpandedFolders();
+ ApplyTreeViewMode();
}
///
@@ -1176,7 +1683,10 @@ namespace FlaxEditor.Windows
if (_isWorkspaceDirty)
{
_isWorkspaceDirty = false;
- RefreshView();
+ if (_showAllContentInTree)
+ RefreshTreeItems();
+ else
+ RefreshView();
}
base.Update(deltaTime);
@@ -1186,7 +1696,15 @@ namespace FlaxEditor.Windows
public override void OnExit()
{
// Save last viewed folder
- var lastViewedFolder = _tree.Selection.Count == 1 ? _tree.SelectedNode as ContentTreeNode : null;
+ ContentFolderTreeNode lastViewedFolder = null;
+ if (_tree.Selection.Count == 1)
+ {
+ var selectedNode = _tree.SelectedNode;
+ if (selectedNode is ContentItemTreeNode itemNode)
+ lastViewedFolder = itemNode.Item?.ParentFolder?.Node;
+ else
+ lastViewedFolder = selectedNode as ContentFolderTreeNode;
+ }
Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, lastViewedFolder?.Path ?? string.Empty);
// Clear view
@@ -1197,7 +1715,7 @@ namespace FlaxEditor.Windows
{
while (_root.HasChildren)
{
- _root.RemoveChild((ContentTreeNode)_root.GetChild(0));
+ _root.RemoveChild((ContentFolderTreeNode)_root.GetChild(0));
}
}
}
@@ -1232,7 +1750,12 @@ namespace FlaxEditor.Windows
{
ShowContextMenuForItem(null, ref location, false);
}
- else if (c is ContentTreeNode node)
+ else if (c is ContentItemTreeNode itemNode)
+ {
+ _tree.Select(itemNode);
+ ShowContextMenuForItem(itemNode.Item, ref location, false);
+ }
+ else if (c is ContentFolderTreeNode node)
{
_tree.Select(node);
ShowContextMenuForItem(node.Folder, ref location, true);
@@ -1258,11 +1781,16 @@ namespace FlaxEditor.Windows
///
protected override void PerformLayoutBeforeChildren()
{
- UpdateNavigationBarBounds();
-
base.PerformLayoutBeforeChildren();
}
+ ///
+ protected override void PerformLayoutAfterChildren()
+ {
+ base.PerformLayoutAfterChildren();
+ UpdateNavigationBarBounds();
+ }
+
///
public override bool UseLayoutData => true;
@@ -1277,6 +1805,7 @@ namespace FlaxEditor.Windows
writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString());
writer.WriteAttributeString("ShowGeneratedFiles", ShowGeneratedFiles.ToString());
writer.WriteAttributeString("ViewType", _view.ViewType.ToString());
+ writer.WriteAttributeString("TreeViewAllContent", _showAllContentInTree.ToString());
}
///
@@ -1297,6 +1826,9 @@ namespace FlaxEditor.Windows
ShowGeneratedFiles = value2;
if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType))
_view.ViewType = viewType;
+ if (bool.TryParse(node.GetAttribute("TreeViewAllContent"), out value2))
+ _showAllContentInTree = value2;
+ ApplyTreeViewMode();
}
///
@@ -1304,6 +1836,7 @@ namespace FlaxEditor.Windows
{
_split.SplitterValue = 0.2f;
_view.ViewScale = 1.0f;
+ _showAllContentInTree = false;
}
///
@@ -1312,10 +1845,17 @@ namespace FlaxEditor.Windows
_foldersSearchBox = null;
_itemsSearchBox = null;
_viewDropdown = null;
+ _viewDropdownPanel = null;
+ _treePanelRoot = null;
+ _treeHeaderPanel = null;
+ _treeOnlyPanel = null;
+ _contentItemsSearchPanel = null;
Editor.Options.OptionsChanged -= OnOptionsChanged;
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd;
+ if (Editor?.ContentDatabase != null)
+ Editor.ContentDatabase.ItemAdded -= OnContentDatabaseItemAdded;
base.OnDestroy();
}