Add tree view mode for content window.

This commit is contained in:
Chandler Cox
2026-02-28 12:38:07 -06:00
parent c51a023e61
commit 24a11ac2a8
14 changed files with 1394 additions and 460 deletions

View File

@@ -19,7 +19,7 @@ namespace FlaxEditor.Content.GUI
/// <summary>
/// Gets the target node.
/// </summary>
public ContentTreeNode TargetNode { get; }
public ContentFolderTreeNode TargetNode { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ContentNavigationButton"/> class.
@@ -28,7 +28,7 @@ namespace FlaxEditor.Content.GUI
/// <param name="x">The x position.</param>
/// <param name="y">The y position.</param>
/// <param name="height">The height.</param>
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;

View File

@@ -59,7 +59,7 @@ namespace FlaxEditor.Content
/// <summary>
/// Gets the content node.
/// </summary>
public ContentTreeNode Node { get; }
public ContentFolderTreeNode Node { get; }
/// <summary>
/// The subitems of this folder.
@@ -72,7 +72,7 @@ namespace FlaxEditor.Content
/// <param name="type">The folder type.</param>
/// <param name="path">The path to the item.</param>
/// <param name="node">The folder parent node.</param>
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;
}
}

View File

@@ -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;
/// <summary>
/// Content folder tree node.
/// </summary>
/// <seealso cref="TreeNode" />
public class ContentFolderTreeNode : TreeNode
{
private DragItems _dragOverItems;
private DragActors _dragActors;
private List<Rectangle> _highlights;
/// <summary>
/// The folder.
/// </summary>
protected ContentFolder _folder;
/// <summary>
/// Whether this node can be deleted.
/// </summary>
public virtual bool CanDelete => true;
/// <summary>
/// Whether this node can be duplicated.
/// </summary>
public virtual bool CanDuplicate => true;
/// <summary>
/// Gets the content folder item.
/// </summary>
public ContentFolder Folder => _folder;
/// <summary>
/// Gets the type of the folder.
/// </summary>
public ContentFolderType FolderType => _folder.FolderType;
/// <summary>
/// Returns true if that folder can import/manage scripts.
/// </summary>
public bool CanHaveScripts => _folder.CanHaveScripts;
/// <summary>
/// Returns true if that folder can import/manage assets.
/// </summary>
/// <returns>True if can contain assets for project, otherwise false</returns>
public bool CanHaveAssets => _folder.CanHaveAssets;
/// <summary>
/// Gets the parent node.
/// </summary>
public ContentFolderTreeNode ParentNode => Parent as ContentFolderTreeNode;
/// <summary>
/// Gets the folder path.
/// </summary>
public string Path => _folder.Path;
/// <summary>
/// Gets the navigation button label.
/// </summary>
public virtual string NavButtonLabel => _folder.ShortName;
/// <summary>
/// Initializes a new instance of the <see cref="ContentFolderTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="path">The folder path.</param>
public ContentFolderTreeNode(ContentFolderTreeNode parent, string path)
: this(parent, parent?.FolderType ?? ContentFolderType.Other, path)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentFolderTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="type">The folder type.</param>
/// <param name="path">The folder path.</param>
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);
}
/// <summary>
/// Shows the rename popup for the item.
/// </summary>
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); };
}
/// <summary>
/// Updates the query search filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
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<Rectangle>(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;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override void OnDestroy()
{
// Delete folder item
_folder.Dispose();
base.OnDestroy();
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void OnDragLeaveHeader()
{
_dragOverItems?.OnDragLeave();
_dragActors?.OnDragLeave();
base.OnDragLeaveHeader();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
protected override void DoDragDrop()
{
DoDragDrop(DragItems.GetDragData(_folder));
}
/// <inheritdoc />
protected override void OnLongPress()
{
Select();
StartRenaming();
}
/// <inheritdoc />
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);
}
}

View File

@@ -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;
/// <summary>
/// Tree node for non-folder content items.
/// </summary>
public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner
{
private List<Rectangle> _highlights;
/// <summary>
/// The content item.
/// </summary>
public ContentItem Item { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ContentItemTreeNode"/> class.
/// </summary>
/// <param name="item">The content item.</param>
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;
}
/// <summary>
/// Updates the query search filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
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<Rectangle>(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;
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void DoDragDrop()
{
DoDragDrop(DragItems.GetDragData(Item));
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
void IContentItemOwner.OnItemDeleted(ContentItem item)
{
}
/// <inheritdoc />
void IContentItemOwner.OnItemRenamed(ContentItem item)
{
UpdateDisplayedName();
}
/// <inheritdoc />
void IContentItemOwner.OnItemReimported(ContentItem item)
{
}
/// <inheritdoc />
void IContentItemOwner.OnItemDispose(ContentItem item)
{
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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;
}
}

View File

@@ -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
{
/// <summary>
/// Content folder tree node.
/// </summary>
/// <seealso cref="TreeNode" />
public class ContentTreeNode : TreeNode
{
private DragItems _dragOverItems;
private List<Rectangle> _highlights;
/// <summary>
/// The folder.
/// </summary>
protected ContentFolder _folder;
/// <summary>
/// Whether this node can be deleted.
/// </summary>
public virtual bool CanDelete => true;
/// <summary>
/// Whether this node can be duplicated.
/// </summary>
public virtual bool CanDuplicate => true;
/// <summary>
/// Gets the content folder item.
/// </summary>
public ContentFolder Folder => _folder;
/// <summary>
/// Gets the type of the folder.
/// </summary>
public ContentFolderType FolderType => _folder.FolderType;
/// <summary>
/// Returns true if that folder can import/manage scripts.
/// </summary>
public bool CanHaveScripts => _folder.CanHaveScripts;
/// <summary>
/// Returns true if that folder can import/manage assets.
/// </summary>
/// <returns>True if can contain assets for project, otherwise false</returns>
public bool CanHaveAssets => _folder.CanHaveAssets;
/// <summary>
/// Gets the parent node.
/// </summary>
public ContentTreeNode ParentNode => Parent as ContentTreeNode;
/// <summary>
/// Gets the folder path.
/// </summary>
public string Path => _folder.Path;
/// <summary>
/// Gets the navigation button label.
/// </summary>
public virtual string NavButtonLabel => _folder.ShortName;
/// <summary>
/// Initializes a new instance of the <see cref="ContentTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="path">The folder path.</param>
public ContentTreeNode(ContentTreeNode parent, string path)
: this(parent, parent?.FolderType ?? ContentFolderType.Other, path)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent node.</param>
/// <param name="type">The folder type.</param>
/// <param name="path">The folder path.</param>
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;
}
/// <summary>
/// Shows the rename popup for the item.
/// </summary>
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); };
}
/// <summary>
/// Updates the query search filter.
/// </summary>
/// <param name="filterText">The filter text.</param>
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<Rectangle>(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;
}
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override DragDropEffect OnDragEnterHeader(DragData data)
{
if (_dragOverItems == null)
_dragOverItems = new DragItems(ValidateDragItem);
_dragOverItems.OnDragEnter(data);
return GetDragEffect(data);
}
/// <inheritdoc />
protected override DragDropEffect OnDragMoveHeader(DragData data)
{
return GetDragEffect(data);
}
/// <inheritdoc />
protected override void OnDragLeaveHeader()
{
_dragOverItems.OnDragLeave();
base.OnDragLeaveHeader();
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
protected override void DoDragDrop()
{
DoDragDrop(DragItems.GetDragData(_folder));
}
/// <inheritdoc />
protected override void OnLongPress()
{
Select();
StartRenaming();
}
/// <inheritdoc />
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);
}
}
}

View File

@@ -7,8 +7,8 @@ namespace FlaxEditor.Content
/// <summary>
/// Content tree node used for main directories.
/// </summary>
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
public class MainContentTreeNode : ContentTreeNode
/// <seealso cref="ContentFolderTreeNode" />
public class MainContentFolderTreeNode : ContentFolderTreeNode
{
private FileSystemWatcher _watcher;
@@ -19,12 +19,12 @@ namespace FlaxEditor.Content
public override bool CanDuplicate => false;
/// <summary>
/// Initializes a new instance of the <see cref="MainContentTreeNode"/> class.
/// Initializes a new instance of the <see cref="MainContentFolderTreeNode"/> class.
/// </summary>
/// <param name="parent">The parent project.</param>
/// <param name="type">The folder type.</param>
/// <param name="path">The folder path.</param>
public MainContentTreeNode(ProjectTreeNode parent, ContentFolderType type, string path)
public MainContentFolderTreeNode(ProjectFolderTreeNode parent, ContentFolderType type, string path)
: base(parent, type, path)
{
_watcher = new FileSystemWatcher(path)

View File

@@ -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
/// <summary>
/// Root tree node for the project workspace.
/// </summary>
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
public sealed class ProjectTreeNode : ContentTreeNode
/// <seealso cref="ContentFolderTreeNode" />
public sealed class ProjectFolderTreeNode : ContentFolderTreeNode
{
/// <summary>
/// The project/
@@ -18,18 +19,18 @@ namespace FlaxEditor.Content
/// <summary>
/// The project content directory.
/// </summary>
public MainContentTreeNode Content;
public MainContentFolderTreeNode Content;
/// <summary>
/// The project source code directory.
/// </summary>
public MainContentTreeNode Source;
public MainContentFolderTreeNode Source;
/// <summary>
/// Initializes a new instance of the <see cref="ProjectTreeNode"/> class.
/// Initializes a new instance of the <see cref="ProjectFolderTreeNode"/> class.
/// </summary>
/// <param name="project">The project.</param>
public ProjectTreeNode(ProjectInfo project)
public ProjectFolderTreeNode(ProjectInfo project)
: base(null, project.ProjectFolderPath)
{
Project = project;
@@ -48,9 +49,29 @@ namespace FlaxEditor.Content
/// <inheritdoc />
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);
}
}

View File

@@ -5,13 +5,13 @@ namespace FlaxEditor.Content
/// <summary>
/// Root tree node for the content workspace.
/// </summary>
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
public sealed class RootContentTreeNode : ContentTreeNode
/// <seealso cref="ContentFolderTreeNode" />
public sealed class RootContentFolderTreeNode : ContentFolderTreeNode
{
/// <summary>
/// Initializes a new instance of the <see cref="RootContentTreeNode"/> class.
/// Initializes a new instance of the <see cref="RootContentFolderTreeNode"/> class.
/// </summary>
public RootContentTreeNode()
public RootContentFolderTreeNode()
: base(null, string.Empty)
{
}

View File

@@ -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);
}
/// <inheritdoc />

View File

@@ -24,22 +24,22 @@ namespace FlaxEditor.Modules
private bool _rebuildInitFlag;
private int _itemsCreated;
private int _itemsDeleted;
private readonly HashSet<MainContentTreeNode> _dirtyNodes = new HashSet<MainContentTreeNode>();
private readonly HashSet<MainContentFolderTreeNode> _dirtyNodes = new HashSet<MainContentFolderTreeNode>();
/// <summary>
/// The project directory.
/// </summary>
public ProjectTreeNode Game { get; private set; }
public ProjectFolderTreeNode Game { get; private set; }
/// <summary>
/// The engine directory.
/// </summary>
public ProjectTreeNode Engine { get; private set; }
public ProjectFolderTreeNode Engine { get; private set; }
/// <summary>
/// The list of all projects workspace directories (including game, engine and plugins projects).
/// </summary>
public readonly List<ProjectTreeNode> Projects = new List<ProjectTreeNode>();
public readonly List<ProjectFolderTreeNode> Projects = new List<ProjectFolderTreeNode>();
/// <summary>
/// The list with all content items proxy objects. Use <see cref="AddProxy"/> and <see cref="RemoveProxy"/> to modify this or <see cref="Rebuild"/> to refresh database when adding new item proxy types.
@@ -116,7 +116,7 @@ namespace FlaxEditor.Modules
/// </summary>
/// <param name="project">The project.</param>
/// <returns>The project workspace or null if not loaded into database.</returns>
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)

View File

@@ -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);
}

View File

@@ -10,15 +10,41 @@ namespace FlaxEditor.Windows
{
public partial class ContentWindow
{
private static readonly List<ContentTreeNode> NavUpdateCache = new List<ContentTreeNode>(8);
private static readonly List<ContentFolderTreeNode> NavUpdateCache = new List<ContentFolderTreeNode>(8);
private void OnTreeSelectionChanged(List<TreeNode> from, List<TreeNode> 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.
/// </summary>
/// <param name="target">The target.</param>
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
/// </summary>
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
/// <summary>
/// Gets the selected tree node.
/// </summary>
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;
}
}
/// <summary>
/// Gets the current view folder.
/// </summary>
public ContentFolder CurrentViewFolder => SelectedNode?.Folder;
private TreeNode GetActiveTreeSelection(List<TreeNode> selection)
{
if (selection == null || selection.Count == 0)
return null;
return _showAllContentInTree && selection.Count > 1
? selection[^1]
: selection[0];
}
/// <summary>
/// Shows the root folder.
/// </summary>
@@ -236,5 +288,10 @@ namespace FlaxEditor.Windows
{
_tree.Select(_root);
}
private void SaveLastViewedFolder(ContentFolderTreeNode node)
{
Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, node?.Path ?? string.Empty);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<string> _expandedFolderPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private bool _renameInTree;
private RootContentTreeNode _root;
private RootContentFolderTreeNode _root;
private bool _navigationUnlocked;
private readonly Stack<ContentTreeNode> _navigationUndo = new Stack<ContentTreeNode>(32);
private readonly Stack<ContentTreeNode> _navigationRedo = new Stack<ContentTreeNode>(32);
private readonly Stack<ContentFolderTreeNode> _navigationUndo = new Stack<ContentFolderTreeNode>(32);
private readonly Stack<ContentFolderTreeNode> _navigationRedo = new Stack<ContentFolderTreeNode>(32);
private NewItem _newElement;
@@ -133,6 +143,9 @@ namespace FlaxEditor.Windows
}
}
internal bool IsTreeOnlyMode => _showAllContentInTree;
internal SortType CurrentSortType => _sortType;
/// <summary>
/// Initializes a new instance of the <see cref="ContentWindow"/> class.
/// </summary>
@@ -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;
}
}
/// <summary>
/// Opens the specified content item.
/// </summary>
@@ -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;
}
/// <summary>
/// Refreshes the current view items collection.
/// </summary>
public void RefreshView()
{
if (_view.IsSearching)
if (_showAllContentInTree)
RefreshTreeItems();
else if (_view.IsSearching)
UpdateItemsSearch();
else
RefreshView(SelectedNode);
return;
}
/// <summary>
/// Refreshes the view.
/// </summary>
/// <param name="target">The target location.</param>
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<ContentItem>(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);
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
@@ -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
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
UpdateNavigationBarBounds();
base.PerformLayoutBeforeChildren();
}
/// <inheritdoc />
protected override void PerformLayoutAfterChildren()
{
base.PerformLayoutAfterChildren();
UpdateNavigationBarBounds();
}
/// <inheritdoc />
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());
}
/// <inheritdoc />
@@ -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();
}
/// <inheritdoc />
@@ -1264,6 +1795,7 @@ namespace FlaxEditor.Windows
{
_split.SplitterValue = 0.2f;
_view.ViewScale = 1.0f;
_showAllContentInTree = false;
}
/// <inheritdoc />
@@ -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();
}