using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using UnityEngine;
using UnityEngine.UIElements;
#if ENABLE_RUNTIME_DATA_BINDINGS
using Unity.Properties;
#endif
namespace UnityEditor.U2D.Animation.SpriteLibraryEditor
{
///
/// A view containing recycled rows with items inside.
///
#if ENABLE_UXML_SERIALIZED_DATA
[UxmlElement]
#endif
internal partial class GridView : BindableElement, ISerializationCallbackReceiver
{
#if ENABLE_RUNTIME_DATA_BINDINGS
internal static readonly BindingId itemHeightProperty = new BindingId(nameof(itemHeight));
internal static readonly BindingId itemSquareProperty = new BindingId(nameof(itemSquare));
internal static readonly BindingId selectionTypeProperty = new BindingId(nameof(selectionType));
internal static readonly BindingId allowNoSelectionProperty = new BindingId(nameof(allowNoSelection));
#endif
///
/// Available Operations.
///
[Flags]
public enum GridOperations
{
///
/// No operation.
///
None = 0,
///
/// Select all items.
///
SelectAll = 1 << 0,
///
/// Cancel selection.
///
Cancel = 1 << 1,
///
/// Move selection cursor left.
///
Left = 1 << 2,
///
/// Move selection cursor right.
///
Right = 1 << 3,
///
/// Move selection cursor up.
///
Up = 1 << 4,
///
/// Move selection cursor down.
///
Down = 1 << 5,
///
/// Move selection cursor to the beginning of the list.
///
Begin = 1 << 6,
///
/// Move selection cursor to the end of the list.
///
End = 1 << 7,
///
/// Choose selected items.
///
Choose = 1 << 8,
}
const float k_PageSizeFactor = 0.25f;
const int k_ExtraVisibleRows = 2;
///
/// The USS class name for GridView elements.
///
///
/// Unity adds this USS class to every instance of the GridView element. Any styling applied to
/// this class affects every GridView located beside, or below the stylesheet in the visual tree.
///
const string k_UssClassName = "unity2d-grid-view";
///
/// The USS class name for GridView elements with a border.
///
///
/// Unity adds this USS class to an instance of the GridView element if the instance's
/// property is set to true. Any styling applied to this class
/// affects every such GridView located beside, or below the stylesheet in the visual tree.
///
const string k_BorderUssClassName = k_UssClassName + "--with-border";
///
/// The USS class name of item elements in GridView elements.
///
///
/// Unity adds this USS class to every item element the GridView contains. Any styling applied to
/// this class affects every item element located beside, or below the stylesheet in the visual tree.
///
const string k_ItemUssClassName = k_UssClassName + "__item";
///
/// The first column USS class name for GridView elements.
///
static readonly string k_FirstColumnUssClassName = k_UssClassName + "__first-column";
///
/// The last column USS class name for GridView elements.
///
static readonly string k_LastColumnUssClassName = k_UssClassName + "__last-column";
///
/// The USS class name of selected item elements in the GridView.
///
///
/// Unity adds this USS class to every selected element in the GridView. The
/// property decides if zero, one, or more elements can be selected. Any styling applied to
/// this class affects every GridView item located beside, or below the stylesheet in the visual tree.
///
internal const string itemSelectedVariantUssClassName = k_ItemUssClassName + "--selected";
///
/// The USS class name of rows in the GridView.
///
const string k_RowUssClassName = k_UssClassName + "__row";
internal const int defaultItemHeight = 30;
const float k_DpiScaling = 1f;
const bool k_DefaultPreventScrollWithModifiers = true;
static CustomStyleProperty s_ItemHeightProperty = new CustomStyleProperty("--unity-item-height");
///
/// The used by the GridView.
///
public ScrollView scrollView { get; }
Func m_MakeItem;
Action m_BindItem;
Func m_GetItemId;
readonly List m_SelectedIds = new List();
readonly List m_SelectedIndices = new List();
readonly List m_SelectedItems = new List();
readonly List m_PreviouslySelectedIndices = new List();
readonly List m_OriginalSelection = new List();
float m_OriginalScrollOffset;
int m_SoftSelectIndex = -1;
int m_ColumnCount = 1;
int m_FirstVisibleIndex;
int m_ItemHeight = defaultItemHeight;
float m_LastHeight;
bool m_ItemHeightIsInline;
bool m_ItemSquare;
IList m_ItemsSource;
int m_RangeSelectionOrigin = -1;
bool m_IsRangeSelectionDirectionUp;
List m_RowPool = new List();
// We keep this list in order to minimize temporary gc allocations.
List m_ScrollInsertionList = new List();
// Persisted.
float m_ScrollOffset;
SelectionType m_SelectionType;
bool m_AllowNoSelection = true;
int m_VisibleRowCount;
bool m_IsList;
NavigationMoveEvent m_NavigationMoveAdapter;
NavigationCancelEvent m_NavigationCancelAdapter;
bool m_HasPointerMoved;
bool m_SoftSelectIndexWasPreviouslySelected;
///
/// Creates a with all default properties. The ,
/// , and properties
/// must all be set for the GridView to function properly.
///
public GridView()
{
AddToClassList(k_UssClassName);
styleSheets.Add(ResourceLoader.Load("SpriteLibraryEditor/GridView.uss"));
selectionType = SelectionType.Single;
m_ScrollOffset = 0.0f;
scrollView = new ScrollView
{
viewDataKey = "grid-view__scroll-view",
horizontalScrollerVisibility = ScrollerVisibility.Hidden
};
scrollView.StretchToParentSize();
scrollView.verticalScroller.valueChanged += OnScroll;
dragger = new Dragger(OnDraggerStarted, OnDraggerMoved, OnDraggerEnded, OnDraggerCanceled);
RegisterCallback(OnSizeChanged);
RegisterCallback(OnCustomStyleResolved);
RegisterCallback(OnAttachToPanel);
RegisterCallback(OnDetachFromPanel);
hierarchy.Add(scrollView);
scrollView.contentContainer.usageHints &= ~UsageHints.GroupTransform; // Scroll views with virtualized content shouldn't have the "view transform" optimization
focusable = true;
dragger.acceptStartDrag = DefaultAcceptStartDrag;
}
///
/// Constructs a , with all required properties provided.
///
/// The list of items to use as a data source.
/// The factory method to call to create a display item. The method should return a
/// VisualElement that can be bound to a data item.
/// The method to call to bind a data item to a display item. The method
/// receives as parameters the display item to bind, and the index of the data item to bind it to.
public GridView(IList itemsSource, Func makeItem, Action bindItem)
: this()
{
m_ItemsSource = itemsSource;
m_ItemHeightIsInline = true;
m_MakeItem = makeItem;
m_BindItem = bindItem;
operationMask = ~GridOperations.None;
}
bool Apply(GridOperations operation, bool shiftKey)
{
if ((operation & operationMask) == 0)
return false;
void HandleSelectionAndScroll(int index)
{
if (selectionType == SelectionType.Multiple && shiftKey && m_SelectedIndices.Count != 0)
DoRangeSelection(index, true, true);
else
selectedIndex = index;
ScrollToItem(index);
}
switch (operation)
{
case GridOperations.None:
break;
case GridOperations.SelectAll:
SelectAll();
return true;
case GridOperations.Cancel:
ClearSelection();
return true;
case GridOperations.Left:
{
var newIndex = Mathf.Max(selectedIndex - 1, 0);
if (newIndex != selectedIndex)
{
HandleSelectionAndScroll(newIndex);
return true;
}
}
break;
case GridOperations.Right:
{
var newIndex = Mathf.Min(selectedIndex + 1, itemsSource.Count - 1);
if (newIndex != selectedIndex)
{
HandleSelectionAndScroll(newIndex);
return true;
}
}
break;
case GridOperations.Up:
{
var newIndex = Mathf.Max(selectedIndex - columnCount, 0);
if (newIndex != selectedIndex)
{
HandleSelectionAndScroll(newIndex);
return true;
}
}
break;
case GridOperations.Down:
{
var newIndex = Mathf.Min(selectedIndex + columnCount, itemsSource.Count - 1);
if (newIndex != selectedIndex)
{
HandleSelectionAndScroll(newIndex);
return true;
}
}
break;
case GridOperations.Begin:
HandleSelectionAndScroll(0);
return true;
case GridOperations.End:
HandleSelectionAndScroll(itemsSource.Count - 1);
return true;
case GridOperations.Choose:
if (m_SelectedIndices.Count > 0)
itemsChosen?.Invoke(selectedItems);
return true;
default:
throw new ArgumentOutOfRangeException(nameof(operation), operation, null);
}
return false;
}
void Apply(GridOperations operation, EventBase sourceEvent)
{
if ((operation & operationMask) != 0 && Apply(operation, (sourceEvent as IKeyboardEvent)?.shiftKey ?? false))
{
sourceEvent?.StopPropagation();
}
}
///
/// Internal use only.
///
///
internal void Apply(GridOperations operation) => Apply(operation, null);
void OnDraggerStarted(PointerMoveEvent evt)
{
dragStarted?.Invoke(evt);
}
void OnDraggerMoved(PointerMoveEvent evt)
{
dragUpdated?.Invoke(evt);
}
void OnDraggerEnded(PointerUpEvent evt)
{
dragFinished?.Invoke(evt);
CancelSoftSelect();
}
void OnDraggerCanceled()
{
dragCanceled?.Invoke();
CancelSoftSelect();
}
///
/// Cancel drag operation.
///
public void CancelDrag()
{
dragger?.Cancel();
}
void OnKeyDown(KeyDownEvent evt)
{
var operation = evt.keyCode switch
{
KeyCode.A when evt.actionKey => GridOperations.SelectAll,
KeyCode.Escape => GridOperations.Cancel,
KeyCode.Home => GridOperations.Begin,
KeyCode.End => GridOperations.End,
KeyCode.UpArrow => GridOperations.Up,
KeyCode.DownArrow => GridOperations.Down,
KeyCode.LeftArrow => GridOperations.Left,
KeyCode.RightArrow => GridOperations.Right,
KeyCode.KeypadEnter or KeyCode.Return => GridOperations.Choose,
_ => GridOperations.None
};
Apply(operation, evt);
}
static void OnNavigationMove(NavigationMoveEvent evt) => evt.StopPropagation();
static void OnNavigationCancel(NavigationCancelEvent evt) => evt.StopPropagation();
///
/// Callback for binding a data item to the visual element.
///
///
/// The method called by this callback receives the VisualElement to bind, and the index of the
/// element to bind it to.
///
public Action bindItem
{
get => m_BindItem;
set
{
m_BindItem = value;
Refresh();
}
}
///
/// The number of columns for this grid.
///
public int columnCount
{
get => m_ColumnCount;
set
{
if (m_ColumnCount != value && value > 0)
{
m_ScrollOffset = 0;
m_ColumnCount = value;
Refresh();
}
}
}
///
/// The manipulator used by this .
///
public Dragger dragger { get; }
///
/// A mask describing available operations in this when the user interacts with it.
///
public GridOperations operationMask { get; set; } =
GridOperations.Choose |
GridOperations.SelectAll | GridOperations.Cancel |
GridOperations.Begin | GridOperations.End |
GridOperations.Left | GridOperations.Right |
GridOperations.Up | GridOperations.Down;
///
/// Returns the content container for the . Because the GridView control automatically manages
/// its content, this always returns null.
///
public override VisualElement contentContainer => null;
///
/// The height of a single item in the list, in pixels.
///
///
/// GridView requires that all visual elements have the same height so that it can calculate the
/// scroller size.
///
/// This property must be set for the list view to function.
///
#if ENABLE_RUNTIME_DATA_BINDINGS
[CreateProperty]
#endif
#if ENABLE_UXML_SERIALIZED_DATA
[UxmlAttribute]
#endif
public int itemHeight
{
get => m_ItemHeight;
set
{
if (m_ItemHeight != value && value > 0)
{
m_ItemHeightIsInline = true;
m_ItemHeight = value;
scrollView.verticalPageSize = m_ItemHeight * k_PageSizeFactor;
Refresh();
#if ENABLE_RUNTIME_DATA_BINDINGS
NotifyPropertyChanged(in itemHeightProperty);
#endif
}
}
}
///
/// Decides whether the item should have 1:1 aspect ratio.
///
#if ENABLE_RUNTIME_DATA_BINDINGS
[CreateProperty]
#endif
#if ENABLE_UXML_SERIALIZED_DATA
[UxmlAttribute]
#endif
public bool itemSquare
{
get => m_ItemSquare;
set
{
if (m_ItemSquare != value)
{
m_ItemSquare = value;
Refresh();
#if ENABLE_RUNTIME_DATA_BINDINGS
NotifyPropertyChanged(in itemSquareProperty);
#endif
}
}
}
///
/// The width of the item.
///
public float itemWidth => scrollView.contentViewport.layout.width / columnCount;
///
/// The data source for list items.
///
///
/// This list contains the items that the displays.
/// This property must be set for the list view to function.
///
public IList itemsSource
{
get => m_ItemsSource;
set
{
if (m_ItemsSource is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= OnItemsSourceCollectionChanged;
}
m_ItemsSource = value;
if (m_ItemsSource is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += OnItemsSourceCollectionChanged;
}
Refresh();
}
}
///
/// Callback for constructing the VisualElement that is the template for each recycled and re-bound element in the list.
///
///
/// This callback needs to call a function that constructs a blank that is
/// bound to an element from the list.
///
/// The GridView automatically creates enough elements to fill the visible area, and adds more if the area
/// is expanded. As the user scrolls, the GridView cycles elements in and out as they appear or disappear.
///
/// This property must be set for the list view to function.
///
public Func makeItem
{
get => m_MakeItem;
set
{
if (m_MakeItem == value)
return;
m_MakeItem = value;
Refresh();
}
}
///
/// The computed pixel-aligned height for the list elements.
///
///
/// This value changes depending on the current panel's DPI scaling.
///
///
public float resolvedItemHeight => Mathf.Round(itemHeight * k_DpiScaling) / k_DpiScaling;
///
/// The computed pixel-aligned width for the list elements.
///
///
/// This value changes depending on the current panel's DPI scaling.
///
///
public float resolvedItemWidth => Mathf.Round(itemWidth * k_DpiScaling) / k_DpiScaling;
///
/// Returns or sets the selected item's index in the data source. If multiple items are selected, returns the
/// first selected item's index. If multiple items are provided, sets them all as selected.
///
public int selectedIndex
{
get => m_SelectedIndices.Count == 0 ? -1 : m_SelectedIndices[0];
set => SetSelection(value);
}
///
/// Returns the indices of selected items in the data source. Always returns an enumerable, even if no item is selected, or a
/// single item is selected.
///
public IEnumerable selectedIndices => m_SelectedIndices;
///
/// Returns the selected item from the data source. If multiple items are selected, returns the first selected item.
///
public object selectedItem => m_SelectedItems.Count == 0 ? null : m_SelectedItems[0];
///
/// Returns the selected items from the data source. Always returns an enumerable, even if no item is selected, or a single
/// item is selected.
///
public IEnumerable selectedItems => m_SelectedItems;
///
/// Returns the IDs of selected items in the data source. Always returns an enumerable, even if no item is selected, or a
/// single item is selected.
///
public IEnumerable selectedIds => m_SelectedIds;
///
/// Controls the selection type.
///
///
/// You can set the GridView to make one item selectable at a time, make multiple items selectable, or disable selections completely.
///
/// When you set the GridView to disable selections, any current selection is cleared.
///
#if ENABLE_RUNTIME_DATA_BINDINGS
[CreateProperty]
#endif
#if ENABLE_UXML_SERIALIZED_DATA
[UxmlAttribute]
#endif
public SelectionType selectionType
{
get { return m_SelectionType; }
set
{
var changed = m_SelectionType != value;
m_SelectionType = value;
if (m_SelectionType == SelectionType.None)
{
ClearSelectionWithoutValidation();
}
else
{
if (allowNoSelection)
ClearSelectionWithoutValidation();
else if (m_ItemsSource.Count > 0)
SetSelectionInternal(new[] { 0 }, false, false);
else
ClearSelectionWithoutValidation();
}
m_RangeSelectionOrigin = -1;
PostSelection(updatePreviousSelection: true, sendNotification: true);
#if ENABLE_RUNTIME_DATA_BINDINGS
if (changed)
NotifyPropertyChanged(in selectionTypeProperty);
#endif
}
}
///
/// Whether the GridView allows to have no selection when the selection type is or .
///
#if ENABLE_RUNTIME_DATA_BINDINGS
[CreateProperty]
#endif
#if ENABLE_UXML_SERIALIZED_DATA
[UxmlAttribute]
#endif
public bool allowNoSelection
{
get => m_AllowNoSelection;
set
{
var changed = m_AllowNoSelection != value;
m_AllowNoSelection = value;
if (HasValidDataAndBindings() && !m_AllowNoSelection && m_SelectedIndices.Count == 0 && m_ItemsSource.Count > 0)
SetSelectionInternal(new[] { 0 }, true, true);
#if ENABLE_RUNTIME_DATA_BINDINGS
if (changed)
NotifyPropertyChanged(in allowNoSelectionProperty);
#endif
}
}
///
/// Returns true if the soft-selection is in progress.
///
public bool isSelecting => m_SoftSelectIndex != -1;
///
/// Enable this property to display a border around the GridView.
///
///
/// If set to true, a border appears around the ScrollView.
///
public bool showBorder
{
get => ClassListContains(k_BorderUssClassName);
set => EnableInClassList(k_BorderUssClassName, value);
}
///
/// Prevents the grid view from scrolling when the user presses a modifier key at the same time as scrolling.
///
public bool preventScrollWithModifiers { get; set; } = k_DefaultPreventScrollWithModifiers;
///
/// Callback for unbinding a data item from the VisualElement.
///
///
/// The method called by this callback receives the VisualElement to unbind, and the index of the
/// element to unbind it from.
///
public Action unbindItem { get; set; }
///
/// Callback for getting the ID of an item.
///
///
/// The method called by this callback receives the index of the item to get the ID from.
///
public Func getItemId
{
get => m_GetItemId;
set
{
m_GetItemId = value;
Refresh();
}
}
bool DefaultAcceptStartDrag(Vector2 worldPosition)
{
if (!HasValidDataAndBindings())
return false;
var idx = GetIndexByWorldPosition(worldPosition);
return idx >= 0 && idx < itemsSource.Count;
}
internal List rowPool => m_RowPool;
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
Refresh();
}
void ISerializationCallbackReceiver.OnBeforeSerialize() { }
///
/// Callback triggered when the user acts on a selection of one or more items, for example by double-clicking or pressing Enter.
///
///
/// This callback receives an enumerable that contains the item or items chosen.
///
public event Action> itemsChosen;
///
/// Callback triggered when the selection changes.
///
///
/// This callback receives an enumerable that contains the item or items selected.
///
public event Action> selectionChanged;
///
/// Callback triggered when the selection changes.
///
///
/// This callback receives an enumerable that contains the indices of selected items.
///
public event Action> selectedIndicesChanged;
///
/// Callback triggered when the user right-clicks on an item.
///
///
/// This callback receives an enumerable that contains the item or items selected.
///
public event Action contextClicked;
///
/// Callback triggered when the user double-clicks on an item.
///
public event Action doubleClicked;
///
/// Callback triggered when drag has started.
///
public event Action dragStarted;
///
/// Callback triggered when items are dragged.
///
public event Action dragUpdated;
///
/// Callback triggered when drag has finished.
///
public event Action dragFinished;
///
/// Callback triggered when drag has been canceled.
///
public event Action dragCanceled;
///
/// Adds an item to the collection of selected items.
///
/// Item index.
public void AddToSelection(int index)
{
AddToSelection(new[] { index }, true, true);
}
internal void AddToSelection(int index, bool updatePrevious, bool notify)
{
AddToSelection(new[] { index }, updatePrevious, notify);
}
///
/// Deselects any selected items.
///
public void ClearSelection()
{
if (m_SelectedIndices.Count == 0 || !allowNoSelection)
return;
ClearSelectionWithoutValidation();
PostSelection(true, true);
}
///
/// Clear the selection without triggering selection changed event.
///
public void ClearSelectionWithoutNotify()
{
if (m_SelectedIndices.Count == 0 || !allowNoSelection)
return;
ClearSelectionWithoutValidation();
m_RangeSelectionOrigin = -1;
m_PreviouslySelectedIndices.Clear();
}
///
/// Clears the GridView, recreates all visible visual elements, and rebinds all items.
///
///
/// Call this method whenever the data source changes.
///
public void Refresh()
{
foreach (var recycledRow in m_RowPool)
{
recycledRow.Clear();
}
m_RowPool.Clear();
scrollView.Clear();
m_VisibleRowCount = 0;
m_SelectedIndices.Clear();
m_SelectedItems.Clear();
m_SoftSelectIndex = -1;
m_SoftSelectIndexWasPreviouslySelected = false;
m_OriginalSelection.Clear();
var newSelectedIds = new List();
// O(n)
if (m_SelectedIds.Count > 0)
{
// Add selected objects to working lists.
for (var index = 0; index < m_ItemsSource.Count; ++index)
{
var id = GetIdFromIndex(index);
if (!m_SelectedIds.Contains(id))
continue;
m_SelectedIndices.Add(index);
m_SelectedItems.Add(m_ItemsSource[index]);
newSelectedIds.Add(id);
}
}
m_SelectedIds.Clear();
m_SelectedIds.AddRange(newSelectedIds);
if (!HasValidDataAndBindings())
return;
m_LastHeight = scrollView.layout.height;
if (float.IsNaN(m_LastHeight))
return;
m_FirstVisibleIndex = Math.Min((int)(m_ScrollOffset / resolvedItemHeight) * columnCount, m_ItemsSource.Count - 1);
ResizeHeight(m_LastHeight);
if (!allowNoSelection && m_SelectedIds.Count == 0)
{
if (m_ItemsSource.Count > 0)
SetSelectionInternal(new[]
{
m_FirstVisibleIndex >= 0 ? m_FirstVisibleIndex : 0
}, true, true);
}
else
{
PostSelection(true, true);
}
}
///
/// Rebinds a single item if it is currently visible in the collection view.
///
/// The item index.
internal void RefreshItem(int index)
{
foreach (var recycledRow in m_RowPool)
{
if (recycledRow.ContainsIndex(index, out var indexInRow))
{
var item = makeItem != null && index < itemsSource.Count ? makeItem.Invoke() : CreateDummyItemElement();
SetupItemElement(item);
recycledRow.RemoveAt(indexInRow);
recycledRow.Insert(indexInRow, item);
bindItem.Invoke(item, recycledRow.indices[indexInRow]);
recycledRow.SetSelected(indexInRow, m_SelectedIds.Contains(recycledRow.ids[indexInRow]));
break;
}
}
}
///
/// Removes an item from the collection of selected items.
///
/// The item index.
public void RemoveFromSelection(int index)
{
RemoveFromSelectionInternal(index, true, true);
}
internal void RemoveFromSelectionInternal(int index, bool updatePrevious, bool notify)
{
if (!HasValidDataAndBindings())
return;
if (m_SelectedIndices.Count == 1 && m_SelectedIndices[0] == index && !allowNoSelection)
return;
RemoveFromSelectionWithoutValidation(index);
PostSelection(updatePrevious, notify);
}
///
/// Scrolls to a specific item index and makes it visible.
///
/// Item index to scroll to. Specify -1 to make the last item visible.
public void ScrollToItem(int index)
{
if (!HasValidDataAndBindings())
return;
if (m_VisibleRowCount == 0 || index < -1)
return;
var pixelAlignedItemHeight = resolvedItemHeight;
var lastRowIndex = Mathf.FloorToInt((itemsSource.Count - 1) / (float)columnCount);
var maxOffset = Mathf.Max(0, lastRowIndex * pixelAlignedItemHeight - m_LastHeight + pixelAlignedItemHeight);
var targetRowIndex = Mathf.FloorToInt(index / (float)columnCount);
var targetOffset = targetRowIndex * pixelAlignedItemHeight;
var currentOffset = scrollView.scrollOffset.y;
var d = targetOffset - currentOffset;
if (index == -1)
{
scrollView.scrollOffset = Vector2.up * maxOffset;
}
else if (d < 0)
{
scrollView.scrollOffset = Vector2.up * targetOffset;
}
else if (d > m_LastHeight - pixelAlignedItemHeight)
{
// need to scroll up so the item should be visible in last row
targetOffset += pixelAlignedItemHeight - m_LastHeight;
scrollView.scrollOffset = Vector2.up * Mathf.Min(maxOffset, targetOffset);
}
// else do nothing because the item is already entirely visible
schedule.Execute(() => ResizeHeight(m_LastHeight)).ExecuteLater(2L);
}
///
/// Sets the currently selected item.
///
/// The item index.
public void SetSelection(int index)
{
if (index < 0 || itemsSource == null || index >= itemsSource.Count)
{
ClearSelection();
return;
}
SetSelection(new[] { index });
}
///
/// Sets a collection of selected items.
///
/// The collection of the indices of the items to be selected.
public void SetSelection(IEnumerable indices)
{
SetSelectionInternal(indices, true, true);
}
///
/// Sets a collection of selected items without triggering a selection change callback.
///
/// The collection of items to be selected.
public void SetSelectionWithoutNotify(IEnumerable indices)
{
indices ??= Array.Empty();
switch (selectionType)
{
case SelectionType.None:
return;
case SelectionType.Single:
var lastIndex = -1;
var count = 0;
foreach (var index in indices)
{
lastIndex = index;
count++;
}
if (count > 1)
indices = new[] { lastIndex };
break;
case SelectionType.Multiple:
break;
default:
throw new ArgumentOutOfRangeException();
}
SetSelectionInternal(indices, true, false);
}
internal void AddToSelection(IList indexes, bool updatePrevious, bool notify)
{
if (!HasValidDataAndBindings() || indexes == null || indexes.Count == 0)
return;
foreach (var index in indexes)
AddToSelectionWithoutValidation(index);
PostSelection(updatePrevious, notify);
}
internal void SelectAll()
{
if (selectionType != SelectionType.Multiple)
{
return;
}
for (var index = 0; index < itemsSource.Count; index++)
{
var id = GetIdFromIndex(index);
var item = m_ItemsSource[index];
foreach (var recycledRow in m_RowPool)
{
if (recycledRow.ContainsId(id, out var indexInRow))
recycledRow.SetSelected(indexInRow, true);
}
if (!m_SelectedIds.Contains(id))
{
m_SelectedIds.Add(id);
m_SelectedIndices.Add(index);
m_SelectedItems.Add(item);
}
}
PostSelection(true, true);
}
internal void SetSelectionInternal(IEnumerable indices, bool updatePrevious, bool sendNotification)
{
indices ??= Array.Empty();
switch (selectionType)
{
case SelectionType.None:
return;
case SelectionType.Single:
var lastIndex = -1;
var count = 0;
foreach (var index in indices)
{
lastIndex = index;
count++;
}
if (count > 1)
indices = new[] { lastIndex };
break;
case SelectionType.Multiple:
break;
default:
throw new ArgumentOutOfRangeException();
}
if (!allowNoSelection)
{
// Check if empty.
using var enumerator = indices.GetEnumerator();
if (!enumerator.MoveNext())
return;
}
ClearSelectionWithoutValidation();
foreach (var index in indices)
AddToSelectionWithoutValidation(index);
PostSelection(updatePrevious, sendNotification);
}
void AddToSelectionWithoutValidation(int index)
{
if (m_ItemsSource == null || index < 0 || index >= m_ItemsSource.Count || m_SelectedIndices.Contains(index))
return;
var id = GetIdFromIndex(index);
var item = m_ItemsSource[index];
foreach (var recycledRow in m_RowPool)
{
if (recycledRow.ContainsId(id, out var indexInRow))
recycledRow.SetSelected(indexInRow, true);
}
m_SelectedIds.Add(id);
m_SelectedIndices.Add(index);
m_SelectedItems.Add(item);
}
internal VisualElement GetVisualElement(int index)
{
if (index < 0 || index >= m_ItemsSource.Count || !m_SelectedIndices.Contains(index))
return null;
return GetVisualElementInternal(index);
}
internal VisualElement GetVisualElementWithoutSelection(int index)
{
if (index < 0 || index >= m_ItemsSource.Count)
return null;
return GetVisualElementInternal(index);
}
VisualElement GetVisualElementInternal(int index)
{
var id = GetIdFromIndex(index);
foreach (var recycledRow in m_RowPool)
{
if (recycledRow.ContainsId(id, out var indexInRow))
return recycledRow.ElementAt(indexInRow);
}
return null;
}
void ClearSelectionWithoutValidation()
{
foreach (var recycledRow in m_RowPool)
recycledRow.ClearSelection();
m_SelectedIds.Clear();
m_SelectedIndices.Clear();
m_SelectedItems.Clear();
}
VisualElement CreateDummyItemElement()
{
var item = new VisualElement { pickingMode = PickingMode.Ignore };
SetupItemElement(item);
return item;
}
void DoRangeSelection(int rangeSelectionFinalIndex, bool updatePrevious, bool notify)
{
var max = -1;
var min = -1;
foreach (var i in m_SelectedIndices)
{
if (max == -1 || i > max)
max = i;
if (min == -1 || i < min)
min = i;
}
m_RangeSelectionOrigin = m_IsRangeSelectionDirectionUp ? max : min;
ClearSelectionWithoutValidation();
// Add range
var range = new List();
m_IsRangeSelectionDirectionUp = rangeSelectionFinalIndex < m_RangeSelectionOrigin;
if (m_IsRangeSelectionDirectionUp)
{
for (var i = rangeSelectionFinalIndex; i <= m_RangeSelectionOrigin; i++)
range.Add(i);
}
else
{
for (var i = rangeSelectionFinalIndex; i >= m_RangeSelectionOrigin; i--)
range.Add(i);
}
AddToSelection(range, updatePrevious, notify);
}
void DoContextClickAfterSelect(PointerDownEvent evt)
{
contextClicked?.Invoke(evt);
}
void DoSoftSelect(PointerDownEvent evt, int clickCount)
{
var clickedIndex = GetIndexByWorldPosition(evt.position);
if (clickedIndex > m_ItemsSource.Count - 1 || clickedIndex < 0)
{
if (evt.button == (int)MouseButton.LeftMouse && allowNoSelection)
ClearSelection();
return;
}
m_SoftSelectIndex = clickedIndex;
m_SoftSelectIndexWasPreviouslySelected = m_SelectedIndices.Contains(clickedIndex);
if (clickCount == 1)
{
if (selectionType == SelectionType.None)
return;
if (selectionType == SelectionType.Multiple && evt.actionKey)
{
m_RangeSelectionOrigin = clickedIndex;
// Add/remove single clicked element
var clickedItemId = GetIdFromIndex(clickedIndex);
if (m_SelectedIds.Contains(clickedItemId))
RemoveFromSelectionInternal(clickedIndex, false, false);
else
AddToSelection(clickedIndex, false, false);
}
else if (selectionType == SelectionType.Multiple && evt.shiftKey)
{
if (m_RangeSelectionOrigin == -1 || m_SelectedIndices.Count == 0)
{
m_RangeSelectionOrigin = clickedIndex;
SetSelectionInternal(new[] { clickedIndex }, false, false);
}
else
{
DoRangeSelection(clickedIndex, false, false);
}
}
else if (selectionType == SelectionType.Multiple && m_SoftSelectIndexWasPreviouslySelected)
{
// Do noting, selection will be processed OnPointerUp.
// If drag and drop will be started GridViewDragger will capture the mouse and GridView will not receive the mouse up event.
}
else // single
{
m_RangeSelectionOrigin = clickedIndex;
if (!(m_SelectedIndices.Count == 1 && m_SelectedIndices[0] == clickedIndex))
{
SetSelectionInternal(new[] { clickedIndex }, false, false);
}
}
}
ScrollToItem(clickedIndex);
}
int GetIdFromIndex(int index)
{
if (m_GetItemId == null)
return index;
return m_GetItemId(index);
}
bool HasValidDataAndBindings()
{
return itemsSource != null && makeItem != null && bindItem != null;
}
void PostSelection(bool updatePreviousSelection, bool sendNotification)
{
if (AreSequencesEqual(m_PreviouslySelectedIndices, m_SelectedIndices))
return;
if (updatePreviousSelection)
{
m_PreviouslySelectedIndices.Clear();
m_PreviouslySelectedIndices.AddRange(m_SelectedIndices);
}
if (sendNotification)
{
selectionChanged?.Invoke(m_SelectedItems);
selectedIndicesChanged?.Invoke(m_SelectedIndices);
}
}
void OnAttachToPanel(AttachToPanelEvent evt)
{
if (evt.destinationPanel == null)
return;
if (!UnityEngine.Device.Application.isMobilePlatform)
scrollView.AddManipulator(dragger);
scrollView.RegisterCallback(OnClick);
scrollView.RegisterCallback(OnPointerDown);
scrollView.RegisterCallback(OnPointerUp);
scrollView.RegisterCallback(OnPointerMove);
RegisterCallback(OnKeyDown);
RegisterCallback(OnNavigationMove);
RegisterCallback(OnNavigationCancel);
scrollView.RegisterCallback(OnWheel, TrickleDown.TrickleDown);
}
void OnCustomStyleResolved(CustomStyleResolvedEvent e)
{
if (!m_ItemHeightIsInline && e.customStyle.TryGetValue(s_ItemHeightProperty, out var height))
{
if (m_ItemHeight != height)
{
m_ItemHeight = height;
Refresh();
}
}
}
void OnDetachFromPanel(DetachFromPanelEvent evt)
{
if (evt.originPanel == null)
return;
scrollView.RemoveManipulator(dragger);
scrollView.UnregisterCallback(OnClick);
scrollView.UnregisterCallback(OnPointerDown);
scrollView.UnregisterCallback(OnPointerUp);
scrollView.UnregisterCallback(OnPointerMove);
UnregisterCallback(OnKeyDown);
UnregisterCallback(OnNavigationMove);
UnregisterCallback(OnNavigationCancel);
scrollView.UnregisterCallback(OnWheel, TrickleDown.TrickleDown);
}
void OnWheel(WheelEvent evt)
{
if (preventScrollWithModifiers && evt.modifiers != EventModifiers.None)
evt.StopImmediatePropagation();
}
void OnClick(ClickEvent evt)
{
if (!HasValidDataAndBindings())
return;
if (evt.clickCount == 2)
{
var clickedIndex = GetIndexByWorldPosition(evt.position);
if (clickedIndex >= 0 && clickedIndex < m_ItemsSource.Count)
{
doubleClicked?.Invoke(clickedIndex);
Apply(GridOperations.Choose, evt.shiftKey);
}
}
}
void OnPointerMove(PointerMoveEvent evt)
{
m_HasPointerMoved = true;
}
void OnPointerDown(PointerDownEvent evt)
{
evt.StopImmediatePropagation();
if (!HasValidDataAndBindings())
return;
if (!evt.isPrimary)
return;
var capturingElement = panel?.GetCapturingElement(evt.pointerId);
// if the pointer is captured by a child of the scroll view, abort any selection
if (capturingElement is VisualElement ve &&
ve != scrollView &&
ve.FindCommonAncestor(scrollView) != null)
return;
m_OriginalSelection.Clear();
m_OriginalSelection.AddRange(m_SelectedIndices);
m_OriginalScrollOffset = m_ScrollOffset;
m_SoftSelectIndex = -1;
var clickCount = m_HasPointerMoved ? 1 : evt.clickCount;
m_HasPointerMoved = false;
DoSoftSelect(evt, clickCount);
if (evt.button == (int)MouseButton.RightMouse)
DoContextClickAfterSelect(evt);
}
void OnPointerUp(PointerUpEvent evt)
{
if (!HasValidDataAndBindings())
return;
if (!evt.isPrimary)
return;
if (m_SoftSelectIndex == -1)
return;
var index = m_SoftSelectIndex;
m_SoftSelectIndex = -1;
if (m_SoftSelectIndexWasPreviouslySelected &&
evt.button == (int)MouseButton.LeftMouse &&
evt.modifiers == EventModifiers.None)
{
ProcessSingleClick(index);
return;
}
PostSelection(true, true);
}
void CancelSoftSelect()
{
if (m_SoftSelectIndex != -1)
{
SetSelectionInternal(m_OriginalSelection, false, false);
scrollView.verticalScroller.value = m_OriginalScrollOffset;
}
m_SoftSelectIndex = -1;
}
///
/// Returns the index of the item at the given position.
///
///
/// The position is relative to the top left corner of the grid. No check is made to see if the index is valid.
///
/// The position of the item in the world-space.
/// The index of the item at the given position.
public int GetIndexByWorldPosition(Vector2 worldPosition)
{
var localPosition = scrollView.contentContainer.WorldToLocal(worldPosition);
return Mathf.FloorToInt(localPosition.y / resolvedItemHeight) * columnCount + Mathf.FloorToInt(localPosition.x / resolvedItemWidth);
}
internal VisualElement GetElementAt(int index)
{
foreach (var row in m_RowPool)
{
if (row.ContainsId(index, out var indexInRow))
return row[indexInRow];
}
return null;
}
void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
Refresh();
}
void OnScroll(float offset)
{
if (!HasValidDataAndBindings())
return;
m_ScrollOffset = offset;
var pixelAlignedItemHeight = resolvedItemHeight;
var firstVisibleIndex = Mathf.FloorToInt(offset / pixelAlignedItemHeight) * columnCount;
scrollView.contentContainer.style.paddingTop = Mathf.FloorToInt(firstVisibleIndex / (float)columnCount) * pixelAlignedItemHeight;
scrollView.contentContainer.style.height = (Mathf.CeilToInt(itemsSource.Count / (float)columnCount) * pixelAlignedItemHeight);
if (firstVisibleIndex != m_FirstVisibleIndex)
{
m_FirstVisibleIndex = firstVisibleIndex;
if (m_RowPool.Count > 0)
{
// we try to avoid rebinding a few items
if (m_FirstVisibleIndex < m_RowPool[0].firstIndex) //we're scrolling up
{
//How many do we have to swap back
var count = m_RowPool[0].firstIndex - m_FirstVisibleIndex;
var inserting = m_ScrollInsertionList;
for (var i = 0; i < count && m_RowPool.Count > 0; ++i)
{
var last = m_RowPool[^1];
inserting.Add(last);
m_RowPool.RemoveAt(m_RowPool.Count - 1); //we remove from the end
last.SendToBack(); //We send the element to the top of the list (back in z-order)
}
inserting.Reverse();
m_ScrollInsertionList = m_RowPool;
m_RowPool = inserting;
m_RowPool.AddRange(m_ScrollInsertionList);
m_ScrollInsertionList.Clear();
}
else if (m_FirstVisibleIndex > m_RowPool[0].firstIndex) //down
{
var inserting = m_ScrollInsertionList;
var checkIndex = 0;
while (checkIndex < m_RowPool.Count && m_FirstVisibleIndex > m_RowPool[checkIndex].firstIndex)
{
var first = m_RowPool[checkIndex];
inserting.Add(first);
first.BringToFront(); //We send the element to the bottom of the list (front in z-order)
checkIndex++;
}
m_RowPool.RemoveRange(0, checkIndex); //we remove them all at once
m_RowPool.AddRange(inserting); // add them back to the end
inserting.Clear();
}
//Let's rebind everything
for (var rowIndex = 0; rowIndex < m_RowPool.Count; rowIndex++)
{
for (var colIndex = 0; colIndex < columnCount; colIndex++)
{
var index = rowIndex * columnCount + colIndex + m_FirstVisibleIndex;
var isFirstColumn = colIndex == 0;
var isLastColumn = colIndex == columnCount - 1;
if (index < itemsSource.Count)
{
var item = m_RowPool[rowIndex].ElementAt(colIndex);
if (m_RowPool[rowIndex].indices[colIndex] == RecycledRow.kUndefinedIndex)
{
var newItem = makeItem != null ? makeItem.Invoke() : CreateDummyItemElement();
SetupItemElement(newItem);
m_RowPool[rowIndex].RemoveAt(colIndex);
m_RowPool[rowIndex].Insert(colIndex, newItem);
item = newItem;
}
Setup(item, index);
item.EnableInClassList(k_FirstColumnUssClassName, isFirstColumn);
item.EnableInClassList(k_LastColumnUssClassName, isLastColumn);
}
else
{
var remainingOldItems = columnCount - colIndex;
while (remainingOldItems > 0)
{
m_RowPool[rowIndex].RemoveAt(colIndex);
m_RowPool[rowIndex].Insert(colIndex, CreateDummyItemElement());
m_RowPool[rowIndex][colIndex].EnableInClassList(k_FirstColumnUssClassName, isFirstColumn);
m_RowPool[rowIndex][colIndex].EnableInClassList(k_LastColumnUssClassName, isLastColumn);
m_RowPool[rowIndex].ids.RemoveAt(colIndex);
m_RowPool[rowIndex].ids.Insert(colIndex, RecycledRow.kUndefinedIndex);
m_RowPool[rowIndex].indices.RemoveAt(colIndex);
m_RowPool[rowIndex].indices.Insert(colIndex, RecycledRow.kUndefinedIndex);
remainingOldItems--;
}
}
}
}
}
}
}
void OnSizeChanged(GeometryChangedEvent evt)
{
if (!HasValidDataAndBindings())
return;
if (Mathf.Approximately(evt.newRect.height, evt.oldRect.height))
return;
ResizeHeight(evt.newRect.height);
}
void ProcessSingleClick(int clickedIndex)
{
m_RangeSelectionOrigin = clickedIndex;
SetSelection(clickedIndex);
}
void RemoveFromSelectionWithoutValidation(int index)
{
if (!m_SelectedIndices.Contains(index))
return;
var id = GetIdFromIndex(index);
var item = m_ItemsSource[index];
foreach (var recycledRow in m_RowPool)
{
if (recycledRow.ContainsId(id, out var indexInRow))
recycledRow.SetSelected(indexInRow, false);
}
m_SelectedIds.Remove(id);
m_SelectedIndices.Remove(index);
m_SelectedItems.Remove(item);
}
void ResizeHeight(float height)
{
if (!HasValidDataAndBindings())
return;
var pixelAlignedItemHeight = resolvedItemHeight;
var rowCountForSource = Mathf.CeilToInt(itemsSource.Count / (float)columnCount);
var contentHeight = rowCountForSource * pixelAlignedItemHeight;
scrollView.contentContainer.style.height = contentHeight;
var scrollableHeight = Mathf.Max(0, contentHeight - scrollView.contentViewport.layout.height);
scrollView.verticalScroller.highValue = scrollableHeight;
scrollView.verticalScroller.value = Mathf.Min(m_ScrollOffset, scrollView.verticalScroller.highValue);
var rowCountForHeight = Mathf.FloorToInt(height / pixelAlignedItemHeight) + k_ExtraVisibleRows;
var rowCount = Math.Min(rowCountForHeight, rowCountForSource);
if (m_VisibleRowCount != rowCount)
{
if (m_VisibleRowCount > rowCount)
{
// Shrink
var removeCount = m_VisibleRowCount - rowCount;
for (var i = 0; i < removeCount; i++)
{
var lastIndex = m_RowPool.Count - 1;
m_RowPool[lastIndex].Clear();
scrollView.Remove(m_RowPool[lastIndex]);
m_RowPool.RemoveAt(lastIndex);
}
}
else
{
// Grow
var addCount = rowCount - m_VisibleRowCount;
for (var i = 0; i < addCount; i++)
{
var recycledRow = new RecycledRow(resolvedItemHeight);
for (var indexInRow = 0; indexInRow < columnCount; indexInRow++)
{
var index = m_RowPool.Count * columnCount + indexInRow + m_FirstVisibleIndex;
var item = makeItem != null && index < itemsSource.Count ? makeItem.Invoke() : CreateDummyItemElement();
SetupItemElement(item);
recycledRow.Add(item);
if (index < itemsSource.Count)
{
Setup(item, index);
}
else
{
recycledRow.ids.Add(RecycledRow.kUndefinedIndex);
recycledRow.indices.Add(RecycledRow.kUndefinedIndex);
}
var isFirstColumn = indexInRow == 0;
var isLastColumn = indexInRow == columnCount - 1;
item.EnableInClassList(k_FirstColumnUssClassName, isFirstColumn);
item.EnableInClassList(k_LastColumnUssClassName, isLastColumn);
}
m_RowPool.Add(recycledRow);
recycledRow.style.height = pixelAlignedItemHeight;
scrollView.Add(recycledRow);
}
}
m_VisibleRowCount = rowCount;
}
m_LastHeight = height;
}
void Setup(VisualElement item, int newIndex)
{
var newId = GetIdFromIndex(newIndex);
if (!(item.parent is RecycledRow recycledRow))
throw new Exception("The item to setup can't be orphan");
var indexInRow = recycledRow.IndexOf(item);
if (recycledRow.indices.Count <= indexInRow)
{
recycledRow.indices.Add(RecycledRow.kUndefinedIndex);
recycledRow.ids.Add(RecycledRow.kUndefinedIndex);
}
if (recycledRow.indices[indexInRow] == newIndex)
return;
if (recycledRow.indices[indexInRow] != RecycledRow.kUndefinedIndex)
unbindItem?.Invoke(item, recycledRow.indices[indexInRow]);
recycledRow.indices[indexInRow] = newIndex;
recycledRow.ids[indexInRow] = newId;
bindItem.Invoke(item, recycledRow.indices[indexInRow]);
recycledRow.SetSelected(indexInRow, m_SelectedIds.Contains(recycledRow.ids[indexInRow]));
}
static bool AreSequencesEqual(IList first, IList second)
{
if (first == null || second == null || first.Count != second.Count)
return false;
for (var i = 0; i < first.Count; i++)
{
if (!first[i].Equals(second[i]))
return false;
}
return true;
}
void SetupItemElement(VisualElement item)
{
item.AddToClassList(k_ItemUssClassName);
item.style.position = Position.Relative;
if (itemSquare)
{
var itemSize = (float)itemHeight;
item.style.height = item.style.width = itemSize;
}
else
{
item.style.flexBasis = 0;
item.style.flexGrow = 1f;
item.style.flexShrink = 1f;
}
}
#if ENABLE_UXML_TRAITS
///
/// Instantiates a using data from a UXML file.
///
///
/// This class is added to every created from UXML.
///
public new class UxmlFactory : UxmlFactory { }
///
/// Defines for the .
///
///
/// This class defines the GridView element properties that you can use in a UI document asset (UXML file).
///
public new class UxmlTraits : BindableElement.UxmlTraits
{
readonly UxmlIntAttributeDescription m_ItemHeight = new UxmlIntAttributeDescription
{
name = "item-height",
obsoleteNames = new[] { "itemHeight" },
defaultValue = defaultItemHeight
};
readonly UxmlBoolAttributeDescription m_ItemSquare = new UxmlBoolAttributeDescription
{
name = "item-square",
defaultValue = false
};
readonly UxmlEnumAttributeDescription m_SelectionType = new UxmlEnumAttributeDescription
{
name = "selection-type",
defaultValue = SelectionType.Single
};
readonly UxmlBoolAttributeDescription m_ShowBorder = new UxmlBoolAttributeDescription
{
name = "show-border",
defaultValue = false
};
readonly UxmlBoolAttributeDescription m_PreventScrollWithModifiers = new UxmlBoolAttributeDescription
{
name = "prevent-scroll-with-modifiers",
defaultValue = k_DefaultPreventScrollWithModifiers
};
readonly UxmlBoolAttributeDescription m_AllowNoSelection = new UxmlBoolAttributeDescription
{
name = "allow-no-selection",
defaultValue = true
};
///
/// Returns an empty enumerable, because list views usually do not have child elements.
///
/// An empty enumerable.
public override IEnumerable uxmlChildElementsDescription
{
get { yield break; }
}
///
/// Initializes properties using values from the attribute bag.
///
/// The object to initialize.
/// The attribute bag.
/// The creation context; unused.
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
var view = (GridView)ve;
// Avoid setting itemHeight unless it's explicitly defined.
// Setting itemHeight property will activate inline property mode.
var itemHeight = 0;
if (m_ItemHeight.TryGetValueFromBag(bag, cc, ref itemHeight))
view.itemHeight = itemHeight;
view.itemSquare = m_ItemSquare.GetValueFromBag(bag, cc);
view.showBorder = m_ShowBorder.GetValueFromBag(bag, cc);
view.preventScrollWithModifiers = m_PreventScrollWithModifiers.GetValueFromBag(bag, cc);
view.selectionType = m_SelectionType.GetValueFromBag(bag, cc);
view.allowNoSelection = m_AllowNoSelection.GetValueFromBag(bag, cc);
}
}
#endif
internal class RecycledRow : VisualElement
{
public const int kUndefinedIndex = -1;
public readonly List ids;
public readonly List indices;
public RecycledRow(float height)
{
AddToClassList(k_RowUssClassName);
pickingMode = PickingMode.Ignore;
style.height = height;
indices = new List();
ids = new List();
}
public int firstIndex => indices.Count > 0 ? indices[0] : kUndefinedIndex;
public int lastIndex => indices.Count > 0 ? indices[^1] : kUndefinedIndex;
public void ClearSelection()
{
for (var i = 0; i < childCount; i++)
{
SetSelected(i, false);
}
}
public bool ContainsId(int id, out int indexInRow)
{
indexInRow = ids.IndexOf(id);
return indexInRow >= 0;
}
public bool ContainsIndex(int index, out int indexInRow)
{
indexInRow = indices.IndexOf(index);
return indexInRow >= 0;
}
public void SetSelected(int indexInRow, bool selected)
{
if (childCount > indexInRow && indexInRow >= 0)
{
if (selected)
ElementAt(indexInRow).AddToClassList(itemSelectedVariantUssClassName);
else
ElementAt(indexInRow).RemoveFromClassList(itemSelectedVariantUssClassName);
}
}
}
}
}