// Copyright (c) 2012-2021 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 selection. Otherwise selection will be cleared before. public void ShowItems(List items, SortType sortType, bool additive = 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; // 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); } // 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 (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 rename item. /// /// The item. public void OnItemDoubleClickName(ContentItem item) { OnRename?.Invoke(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(Vector2.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(Vector2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center); } } /// public override bool OnMouseWheel(Vector2 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) OnOpen?.Invoke(e); return true; } // Movement with arrows { var root = _selection[0]; Vector2 size = root.Size; Vector2 offset = Vector2.Minimum; ContentItem item = null; if (key == KeyboardKeys.ArrowUp) { offset = new Vector2(0, -size.Y); } else if (key == KeyboardKeys.ArrowDown) { offset = new Vector2(0, size.Y); } else if (key == KeyboardKeys.ArrowRight) { offset = new Vector2(size.X, 0); } else if (key == KeyboardKeys.ArrowLeft) { offset = new Vector2(-size.X, 0); } if (offset != Vector2.Minimum) { item = GetChildAt(root.Location + size / 2 + offset) as ContentItem; } if (item != null) { OnItemClick(item); return true; } } } return base.OnKeyDown(key); } /// 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); float itemsWidth = width / Mathf.Max(itemsToFit, 1); float itemsHeight = itemsWidth / defaultItemsWidth * (ContentItem.DefaultHeight * viewScale); for (int i = 0; i < _children.Count; i++) { var c = _children[i]; c.Bounds = new Rectangle(x, y, itemsWidth, itemsHeight); x += itemsWidth; if (x + itemsWidth > width) { x = 0; y += itemsHeight + 1; } } 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(); } /// public override void OnDestroy() { // Ensure to unlink all items ClearItems(); base.OnDestroy(); } } }