Files
FlaxEngine/Source/Editor/Content/GUI/ContentView.cs
2024-02-26 19:00:48 +01:00

843 lines
26 KiB
C#

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Options;
using FlaxEditor.Windows;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Content.GUI
{
/// <summary>
/// The content items view modes.
/// </summary>
[HideInEditor]
public enum ContentViewType
{
/// <summary>
/// The uniform tiles.
/// </summary>
Tiles,
/// <summary>
/// The vertical list.
/// </summary>
List,
}
/// <summary>
/// The method sort for items.
/// </summary>
public enum SortType
{
/// <summary>
/// The classic alphabetic sort method (A-Z).
/// </summary>
AlphabeticOrder,
/// <summary>
/// The reverse alphabetic sort method (Z-A).
/// </summary>
AlphabeticReverse
}
/// <summary>
/// Main control for <see cref="ContentWindow"/> used to present collection of <see cref="ContentItem"/>.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
/// <seealso cref="FlaxEditor.Content.IContentItemOwner" />
[HideInEditor]
public partial class ContentView : ContainerControl, IContentItemOwner
{
private readonly List<ContentItem> _items = new List<ContentItem>(256);
private readonly List<ContentItem> _selection = new List<ContentItem>(16);
private float _viewScale = 1.0f;
private ContentViewType _viewType = ContentViewType.Tiles;
private bool _isRubberBandSpanning = false;
private Float2 _mousePresslocation;
private Rectangle _rubberBandRectangle;
#region External Events
/// <summary>
/// Called when user wants to open the item.
/// </summary>
public event Action<ContentItem> OnOpen;
/// <summary>
/// Called when user wants to rename the item.
/// </summary>
public event Action<ContentItem> OnRename;
/// <summary>
/// Called when user wants to delete the item.
/// </summary>
public event Action<List<ContentItem>> OnDelete;
/// <summary>
/// Called when user wants to paste the files/folders.
/// </summary>
public event Action<string[]> OnPaste;
/// <summary>
/// Called when user wants to duplicate the item(s).
/// </summary>
public event Action<List<ContentItem>> OnDuplicate;
/// <summary>
/// Called when user wants to navigate backward.
/// </summary>
public event Action OnNavigateBack;
/// <summary>
/// Occurs when view scale gets changed.
/// </summary>
public event Action ViewScaleChanged;
/// <summary>
/// Occurs when view type gets changed.
/// </summary>
public event Action ViewTypeChanged;
#endregion
/// <summary>
/// Gets the items.
/// </summary>
public List<ContentItem> Items => _items;
/// <summary>
/// Gets the items count.
/// </summary>
public int ItemsCount => _items.Count;
/// <summary>
/// Gets the selected items.
/// </summary>
public List<ContentItem> Selection => _selection;
/// <summary>
/// Gets the selected count.
/// </summary>
public int SelectedCount => _selection.Count;
/// <summary>
/// Gets a value indicating whether any item is selected.
/// </summary>
public bool HasSelection => _selection.Count > 0;
/// <summary>
/// Gets or sets the view scale.
/// </summary>
public float ViewScale
{
get => _viewScale;
set
{
value = Mathf.Clamp(value, 0.3f, 3.0f);
if (!Mathf.NearEqual(value, _viewScale))
{
_viewScale = value;
ViewScaleChanged?.Invoke();
PerformLayout();
}
}
}
/// <summary>
/// Gets or sets the type of the view.
/// </summary>
public ContentViewType ViewType
{
get => _viewType;
set
{
if (_viewType != value)
{
_viewType = value;
ViewTypeChanged?.Invoke();
PerformLayout();
}
}
}
/// <summary>
/// Flag is used to indicate if user is searching for items. Used to show a proper message to the user.
/// </summary>
public bool IsSearching;
/// <summary>
/// Flag used to indicate whenever show full file names including extensions.
/// </summary>
public bool ShowFileExtensions;
/// <summary>
/// The input actions collection to processed during user input.
/// </summary>
public readonly InputActionsContainer InputActions;
/// <summary>
/// Initializes a new instance of the <see cref="ContentView"/> class.
/// </summary>
public ContentView()
{
// Setup input actions
InputActions = new InputActionsContainer(new[]
{
new InputActionsContainer.Binding(options => options.Delete, () =>
{
if (HasSelection)
OnDelete?.Invoke(_selection);
}),
new InputActionsContainer.Binding(options => options.SelectAll, SelectAll),
new InputActionsContainer.Binding(options => options.Rename, () =>
{
if (HasSelection && _selection[0].CanRename)
{
if (_selection.Count > 1)
Select(_selection[0]);
OnRename?.Invoke(_selection[0]);
}
}),
new InputActionsContainer.Binding(options => options.Copy, Copy),
new InputActionsContainer.Binding(options => options.Paste, Paste),
new InputActionsContainer.Binding(options => options.Duplicate, Duplicate),
});
}
/// <summary>
/// Clears the items in the view.
/// </summary>
public void ClearItems()
{
// Lock layout
var wasLayoutLocked = IsLayoutLocked;
IsLayoutLocked = true;
// Deselect items first
ClearSelection();
// Remove references and unlink items
for (int i = 0; i < _items.Count; i++)
{
var item = _items[i];
item.Parent = null;
item.RemoveReference(this);
}
_items.Clear();
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
/// <summary>
/// Shows the items collection in the view.
/// </summary>
/// <param name="items">The items to show.</param>
/// <param name="sortType">The sort method for items.</param>
/// <param name="additive">If set to <c>true</c> items will be added to the current list. Otherwise items list will be cleared before.</param>
/// <param name="keepSelection">If set to <c>true</c> selected items list will be preserved. Otherwise selection will be cleared before.</param>
public void ShowItems(List<ContentItem> items, SortType sortType, bool additive = false, bool keepSelection = false)
{
if (items == null)
throw new ArgumentNullException();
// Check if show nothing or not change view
if (items.Count == 0)
{
// Deselect items if need to
if (!additive)
ClearItems();
return;
}
// Lock layout
var wasLayoutLocked = IsLayoutLocked;
IsLayoutLocked = true;
var selection = !additive && keepSelection ? _selection.ToArray() : null;
// Deselect items if need to
if (!additive)
ClearItems();
// Add references and link items
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
if (item.Visible && !_items.Contains(item))
{
item.Parent = this;
item.AddReference(this);
_items.Add(item);
}
}
if (selection != null)
{
_selection.Clear();
_selection.AddRange(selection);
}
// Sort items depending on sortMethod parameter
_children.Sort(((control, control1) =>
{
if (control == null || control1 == null)
return 0;
if (sortType == SortType.AlphabeticReverse)
{
if (control.CompareTo(control1) > 0)
return -1;
if (control.CompareTo(control1) == 0)
return 0;
return 1;
}
return control.CompareTo(control1);
}));
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
/// <summary>
/// Determines whether the specified item is selected.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if the specified item is selected; otherwise, <c>false</c>.</returns>
public bool IsSelected(ContentItem item)
{
return _selection.Contains(item);
}
/// <summary>
/// Clears the selected items collection.
/// </summary>
public void ClearSelection()
{
if (_selection.Count == 0)
return;
_selection.Clear();
}
/// <summary>
/// Selects the specified items.
/// </summary>
/// <param name="items">The items.</param>
/// <param name="additive">If set to <c>true</c> items will be added to the current selection. Otherwise selection will be cleared before.</param>
public void Select(List<ContentItem> items, bool additive = false)
{
if (items == null)
throw new ArgumentNullException();
// Check if nothing to select
if (items.Count == 0)
{
// Deselect items if need to
if (!additive)
ClearSelection();
return;
}
// Lock layout
var wasLayoutLocked = IsLayoutLocked;
IsLayoutLocked = true;
// Select items
if (additive)
{
for (int i = 0; i < items.Count; i++)
{
if (!_selection.Contains(items[i]))
_selection.Add(items[i]);
}
}
else
{
_selection.Clear();
_selection.AddRange(items);
}
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
/// <summary>
/// Selects the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="additive">If set to <c>true</c> item will be added to the current selection. Otherwise selection will be cleared before.</param>
public void Select(ContentItem item, bool additive = false)
{
if (item == null)
throw new ArgumentNullException();
// Lock layout
var wasLayoutLocked = IsLayoutLocked;
IsLayoutLocked = true;
// Select item
if (additive)
{
if (!_selection.Contains(item))
_selection.Add(item);
}
else
{
_selection.Clear();
_selection.Add(item);
}
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
/// <summary>
/// Selects all the items.
/// </summary>
public void SelectAll()
{
// Lock layout
var wasLayoutLocked = IsLayoutLocked;
IsLayoutLocked = true;
// Select items
_selection.Clear();
_selection.AddRange(_items);
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
/// <summary>
/// Deselects the specified item.
/// </summary>
/// <param name="item">The item.</param>
public void Deselect(ContentItem item)
{
if (item == null)
throw new ArgumentNullException();
// Lock layout
var wasLayoutLocked = IsLayoutLocked;
IsLayoutLocked = true;
// Deselect item
if (_selection.Contains(item))
_selection.Remove(item);
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
/// <summary>
/// Duplicates the selected items.
/// </summary>
public void Duplicate()
{
OnDuplicate?.Invoke(_selection);
}
/// <summary>
/// Copies the selected items (to the system clipboard).
/// </summary>
public void Copy()
{
if (_selection.Count == 0)
return;
var files = _selection.ConvertAll(x => x.Path).ToArray();
Clipboard.Files = files;
}
/// <summary>
/// Returns true if user can paste data to the view (copied any files before).
/// </summary>
/// <returns>True if can paste files.</returns>
public bool CanPaste()
{
var files = Clipboard.Files;
return files != null && files.Length > 0;
}
/// <summary>
/// Pastes the copied items (from the system clipboard).
/// </summary>
public void Paste()
{
var files = Clipboard.Files;
if (files == null || files.Length == 0)
return;
OnPaste?.Invoke(files);
}
/// <summary>
/// Gives focus and selects the first item in the view.
/// </summary>
public void SelectFirstItem()
{
if (_items.Count > 0)
{
_items[0].Focus();
Select(_items[0]);
}
else
{
Focus();
}
}
/// <summary>
/// Refreshes thumbnails of all items in the <see cref="ContentView"/>.
/// </summary>
public void RefreshThumbnails()
{
for (int i = 0; i < _items.Count; i++)
_items[i].RefreshThumbnail();
}
#region Internal events
/// <summary>
/// Called when user clicks on an item.
/// </summary>
/// <param name="item">The item.</param>
public void OnItemClick(ContentItem item)
{
bool isSelected = _selection.Contains(item);
// Add/remove from selection
if (Root.GetKey(KeyboardKeys.Control))
{
if (isSelected)
Deselect(item);
else
Select(item, true);
}
// Range select
else if (_selection.Count != 0 && Root.GetKey(KeyboardKeys.Shift))
{
int min = _selection.Min(x => x.IndexInParent);
int max = _selection.Max(x => x.IndexInParent);
min = Mathf.Max(Mathf.Min(min, item.IndexInParent), 0);
max = Mathf.Min(Mathf.Max(max, item.IndexInParent), _children.Count - 1);
var selection = new List<ContentItem>(_selection);
for (int i = min; i <= max; i++)
{
if (_children[i] is ContentItem cc && !selection.Contains(cc))
{
selection.Add(cc);
}
}
Select(selection);
}
// Select
else
{
Select(item);
}
}
/// <summary>
/// Called when user wants to open item.
/// </summary>
/// <param name="item">The item.</param>
public void OnItemDoubleClick(ContentItem item)
{
OnOpen?.Invoke(item);
}
#endregion
#region IContentItemOwner
/// <inheritdoc />
void IContentItemOwner.OnItemDeleted(ContentItem item)
{
_selection.Remove(item);
_items.Remove(item);
}
/// <inheritdoc />
void IContentItemOwner.OnItemRenamed(ContentItem item)
{
}
/// <inheritdoc />
void IContentItemOwner.OnItemReimported(ContentItem item)
{
}
/// <inheritdoc />
void IContentItemOwner.OnItemDispose(ContentItem item)
{
_selection.Remove(item);
_items.Remove(item);
}
#endregion
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var style = Style.Current;
// Check if drag is over
if (IsDragOver && _validDragOver)
{
Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), style.BackgroundSelected * 0.4f);
}
// Check if it's an empty thing
if (_items.Count == 0)
{
Render2D.DrawText(style.FontSmall, IsSearching ? "No results" : "Empty", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
}
if (_isRubberBandSpanning)
{
Render2D.FillRectangle(_rubberBandRectangle, Color.Orange * 0.4f);
Render2D.DrawRectangle(_rubberBandRectangle, Color.Orange);
}
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
if (button == MouseButton.Left)
{
_mousePresslocation = location;
_rubberBandRectangle = new Rectangle(_mousePresslocation, 0, 0);
_isRubberBandSpanning = true;
StartMouseCapture();
}
return AutoFocus && Focus(this);
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
if (_isRubberBandSpanning)
{
_rubberBandRectangle.Width = location.X - _mousePresslocation.X;
_rubberBandRectangle.Height = location.Y - _mousePresslocation.Y;
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (_isRubberBandSpanning)
{
_isRubberBandSpanning = false;
EndMouseCapture();
if (_rubberBandRectangle.Width < 0 || _rubberBandRectangle.Height < 0)
{
// make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner
var size = _rubberBandRectangle.Size;
_rubberBandRectangle.X = Mathf.Min(_rubberBandRectangle.X, _rubberBandRectangle.X + _rubberBandRectangle.Width);
_rubberBandRectangle.Y = Mathf.Min(_rubberBandRectangle.Y, _rubberBandRectangle.Y + _rubberBandRectangle.Height);
size.X = Mathf.Abs(size.X);
size.Y = Mathf.Abs(size.Y);
_rubberBandRectangle.Size = size;
}
var itemsInRectangle = _items.Where(t => _rubberBandRectangle.Intersects(t.Bounds)).ToList();
Select(itemsInRectangle, Input.GetKey(KeyboardKeys.Shift) || Input.GetKey(KeyboardKeys.Control));
return true;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseWheel(Float2 location, float delta)
{
// Check if pressing control key
if (Root.GetKey(KeyboardKeys.Control))
{
// Zoom
ViewScale += delta * 0.05f;
// Handled
return true;
}
return base.OnMouseWheel(location, delta);
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
// Navigate backward
if (key == KeyboardKeys.Backspace)
{
OnNavigateBack?.Invoke();
return true;
}
if (InputActions.Process(Editor.Instance, this, key))
return true;
// Check if sth is selected
if (HasSelection)
{
// Open
if (key == KeyboardKeys.Return && _selection.Count != 0)
{
foreach (var e in _selection.ToArray())
OnOpen?.Invoke(e);
return true;
}
// Movement with arrows
{
var root = _selection[0];
var size = root.Size;
var offset = Float2.Minimum;
ContentItem item = null;
if (key == KeyboardKeys.ArrowUp)
{
offset = new Float2(0, -size.Y);
}
else if (key == KeyboardKeys.ArrowDown)
{
offset = new Float2(0, size.Y);
}
else if (key == KeyboardKeys.ArrowRight)
{
offset = new Float2(size.X, 0);
}
else if (key == KeyboardKeys.ArrowLeft)
{
offset = new Float2(-size.X, 0);
}
if (offset != Float2.Minimum)
{
item = GetChildAt(root.Location + size / 2 + offset) as ContentItem;
}
if (item != null)
{
OnItemClick(item);
return true;
}
}
}
return base.OnKeyDown(key);
}
/// <inheritdoc />
public override bool OnCharInput(char c)
{
if (base.OnCharInput(c))
return true;
if (char.IsLetterOrDigit(c) && _items.Count != 0)
{
// Jump to the item starting with this character
c = char.ToLowerInvariant(c);
for (int i = 0; i < _items.Count; i++)
{
var item = _items[i];
var name = item.ShortName;
if (!string.IsNullOrEmpty(name) && char.ToLowerInvariant(name[0]) == c)
{
Select(item);
if (Parent is Panel panel)
panel.ScrollViewTo(item, true);
break;
}
}
}
return false;
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
float width = GetClientArea().Width;
float x = 0, y = 1;
float viewScale = _viewScale * 0.97f;
switch (ViewType)
{
case ContentViewType.Tiles:
{
float defaultItemsWidth = ContentItem.DefaultWidth * viewScale;
int itemsToFit = Mathf.FloorToInt(width / defaultItemsWidth) - 1;
if (itemsToFit < 1)
itemsToFit = 1;
int xSpace = 4;
float itemsWidth = width / Mathf.Max(itemsToFit, 1) - xSpace;
float itemsHeight = itemsWidth / defaultItemsWidth * (ContentItem.DefaultHeight * viewScale);
var flooredItemsWidth = Mathf.Floor(itemsWidth);
var flooredItemsHeight = Mathf.Floor(itemsHeight);
x = itemsToFit == 1 ? 1 : itemsWidth / itemsToFit + xSpace;
for (int i = 0; i < _children.Count; i++)
{
var c = _children[i];
c.Bounds = new Rectangle(Mathf.Floor(x), Mathf.Floor(y), flooredItemsWidth, flooredItemsHeight);
x += (itemsWidth + xSpace) + (itemsWidth + xSpace) / itemsToFit;
if (x + itemsWidth > width)
{
x = itemsToFit == 1 ? 1 : itemsWidth / itemsToFit + xSpace;
y += itemsHeight + 7;
}
}
if (x > 0)
y += itemsHeight;
break;
}
case ContentViewType.List:
{
float itemsHeight = 50.0f * viewScale;
for (int i = 0; i < _children.Count; i++)
{
var c = _children[i];
c.Bounds = new Rectangle(x, y, width, itemsHeight);
y += itemsHeight + 1;
}
y += 40.0f;
break;
}
default: throw new ArgumentOutOfRangeException();
}
// Set maximum size and fit the parent container
if (HasParent)
y = Mathf.Max(y, Parent.Height);
Height = y;
base.PerformLayoutBeforeChildren();
}
/// <inheritdoc />
public override void OnDestroy()
{
// Ensure to unlink all items
ClearItems();
base.OnDestroy();
}
}
}