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}