From 24a11ac2a8a7c8dddc56e2f912ee633403b45000 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 28 Feb 2026 12:38:07 -0600 Subject: [PATCH] Add tree view mode for content window. --- .../Editor/Content/GUI/ContentNavigation.cs | 8 +- Source/Editor/Content/Items/ContentFolder.cs | 6 +- .../Content/Tree/ContentFolderTreeNode.cs | 408 +++++++++++ .../Content/Tree/ContentItemTreeNode.cs | 221 ++++++ Source/Editor/Content/Tree/ContentTreeNode.cs | 333 --------- .../Content/Tree/MainContentTreeNode.cs | 8 +- Source/Editor/Content/Tree/ProjectTreeNode.cs | 39 +- .../Content/Tree/RootContentTreeNode.cs | 8 +- Source/Editor/GUI/NavigationBar.cs | 18 +- .../Editor/Modules/ContentDatabaseModule.cs | 38 +- .../Windows/ContentWindow.ContextMenu.cs | 12 +- .../Windows/ContentWindow.Navigation.cs | 95 ++- Source/Editor/Windows/ContentWindow.Search.cs | 11 +- Source/Editor/Windows/ContentWindow.cs | 649 ++++++++++++++++-- 14 files changed, 1394 insertions(+), 460 deletions(-) create mode 100644 Source/Editor/Content/Tree/ContentFolderTreeNode.cs create mode 100644 Source/Editor/Content/Tree/ContentItemTreeNode.cs delete mode 100644 Source/Editor/Content/Tree/ContentTreeNode.cs 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..93fa1f0d3 --- /dev/null +++ b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs @@ -0,0 +1,408 @@ +// 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 + Editor.Instance?.Windows?.ContentWin?.TryAutoExpandContentNode(this); + } + + /// + /// 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; + } + } + + 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 8629296fc..000000000 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ /dev/null @@ -1,333 +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; - } - } - - 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 1ad225997..28364e6dd 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -59,7 +59,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)); } @@ -77,7 +77,7 @@ 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", () => { @@ -130,7 +130,7 @@ namespace FlaxEditor.Windows } } - if (isFolder && folder.Node is MainContentTreeNode) + if (isFolder && folder.Node is MainContentFolderTreeNode) { cm.AddSeparator(); } @@ -146,7 +146,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 } @@ -179,14 +179,14 @@ namespace FlaxEditor.Windows cm.AddSeparator(); // 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 = cm.AddButton("New module"); button.CloseMenuOnClick = false; button.Clicked += () => NewModule(button, parentFolderNode.Source.Path); } - if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode)) + if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode)) { cm.AddButton("New folder", NewFolder); } 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 f28dc4834..87564597a 100644 --- a/Source/Editor/Windows/ContentWindow.Search.cs +++ b/Source/Editor/Windows/ContentWindow.Search.cs @@ -115,11 +115,13 @@ namespace FlaxEditor.Windows var root = _root; root.LockChildrenRecursive(); + _suppressExpandedStateSave = true; // Update tree var query = _foldersSearchBox.Text; root.UpdateFilter(query); + _suppressExpandedStateSave = false; root.UnlockChildrenRecursive(); PerformLayout(); PerformLayout(); @@ -160,6 +162,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) @@ -199,7 +206,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); } } @@ -221,7 +228,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 41382e60c..780be5c04 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; @@ -42,18 +47,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; @@ -133,6 +143,9 @@ namespace FlaxEditor.Windows } } + internal bool IsTreeOnlyMode => _showAllContentInTree; + internal SortType CurrentSortType => _sortType; + /// /// Initializes a new instance of the class. /// @@ -161,14 +174,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) { @@ -178,19 +183,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; @@ -198,55 +222,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, @@ -266,9 +309,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 ContextMenu OnViewDropdownPopupCreate(ComboBox comboBox) @@ -287,6 +335,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) @@ -294,13 +343,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; @@ -381,9 +440,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) @@ -443,15 +556,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); @@ -471,12 +627,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) @@ -889,6 +1057,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. /// @@ -905,7 +1083,8 @@ namespace FlaxEditor.Windows var folder = (ContentFolder)item; folder.Node.Expand(); _tree.Select(folder.Node); - _view.SelectFirstItem(); + if (!_showAllContentInTree) + _view.SelectFirstItem(); return; } @@ -946,6 +1125,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); @@ -957,23 +1166,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) { @@ -981,7 +1212,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); } @@ -1000,12 +1231,262 @@ 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); + _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) @@ -1025,20 +1506,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 += () => @@ -1057,6 +1560,7 @@ namespace FlaxEditor.Windows ShowRoot(); }; + LoadExpandedFolders(); Refresh(); // Load last viewed folder @@ -1072,7 +1576,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); @@ -1093,7 +1597,7 @@ namespace FlaxEditor.Windows private void Refresh() { // Setup content root node - _root = new RootContentTreeNode + _root = new RootContentFolderTreeNode { ChildrenIndent = 0 }; @@ -1116,7 +1620,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 @@ -1127,6 +1631,8 @@ namespace FlaxEditor.Windows // Update UI layout _isLayoutLocked = false; PerformLayout(); + ApplyExpandedFolders(); + ApplyTreeViewMode(); } /// @@ -1136,7 +1642,10 @@ namespace FlaxEditor.Windows if (_isWorkspaceDirty) { _isWorkspaceDirty = false; - RefreshView(); + if (_showAllContentInTree) + RefreshTreeItems(); + else + RefreshView(); } base.Update(deltaTime); @@ -1146,7 +1655,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 @@ -1157,7 +1674,7 @@ namespace FlaxEditor.Windows { while (_root.HasChildren) { - _root.RemoveChild((ContentTreeNode)_root.GetChild(0)); + _root.RemoveChild((ContentFolderTreeNode)_root.GetChild(0)); } } } @@ -1192,7 +1709,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); @@ -1218,11 +1740,16 @@ namespace FlaxEditor.Windows /// protected override void PerformLayoutBeforeChildren() { - UpdateNavigationBarBounds(); - base.PerformLayoutBeforeChildren(); } + /// + protected override void PerformLayoutAfterChildren() + { + base.PerformLayoutAfterChildren(); + UpdateNavigationBarBounds(); + } + /// public override bool UseLayoutData => true; @@ -1237,6 +1764,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()); } /// @@ -1257,6 +1785,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(); } /// @@ -1264,6 +1795,7 @@ namespace FlaxEditor.Windows { _split.SplitterValue = 0.2f; _view.ViewScale = 1.0f; + _showAllContentInTree = false; } /// @@ -1272,10 +1804,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(); }