A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Collections.Specialized;
5using System.Linq;
6using UnityEngine;
7using UnityEngine.UIElements;
8
9namespace UnityEditor.Tilemaps.External
10{
11 /// <summary>
12 /// A view containing recycled rows with items inside.
13 /// </summary>
14 [UxmlElement]
15 internal partial class GridView : BindableElement, ISerializationCallbackReceiver
16 {
17 const int k_ExtraVisibleRows = 2;
18
19 /// <summary>
20 /// The USS class name for GridView elements.
21 /// </summary>
22 /// <remarks>
23 /// Unity adds this USS class to every instance of the GridView element. Any styling applied to
24 /// this class affects every GridView located beside, or below the stylesheet in the visual tree.
25 /// </remarks>
26 const string k_UssClassName = "unity-grid-view";
27
28 /// <summary>
29 /// The USS class name for GridView elements with a border.
30 /// </summary>
31 /// <remarks>
32 /// Unity adds this USS class to an instance of the GridView element if the instance's
33 /// <see cref="GridView.showBorder"/> property is set to true. Any styling applied to this class
34 /// affects every such GridView located beside, or below the stylesheet in the visual tree.
35 /// </remarks>
36 const string k_BorderUssClassName = k_UssClassName + "--with-border";
37
38 /// <summary>
39 /// The USS class name of item elements in GridView elements.
40 /// </summary>
41 /// <remarks>
42 /// Unity adds this USS class to every item element the GridView contains. Any styling applied to
43 /// this class affects every item element located beside, or below the stylesheet in the visual tree.
44 /// </remarks>
45 const string k_ItemUssClassName = k_UssClassName + "__item";
46
47 /// <summary>
48 /// The USS class name of selected item elements in the GridView.
49 /// </summary>
50 /// <remarks>
51 /// Unity adds this USS class to every selected element in the GridView. The <see cref="GridView.selectionType"/>
52 /// property decides if zero, one, or more elements can be selected. Any styling applied to
53 /// this class affects every GridView item located beside, or below the stylesheet in the visual tree.
54 /// </remarks>
55 internal const string itemSelectedVariantUssClassName = k_ItemUssClassName + "--selected";
56
57 /// <summary>
58 /// The USS class name of rows in the GridView.
59 /// </summary>
60 const string k_RowUssClassName = k_UssClassName + "__row";
61
62 const int k_DefaultItemHeight = 30;
63
64 static CustomStyleProperty<int> s_ItemHeightProperty = new CustomStyleProperty<int>("--unity-item-height");
65
66 internal readonly ScrollView scrollView;
67
68 readonly List<int> m_SelectedIds = new List<int>();
69
70 readonly List<int> m_SelectedIndices = new List<int>();
71
72 readonly List<object> m_SelectedItems = new List<object>();
73
74 Action<VisualElement, int> m_BindItem;
75
76 int m_ColumnCount = 1;
77
78 int m_FirstVisibleIndex;
79
80 Func<int, int> m_GetItemId;
81
82 int m_ItemHeight = k_DefaultItemHeight;
83
84 bool m_ItemHeightIsInline;
85
86 IList m_ItemsSource;
87
88 float m_LastHeight;
89
90 Func<VisualElement> m_MakeItem;
91
92 int m_RangeSelectionOrigin = -1;
93
94 List<RecycledRow> m_RowPool = new List<RecycledRow>();
95
96 // we keep this list in order to minimize temporary gc allocs
97 List<RecycledRow> m_ScrollInsertionList = new List<RecycledRow>();
98
99 // Persisted.
100 float m_ScrollOffset;
101
102 SelectionType m_SelectionType;
103
104 Vector3 m_TouchDownPosition;
105
106 long m_TouchDownTime;
107
108 int m_VisibleRowCount;
109
110 /// <summary>
111 /// Creates a <see cref="GridView"/> with all default properties. The <see cref="GridView.itemsSource"/>,
112 /// <see cref="GridView.itemHeight"/>, <see cref="GridView.makeItem"/> and <see cref="GridView.bindItem"/> properties
113 /// must all be set for the GridView to function properly.
114 /// </summary>
115 public GridView()
116 {
117 AddStyleSheetPath("Packages/com.unity.2d.tilemap/Editor/UI/External/GridView.uss");
118 AddToClassList(k_UssClassName);
119
120 selectionType = SelectionType.Multiple;
121
122 m_ScrollOffset = 0.0f;
123
124 scrollView = new ScrollView { viewDataKey = "grid-view__scroll-view" };
125 scrollView.StretchToParentSize();
126 scrollView.verticalScroller.valueChanged += OnScroll;
127
128 RegisterCallback<GeometryChangedEvent>(OnSizeChanged);
129 RegisterCallback<CustomStyleResolvedEvent>(OnCustomStyleResolved);
130
131 scrollView.contentContainer.RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
132 scrollView.contentContainer.RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
133
134 hierarchy.Add(scrollView);
135
136 scrollView.contentContainer.focusable = true;
137 scrollView.contentContainer.usageHints &= ~UsageHints.GroupTransform; // Scroll views with virtualized content shouldn't have the "view transform" optimization
138 }
139
140 void OnKeyPress(KeyDownEvent evt)
141 {
142 switch (evt.keyCode)
143 {
144 case KeyCode.A when evt.actionKey:
145 SelectAll();
146 evt.StopPropagation();
147 break;
148 case KeyCode.Home:
149 ScrollToItem(0);
150 evt.StopPropagation();
151 break;
152 case KeyCode.End:
153 ScrollToItem(-1);
154 evt.StopPropagation();
155 break;
156 case KeyCode.Escape:
157 ClearSelection();
158 evt.StopPropagation();
159 break;
160 case KeyCode.Return:
161 onItemsChosen?.Invoke(m_SelectedItems);
162 evt.StopPropagation();
163 break;
164 case KeyCode.LeftArrow:
165 var firstIndexInRow = selectedIndex - selectedIndex % columnCount;
166 if (selectedIndex >= 0 && selectedIndex > firstIndexInRow)
167 {
168 var next = evt.actionKey ? firstIndexInRow : selectedIndex - 1;
169 if (next < firstIndexInRow)
170 next = firstIndexInRow;
171
172 if (evt.shiftKey)
173 DoRangeSelection(next);
174 else
175 m_RangeSelectionOrigin = selectedIndex = next;
176
177 evt.StopPropagation();
178 }
179 break;
180 case KeyCode.RightArrow:
181 {
182 var currentRow = selectedIndex / columnCount;
183 var lastIndexInRow = Math.Min((currentRow + 1) * columnCount - 1, itemsSource.Count - 1);
184 if (selectedIndex >= 0 && selectedIndex < lastIndexInRow)
185 {
186 var next = evt.actionKey ? lastIndexInRow : selectedIndex + 1;
187 if (next > lastIndexInRow)
188 next = lastIndexInRow;
189
190 if (evt.shiftKey)
191 DoRangeSelection(next);
192 else
193 m_RangeSelectionOrigin = selectedIndex = next;
194
195 evt.StopPropagation();
196 }
197 break;
198 }
199 case KeyCode.UpArrow:
200 if (selectedIndex >= 0)
201 {
202 var next = evt.actionKey ?
203 selectedIndex % columnCount :
204 selectedIndex - columnCount;
205 if (next >= 0 && selectedIndex != next)
206 {
207 if (evt.shiftKey)
208 DoRangeSelection(next);
209 else
210 m_RangeSelectionOrigin = selectedIndex = next;
211
212 ScrollToItem(evt.actionKey ? 0 : selectedIndex);
213
214 evt.StopPropagation();
215 }
216 }
217 break;
218 case KeyCode.DownArrow:
219 {
220 if (selectedIndex >= 0)
221 {
222 var targetId = (Mathf.FloorToInt((float)itemsSource.Count / columnCount)) * columnCount + selectedIndex % columnCount;
223 var next = evt.actionKey ?
224 targetId >= itemsSource.Count ? targetId - columnCount : targetId :
225 selectedIndex + columnCount;
226 if (next < itemsSource.Count && selectedIndex != next)
227 {
228 if (evt.shiftKey)
229 DoRangeSelection(next);
230 else
231 m_RangeSelectionOrigin = selectedIndex = next;
232
233 ScrollToItem(evt.actionKey ? -1 : selectedIndex);
234
235 evt.StopPropagation();
236 }
237 }
238 break;
239 }
240 }
241 }
242
243 /// <summary>
244 /// Constructs a <see cref="GridView"/>, with all required properties provided.
245 /// </summary>
246 /// <param name="itemsSource">The list of items to use as a data source.</param>
247 /// <param name="itemHeight">The height of each item, in pixels.</param>
248 /// <param name="makeItem">The factory method to call to create a display item. The method should return a
249 /// VisualElement that can be bound to a data item.</param>
250 /// <param name="bindItem">The method to call to bind a data item to a display item. The method
251 /// receives as parameters the display item to bind, and the index of the data item to bind it to.</param>
252 public GridView(IList itemsSource, int itemHeight, Func<VisualElement> makeItem, Action<VisualElement, int> bindItem)
253 : this()
254 {
255 m_ItemsSource = itemsSource;
256 m_ItemHeight = itemHeight;
257 m_ItemHeightIsInline = true;
258
259 m_MakeItem = makeItem;
260 m_BindItem = bindItem;
261 }
262
263 /// <summary>
264 /// Callback for binding a data item to the visual element.
265 /// </summary>
266 /// <remarks>
267 /// The method called by this callback receives the VisualElement to bind, and the index of the
268 /// element to bind it to.
269 /// </remarks>
270 public Action<VisualElement, int> bindItem
271 {
272 get { return m_BindItem; }
273 set
274 {
275 m_BindItem = value;
276 Refresh();
277 }
278 }
279
280 /// <summary>
281 /// The number of columns for this grid.
282 /// </summary>
283 public int columnCount
284 {
285 get => m_ColumnCount;
286
287 set
288 {
289 if (m_ColumnCount != value && value > 0)
290 {
291 m_ScrollOffset = 0;
292 m_ColumnCount = value;
293 Refresh();
294 }
295 }
296 }
297
298 /// <summary>
299 /// Returns the content container for the <see cref="GridView"/>. Because the GridView control automatically manages
300 /// its content, this always returns null.
301 /// </summary>
302 public override VisualElement contentContainer => null;
303
304 /// <summary>
305 /// The height of a single item in the list, in pixels.
306 /// </summary>
307 /// <remarks>
308 /// GridView requires that all visual elements have the same height so that it can calculate the
309 /// scroller size.
310 ///
311 /// This property must be set for the list view to function.
312 /// </remarks>
313 [UxmlAttribute]
314 public int itemHeight
315 {
316 get { return m_ItemHeight; }
317 set
318 {
319 if (m_ItemHeight != value && value > 0)
320 {
321 m_ItemHeightIsInline = true;
322 m_ItemHeight = value;
323 Refresh();
324 }
325 }
326 }
327
328 /// <summary>
329 ///
330 /// </summary>
331 public float itemWidth => (scrollView.contentViewport.layout.width / columnCount);
332
333 /// <summary>
334 /// The data source for list items.
335 /// </summary>
336 /// <remarks>
337 /// This list contains the items that the <see cref="GridView"/> displays.
338 ///
339 /// This property must be set for the list view to function.
340 /// </remarks>
341 public IList itemsSource
342 {
343 get { return m_ItemsSource; }
344 set
345 {
346 if (m_ItemsSource is INotifyCollectionChanged oldCollection)
347 {
348 oldCollection.CollectionChanged -= OnItemsSourceCollectionChanged;
349 }
350
351 m_ItemsSource = value;
352 if (m_ItemsSource is INotifyCollectionChanged newCollection)
353 {
354 newCollection.CollectionChanged += OnItemsSourceCollectionChanged;
355 }
356
357 Refresh();
358 }
359 }
360
361 /// <summary>
362 /// Callback for constructing the VisualElement that is the template for each recycled and re-bound element in the list.
363 /// </summary>
364 /// <remarks>
365 /// This callback needs to call a function that constructs a blank <see cref="VisualElement"/> that is
366 /// bound to an element from the list.
367 ///
368 /// The GridView automatically creates enough elements to fill the visible area, and adds more if the area
369 /// is expanded. As the user scrolls, the GridView cycles elements in and out as they appear or disappear.
370 ///
371 /// This property must be set for the list view to function.
372 /// </remarks>
373 public Func<VisualElement> makeItem
374 {
375 get { return m_MakeItem; }
376 set
377 {
378 if (m_MakeItem == value)
379 return;
380 m_MakeItem = value;
381 Refresh();
382 }
383 }
384
385 /// <summary>
386 /// The computed pixel-aligned height for the list elements.
387 /// </summary>
388 /// <remarks>
389 /// This value changes depending on the current panel's DPI scaling.
390 /// </remarks>
391 /// <seealso cref="GridView.itemHeight"/>
392 public float resolvedItemHeight
393 {
394 get
395 {
396 var dpiScaling = 1;//this.GetScaledPixelsPerPoint();
397 return Mathf.Round(itemHeight * dpiScaling) / dpiScaling;
398 }
399 }
400
401 /// <summary>
402 ///
403 /// </summary>
404 public float resolvedItemWidth
405 {
406 get
407 {
408 var dpiScaling = 1;//this.GetScaledPixelsPerPoint();
409 return Mathf.Round(itemWidth * dpiScaling) / dpiScaling;
410 }
411 }
412
413 /// <summary>
414 /// Returns or sets the selected item's index in the data source. If multiple items are selected, returns the
415 /// first selected item's index. If multiple items are provided, sets them all as selected.
416 /// </summary>
417 public int selectedIndex
418 {
419 get { return m_SelectedIndices.Count == 0 ? -1 : m_SelectedIndices.First(); }
420 set { SetSelection(value); }
421 }
422
423 /// <summary>
424 /// Returns the indices of selected items in the data source. Always returns an enumerable, even if no item is selected, or a
425 /// single item is selected.
426 /// </summary>
427 public IEnumerable<int> selectedIndices => m_SelectedIndices;
428
429 /// <summary>
430 /// Returns the selected item from the data source. If multiple items are selected, returns the first selected item.
431 /// </summary>
432 public object selectedItem => m_SelectedItems.Count == 0 ? null : m_SelectedItems.First();
433
434 /// <summary>
435 /// Returns the selected items from the data source. Always returns an enumerable, even if no item is selected, or a single
436 /// item is selected.
437 /// </summary>
438 public IEnumerable<object> selectedItems => m_SelectedItems;
439
440 /// <summary>
441 /// Returns the IDs of selected items in the data source. Always returns an enumerable, even if no item is selected, or a
442 /// single item is selected.
443 /// </summary>
444 public IEnumerable<int> selectedIds => m_SelectedIds;
445
446 /// <summary>
447 /// Controls the selection type.
448 /// </summary>
449 /// <remarks>
450 /// You can set the GridView to make one item selectable at a time, make multiple items selectable, or disable selections completely.
451 ///
452 /// When you set the GridView to disable selections, any current selection is cleared.
453 /// </remarks>
454 [UxmlAttribute]
455 public SelectionType selectionType
456 {
457 get { return m_SelectionType; }
458 set
459 {
460 m_SelectionType = value;
461 if (m_SelectionType == SelectionType.None || (m_SelectionType == SelectionType.Single && m_SelectedIndices.Count > 1))
462 {
463 ClearSelection();
464 }
465 }
466 }
467
468 /// <summary>
469 /// Enable this property to display a border around the GridView.
470 /// </summary>
471 /// <remarks>
472 /// If set to true, a border appears around the ScrollView.
473 /// </remarks>
474 [UxmlAttribute]
475 public bool showBorder
476 {
477 get => ClassListContains(k_BorderUssClassName);
478 set => EnableInClassList(k_BorderUssClassName, value);
479 }
480
481 /// <summary>
482 /// Callback for unbinding a data item from the VisualElement.
483 /// </summary>
484 /// <remarks>
485 /// The method called by this callback receives the VisualElement to unbind, and the index of the
486 /// element to unbind it from.
487 /// </remarks>
488 public Action<VisualElement, int> unbindItem { get; set; }
489
490 internal Func<int, int> getItemId
491 {
492 get { return m_GetItemId; }
493 set
494 {
495 m_GetItemId = value;
496 Refresh();
497 }
498 }
499
500 internal List<RecycledRow> rowPool
501 {
502 get { return m_RowPool; }
503 }
504
505 void ISerializationCallbackReceiver.OnAfterDeserialize()
506 {
507 Refresh();
508 }
509
510 void ISerializationCallbackReceiver.OnBeforeSerialize() {}
511
512 /// <summary>
513 /// Callback triggered when the user acts on a selection of one or more items, for example by double-clicking or pressing Enter.
514 /// </summary>
515 /// <remarks>
516 /// This callback receives an enumerable that contains the item or items chosen.
517 /// </remarks>
518 public event Action<IEnumerable<object>> onItemsChosen;
519
520 /// <summary>
521 /// Callback triggered when the selection changes.
522 /// </summary>
523 /// <remarks>
524 /// This callback receives an enumerable that contains the item or items selected.
525 /// </remarks>
526 public event Action<IEnumerable<object>> onSelectionChange;
527
528 /// <summary>
529 /// Adds an item to the collection of selected items.
530 /// </summary>
531 /// <param name="index">Item index.</param>
532 public void AddToSelection(int index)
533 {
534 AddToSelection(new[] { index });
535 }
536
537 /// <summary>
538 /// Deselects any selected items.
539 /// </summary>
540 public void ClearSelection()
541 {
542 if (!HasValidDataAndBindings() || m_SelectedIds.Count == 0)
543 return;
544
545 ClearSelectionWithoutValidation();
546 NotifyOfSelectionChange();
547 }
548
549 /// <summary>
550 /// Clears the GridView, recreates all visible visual elements, and rebinds all items.
551 /// </summary>
552 /// <remarks>
553 /// Call this method whenever the data source changes.
554 /// </remarks>
555 public void Refresh()
556 {
557 foreach (var recycledRow in m_RowPool)
558 {
559 recycledRow.Clear();
560 }
561
562 m_RowPool.Clear();
563 scrollView.Clear();
564 m_VisibleRowCount = 0;
565
566 m_SelectedIndices.Clear();
567 m_SelectedItems.Clear();
568
569 // O(n)
570 if (m_SelectedIds.Count > 0)
571 {
572 // Add selected objects to working lists.
573 for (var index = 0; index < m_ItemsSource.Count; ++index)
574 {
575 if (!m_SelectedIds.Contains(GetIdFromIndex(index))) continue;
576
577 m_SelectedIndices.Add(index);
578 m_SelectedItems.Add(m_ItemsSource[index]);
579 }
580 }
581
582 if (!HasValidDataAndBindings())
583 return;
584
585 m_LastHeight = scrollView.layout.height;
586
587 if (float.IsNaN(m_LastHeight))
588 return;
589
590 m_FirstVisibleIndex = Math.Min((int)(m_ScrollOffset / resolvedItemHeight) * columnCount, m_ItemsSource.Count - 1);
591 ResizeHeight(m_LastHeight);
592 }
593
594 /// <summary>
595 /// Rebinds a single item if it is currently visible in the collection view.
596 /// </summary>
597 /// <param name="index">The item index.</param>
598 internal void RefreshItem(int index)
599 {
600 foreach (var recycledRow in m_RowPool)
601 {
602 if (recycledRow.ContainsIndex(index, out var indexInRow))
603 {
604 var item = makeItem != null && index < itemsSource.Count ? makeItem.Invoke() : CreateDummyItemElement();
605 SetupItemElement(item);
606
607 recycledRow.RemoveAt(indexInRow);
608 recycledRow.Insert(indexInRow, item);
609
610 bindItem.Invoke(item, recycledRow.indices[indexInRow]);
611 recycledRow.SetSelected(indexInRow, m_SelectedIds.Contains(recycledRow.ids[indexInRow]));
612 break;
613 }
614 }
615 }
616
617 /// <summary>
618 /// Removes an item from the collection of selected items.
619 /// </summary>
620 /// <param name="index">The item index.</param>
621 public void RemoveFromSelection(int index)
622 {
623 if (!HasValidDataAndBindings())
624 return;
625
626 RemoveFromSelectionWithoutValidation(index);
627 NotifyOfSelectionChange();
628
629 //SaveViewData();
630 }
631
632 /// <summary>
633 /// Scrolls to a specific item index and makes it visible.
634 /// </summary>
635 /// <param name="index">Item index to scroll to. Specify -1 to make the last item visible.</param>
636 public void ScrollToItem(int index)
637 {
638 if (!HasValidDataAndBindings())
639 return;
640
641 if (m_VisibleRowCount == 0 || index < -1)
642 return;
643
644 var pixelAlignedItemHeight = resolvedItemHeight;
645 var actualCount = Math.Min(Mathf.FloorToInt(m_LastHeight / pixelAlignedItemHeight) * columnCount, itemsSource.Count);
646
647 if (index == -1)
648 {
649 // Scroll to last item
650 if (itemsSource.Count < actualCount)
651 scrollView.scrollOffset = new Vector2(0, 0);
652 else
653 scrollView.scrollOffset = new Vector2(0, Mathf.FloorToInt(itemsSource.Count / (float)columnCount) * pixelAlignedItemHeight);
654 }
655 else if (m_FirstVisibleIndex >= index)
656 {
657 scrollView.scrollOffset = Vector2.up * (pixelAlignedItemHeight * Mathf.FloorToInt(index / (float)columnCount));
658 }
659 else // index > first
660 {
661 if (index < m_FirstVisibleIndex + actualCount)
662 return;
663
664 var d = Mathf.FloorToInt(index - actualCount / (float)columnCount);
665 var visibleOffset = pixelAlignedItemHeight - (m_LastHeight - Mathf.FloorToInt(actualCount / (float)columnCount) * pixelAlignedItemHeight);
666 var yScrollOffset = pixelAlignedItemHeight * d + visibleOffset;
667
668 scrollView.scrollOffset = new Vector2(scrollView.scrollOffset.x, yScrollOffset);
669 }
670 }
671
672 /// <summary>
673 /// Sets the currently selected item.
674 /// </summary>
675 /// <param name="index">The item index.</param>
676 public void SetSelection(int index)
677 {
678 if (index < 0 || itemsSource == null || index >= itemsSource.Count)
679 {
680 ClearSelection();
681 return;
682 }
683
684 SetSelection(new[] { index });
685 }
686
687 /// <summary>
688 /// Sets a collection of selected items.
689 /// </summary>
690 /// <param name="indices">The collection of the indices of the items to be selected.</param>
691 public void SetSelection(IEnumerable<int> indices)
692 {
693 switch (selectionType)
694 {
695 case SelectionType.None:
696 return;
697 case SelectionType.Single:
698 if (indices != null)
699 indices = new[] { indices.Last() };
700 break;
701 case SelectionType.Multiple:
702 break;
703 default:
704 throw new ArgumentOutOfRangeException();
705 }
706
707 SetSelectionInternal(indices, true);
708 }
709
710 /// <summary>
711 /// Sets a collection of selected items without triggering a selection change callback.
712 /// </summary>
713 /// <param name="indices">The collection of items to be selected.</param>
714 public void SetSelectionWithoutNotify(IEnumerable<int> indices)
715 {
716 SetSelectionInternal(indices, false);
717 }
718
719 internal void AddToSelection(IList<int> indexes)
720 {
721 if (!HasValidDataAndBindings() || indexes == null || indexes.Count == 0)
722 return;
723
724 foreach (var index in indexes)
725 {
726 AddToSelectionWithoutValidation(index);
727 }
728
729 NotifyOfSelectionChange();
730
731 //SaveViewData();
732 }
733
734 internal void SelectAll()
735 {
736 if (!HasValidDataAndBindings())
737 return;
738
739 if (selectionType != SelectionType.Multiple)
740 {
741 return;
742 }
743
744 for (var index = 0; index < itemsSource.Count; index++)
745 {
746 var id = GetIdFromIndex(index);
747 var item = m_ItemsSource[index];
748
749 foreach (var recycledRow in m_RowPool)
750 {
751 if (recycledRow.ContainsId(id, out var indexInRow))
752 recycledRow.SetSelected(indexInRow, true);
753 }
754
755 if (!m_SelectedIds.Contains(id))
756 {
757 m_SelectedIds.Add(id);
758 m_SelectedIndices.Add(index);
759 m_SelectedItems.Add(item);
760 }
761 }
762
763 NotifyOfSelectionChange();
764
765 //SaveViewData();
766 }
767
768 internal void SetSelectionInternal(IEnumerable<int> indices, bool sendNotification)
769 {
770 if (!HasValidDataAndBindings() || indices == null)
771 return;
772
773 ClearSelectionWithoutValidation();
774 foreach (var index in indices.Where(index => index != -1))
775 {
776 AddToSelectionWithoutValidation(index);
777 }
778
779 if (sendNotification)
780 NotifyOfSelectionChange();
781
782 //SaveViewData();
783 }
784
785 void AddToSelectionWithoutValidation(int index)
786 {
787 if (m_SelectedIndices.Contains(index))
788 return;
789
790 var id = GetIdFromIndex(index);
791 var item = m_ItemsSource[index];
792
793 foreach (var recycledRow in m_RowPool)
794 {
795 if (recycledRow.ContainsId(id, out var indexInRow))
796 recycledRow.SetSelected(indexInRow, true);
797 }
798
799 m_SelectedIds.Add(id);
800 m_SelectedIndices.Add(index);
801 m_SelectedItems.Add(item);
802 }
803
804 void ClearSelectionWithoutValidation()
805 {
806 foreach (var recycledRow in m_RowPool)
807 {
808 recycledRow.ClearSelection();
809 }
810
811 m_SelectedIds.Clear();
812 m_SelectedIndices.Clear();
813 m_SelectedItems.Clear();
814 }
815
816 VisualElement CreateDummyItemElement()
817 {
818 var item = new VisualElement();
819 SetupItemElement(item);
820 return item;
821 }
822
823 void DoRangeSelection(int rangeSelectionFinalIndex)
824 {
825 ClearSelectionWithoutValidation();
826
827 // Add range
828 var range = new List<int>();
829 if (rangeSelectionFinalIndex < m_RangeSelectionOrigin)
830 {
831 for (var i = rangeSelectionFinalIndex; i <= m_RangeSelectionOrigin; i++)
832 {
833 range.Add(i);
834 }
835 }
836 else
837 {
838 for (var i = rangeSelectionFinalIndex; i >= m_RangeSelectionOrigin; i--)
839 {
840 range.Add(i);
841 }
842 }
843
844 AddToSelection(range);
845 }
846
847 void DoSelect(Vector2 localPosition, int clickCount, bool actionKey, bool shiftKey)
848 {
849 var clickedIndex = GetIndexByPosition(localPosition);
850 if (clickedIndex > m_ItemsSource.Count - 1)
851 return;
852
853 var clickedItemId = GetIdFromIndex(clickedIndex);
854 switch (clickCount)
855 {
856 case 1:
857 if (selectionType == SelectionType.None)
858 return;
859
860 if (selectionType == SelectionType.Multiple && actionKey)
861 {
862 m_RangeSelectionOrigin = clickedIndex;
863
864 // Add/remove single clicked element
865 if (m_SelectedIds.Contains(clickedItemId))
866 RemoveFromSelection(clickedIndex);
867 else
868 AddToSelection(clickedIndex);
869 }
870 else if (selectionType == SelectionType.Multiple && shiftKey)
871 {
872 if (m_RangeSelectionOrigin == -1)
873 {
874 m_RangeSelectionOrigin = clickedIndex;
875 SetSelection(clickedIndex);
876 }
877 else
878 {
879 DoRangeSelection(clickedIndex);
880 }
881 }
882 else if (selectionType == SelectionType.Multiple && m_SelectedIndices.Contains(clickedIndex))
883 {
884 // Do noting, selection will be processed OnPointerUp.
885 // If drag and drop will be started GridViewDragger will capture the mouse and GridView will not receive the mouse up event.
886 }
887 else // single
888 {
889 m_RangeSelectionOrigin = clickedIndex;
890 SetSelection(clickedIndex);
891 }
892
893 break;
894 case 2:
895 if (onItemsChosen != null)
896 {
897 ProcessSingleClick(clickedIndex);
898 }
899
900 onItemsChosen?.Invoke(m_SelectedItems);
901 break;
902 }
903 }
904
905 int GetIdFromIndex(int index)
906 {
907 if (m_GetItemId == null)
908 return index;
909 return m_GetItemId(index);
910 }
911
912 bool HasValidDataAndBindings()
913 {
914 return itemsSource != null && makeItem != null && bindItem != null;
915 }
916
917 void NotifyOfSelectionChange()
918 {
919 if (!HasValidDataAndBindings())
920 return;
921
922 onSelectionChange?.Invoke(m_SelectedItems);
923 }
924
925 void OnAttachToPanel(AttachToPanelEvent evt)
926 {
927 if (evt.destinationPanel == null)
928 return;
929
930 scrollView.contentContainer.RegisterCallback<PointerDownEvent>(OnPointerDown);
931 scrollView.contentContainer.RegisterCallback<PointerUpEvent>(OnPointerUp);
932 scrollView.contentContainer.RegisterCallback<KeyDownEvent>(OnKeyPress);
933 }
934
935 void OnCustomStyleResolved(CustomStyleResolvedEvent e)
936 {
937 int height;
938 if (!m_ItemHeightIsInline && e.customStyle.TryGetValue(s_ItemHeightProperty, out height))
939 {
940 if (m_ItemHeight != height)
941 {
942 m_ItemHeight = height;
943 Refresh();
944 }
945 }
946 }
947
948 void OnDetachFromPanel(DetachFromPanelEvent evt)
949 {
950 if (evt.originPanel == null)
951 return;
952
953 scrollView.contentContainer.UnregisterCallback<PointerDownEvent>(OnPointerDown);
954 scrollView.contentContainer.UnregisterCallback<PointerUpEvent>(OnPointerUp);
955 scrollView.contentContainer.UnregisterCallback<KeyDownEvent>(OnKeyPress);
956 }
957
958 void OnPointerDown(PointerDownEvent evt)
959 {
960 if (!HasValidDataAndBindings())
961 return;
962
963 if (!evt.isPrimary)
964 return;
965
966 if (evt.button != (int)MouseButton.LeftMouse)
967 return;
968
969 if (evt.pointerType != "mouse")
970 {
971 m_TouchDownTime = evt.timestamp;
972 m_TouchDownPosition = evt.position;
973 return;
974 }
975
976 DoSelect(evt.localPosition, evt.clickCount, evt.actionKey, evt.shiftKey);
977 }
978
979 void OnPointerUp(PointerUpEvent evt)
980 {
981 if (!HasValidDataAndBindings())
982 return;
983
984 if (!evt.isPrimary)
985 return;
986
987 if (evt.button != (int)MouseButton.LeftMouse)
988 return;
989
990 if (evt.pointerType != "mouse")
991 {
992 var delay = evt.timestamp - m_TouchDownTime;
993 var delta = evt.position - m_TouchDownPosition;
994 if (delay < 500 && delta.sqrMagnitude <= 100)
995 {
996 DoSelect(evt.localPosition, evt.clickCount, evt.actionKey, evt.shiftKey);
997 }
998 }
999 else
1000 {
1001 var clickedIndex = GetIndexByPosition(evt.localPosition);
1002 if (selectionType == SelectionType.Multiple
1003 && !evt.shiftKey
1004 && !evt.actionKey
1005 && m_SelectedIndices.Count > 1
1006 && m_SelectedIndices.Contains(clickedIndex))
1007 {
1008 ProcessSingleClick(clickedIndex);
1009 }
1010 }
1011 }
1012
1013 int GetIndexByPosition(Vector2 localPosition)
1014 {
1015 return Mathf.FloorToInt(localPosition.y / resolvedItemHeight) * columnCount + Mathf.FloorToInt(localPosition.x / resolvedItemWidth);
1016 }
1017
1018 internal VisualElement GetElementAt(int index)
1019 {
1020 foreach (var row in m_RowPool)
1021 {
1022 if (row.ContainsId(index, out var indexInRow))
1023 return row[indexInRow];
1024 }
1025
1026 return null;
1027 }
1028
1029 void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
1030 {
1031 Refresh();
1032 }
1033
1034 void OnScroll(float offset)
1035 {
1036 if (!HasValidDataAndBindings())
1037 return;
1038
1039 m_ScrollOffset = offset;
1040 var pixelAlignedItemHeight = resolvedItemHeight;
1041 var firstVisibleIndex = Mathf.FloorToInt(offset / pixelAlignedItemHeight) * columnCount;
1042
1043 scrollView.contentContainer.style.paddingTop = Mathf.FloorToInt(firstVisibleIndex / (float)columnCount) * pixelAlignedItemHeight;
1044 scrollView.contentContainer.style.height = (Mathf.CeilToInt(itemsSource.Count / (float)columnCount) * pixelAlignedItemHeight);
1045
1046 if (firstVisibleIndex != m_FirstVisibleIndex)
1047 {
1048 m_FirstVisibleIndex = firstVisibleIndex;
1049
1050 if (m_RowPool.Count > 0)
1051 {
1052 // we try to avoid rebinding a few items
1053 if (m_FirstVisibleIndex < m_RowPool[0].firstIndex) //we're scrolling up
1054 {
1055 //How many do we have to swap back
1056 var count = m_RowPool[0].firstIndex - m_FirstVisibleIndex;
1057
1058 var inserting = m_ScrollInsertionList;
1059
1060 for (var i = 0; i < count && m_RowPool.Count > 0; ++i)
1061 {
1062 var last = m_RowPool[m_RowPool.Count - 1];
1063 inserting.Add(last);
1064 m_RowPool.RemoveAt(m_RowPool.Count - 1); //we remove from the end
1065
1066 last.SendToBack(); //We send the element to the top of the list (back in z-order)
1067 }
1068
1069 inserting.Reverse();
1070
1071 m_ScrollInsertionList = m_RowPool;
1072 m_RowPool = inserting;
1073 m_RowPool.AddRange(m_ScrollInsertionList);
1074 m_ScrollInsertionList.Clear();
1075 }
1076 else if (m_FirstVisibleIndex > m_RowPool[0].firstIndex) //down
1077 {
1078 var inserting = m_ScrollInsertionList;
1079
1080 var checkIndex = 0;
1081 while (checkIndex < m_RowPool.Count && m_FirstVisibleIndex > m_RowPool[checkIndex].firstIndex)
1082 {
1083 var first = m_RowPool[checkIndex];
1084 inserting.Add(first);
1085 first.BringToFront(); //We send the element to the bottom of the list (front in z-order)
1086 checkIndex++;
1087 }
1088
1089 m_RowPool.RemoveRange(0, checkIndex); //we remove them all at once
1090 m_RowPool.AddRange(inserting); // add them back to the end
1091 inserting.Clear();
1092 }
1093
1094 //Let's rebind everything
1095 for (var rowIndex = 0; rowIndex < m_RowPool.Count; rowIndex++)
1096 {
1097 for (var colIndex = 0; colIndex < columnCount; colIndex++)
1098 {
1099 var index = rowIndex * columnCount + colIndex + m_FirstVisibleIndex;
1100
1101 if (index < itemsSource.Count)
1102 {
1103 var item = m_RowPool[rowIndex].ElementAt(colIndex);
1104 if (m_RowPool[rowIndex].indices[colIndex] == RecycledRow.kUndefinedIndex)
1105 {
1106 var newItem = makeItem != null ? makeItem.Invoke() : CreateDummyItemElement();
1107 SetupItemElement(newItem);
1108 m_RowPool[rowIndex].RemoveAt(colIndex);
1109 m_RowPool[rowIndex].Insert(colIndex, newItem);
1110 item = newItem;
1111 }
1112
1113 Setup(item, index);
1114 }
1115 else
1116 {
1117 var remainingOldItems = columnCount - colIndex;
1118
1119 while (remainingOldItems > 0)
1120 {
1121 m_RowPool[rowIndex].RemoveAt(colIndex);
1122 m_RowPool[rowIndex].Insert(colIndex, CreateDummyItemElement());
1123 m_RowPool[rowIndex].ids.RemoveAt(colIndex);
1124 m_RowPool[rowIndex].ids.Insert(colIndex, RecycledRow.kUndefinedIndex);
1125 m_RowPool[rowIndex].indices.RemoveAt(colIndex);
1126 m_RowPool[rowIndex].indices.Insert(colIndex, RecycledRow.kUndefinedIndex);
1127 remainingOldItems--;
1128 }
1129 }
1130 }
1131 }
1132 }
1133 }
1134 }
1135
1136 void OnSizeChanged(GeometryChangedEvent evt)
1137 {
1138 if (!HasValidDataAndBindings())
1139 return;
1140
1141 if (Mathf.Approximately(evt.newRect.height, evt.oldRect.height))
1142 return;
1143
1144 ResizeHeight(evt.newRect.height);
1145 }
1146
1147 void ProcessSingleClick(int clickedIndex)
1148 {
1149 m_RangeSelectionOrigin = clickedIndex;
1150 SetSelection(clickedIndex);
1151 }
1152
1153 void RemoveFromSelectionWithoutValidation(int index)
1154 {
1155 if (!m_SelectedIndices.Contains(index))
1156 return;
1157
1158 var id = GetIdFromIndex(index);
1159 var item = m_ItemsSource[index];
1160
1161 foreach (var recycledRow in m_RowPool)
1162 {
1163 if (recycledRow.ContainsId(id, out var indexInRow))
1164 recycledRow.SetSelected(indexInRow, false);
1165 }
1166
1167 m_SelectedIds.Remove(id);
1168 m_SelectedIndices.Remove(index);
1169 m_SelectedItems.Remove(item);
1170 }
1171
1172 void ResizeHeight(float height)
1173 {
1174 if (!HasValidDataAndBindings())
1175 return;
1176
1177 var pixelAlignedItemHeight = resolvedItemHeight;
1178 var rowCountForSource = Mathf.CeilToInt(itemsSource.Count / (float)columnCount);
1179 var contentHeight = rowCountForSource * pixelAlignedItemHeight;
1180 scrollView.contentContainer.style.height = contentHeight;
1181
1182 var scrollableHeight = Mathf.Max(0, contentHeight - scrollView.contentViewport.layout.height);
1183 scrollView.verticalScroller.highValue = scrollableHeight;
1184 scrollView.verticalScroller.value = Mathf.Min(m_ScrollOffset, scrollView.verticalScroller.highValue);
1185
1186 var rowCountForHeight = Mathf.FloorToInt(height / pixelAlignedItemHeight) + k_ExtraVisibleRows;
1187 var rowCount = Math.Min(rowCountForHeight, rowCountForSource);
1188
1189 if (m_VisibleRowCount != rowCount)
1190 {
1191 if (m_VisibleRowCount > rowCount)
1192 {
1193 // Shrink
1194 var removeCount = m_VisibleRowCount - rowCount;
1195 for (var i = 0; i < removeCount; i++)
1196 {
1197 var lastIndex = m_RowPool.Count - 1;
1198 m_RowPool[lastIndex].Clear();
1199 scrollView.Remove(m_RowPool[lastIndex]);
1200 m_RowPool.RemoveAt(lastIndex);
1201 }
1202 }
1203 else
1204 {
1205 // Grow
1206 var addCount = rowCount - m_VisibleRowCount;
1207 for (var i = 0; i < addCount; i++)
1208 {
1209 var recycledRow = new RecycledRow(resolvedItemHeight);
1210
1211 for (var indexInRow = 0; indexInRow < columnCount; indexInRow++)
1212 {
1213 var index = m_FirstVisibleIndex + m_RowPool.Count * columnCount + indexInRow;
1214 var item = makeItem != null && index < itemsSource.Count ? makeItem.Invoke() : CreateDummyItemElement();
1215 SetupItemElement(item);
1216
1217 recycledRow.Add(item);
1218
1219 if (index < itemsSource.Count)
1220 {
1221 Setup(item, index);
1222 }
1223 else
1224 {
1225 recycledRow.ids.Add(RecycledRow.kUndefinedIndex);
1226 recycledRow.indices.Add(RecycledRow.kUndefinedIndex);
1227 }
1228 }
1229
1230 m_RowPool.Add(recycledRow);
1231 recycledRow.style.height = pixelAlignedItemHeight;
1232
1233 scrollView.Add(recycledRow);
1234 }
1235 }
1236
1237 m_VisibleRowCount = rowCount;
1238 }
1239
1240 m_LastHeight = height;
1241 }
1242
1243 void Setup(VisualElement item, int newIndex)
1244 {
1245 var newId = GetIdFromIndex(newIndex);
1246
1247 if (!(item.parent is RecycledRow recycledRow))
1248 throw new Exception("The item to setup can't be orphan");
1249
1250 var indexInRow = recycledRow.IndexOf(item);
1251
1252 if (recycledRow.indices.Count <= indexInRow)
1253 {
1254 recycledRow.indices.Add(RecycledRow.kUndefinedIndex);
1255 recycledRow.ids.Add(RecycledRow.kUndefinedIndex);
1256 }
1257
1258 if (recycledRow.indices[indexInRow] == newIndex)
1259 return;
1260
1261 if (recycledRow.indices[indexInRow] != RecycledRow.kUndefinedIndex)
1262 unbindItem?.Invoke(item, recycledRow.indices[indexInRow]);
1263
1264 recycledRow.indices[indexInRow] = newIndex;
1265 recycledRow.ids[indexInRow] = newId;
1266
1267 bindItem.Invoke(item, recycledRow.indices[indexInRow]);
1268
1269 recycledRow.SetSelected(indexInRow, m_SelectedIds.Contains(recycledRow.ids[indexInRow]));
1270 }
1271
1272 void SetupItemElement(VisualElement item)
1273 {
1274 item.AddToClassList(k_ItemUssClassName);
1275 item.style.position = Position.Relative;
1276 item.style.height = itemHeight;
1277 item.style.width = itemWidth;
1278 }
1279
1280 internal class RecycledRow : VisualElement
1281 {
1282 public const int kUndefinedIndex = -1;
1283
1284 public readonly List<int> ids;
1285
1286 public readonly List<int> indices;
1287
1288 public RecycledRow(float height)
1289 {
1290 AddToClassList(k_RowUssClassName);
1291 style.height = height;
1292
1293 indices = new List<int>();
1294 ids = new List<int>();
1295 }
1296
1297 public int firstIndex => indices.Count > 0 ? indices[0] : kUndefinedIndex;
1298 public int lastIndex => indices.Count > 0 ? indices[indices.Count - 1] : kUndefinedIndex;
1299
1300 public void ClearSelection()
1301 {
1302 for (var i = 0; i < childCount; i++)
1303 {
1304 SetSelected(i, false);
1305 }
1306 }
1307
1308 public bool ContainsId(int id, out int indexInRow)
1309 {
1310 indexInRow = ids.IndexOf(id);
1311 return indexInRow >= 0;
1312 }
1313
1314 public bool ContainsIndex(int index, out int indexInRow)
1315 {
1316 indexInRow = indices.IndexOf(index);
1317 return indexInRow >= 0;
1318 }
1319
1320 public void SetSelected(int indexInRow, bool selected)
1321 {
1322 if (childCount > indexInRow && indexInRow >= 0)
1323 {
1324 if (selected)
1325 {
1326 AddElementToClass(ElementAt(indexInRow), itemSelectedVariantUssClassName, true);
1327 }
1328 else
1329 {
1330 RemoveElementFromClass(ElementAt(indexInRow), itemSelectedVariantUssClassName, true);
1331 }
1332 }
1333 }
1334
1335 static void AddElementToClass(VisualElement element, string className, bool includeChildren = false)
1336 {
1337 element.AddToClassList(className);
1338 if (includeChildren)
1339 {
1340 foreach (var child in element.Children())
1341 child.AddToClassList(className);
1342 }
1343 }
1344 static void RemoveElementFromClass(VisualElement element, string className, bool includeChildren = false)
1345 {
1346 element.RemoveFromClassList(className);
1347 if (includeChildren)
1348 {
1349 foreach (var child in element.Children())
1350 child.RemoveFromClassList(className);
1351 }
1352 }
1353 }
1354 }
1355}