Merge branch 'Tryibion-add-content-tree-view'
This commit is contained in:
@@ -19,7 +19,7 @@ namespace FlaxEditor.Content.GUI
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the target node.
|
/// Gets the target node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContentTreeNode TargetNode { get; }
|
public ContentFolderTreeNode TargetNode { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ContentNavigationButton"/> class.
|
/// 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="x">The x position.</param>
|
||||||
/// <param name="y">The y position.</param>
|
/// <param name="y">The y position.</param>
|
||||||
/// <param name="height">The height.</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)
|
: base(x, y, height)
|
||||||
{
|
{
|
||||||
TargetNode = targetNode;
|
TargetNode = targetNode;
|
||||||
@@ -147,7 +147,7 @@ namespace FlaxEditor.Content.GUI
|
|||||||
ClearItems();
|
ClearItems();
|
||||||
foreach (var child in Target.TargetNode.Children)
|
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(...)
|
if (node.Folder.VisibleInHierarchy) // Respect the filter set by ContentFilterConfig.Filter(...)
|
||||||
AddItem(node.Folder.ShortName);
|
AddItem(node.Folder.ShortName);
|
||||||
@@ -180,7 +180,7 @@ namespace FlaxEditor.Content.GUI
|
|||||||
var item = _items[index];
|
var item = _items[index];
|
||||||
foreach (var child in Target.TargetNode.Children)
|
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);
|
Editor.Instance.Windows.ContentWin.Navigate(node);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace FlaxEditor.Content
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the content node.
|
/// Gets the content node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContentTreeNode Node { get; }
|
public ContentFolderTreeNode Node { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The subitems of this folder.
|
/// The subitems of this folder.
|
||||||
@@ -72,7 +72,7 @@ namespace FlaxEditor.Content
|
|||||||
/// <param name="type">The folder type.</param>
|
/// <param name="type">The folder type.</param>
|
||||||
/// <param name="path">The path to the item.</param>
|
/// <param name="path">The path to the item.</param>
|
||||||
/// <param name="node">The folder parent node.</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)
|
: base(path)
|
||||||
{
|
{
|
||||||
FolderType = type;
|
FolderType = type;
|
||||||
@@ -118,7 +118,7 @@ namespace FlaxEditor.Content
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
var hasParentFolder = ParentFolder != null;
|
var hasParentFolder = ParentFolder != null;
|
||||||
var isContentFolder = Node is MainContentTreeNode;
|
var isContentFolder = Node is MainContentFolderTreeNode;
|
||||||
return hasParentFolder && !isContentFolder;
|
return hasParentFolder && !isContentFolder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,433 @@
|
|||||||
|
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FlaxEditor.GUI;
|
||||||
|
using FlaxEditor.GUI.Drag;
|
||||||
|
using FlaxEditor.GUI.Tree;
|
||||||
|
using FlaxEditor.SceneGraph;
|
||||||
|
using FlaxEditor.Utilities;
|
||||||
|
using FlaxEngine;
|
||||||
|
using FlaxEngine.GUI;
|
||||||
|
|
||||||
|
namespace FlaxEditor.Content;
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
UpdateCustomArrowRect();
|
||||||
|
Editor.Instance?.Windows?.ContentWin?.TryAutoExpandContentNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the custom arrow rectangle so it stays aligned with the current layout.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateCustomArrowRect()
|
||||||
|
{
|
||||||
|
var contentWindow = Editor.Instance?.Windows?.ContentWin;
|
||||||
|
var scale = contentWindow != null && contentWindow.IsTreeOnlyMode ? contentWindow.View.ViewScale : 1.0f;
|
||||||
|
var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f);
|
||||||
|
var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
|
||||||
|
|
||||||
|
// Use the current text layout, not just cached values.
|
||||||
|
var textRect = TextRect;
|
||||||
|
var iconLeft = textRect.Left - iconSize - 2.0f;
|
||||||
|
var x = Mathf.Max(iconLeft - arrowSize - 2.0f, 0.0f);
|
||||||
|
var y = Mathf.Max((HeaderHeight - arrowSize) * 0.5f, 0.0f);
|
||||||
|
|
||||||
|
CustomArrowRect = new Rectangle(x, y, arrowSize, arrowSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void PerformLayout(bool force = false)
|
||||||
|
{
|
||||||
|
base.PerformLayout(force);
|
||||||
|
UpdateCustomArrowRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noFilter)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using FlaxEditor.GUI;
|
|
||||||
using FlaxEditor.GUI.Drag;
|
|
||||||
using FlaxEditor.GUI.Tree;
|
|
||||||
using FlaxEditor.Utilities;
|
|
||||||
using FlaxEngine;
|
|
||||||
using FlaxEngine.GUI;
|
|
||||||
|
|
||||||
namespace FlaxEditor.Content
|
|
||||||
{
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!noFilter)
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,8 @@ namespace FlaxEditor.Content
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Content tree node used for main directories.
|
/// Content tree node used for main directories.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
|
/// <seealso cref="ContentFolderTreeNode" />
|
||||||
public class MainContentTreeNode : ContentTreeNode
|
public class MainContentFolderTreeNode : ContentFolderTreeNode
|
||||||
{
|
{
|
||||||
private FileSystemWatcher _watcher;
|
private FileSystemWatcher _watcher;
|
||||||
|
|
||||||
@@ -19,12 +19,12 @@ namespace FlaxEditor.Content
|
|||||||
public override bool CanDuplicate => false;
|
public override bool CanDuplicate => false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MainContentTreeNode"/> class.
|
/// Initializes a new instance of the <see cref="MainContentFolderTreeNode"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parent">The parent project.</param>
|
/// <param name="parent">The parent project.</param>
|
||||||
/// <param name="type">The folder type.</param>
|
/// <param name="type">The folder type.</param>
|
||||||
/// <param name="path">The folder path.</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)
|
: base(parent, type, path)
|
||||||
{
|
{
|
||||||
_watcher = new FileSystemWatcher(path)
|
_watcher = new FileSystemWatcher(path)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||||
|
|
||||||
|
using System;
|
||||||
using FlaxEngine.GUI;
|
using FlaxEngine.GUI;
|
||||||
|
|
||||||
namespace FlaxEditor.Content
|
namespace FlaxEditor.Content
|
||||||
@@ -7,8 +8,8 @@ namespace FlaxEditor.Content
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root tree node for the project workspace.
|
/// Root tree node for the project workspace.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
|
/// <seealso cref="ContentFolderTreeNode" />
|
||||||
public sealed class ProjectTreeNode : ContentTreeNode
|
public sealed class ProjectFolderTreeNode : ContentFolderTreeNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The project/
|
/// The project/
|
||||||
@@ -18,18 +19,18 @@ namespace FlaxEditor.Content
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The project content directory.
|
/// The project content directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MainContentTreeNode Content;
|
public MainContentFolderTreeNode Content;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The project source code directory.
|
/// The project source code directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MainContentTreeNode Source;
|
public MainContentFolderTreeNode Source;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProjectTreeNode"/> class.
|
/// Initializes a new instance of the <see cref="ProjectFolderTreeNode"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="project">The project.</param>
|
/// <param name="project">The project.</param>
|
||||||
public ProjectTreeNode(ProjectInfo project)
|
public ProjectFolderTreeNode(ProjectInfo project)
|
||||||
: base(null, project.ProjectFolderPath)
|
: base(null, project.ProjectFolderPath)
|
||||||
{
|
{
|
||||||
Project = project;
|
Project = project;
|
||||||
@@ -48,9 +49,29 @@ namespace FlaxEditor.Content
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override int Compare(Control other)
|
public override int Compare(Control other)
|
||||||
{
|
{
|
||||||
// Move the main game project to the top
|
if (other is ProjectFolderTreeNode otherProject)
|
||||||
if (Project.Name == Editor.Instance.GameProject.Name)
|
{
|
||||||
return -1;
|
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);
|
return base.Compare(other);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ namespace FlaxEditor.Content
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root tree node for the content workspace.
|
/// Root tree node for the content workspace.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="FlaxEditor.Content.ContentTreeNode" />
|
/// <seealso cref="ContentFolderTreeNode" />
|
||||||
public sealed class RootContentTreeNode : ContentTreeNode
|
public sealed class RootContentFolderTreeNode : ContentFolderTreeNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RootContentTreeNode"/> class.
|
/// Initializes a new instance of the <see cref="RootContentFolderTreeNode"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RootContentTreeNode()
|
public RootContentFolderTreeNode()
|
||||||
: base(null, string.Empty)
|
: base(null, string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,9 +76,23 @@ namespace FlaxEditor.GUI
|
|||||||
toolstrip.IsLayoutLocked = toolstripLocked;
|
toolstrip.IsLayoutLocked = toolstripLocked;
|
||||||
toolstrip.ItemsMargin = toolstripMargin;
|
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;
|
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 />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -24,22 +24,22 @@ namespace FlaxEditor.Modules
|
|||||||
private bool _rebuildInitFlag;
|
private bool _rebuildInitFlag;
|
||||||
private int _itemsCreated;
|
private int _itemsCreated;
|
||||||
private int _itemsDeleted;
|
private int _itemsDeleted;
|
||||||
private readonly HashSet<MainContentTreeNode> _dirtyNodes = new HashSet<MainContentTreeNode>();
|
private readonly HashSet<MainContentFolderTreeNode> _dirtyNodes = new HashSet<MainContentFolderTreeNode>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The project directory.
|
/// The project directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProjectTreeNode Game { get; private set; }
|
public ProjectFolderTreeNode Game { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The engine directory.
|
/// The engine directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProjectTreeNode Engine { get; private set; }
|
public ProjectFolderTreeNode Engine { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of all projects workspace directories (including game, engine and plugins projects).
|
/// The list of all projects workspace directories (including game, engine and plugins projects).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly List<ProjectTreeNode> Projects = new List<ProjectTreeNode>();
|
public readonly List<ProjectFolderTreeNode> Projects = new List<ProjectFolderTreeNode>();
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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>
|
/// </summary>
|
||||||
/// <param name="project">The project.</param>
|
/// <param name="project">The project.</param>
|
||||||
/// <returns>The project workspace or null if not loaded into database.</returns>
|
/// <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);
|
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)
|
if (node == null)
|
||||||
return;
|
return;
|
||||||
@@ -953,7 +953,7 @@ namespace FlaxEditor.Modules
|
|||||||
if (childFolderNode == null)
|
if (childFolderNode == null)
|
||||||
{
|
{
|
||||||
// Create node
|
// Create node
|
||||||
ContentTreeNode n = new ContentTreeNode(node, childPath);
|
ContentFolderTreeNode n = new ContentFolderTreeNode(node, childPath);
|
||||||
if (!_isDuringFastSetup)
|
if (!_isDuringFastSetup)
|
||||||
sortChildren = true;
|
sortChildren = true;
|
||||||
|
|
||||||
@@ -978,7 +978,7 @@ namespace FlaxEditor.Modules
|
|||||||
node.SortChildren();
|
node.SortChildren();
|
||||||
|
|
||||||
// Ignore some special folders
|
// 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;
|
var mainNodeChild = mainNode.Folder.Find(StringUtils.CombinePaths(mainNode.Path, "obj")) as ContentFolder;
|
||||||
if (mainNodeChild != null)
|
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++)
|
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++)
|
for (int i = 0; i < files.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -1093,20 +1093,20 @@ namespace FlaxEditor.Modules
|
|||||||
var workspace = GetProjectWorkspace(project);
|
var workspace = GetProjectWorkspace(project);
|
||||||
if (workspace == null)
|
if (workspace == null)
|
||||||
{
|
{
|
||||||
workspace = new ProjectTreeNode(project);
|
workspace = new ProjectFolderTreeNode(project);
|
||||||
Projects.Add(workspace);
|
Projects.Add(workspace);
|
||||||
|
|
||||||
var contentFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Content");
|
var contentFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Content");
|
||||||
if (Directory.Exists(contentFolder))
|
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;
|
workspace.Content.Folder.ParentFolder = workspace.Folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Source");
|
var sourceFolder = StringUtils.CombinePaths(project.ProjectFolderPath, "Source");
|
||||||
if (Directory.Exists(sourceFolder))
|
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;
|
workspace.Source.Folder.ParentFolder = workspace.Folder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1213,16 +1213,16 @@ namespace FlaxEditor.Modules
|
|||||||
Proxy.Add(new GenericJsonAssetProxy());
|
Proxy.Add(new GenericJsonAssetProxy());
|
||||||
|
|
||||||
// Create content folders nodes
|
// 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)
|
if (Editor.GameProject != Editor.EngineProject)
|
||||||
{
|
{
|
||||||
Game = new ProjectTreeNode(Editor.GameProject)
|
Game = new ProjectFolderTreeNode(Editor.GameProject)
|
||||||
{
|
{
|
||||||
Content = new MainContentTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder),
|
Content = new MainContentFolderTreeNode(Game, ContentFolderType.Content, Globals.ProjectContentFolder),
|
||||||
Source = new MainContentTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder),
|
Source = new MainContentFolderTreeNode(Game, ContentFolderType.Source, Globals.ProjectSourceFolder),
|
||||||
};
|
};
|
||||||
// TODO: why it's required? the code above should work for linking the nodes hierarchy
|
// TODO: why it's required? the code above should work for linking the nodes hierarchy
|
||||||
Game.Content.Folder.ParentFolder = Game.Folder;
|
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
|
// Ensure to be ready for external events
|
||||||
if (_isDuringFastSetup)
|
if (_isDuringFastSetup)
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace FlaxEditor.Windows
|
|||||||
cm.AddSeparator();
|
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));
|
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(CurrentViewFolder.Path));
|
||||||
}
|
}
|
||||||
@@ -76,8 +76,8 @@ namespace FlaxEditor.Windows
|
|||||||
|
|
||||||
cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path)));
|
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", () =>
|
cm.AddButton("Show in Content Panel", () =>
|
||||||
{
|
{
|
||||||
Editor.Instance.Windows.ContentWin.ClearItemsSearch();
|
Editor.Instance.Windows.ContentWin.ClearItemsSearch();
|
||||||
@@ -129,7 +129,7 @@ namespace FlaxEditor.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFolder && folder.Node is MainContentTreeNode)
|
if (isFolder && folder.Node is MainContentFolderTreeNode)
|
||||||
{
|
{
|
||||||
cm.AddSeparator();
|
cm.AddSeparator();
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@ namespace FlaxEditor.Windows
|
|||||||
b = cm.AddButton("Paste", _view.Paste);
|
b = cm.AddButton("Paste", _view.Paste);
|
||||||
b.Enabled = _view.CanPaste();
|
b.Enabled = _view.CanPaste();
|
||||||
|
|
||||||
if (isFolder && folder.Node is MainContentTreeNode)
|
if (isFolder && folder.Node is MainContentFolderTreeNode)
|
||||||
{
|
{
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@ namespace FlaxEditor.Windows
|
|||||||
private void CreateNewModuleMenu(ContextMenu menu, ContentFolder folder, bool disableUncreatable = false)
|
private void CreateNewModuleMenu(ContextMenu menu, ContentFolder folder, bool disableUncreatable = false)
|
||||||
{
|
{
|
||||||
// Check if is source folder to add new module
|
// Check if is source folder to add new module
|
||||||
if (folder?.ParentFolder?.Node is ProjectTreeNode parentFolderNode && folder.Node == parentFolderNode.Source)
|
if (folder?.ParentFolder?.Node is ProjectFolderTreeNode parentFolderNode && folder.Node == parentFolderNode.Source)
|
||||||
{
|
{
|
||||||
var button = menu.AddButton("New module");
|
var button = menu.AddButton("New module");
|
||||||
button.CloseMenuOnClick = false;
|
button.CloseMenuOnClick = false;
|
||||||
@@ -216,7 +216,7 @@ namespace FlaxEditor.Windows
|
|||||||
|
|
||||||
private bool CanCreateFolder(ContentItem item = null)
|
private bool CanCreateFolder(ContentItem item = null)
|
||||||
{
|
{
|
||||||
bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectTreeNode);
|
bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode);
|
||||||
return canCreateFolder;
|
return canCreateFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,41 @@ namespace FlaxEditor.Windows
|
|||||||
{
|
{
|
||||||
public partial class ContentWindow
|
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)
|
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
|
// Navigate
|
||||||
var source = from.Count > 0 ? from[0] as ContentTreeNode : null;
|
var source = from.Count > 0 ? from[0] as ContentFolderTreeNode : null;
|
||||||
var target = to.Count > 0 ? to[0] as ContentTreeNode : 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);
|
Navigate(source, target);
|
||||||
|
|
||||||
|
SaveLastViewedFolder(target);
|
||||||
target?.Focus();
|
target?.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +52,12 @@ namespace FlaxEditor.Windows
|
|||||||
/// Navigates to the specified target content location.
|
/// Navigates to the specified target content location.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target">The target.</param>
|
/// <param name="target">The target.</param>
|
||||||
public void Navigate(ContentTreeNode target)
|
public void Navigate(ContentFolderTreeNode target)
|
||||||
{
|
{
|
||||||
Navigate(SelectedNode, target);
|
Navigate(SelectedNode, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Navigate(ContentTreeNode source, ContentTreeNode target)
|
private void Navigate(ContentFolderTreeNode source, ContentFolderTreeNode target)
|
||||||
{
|
{
|
||||||
if (target == null)
|
if (target == null)
|
||||||
target = _root;
|
target = _root;
|
||||||
@@ -50,7 +76,8 @@ namespace FlaxEditor.Windows
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show folder contents and select tree node
|
// Show folder contents and select tree node
|
||||||
RefreshView(target);
|
if (!_showAllContentInTree)
|
||||||
|
RefreshView(target);
|
||||||
_tree.Select(target);
|
_tree.Select(target);
|
||||||
target.ExpandAllParents();
|
target.ExpandAllParents();
|
||||||
|
|
||||||
@@ -62,7 +89,8 @@ namespace FlaxEditor.Windows
|
|||||||
//UndoList.SetSize(32);
|
//UndoList.SetSize(32);
|
||||||
|
|
||||||
// Update search
|
// Update search
|
||||||
UpdateItemsSearch();
|
if (!_showAllContentInTree)
|
||||||
|
UpdateItemsSearch();
|
||||||
|
|
||||||
// Unlock navigation
|
// Unlock navigation
|
||||||
_navigationUnlocked = true;
|
_navigationUnlocked = true;
|
||||||
@@ -81,7 +109,7 @@ namespace FlaxEditor.Windows
|
|||||||
if (_navigationUnlocked && _navigationUndo.Count > 0)
|
if (_navigationUnlocked && _navigationUndo.Count > 0)
|
||||||
{
|
{
|
||||||
// Pop node
|
// Pop node
|
||||||
ContentTreeNode node = _navigationUndo.Pop();
|
ContentFolderTreeNode node = _navigationUndo.Pop();
|
||||||
|
|
||||||
// Lock navigation
|
// Lock navigation
|
||||||
_navigationUnlocked = false;
|
_navigationUnlocked = false;
|
||||||
@@ -90,7 +118,8 @@ namespace FlaxEditor.Windows
|
|||||||
_navigationRedo.Push(SelectedNode);
|
_navigationRedo.Push(SelectedNode);
|
||||||
|
|
||||||
// Select node
|
// Select node
|
||||||
RefreshView(node);
|
if (!_showAllContentInTree)
|
||||||
|
RefreshView(node);
|
||||||
_tree.Select(node);
|
_tree.Select(node);
|
||||||
node.ExpandAllParents();
|
node.ExpandAllParents();
|
||||||
|
|
||||||
@@ -99,14 +128,16 @@ namespace FlaxEditor.Windows
|
|||||||
//UndoList.SetSize(32);
|
//UndoList.SetSize(32);
|
||||||
|
|
||||||
// Update search
|
// Update search
|
||||||
UpdateItemsSearch();
|
if (!_showAllContentInTree)
|
||||||
|
UpdateItemsSearch();
|
||||||
|
|
||||||
// Unlock navigation
|
// Unlock navigation
|
||||||
_navigationUnlocked = true;
|
_navigationUnlocked = true;
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
UpdateUI();
|
UpdateUI();
|
||||||
_view.SelectFirstItem();
|
if (!_showAllContentInTree)
|
||||||
|
_view.SelectFirstItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +150,7 @@ namespace FlaxEditor.Windows
|
|||||||
if (_navigationUnlocked && _navigationRedo.Count > 0)
|
if (_navigationUnlocked && _navigationRedo.Count > 0)
|
||||||
{
|
{
|
||||||
// Pop node
|
// Pop node
|
||||||
ContentTreeNode node = _navigationRedo.Pop();
|
ContentFolderTreeNode node = _navigationRedo.Pop();
|
||||||
|
|
||||||
// Lock navigation
|
// Lock navigation
|
||||||
_navigationUnlocked = false;
|
_navigationUnlocked = false;
|
||||||
@@ -128,7 +159,8 @@ namespace FlaxEditor.Windows
|
|||||||
_navigationUndo.Push(SelectedNode);
|
_navigationUndo.Push(SelectedNode);
|
||||||
|
|
||||||
// Select node
|
// Select node
|
||||||
RefreshView(node);
|
if (!_showAllContentInTree)
|
||||||
|
RefreshView(node);
|
||||||
_tree.Select(node);
|
_tree.Select(node);
|
||||||
node.ExpandAllParents();
|
node.ExpandAllParents();
|
||||||
|
|
||||||
@@ -137,14 +169,16 @@ namespace FlaxEditor.Windows
|
|||||||
//UndoList.SetSize(32);
|
//UndoList.SetSize(32);
|
||||||
|
|
||||||
// Update search
|
// Update search
|
||||||
UpdateItemsSearch();
|
if (!_showAllContentInTree)
|
||||||
|
UpdateItemsSearch();
|
||||||
|
|
||||||
// Unlock navigation
|
// Unlock navigation
|
||||||
_navigationUnlocked = true;
|
_navigationUnlocked = true;
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
UpdateUI();
|
UpdateUI();
|
||||||
_view.SelectFirstItem();
|
if (!_showAllContentInTree)
|
||||||
|
_view.SelectFirstItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +187,8 @@ namespace FlaxEditor.Windows
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void NavigateUp()
|
public void NavigateUp()
|
||||||
{
|
{
|
||||||
ContentTreeNode target = _root;
|
ContentFolderTreeNode target = _root;
|
||||||
ContentTreeNode current = SelectedNode;
|
ContentFolderTreeNode current = SelectedNode;
|
||||||
|
|
||||||
if (current?.Folder.ParentFolder != null)
|
if (current?.Folder.ParentFolder != null)
|
||||||
{
|
{
|
||||||
@@ -188,7 +222,7 @@ namespace FlaxEditor.Windows
|
|||||||
// Spawn buttons
|
// Spawn buttons
|
||||||
var nodes = NavUpdateCache;
|
var nodes = NavUpdateCache;
|
||||||
nodes.Clear();
|
nodes.Clear();
|
||||||
ContentTreeNode node = SelectedNode;
|
ContentFolderTreeNode node = SelectedNode;
|
||||||
while (node != null)
|
while (node != null)
|
||||||
{
|
{
|
||||||
nodes.Add(node);
|
nodes.Add(node);
|
||||||
@@ -222,13 +256,31 @@ namespace FlaxEditor.Windows
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the selected tree node.
|
/// Gets the selected tree node.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Gets the current view folder.
|
/// Gets the current view folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ContentFolder CurrentViewFolder => SelectedNode?.Folder;
|
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>
|
/// <summary>
|
||||||
/// Shows the root folder.
|
/// Shows the root folder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -236,5 +288,10 @@ namespace FlaxEditor.Windows
|
|||||||
{
|
{
|
||||||
_tree.Select(_root);
|
_tree.Select(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SaveLastViewedFolder(ContentFolderTreeNode node)
|
||||||
|
{
|
||||||
|
Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, node?.Path ?? string.Empty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,12 +115,14 @@ namespace FlaxEditor.Windows
|
|||||||
|
|
||||||
var root = _root;
|
var root = _root;
|
||||||
root.LockChildrenRecursive();
|
root.LockChildrenRecursive();
|
||||||
|
_suppressExpandedStateSave = true;
|
||||||
PerformLayout();
|
PerformLayout();
|
||||||
|
|
||||||
// Update tree
|
// Update tree
|
||||||
var query = _foldersSearchBox.Text;
|
var query = _foldersSearchBox.Text;
|
||||||
root.UpdateFilter(query);
|
root.UpdateFilter(query);
|
||||||
|
|
||||||
|
_suppressExpandedStateSave = false;
|
||||||
root.UnlockChildrenRecursive();
|
root.UnlockChildrenRecursive();
|
||||||
PerformLayout();
|
PerformLayout();
|
||||||
PerformLayout();
|
PerformLayout();
|
||||||
@@ -161,6 +163,11 @@ namespace FlaxEditor.Windows
|
|||||||
// Skip events during setup or init stuff
|
// Skip events during setup or init stuff
|
||||||
if (IsLayoutLocked)
|
if (IsLayoutLocked)
|
||||||
return;
|
return;
|
||||||
|
if (_showAllContentInTree)
|
||||||
|
{
|
||||||
|
RefreshTreeItems();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if clear filters
|
// Check if clear filters
|
||||||
if (_itemsSearchBox.TextLength == 0 && !_viewDropdown.HasSelection)
|
if (_itemsSearchBox.TextLength == 0 && !_viewDropdown.HasSelection)
|
||||||
@@ -200,7 +207,7 @@ namespace FlaxEditor.Windows
|
|||||||
// Special case for root folder
|
// Special case for root folder
|
||||||
for (int i = 0; i < _root.ChildrenCount; i++)
|
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);
|
UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +229,7 @@ namespace FlaxEditor.Windows
|
|||||||
// Special case for root folder
|
// Special case for root folder
|
||||||
for (int i = 0; i < _root.ChildrenCount; i++)
|
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);
|
UpdateItemsSearchFilter(node.Folder, items, filters, showAllFiles, query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,15 @@ namespace FlaxEditor.Windows
|
|||||||
public sealed partial class ContentWindow : EditorWindow
|
public sealed partial class ContentWindow : EditorWindow
|
||||||
{
|
{
|
||||||
private const string ProjectDataLastViewedFolder = "LastViewedFolder";
|
private const string ProjectDataLastViewedFolder = "LastViewedFolder";
|
||||||
|
private const string ProjectDataExpandedFolders = "ExpandedFolders";
|
||||||
private bool _isWorkspaceDirty;
|
private bool _isWorkspaceDirty;
|
||||||
private string _workspaceRebuildLocation;
|
private string _workspaceRebuildLocation;
|
||||||
private string _lastViewedFolderBeforeReload;
|
private string _lastViewedFolderBeforeReload;
|
||||||
private SplitPanel _split;
|
private SplitPanel _split;
|
||||||
|
private Panel _treeOnlyPanel;
|
||||||
|
private ContainerControl _treePanelRoot;
|
||||||
|
private ContainerControl _treeHeaderPanel;
|
||||||
|
private Panel _contentItemsSearchPanel;
|
||||||
private Panel _contentViewPanel;
|
private Panel _contentViewPanel;
|
||||||
private Panel _contentTreePanel;
|
private Panel _contentTreePanel;
|
||||||
private ContentView _view;
|
private ContentView _view;
|
||||||
@@ -43,18 +48,23 @@ namespace FlaxEditor.Windows
|
|||||||
private readonly ToolStripButton _navigateUpButton;
|
private readonly ToolStripButton _navigateUpButton;
|
||||||
|
|
||||||
private NavigationBar _navigationBar;
|
private NavigationBar _navigationBar;
|
||||||
|
private Panel _viewDropdownPanel;
|
||||||
private Tree _tree;
|
private Tree _tree;
|
||||||
private TextBox _foldersSearchBox;
|
private TextBox _foldersSearchBox;
|
||||||
private TextBox _itemsSearchBox;
|
private TextBox _itemsSearchBox;
|
||||||
private ViewDropdown _viewDropdown;
|
private ViewDropdown _viewDropdown;
|
||||||
private SortType _sortType;
|
private SortType _sortType;
|
||||||
private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true, _showGeneratedFiles = false;
|
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 bool _navigationUnlocked;
|
||||||
private readonly Stack<ContentTreeNode> _navigationUndo = new Stack<ContentTreeNode>(32);
|
private readonly Stack<ContentFolderTreeNode> _navigationUndo = new Stack<ContentFolderTreeNode>(32);
|
||||||
private readonly Stack<ContentTreeNode> _navigationRedo = new Stack<ContentTreeNode>(32);
|
private readonly Stack<ContentFolderTreeNode> _navigationRedo = new Stack<ContentFolderTreeNode>(32);
|
||||||
|
|
||||||
private NewItem _newElement;
|
private NewItem _newElement;
|
||||||
|
|
||||||
@@ -134,6 +144,9 @@ namespace FlaxEditor.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool IsTreeOnlyMode => _showAllContentInTree;
|
||||||
|
internal SortType CurrentSortType => _sortType;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ContentWindow"/> class.
|
/// Initializes a new instance of the <see cref="ContentWindow"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -163,14 +176,6 @@ namespace FlaxEditor.Windows
|
|||||||
_navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up.");
|
_navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up.");
|
||||||
_toolStrip.AddSeparator();
|
_toolStrip.AddSeparator();
|
||||||
|
|
||||||
// Navigation bar
|
|
||||||
_navigationBar = new NavigationBar
|
|
||||||
{
|
|
||||||
Parent = _toolStrip,
|
|
||||||
ScrollbarTrackColor = style.Background,
|
|
||||||
ScrollbarThumbColor = style.ForegroundGrey,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Split panel
|
// Split panel
|
||||||
_split = new SplitPanel(options.Options.Interface.ContentWindowOrientation, ScrollBars.None, ScrollBars.None)
|
_split = new SplitPanel(options.Options.Interface.ContentWindowOrientation, ScrollBars.None, ScrollBars.None)
|
||||||
{
|
{
|
||||||
@@ -180,19 +185,38 @@ namespace FlaxEditor.Windows
|
|||||||
Parent = this,
|
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
|
// Content structure tree searching query input box
|
||||||
var headerPanel = new ContainerControl
|
_treeHeaderPanel = new ContainerControl
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
||||||
BackgroundColor = style.Background,
|
BackgroundColor = style.Background,
|
||||||
IsScrollable = false,
|
IsScrollable = false,
|
||||||
Offsets = new Margin(0, 0, 0, 18 + 6),
|
Offsets = new Margin(0, 0, 0, 18 + 6),
|
||||||
|
Parent = _treePanelRoot,
|
||||||
};
|
};
|
||||||
|
|
||||||
_foldersSearchBox = new SearchBox
|
_foldersSearchBox = new SearchBox
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
|
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
|
||||||
Parent = headerPanel,
|
Parent = _treeHeaderPanel,
|
||||||
Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18),
|
Bounds = new Rectangle(4, 4, _treeHeaderPanel.Width - 8, 18),
|
||||||
};
|
};
|
||||||
_foldersSearchBox.TextChanged += OnFoldersSearchBoxTextChanged;
|
_foldersSearchBox.TextChanged += OnFoldersSearchBoxTextChanged;
|
||||||
|
|
||||||
@@ -200,55 +224,74 @@ namespace FlaxEditor.Windows
|
|||||||
_contentTreePanel = new Panel
|
_contentTreePanel = new Panel
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.StretchAll,
|
AnchorPreset = AnchorPresets.StretchAll,
|
||||||
Offsets = new Margin(0, 0, headerPanel.Bottom, 0),
|
Offsets = new Margin(0, 0, _treeHeaderPanel.Bottom, 0),
|
||||||
IsScrollable = true,
|
IsScrollable = true,
|
||||||
ScrollBars = ScrollBars.Both,
|
ScrollBars = ScrollBars.Both,
|
||||||
Parent = _split.Panel1,
|
Parent = _treePanelRoot,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Content structure tree
|
// Content structure tree
|
||||||
_tree = new Tree(false)
|
_tree = new Tree(true)
|
||||||
{
|
{
|
||||||
DrawRootTreeLine = false,
|
DrawRootTreeLine = false,
|
||||||
Parent = _contentTreePanel,
|
Parent = _contentTreePanel,
|
||||||
};
|
};
|
||||||
_tree.SelectedChanged += OnTreeSelectionChanged;
|
_tree.SelectedChanged += OnTreeSelectionChanged;
|
||||||
headerPanel.Parent = _split.Panel1;
|
|
||||||
|
|
||||||
// Content items searching query input box and filters selector
|
// Content items searching query input box and filters selector
|
||||||
var contentItemsSearchPanel = new ContainerControl
|
_contentItemsSearchPanel = new Panel
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
||||||
IsScrollable = true,
|
IsScrollable = true,
|
||||||
Offsets = new Margin(0, 0, 0, 18 + 8),
|
Offsets = new Margin(0, 0, 0, 18 + 8),
|
||||||
Parent = _split.Panel2,
|
Parent = _split.Panel2,
|
||||||
};
|
};
|
||||||
const float viewDropdownWidth = 50.0f;
|
|
||||||
_itemsSearchBox = new SearchBox
|
_itemsSearchBox = new SearchBox
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
|
AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
|
||||||
Parent = contentItemsSearchPanel,
|
Parent = _contentItemsSearchPanel,
|
||||||
Bounds = new Rectangle(viewDropdownWidth + 8, 4, contentItemsSearchPanel.Width - 12 - viewDropdownWidth, 18),
|
Bounds = new Rectangle(4, 4, _contentItemsSearchPanel.Width - 8, 18),
|
||||||
};
|
};
|
||||||
_itemsSearchBox.TextChanged += UpdateItemsSearch;
|
_itemsSearchBox.TextChanged += UpdateItemsSearch;
|
||||||
|
|
||||||
|
_viewDropdownPanel = new Panel
|
||||||
|
{
|
||||||
|
Width = 50.0f,
|
||||||
|
Parent = this,
|
||||||
|
AnchorPreset = AnchorPresets.TopLeft,
|
||||||
|
BackgroundColor = Color.Transparent,
|
||||||
|
};
|
||||||
|
|
||||||
_viewDropdown = new ViewDropdown
|
_viewDropdown = new ViewDropdown
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.MiddleLeft,
|
|
||||||
SupportMultiSelect = true,
|
SupportMultiSelect = true,
|
||||||
TooltipText = "Change content view and filter options",
|
TooltipText = "Change content view and filter options",
|
||||||
Parent = contentItemsSearchPanel,
|
Offsets = Margin.Zero,
|
||||||
Offsets = new Margin(4, viewDropdownWidth, -9, 18),
|
Width = 46.0f,
|
||||||
|
Height = 18.0f,
|
||||||
|
Parent = _viewDropdownPanel,
|
||||||
};
|
};
|
||||||
|
_viewDropdown.LocalX += 2.0f;
|
||||||
|
_viewDropdown.LocalY += _toolStrip.ItemsHeight * 0.5f - 9.0f;
|
||||||
_viewDropdown.SelectedIndexChanged += e => UpdateItemsSearch();
|
_viewDropdown.SelectedIndexChanged += e => UpdateItemsSearch();
|
||||||
for (int i = 0; i <= (int)ContentItemSearchFilter.Other; i++)
|
for (int i = 0; i <= (int)ContentItemSearchFilter.Other; i++)
|
||||||
_viewDropdown.Items.Add(((ContentItemSearchFilter)i).ToString());
|
_viewDropdown.Items.Add(((ContentItemSearchFilter)i).ToString());
|
||||||
_viewDropdown.PopupCreate += OnViewDropdownPopupCreate;
|
_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
|
// Content view panel
|
||||||
_contentViewPanel = new Panel
|
_contentViewPanel = new Panel
|
||||||
{
|
{
|
||||||
AnchorPreset = AnchorPresets.StretchAll,
|
AnchorPreset = AnchorPresets.StretchAll,
|
||||||
Offsets = new Margin(0, 0, contentItemsSearchPanel.Bottom + 4, 0),
|
Offsets = new Margin(0, 0, _contentItemsSearchPanel.Bottom + 4, 0),
|
||||||
IsScrollable = true,
|
IsScrollable = true,
|
||||||
ScrollBars = ScrollBars.Vertical,
|
ScrollBars = ScrollBars.Vertical,
|
||||||
Parent = _split.Panel2,
|
Parent = _split.Panel2,
|
||||||
@@ -268,9 +311,14 @@ namespace FlaxEditor.Windows
|
|||||||
_view.OnDelete += Delete;
|
_view.OnDelete += Delete;
|
||||||
_view.OnDuplicate += Duplicate;
|
_view.OnDuplicate += Duplicate;
|
||||||
_view.OnPaste += Paste;
|
_view.OnPaste += Paste;
|
||||||
|
_view.ViewScaleChanged += ApplyTreeViewScale;
|
||||||
|
|
||||||
_view.InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
|
_view.InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
|
||||||
InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
|
InputActions.Add(options => options.Search, () => _itemsSearchBox.Focus());
|
||||||
|
|
||||||
|
LoadExpandedFolders();
|
||||||
|
UpdateViewDropdownBounds();
|
||||||
|
ApplyTreeViewScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCreateNewItemButtonClicked()
|
private void OnCreateNewItemButtonClicked()
|
||||||
@@ -325,6 +373,7 @@ namespace FlaxEditor.Windows
|
|||||||
var viewType = menu.AddChildMenu("View Type");
|
var viewType = menu.AddChildMenu("View Type");
|
||||||
viewType.ContextMenu.AddButton("Tiles", OnViewTypeButtonClicked).Tag = ContentViewType.Tiles;
|
viewType.ContextMenu.AddButton("Tiles", OnViewTypeButtonClicked).Tag = ContentViewType.Tiles;
|
||||||
viewType.ContextMenu.AddButton("List", OnViewTypeButtonClicked).Tag = ContentViewType.List;
|
viewType.ContextMenu.AddButton("List", OnViewTypeButtonClicked).Tag = ContentViewType.List;
|
||||||
|
viewType.ContextMenu.AddButton("Tree View", OnViewTypeButtonClicked).Tag = "Tree";
|
||||||
viewType.ContextMenu.VisibleChanged += control =>
|
viewType.ContextMenu.VisibleChanged += control =>
|
||||||
{
|
{
|
||||||
if (!control.Visible)
|
if (!control.Visible)
|
||||||
@@ -332,13 +381,23 @@ namespace FlaxEditor.Windows
|
|||||||
foreach (var item in ((ContextMenu)control).Items)
|
foreach (var item in ((ContextMenu)control).Items)
|
||||||
{
|
{
|
||||||
if (item is ContextMenuButton button)
|
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 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.TooltipText = "Shows all files with extensions";
|
||||||
b.Checked = View.ShowFileExtensions;
|
b.Checked = View.ShowFileExtensions;
|
||||||
b.CloseMenuOnClick = false;
|
b.CloseMenuOnClick = false;
|
||||||
@@ -419,9 +478,63 @@ namespace FlaxEditor.Windows
|
|||||||
RefreshView();
|
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)
|
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)
|
private void OnFilterClicked(ContextMenuButton filterButton)
|
||||||
@@ -481,15 +594,58 @@ namespace FlaxEditor.Windows
|
|||||||
// Show element in the view
|
// Show element in the view
|
||||||
Select(item, true);
|
Select(item, true);
|
||||||
|
|
||||||
// Disable scrolling in content view
|
// Disable scrolling in proper view
|
||||||
if (_contentViewPanel.VScrollBar != null)
|
_renameInTree = _showAllContentInTree;
|
||||||
_contentViewPanel.VScrollBar.ThumbEnabled = false;
|
if (_renameInTree)
|
||||||
if (_contentViewPanel.HScrollBar != null)
|
{
|
||||||
_contentViewPanel.HScrollBar.ThumbEnabled = false;
|
if (_contentTreePanel.VScrollBar != null)
|
||||||
ScrollingOnContentView(false);
|
_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
|
// 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.Tag = item;
|
||||||
popup.Validate += OnRenameValidate;
|
popup.Validate += OnRenameValidate;
|
||||||
popup.Renamed += renamePopup => Rename((ContentItem)renamePopup.Tag, renamePopup.Text);
|
popup.Renamed += renamePopup => Rename((ContentItem)renamePopup.Tag, renamePopup.Text);
|
||||||
@@ -509,12 +665,24 @@ namespace FlaxEditor.Windows
|
|||||||
|
|
||||||
private void OnRenameClosed(RenamePopup popup)
|
private void OnRenameClosed(RenamePopup popup)
|
||||||
{
|
{
|
||||||
// Restore scrolling in content view
|
// Restore scrolling in proper view
|
||||||
if (_contentViewPanel.VScrollBar != null)
|
if (_renameInTree)
|
||||||
_contentViewPanel.VScrollBar.ThumbEnabled = true;
|
{
|
||||||
if (_contentViewPanel.HScrollBar != null)
|
if (_contentTreePanel.VScrollBar != null)
|
||||||
_contentViewPanel.HScrollBar.ThumbEnabled = true;
|
_contentTreePanel.VScrollBar.ThumbEnabled = true;
|
||||||
ScrollingOnContentView(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
|
// Check if was creating new element
|
||||||
if (_newElement != null)
|
if (_newElement != null)
|
||||||
@@ -927,6 +1095,16 @@ namespace FlaxEditor.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnContentDatabaseItemAdded(ContentItem contentItem)
|
||||||
|
{
|
||||||
|
if (contentItem is ContentFolder folder && _expandedFolderPaths.Contains(StringUtils.NormalizePath(folder.Path)))
|
||||||
|
{
|
||||||
|
_suppressExpandedStateSave = true;
|
||||||
|
folder.Node?.Expand(true);
|
||||||
|
_suppressExpandedStateSave = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens the specified content item.
|
/// Opens the specified content item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -943,7 +1121,8 @@ namespace FlaxEditor.Windows
|
|||||||
var folder = (ContentFolder)item;
|
var folder = (ContentFolder)item;
|
||||||
folder.Node.Expand();
|
folder.Node.Expand();
|
||||||
_tree.Select(folder.Node);
|
_tree.Select(folder.Node);
|
||||||
_view.SelectFirstItem();
|
if (!_showAllContentInTree)
|
||||||
|
_view.SelectFirstItem();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,6 +1163,36 @@ namespace FlaxEditor.Windows
|
|||||||
// Ensure that window is visible
|
// Ensure that window is visible
|
||||||
FocusOrShow();
|
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 to the parent directory
|
||||||
Navigate(parent.Node);
|
Navigate(parent.Node);
|
||||||
|
|
||||||
@@ -995,23 +1204,45 @@ namespace FlaxEditor.Windows
|
|||||||
_view.Focus();
|
_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>
|
/// <summary>
|
||||||
/// Refreshes the current view items collection.
|
/// Refreshes the current view items collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RefreshView()
|
public void RefreshView()
|
||||||
{
|
{
|
||||||
if (_view.IsSearching)
|
if (_showAllContentInTree)
|
||||||
|
RefreshTreeItems();
|
||||||
|
else if (_view.IsSearching)
|
||||||
UpdateItemsSearch();
|
UpdateItemsSearch();
|
||||||
else
|
else
|
||||||
RefreshView(SelectedNode);
|
RefreshView(SelectedNode);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refreshes the view.
|
/// Refreshes the view.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target">The target location.</param>
|
/// <param name="target">The target location.</param>
|
||||||
public void RefreshView(ContentTreeNode target)
|
public void RefreshView(ContentFolderTreeNode target)
|
||||||
{
|
{
|
||||||
|
if (_showAllContentInTree)
|
||||||
|
{
|
||||||
|
RefreshTreeItems();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_view.IsSearching = false;
|
_view.IsSearching = false;
|
||||||
if (target == _root)
|
if (target == _root)
|
||||||
{
|
{
|
||||||
@@ -1019,7 +1250,7 @@ namespace FlaxEditor.Windows
|
|||||||
var items = new List<ContentItem>(8);
|
var items = new List<ContentItem>(8);
|
||||||
for (int i = 0; i < _root.ChildrenCount; i++)
|
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);
|
items.Add(node.Folder);
|
||||||
}
|
}
|
||||||
@@ -1038,12 +1269,263 @@ namespace FlaxEditor.Windows
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshTreeItems()
|
||||||
|
{
|
||||||
|
if (!_showAllContentInTree || _root == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_root.LockChildrenRecursive();
|
||||||
|
RemoveTreeAssetNodes(_root);
|
||||||
|
AddTreeAssetNodes(_root);
|
||||||
|
_root.UnlockChildrenRecursive();
|
||||||
|
_tree.PerformLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTreeItemNames(ContentFolderTreeNode node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < node.ChildrenCount; i++)
|
||||||
|
{
|
||||||
|
if (node.GetChild(i) is ContentFolderTreeNode childFolder)
|
||||||
|
{
|
||||||
|
UpdateTreeItemNames(childFolder);
|
||||||
|
}
|
||||||
|
else if (node.GetChild(i) is ContentItemTreeNode itemNode)
|
||||||
|
{
|
||||||
|
itemNode.UpdateDisplayedName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OnContentTreeNodeExpandedChanged(ContentFolderTreeNode node, bool isExpanded)
|
||||||
|
{
|
||||||
|
if (_suppressExpandedStateSave || node == null || node == _root)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var path = node.Path;
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
return;
|
||||||
|
path = StringUtils.NormalizePath(path);
|
||||||
|
|
||||||
|
if (isExpanded)
|
||||||
|
_expandedFolderPaths.Add(path);
|
||||||
|
else
|
||||||
|
// Remove all sub paths if parent folder is closed.
|
||||||
|
_expandedFolderPaths.RemoveWhere(x => x.Contains(path));
|
||||||
|
|
||||||
|
SaveExpandedFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TryAutoExpandContentNode(ContentFolderTreeNode node)
|
||||||
|
{
|
||||||
|
if (node == null || node == _root)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var path = node.Path;
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
return;
|
||||||
|
path = StringUtils.NormalizePath(path);
|
||||||
|
|
||||||
|
if (!_expandedFolderPaths.Contains(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_suppressExpandedStateSave = true;
|
||||||
|
node.Expand(true);
|
||||||
|
_suppressExpandedStateSave = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadExpandedFolders()
|
||||||
|
{
|
||||||
|
_expandedFolderPaths.Clear();
|
||||||
|
if (Editor.ProjectCache.TryGetCustomData(ProjectDataExpandedFolders, out string data) && !string.IsNullOrWhiteSpace(data))
|
||||||
|
{
|
||||||
|
var entries = data.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
for (int i = 0; i < entries.Length; i++)
|
||||||
|
{
|
||||||
|
var path = entries[i].Trim();
|
||||||
|
if (path.Length == 0)
|
||||||
|
continue;
|
||||||
|
_expandedFolderPaths.Add(StringUtils.NormalizePath(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveExpandedFolders()
|
||||||
|
{
|
||||||
|
if (_expandedFolderPaths.Count == 0)
|
||||||
|
{
|
||||||
|
Editor.ProjectCache.RemoveCustomData(ProjectDataExpandedFolders);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = string.Join("\n", _expandedFolderPaths);
|
||||||
|
Editor.ProjectCache.SetCustomData(ProjectDataExpandedFolders, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyExpandedFolders()
|
||||||
|
{
|
||||||
|
if (_root == null || _expandedFolderPaths.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_suppressExpandedStateSave = true;
|
||||||
|
foreach (var path in _expandedFolderPaths)
|
||||||
|
{
|
||||||
|
if (Editor.ContentDatabase.Find(path) is ContentFolder folder)
|
||||||
|
{
|
||||||
|
folder.Node.ExpandAllParents(true);
|
||||||
|
folder.Node.Expand(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_suppressExpandedStateSave = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveTreeAssetNodes(ContentFolderTreeNode node)
|
||||||
|
{
|
||||||
|
for (int i = node.ChildrenCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (node.GetChild(i) is ContentItemTreeNode itemNode)
|
||||||
|
{
|
||||||
|
node.RemoveChild(itemNode);
|
||||||
|
itemNode.Dispose();
|
||||||
|
}
|
||||||
|
else if (node.GetChild(i) is ContentFolderTreeNode childFolder)
|
||||||
|
{
|
||||||
|
RemoveTreeAssetNodes(childFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTreeAssetNodes(ContentFolderTreeNode node)
|
||||||
|
{
|
||||||
|
if (node.Folder != null)
|
||||||
|
{
|
||||||
|
var children = node.Folder.Children;
|
||||||
|
for (int i = 0; i < children.Count; i++)
|
||||||
|
{
|
||||||
|
var child = children[i];
|
||||||
|
if (child is ContentFolder)
|
||||||
|
continue;
|
||||||
|
if (!ShouldShowTreeItem(child))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var itemNode = new ContentItemTreeNode(child)
|
||||||
|
{
|
||||||
|
Parent = node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < node.ChildrenCount; i++)
|
||||||
|
{
|
||||||
|
if (node.GetChild(i) is ContentFolderTreeNode childFolder)
|
||||||
|
{
|
||||||
|
AddTreeAssetNodes(childFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.SortChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldShowTreeItem(ContentItem item)
|
||||||
|
{
|
||||||
|
if (item == null || !item.Visible)
|
||||||
|
return false;
|
||||||
|
if (_viewDropdown != null && _viewDropdown.HasSelection)
|
||||||
|
{
|
||||||
|
var filterIndex = (int)item.SearchFilter;
|
||||||
|
if (!_viewDropdown.Selection.Contains(filterIndex))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!_showAllFiles && item is FileItem)
|
||||||
|
return false;
|
||||||
|
if (!_showGeneratedFiles && IsGeneratedFile(item.Path))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGeneratedFile(string path)
|
||||||
|
{
|
||||||
|
return path.EndsWith(".Gen.cs", StringComparison.Ordinal) ||
|
||||||
|
path.EndsWith(".Gen.h", StringComparison.Ordinal) ||
|
||||||
|
path.EndsWith(".Gen.cpp", StringComparison.Ordinal) ||
|
||||||
|
path.EndsWith(".csproj", StringComparison.Ordinal) ||
|
||||||
|
path.Contains(".CSharp");
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateUI()
|
private void UpdateUI()
|
||||||
{
|
{
|
||||||
UpdateToolstrip();
|
UpdateToolstrip();
|
||||||
UpdateNavigationBar();
|
UpdateNavigationBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyTreeViewScale()
|
||||||
|
{
|
||||||
|
if (_tree == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var scale = _showAllContentInTree ? View.ViewScale : 1.0f;
|
||||||
|
var headerHeight = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
|
||||||
|
var style = Style.Current;
|
||||||
|
var fontSize = Mathf.Clamp(style.FontSmall.Size * scale, 8.0f, 28.0f);
|
||||||
|
var fontRef = new FontReference(style.FontSmall.Asset, fontSize);
|
||||||
|
var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
|
||||||
|
var textMarginLeft = 2.0f + Mathf.Max(0.0f, iconSize - 16.0f);
|
||||||
|
ApplyTreeNodeScale(_root, headerHeight, fontRef, textMarginLeft);
|
||||||
|
_root?.PerformLayout(true);
|
||||||
|
_tree.PerformLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyTreeNodeScale(ContentFolderTreeNode node, float headerHeight, FontReference fontRef, float textMarginLeft)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var margin = node.TextMargin;
|
||||||
|
margin.Left = textMarginLeft;
|
||||||
|
margin.Top = 2.0f;
|
||||||
|
margin.Right = 2.0f;
|
||||||
|
margin.Bottom = 2.0f;
|
||||||
|
node.TextMargin = margin;
|
||||||
|
node.CustomArrowRect = GetTreeArrowRect(node, headerHeight);
|
||||||
|
node.HeaderHeight = headerHeight;
|
||||||
|
node.TextFont = fontRef;
|
||||||
|
for (int i = 0; i < node.ChildrenCount; i++)
|
||||||
|
{
|
||||||
|
if (node.GetChild(i) is ContentFolderTreeNode child)
|
||||||
|
ApplyTreeNodeScale(child, headerHeight, fontRef, textMarginLeft);
|
||||||
|
else if (node.GetChild(i) is ContentItemTreeNode itemNode)
|
||||||
|
{
|
||||||
|
var itemMargin = itemNode.TextMargin;
|
||||||
|
itemMargin.Left = textMarginLeft;
|
||||||
|
itemMargin.Top = 2.0f;
|
||||||
|
itemMargin.Right = 2.0f;
|
||||||
|
itemMargin.Bottom = 2.0f;
|
||||||
|
itemNode.TextMargin = itemMargin;
|
||||||
|
itemNode.HeaderHeight = headerHeight;
|
||||||
|
itemNode.TextFont = fontRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rectangle GetTreeArrowRect(ContentFolderTreeNode node, float headerHeight)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
return Rectangle.Empty;
|
||||||
|
|
||||||
|
var scale = Editor.Instance?.Windows?.ContentWin?.IsTreeOnlyMode == true
|
||||||
|
? Editor.Instance.Windows.ContentWin.View.ViewScale
|
||||||
|
: 1.0f;
|
||||||
|
var arrowSize = Mathf.Clamp(12.0f * scale, 10.0f, 20.0f);
|
||||||
|
var iconSize = Mathf.Clamp(16.0f * scale, 12.0f, 28.0f);
|
||||||
|
var textRect = node.TextRect;
|
||||||
|
var iconLeft = textRect.Left - iconSize - 2.0f;
|
||||||
|
var x = iconLeft - arrowSize - 2.0f;
|
||||||
|
var y = (headerHeight - arrowSize) * 0.5f;
|
||||||
|
return new Rectangle(Mathf.Max(x, 0.0f), Mathf.Max(y, 0.0f), arrowSize, arrowSize);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateToolstrip()
|
private void UpdateToolstrip()
|
||||||
{
|
{
|
||||||
if (_toolStrip == null)
|
if (_toolStrip == null)
|
||||||
@@ -1063,20 +1545,42 @@ namespace FlaxEditor.Windows
|
|||||||
{
|
{
|
||||||
var bottomPrev = _toolStrip.Bottom;
|
var bottomPrev = _toolStrip.Bottom;
|
||||||
_navigationBar.UpdateBounds(_toolStrip);
|
_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)
|
if (bottomPrev != _toolStrip.Bottom)
|
||||||
{
|
{
|
||||||
// Navigation bar changed toolstrip height
|
// Navigation bar changed toolstrip height
|
||||||
_split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
|
_split.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
|
||||||
|
if (_treeOnlyPanel != null)
|
||||||
|
_treeOnlyPanel.Offsets = new Margin(0, 0, _toolStrip.Bottom, 0);
|
||||||
PerformLayout();
|
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 />
|
/// <inheritdoc />
|
||||||
public override void OnInit()
|
public override void OnInit()
|
||||||
{
|
{
|
||||||
// Content database events
|
// Content database events
|
||||||
Editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true;
|
Editor.ContentDatabase.WorkspaceModified += () => _isWorkspaceDirty = true;
|
||||||
|
Editor.ContentDatabase.ItemAdded += OnContentDatabaseItemAdded;
|
||||||
Editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved;
|
Editor.ContentDatabase.ItemRemoved += OnContentDatabaseItemRemoved;
|
||||||
Editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; };
|
Editor.ContentDatabase.WorkspaceRebuilding += () => { _workspaceRebuildLocation = SelectedNode?.Path; };
|
||||||
Editor.ContentDatabase.WorkspaceRebuilt += () =>
|
Editor.ContentDatabase.WorkspaceRebuilt += () =>
|
||||||
@@ -1095,6 +1599,7 @@ namespace FlaxEditor.Windows
|
|||||||
ShowRoot();
|
ShowRoot();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LoadExpandedFolders();
|
||||||
Refresh();
|
Refresh();
|
||||||
|
|
||||||
// Load last viewed folder
|
// Load last viewed folder
|
||||||
@@ -1110,7 +1615,7 @@ namespace FlaxEditor.Windows
|
|||||||
|
|
||||||
private void OnScriptsReloadBegin()
|
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;
|
_lastViewedFolderBeforeReload = lastViewedFolder?.Path ?? string.Empty;
|
||||||
|
|
||||||
_tree.RemoveChild(_root);
|
_tree.RemoveChild(_root);
|
||||||
@@ -1133,7 +1638,7 @@ namespace FlaxEditor.Windows
|
|||||||
private void Refresh()
|
private void Refresh()
|
||||||
{
|
{
|
||||||
// Setup content root node
|
// Setup content root node
|
||||||
_root = new RootContentTreeNode
|
_root = new RootContentFolderTreeNode
|
||||||
{
|
{
|
||||||
ChildrenIndent = 0
|
ChildrenIndent = 0
|
||||||
};
|
};
|
||||||
@@ -1156,7 +1661,7 @@ namespace FlaxEditor.Windows
|
|||||||
_root.AddChild(Editor.ContentDatabase.Engine);
|
_root.AddChild(Editor.ContentDatabase.Engine);
|
||||||
|
|
||||||
Editor.ContentDatabase.Game?.Expand(true);
|
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);
|
_tree.AddChild(_root);
|
||||||
|
|
||||||
// Setup navigation
|
// Setup navigation
|
||||||
@@ -1167,6 +1672,8 @@ namespace FlaxEditor.Windows
|
|||||||
// Update UI layout
|
// Update UI layout
|
||||||
_isLayoutLocked = false;
|
_isLayoutLocked = false;
|
||||||
PerformLayout();
|
PerformLayout();
|
||||||
|
ApplyExpandedFolders();
|
||||||
|
ApplyTreeViewMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1176,7 +1683,10 @@ namespace FlaxEditor.Windows
|
|||||||
if (_isWorkspaceDirty)
|
if (_isWorkspaceDirty)
|
||||||
{
|
{
|
||||||
_isWorkspaceDirty = false;
|
_isWorkspaceDirty = false;
|
||||||
RefreshView();
|
if (_showAllContentInTree)
|
||||||
|
RefreshTreeItems();
|
||||||
|
else
|
||||||
|
RefreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Update(deltaTime);
|
base.Update(deltaTime);
|
||||||
@@ -1186,7 +1696,15 @@ namespace FlaxEditor.Windows
|
|||||||
public override void OnExit()
|
public override void OnExit()
|
||||||
{
|
{
|
||||||
// Save last viewed folder
|
// 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);
|
Editor.ProjectCache.SetCustomData(ProjectDataLastViewedFolder, lastViewedFolder?.Path ?? string.Empty);
|
||||||
|
|
||||||
// Clear view
|
// Clear view
|
||||||
@@ -1197,7 +1715,7 @@ namespace FlaxEditor.Windows
|
|||||||
{
|
{
|
||||||
while (_root.HasChildren)
|
while (_root.HasChildren)
|
||||||
{
|
{
|
||||||
_root.RemoveChild((ContentTreeNode)_root.GetChild(0));
|
_root.RemoveChild((ContentFolderTreeNode)_root.GetChild(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1232,7 +1750,12 @@ namespace FlaxEditor.Windows
|
|||||||
{
|
{
|
||||||
ShowContextMenuForItem(null, ref location, false);
|
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);
|
_tree.Select(node);
|
||||||
ShowContextMenuForItem(node.Folder, ref location, true);
|
ShowContextMenuForItem(node.Folder, ref location, true);
|
||||||
@@ -1258,11 +1781,16 @@ namespace FlaxEditor.Windows
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void PerformLayoutBeforeChildren()
|
protected override void PerformLayoutBeforeChildren()
|
||||||
{
|
{
|
||||||
UpdateNavigationBarBounds();
|
|
||||||
|
|
||||||
base.PerformLayoutBeforeChildren();
|
base.PerformLayoutBeforeChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void PerformLayoutAfterChildren()
|
||||||
|
{
|
||||||
|
base.PerformLayoutAfterChildren();
|
||||||
|
UpdateNavigationBarBounds();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool UseLayoutData => true;
|
public override bool UseLayoutData => true;
|
||||||
|
|
||||||
@@ -1277,6 +1805,7 @@ namespace FlaxEditor.Windows
|
|||||||
writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString());
|
writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString());
|
||||||
writer.WriteAttributeString("ShowGeneratedFiles", ShowGeneratedFiles.ToString());
|
writer.WriteAttributeString("ShowGeneratedFiles", ShowGeneratedFiles.ToString());
|
||||||
writer.WriteAttributeString("ViewType", _view.ViewType.ToString());
|
writer.WriteAttributeString("ViewType", _view.ViewType.ToString());
|
||||||
|
writer.WriteAttributeString("TreeViewAllContent", _showAllContentInTree.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1297,6 +1826,9 @@ namespace FlaxEditor.Windows
|
|||||||
ShowGeneratedFiles = value2;
|
ShowGeneratedFiles = value2;
|
||||||
if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType))
|
if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType))
|
||||||
_view.ViewType = viewType;
|
_view.ViewType = viewType;
|
||||||
|
if (bool.TryParse(node.GetAttribute("TreeViewAllContent"), out value2))
|
||||||
|
_showAllContentInTree = value2;
|
||||||
|
ApplyTreeViewMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1304,6 +1836,7 @@ namespace FlaxEditor.Windows
|
|||||||
{
|
{
|
||||||
_split.SplitterValue = 0.2f;
|
_split.SplitterValue = 0.2f;
|
||||||
_view.ViewScale = 1.0f;
|
_view.ViewScale = 1.0f;
|
||||||
|
_showAllContentInTree = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -1312,10 +1845,17 @@ namespace FlaxEditor.Windows
|
|||||||
_foldersSearchBox = null;
|
_foldersSearchBox = null;
|
||||||
_itemsSearchBox = null;
|
_itemsSearchBox = null;
|
||||||
_viewDropdown = null;
|
_viewDropdown = null;
|
||||||
|
_viewDropdownPanel = null;
|
||||||
|
_treePanelRoot = null;
|
||||||
|
_treeHeaderPanel = null;
|
||||||
|
_treeOnlyPanel = null;
|
||||||
|
_contentItemsSearchPanel = null;
|
||||||
|
|
||||||
Editor.Options.OptionsChanged -= OnOptionsChanged;
|
Editor.Options.OptionsChanged -= OnOptionsChanged;
|
||||||
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
|
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
|
||||||
ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd;
|
ScriptsBuilder.ScriptsReloadEnd -= OnScriptsReloadEnd;
|
||||||
|
if (Editor?.ContentDatabase != null)
|
||||||
|
Editor.ContentDatabase.ItemAdded -= OnContentDatabaseItemAdded;
|
||||||
|
|
||||||
base.OnDestroy();
|
base.OnDestroy();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user