// Copyright (c) 2012-2023 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
{
///
/// The content items view modes.
///
[HideInEditor]
public enum ContentViewType
{
///
/// The uniform tiles.
///
Tiles,
///
/// The vertical list.
///
List,
}
///
/// The method sort for items.
///
public enum SortType
{
///
/// The classic alphabetic sort method (A-Z).
///
AlphabeticOrder,
///
/// The reverse alphabetic sort method (Z-A).
///
AlphabeticReverse
}
///
/// Main control for used to present collection of .
///
///
///
[HideInEditor]
public partial class ContentView : ContainerControl, IContentItemOwner
{
private readonly List _items = new List(256);
private readonly List _selection = new List(16);
private float _viewScale = 1.0f;
private ContentViewType _viewType = ContentViewType.Tiles;
#region External Events
///
/// Called when user wants to open the item.
///
public event Action OnOpen;
///
/// Called when user wants to rename the item.
///
public event Action OnRename;
///
/// Called when user wants to delete the item.
///
public event Action> OnDelete;
///
/// Called when user wants to paste the files/folders.
///
public event Action OnPaste;
///
/// Called when user wants to duplicate the item(s).
///
public event Action> OnDuplicate;
///
/// Called when user wants to navigate backward.
///
public event Action OnNavigateBack;
///
/// Occurs when view scale gets changed.
///
public event Action ViewScaleChanged;
///
/// Occurs when view type gets changed.
///
public event Action ViewTypeChanged;
#endregion
///
/// Gets the items.
///
public List Items => _items;
///
/// Gets the items count.
///
public int ItemsCount => _items.Count;
///
/// Gets the selected items.
///
public List Selection => _selection;
///
/// Gets the selected count.
///
public int SelectedCount => _selection.Count;
///
/// Gets a value indicating whether any item is selected.
///
public bool HasSelection => _selection.Count > 0;
///
/// Gets or sets the view scale.
///
public float ViewScale
{
get => _viewScale;
set
{
value = Mathf.Clamp(value, 0.3f, 3.0f);
if (!Mathf.NearEqual(value, _viewScale))
{
_viewScale = value;
ViewScaleChanged?.Invoke();
PerformLayout();
}
}
}
///
/// Gets or sets the type of the view.
///
public ContentViewType ViewType
{
get => _viewType;
set
{
if (_viewType != value)
{
_viewType = value;
ViewTypeChanged?.Invoke();
PerformLayout();
}
}
}
///
/// Flag is used to indicate if user is searching for items. Used to show a proper message to the user.
///
public bool IsSearching;
///
/// Flag used to indicate whenever show full file names including extensions.
///
public bool ShowFileExtensions;
///
/// The input actions collection to processed during user input.
///
public readonly InputActionsContainer InputActions;
///
/// Initializes a new instance of the class.
///
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),
});
}
///
/// Clears the items in the view.
///
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++)
{
_items[i].Parent = null;
_items[i].RemoveReference(this);
}
_items.Clear();
// Unload and perform UI layout
IsLayoutLocked = wasLayoutLocked;
PerformLayout();
}
///
/// Shows the items collection in the view.
///
/// The items to show.
/// The sort method for items.
/// If set to true items will be added to the current list. Otherwise items list will be cleared before.
/// If set to true selected items list will be preserved. Otherwise selection will be cleared before.
public void ShowItems(List 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
_items.AddRange(items);
for (int i = 0; i < items.Count; i++)
{
items[i].Parent = this;
items[i].AddReference(this);
}
if (selection != null)
{
_selection.Clear();
_selection.AddRange(selection);
}
// Sort items depending on sortMethod parameter
_children.Sort(((control, control1) =>
{
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();
}
///
/// Determines whether the specified item is selected.
///
/// The item.
/// true if the specified item is selected; otherwise, false.
public bool IsSelected(ContentItem item)
{
return _selection.Contains(item);
}
///
/// Clears the selected items collection.
///
public void ClearSelection()
{
if (_selection.Count == 0)
return;
_selection.Clear();
}
///
/// Selects the specified items.
///
/// The items.
/// If set to true items will be added to the current selection. Otherwise selection will be cleared before.
public void Select(List 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();
}
///
/// Selects the specified item.
///
/// The item.
/// If set to true item will be added to the current selection. Otherwise selection will be cleared before.
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();
}
///
/// Selects all the items.
///
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();
}
///
/// Deselects the specified item.
///
/// The item.
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();
}
///
/// Duplicates the selected items.
///
public void Duplicate()
{
OnDuplicate?.Invoke(_selection);
}
///
/// Copies the selected items (to the system clipboard).
///
public void Copy()
{
if (_selection.Count == 0)
return;
var files = _selection.ConvertAll(x => x.Path).ToArray();
Clipboard.Files = files;
}
///
/// Returns true if user can paste data to the view (copied any files before).
///
/// True if can paste files.
public bool CanPaste()
{
var files = Clipboard.Files;
return files != null && files.Length > 0;
}
///
/// Pastes the copied items (from the system clipboard).
///
public void Paste()
{
var files = Clipboard.Files;
if (files == null || files.Length == 0)
return;
OnPaste?.Invoke(files);
}
///
/// Gives focus and selects the first item in the view.
///
public void SelectFirstItem()
{
if (_items.Count > 0)
{
_items[0].Focus();
Select(_items[0]);
}
else
{
Focus();
}
}
///
/// Refreshes thumbnails of all items in the .
///
public void RefreshThumbnails()
{
for (int i = 0; i < _items.Count; i++)
_items[i].RefreshThumbnail();
}
#region Internal events
///
/// Called when user clicks on an item.
///
/// The item.
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.Min(min, item.IndexInParent);
max = Mathf.Max(max, item.IndexInParent);
var selection = new List(_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);
}
}
///
/// Called when user wants to open item.
///
/// The item.
public void OnItemDoubleClick(ContentItem item)
{
OnOpen?.Invoke(item);
}
#endregion
#region IContentItemOwner
///
void IContentItemOwner.OnItemDeleted(ContentItem item)
{
_selection.Remove(item);
_items.Remove(item);
}
///
void IContentItemOwner.OnItemRenamed(ContentItem item)
{
}
///
void IContentItemOwner.OnItemReimported(ContentItem item)
{
}
///
void IContentItemOwner.OnItemDispose(ContentItem item)
{
_selection.Remove(item);
_items.Remove(item);
}
#endregion
///
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);
}
}
///
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
return AutoFocus && Focus(this);
}
///
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);
}
///
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);
}
///
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;
}
///
protected override void PerformLayoutBeforeChildren()
{
float width = GetClientArea().Width;
float x = 0, y = 0;
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;
float itemsWidth = width / Mathf.Max(itemsToFit, 1);
float itemsHeight = itemsWidth / defaultItemsWidth * (ContentItem.DefaultHeight * viewScale);
var flooredItemsWidth = Mathf.Floor(itemsWidth);
var flooredItemsHeight = Mathf.Floor(itemsHeight);
x = itemsToFit == 1 ? 0 : itemsWidth / itemsToFit;
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 + itemsWidth / itemsToFit;
if (x + itemsWidth > width)
{
x = itemsToFit == 1 ? 0 : itemsWidth / itemsToFit;
y += itemsHeight + 5;
}
}
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 + 5;
}
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();
}
///
public override void OnDestroy()
{
// Ensure to unlink all items
ClearItems();
base.OnDestroy();
}
}
}