using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; using UnityEngine.EventSystems; using System.Collections.Generic; using System; using System.Linq; namespace UIWidgets { /// /// ListViewBase event. /// [Serializable] public class ListViewBaseEvent : UnityEvent { } /// /// ListViewFocus event. /// [Serializable] public class ListViewFocusEvent : UnityEvent { } /// /// ListViewBase. /// You can use it for creating custom ListViews. /// abstract public class ListViewBase : MonoBehaviour, ISelectHandler, IDeselectHandler, ISubmitHandler, ICancelHandler { [SerializeField] [HideInInspector] List items = new List(); List callbacks = new List(); /// /// Gets or sets the items. /// /// Items. public List Items { get { return new List(items); } set { UpdateItems(value); } } /// /// The destroy game objects after setting new items. /// [SerializeField] [HideInInspector] public bool DestroyGameObjects = true; /// /// Allow select multiple items. /// [SerializeField] public bool Multiple; [SerializeField] int selectedIndex = -1; /// /// Gets or sets the index of the selected item. /// /// The index of the selected. public int SelectedIndex { get { return selectedIndex; } set { if (value==-1) { if (selectedIndex!=-1) { Deselect(selectedIndex); } selectedIndex = value; } else { Select(value); } } } [SerializeField] List selectedIndicies = new List(); /// /// Gets or sets indicies of the selected items. /// /// The selected indicies. public List SelectedIndicies { get { return new List(selectedIndicies); } set { var deselect = selectedIndicies.Except(value).ToArray(); var select = value.Except(selectedIndicies).ToArray(); deselect.ForEach(Deselect); select.ForEach(Select); } } /// /// OnSelect event. /// public ListViewBaseEvent OnSelect = new ListViewBaseEvent(); /// /// OnDeselect event. /// public ListViewBaseEvent OnDeselect = new ListViewBaseEvent(); /// /// OnSubmit event. /// public UnityEvent onSubmit = new UnityEvent(); /// /// OnCancel event. /// public UnityEvent onCancel = new UnityEvent(); /// /// OnItemSelect event. /// public UnityEvent onItemSelect = new UnityEvent(); /// /// onItemCancel event. /// public UnityEvent onItemCancel = new UnityEvent(); /// /// The container for items objects. /// [SerializeField] public Transform Container; /// /// OnFocusIn event. /// public ListViewFocusEvent OnFocusIn = new ListViewFocusEvent(); /// /// OnFocusOut event. /// public ListViewFocusEvent OnFocusOut = new ListViewFocusEvent(); /// /// Set item indicies when items updated. /// [NonSerialized] protected bool SetItemIndicies = true; GameObject Unused; void Awake() { Start(); } [System.NonSerialized] bool isStartedListViewBase; /// /// Start this instance. /// public virtual void Start() { if (isStartedListViewBase) { return ; } isStartedListViewBase = true; Unused = new GameObject("unused base"); Unused.SetActive(false); Unused.transform.SetParent(transform, false); if ((selectedIndex!=-1) && (selectedIndicies.Count==0)) { selectedIndicies.Add(selectedIndex); } selectedIndicies.RemoveAll(NotIsValid); if (selectedIndicies.Count==0) { selectedIndex = -1; } } /// /// Determines if item not exists with the specified index. /// /// true, if item not exists, false otherwise. /// Index. protected bool NotIsValid(int index) { return !IsValid(index); } /// /// Updates the items. /// public virtual void UpdateItems() { UpdateItems(items); } /// /// Determines whether this instance is horizontal. Not implemented for ListViewBase. /// /// true if this instance is horizontal; otherwise, false. public virtual bool IsHorizontal() { throw new NotSupportedException(); } /// /// Gets the default height of the item. Not implemented for ListViewBase. /// /// The default item height. public virtual float GetDefaultItemHeight() { throw new NotSupportedException(); } /// /// Gets the default width of the item. Not implemented for ListViewBase. /// /// The default item width. public virtual float GetDefaultItemWidth() { throw new NotSupportedException(); } /// /// Gets the spacing between items. Not implemented for ListViewBase. /// /// The item spacing. public virtual float GetItemSpacing() { throw new NotSupportedException(); } /// /// Removes the callback. /// /// Item. /// Index. void RemoveCallback(ListViewItem item, int index) { if (item == null) { return; } if (index < callbacks.Count) { item.onClick.RemoveListener(callbacks[index]); } item.onSubmit.RemoveListener(Toggle); item.onCancel.RemoveListener(OnItemCancel); item.onSelect.RemoveListener(HighlightColoring); item.onDeselect.RemoveListener(Coloring); item.onMove.RemoveListener(OnItemMove); } /// /// Raises the item cancel event. /// /// Item. void OnItemCancel(ListViewItem item) { if (EventSystem.current.alreadySelecting) { return; } EventSystem.current.SetSelectedGameObject(gameObject); onItemCancel.Invoke(); } /// /// Removes the callbacks. /// void RemoveCallbacks() { if (callbacks.Count > 0) { items.ForEach(RemoveCallback); } callbacks.Clear(); } /// /// Adds the callbacks. /// void AddCallbacks() { items.ForEach(AddCallback); } /// /// Adds the callback. /// /// Item. /// Index. void AddCallback(ListViewItem item, int index) { callbacks.Insert(index, () => Toggle(item)); item.onClick.AddListener(callbacks[index]); item.onSubmit.AddListener(OnItemSubmit); item.onCancel.AddListener(OnItemCancel); item.onSelect.AddListener(HighlightColoring); item.onDeselect.AddListener(Coloring); item.onMove.AddListener(OnItemMove); } /// /// Raises the item select event. /// /// Item. void OnItemSelect(ListViewItem item) { onItemSelect.Invoke(); } /// /// Raises the item submit event. /// /// Item. void OnItemSubmit(ListViewItem item) { Toggle(item); if (!IsSelected(item.Index)) { HighlightColoring(item); } } /// /// Raises the item move event. /// /// Event data. /// Item. protected virtual void OnItemMove(AxisEventData eventData, ListViewItem item) { switch (eventData.moveDir) { case MoveDirection.Left: break; case MoveDirection.Right: break; case MoveDirection.Up: if (item.Index > 0) { SelectComponentByIndex(item.Index - 1); } break; case MoveDirection.Down: if (IsValid(item.Index + 1)) { SelectComponentByIndex(item.Index + 1); } break; } } /// /// Scrolls to item with specifid index. /// /// Index. public virtual void ScrollTo(int index) { } /// /// Add the specified item. /// /// Item. /// Index of added item. public virtual int Add(ListViewItem item) { if (item.transform.parent!=Container) { item.transform.SetParent(Container, false); } AddCallback(item, items.Count); items.Add(item); item.Index = callbacks.Count - 1; return callbacks.Count - 1; } /// /// Clear items of this instance. /// public virtual void Clear() { items.Clear(); UpdateItems(); } /// /// Remove the specified item. /// /// Item. /// Index of removed item. protected virtual int Remove(ListViewItem item) { RemoveCallbacks(); var index = item.Index; selectedIndicies = selectedIndicies.Where(x => x!=index).Select(x => x > index ? x-- : x).ToList(); if (selectedIndex==index) { Deselect(index); selectedIndex = selectedIndicies.Count > 0 ? selectedIndicies.Last() : -1; } else if (selectedIndex > index) { selectedIndex -= 1; } items.Remove(item); Free(item); AddCallbacks(); return index; } /// /// Free the specified item. /// /// Item. void Free(Component item) { if (item==null) { return ; } if (DestroyGameObjects) { if (item.gameObject==null) { return ; } Destroy(item.gameObject); } else { if ((item.transform==null) || (Unused==null) || (Unused.transform==null)) { return ; } item.transform.SetParent(Unused.transform, false); } } /// /// Updates the items. /// /// New items. void UpdateItems(List newItems) { RemoveCallbacks(); items.Where(item => item!=null && !newItems.Contains(item)).ForEach(Free); newItems.ForEach(UpdateItem); //selectedIndicies.Clear(); //selectedIndex = -1; items = newItems; AddCallbacks(); } void UpdateItem(ListViewItem item, int index) { if (item==null) { return ; } if (SetItemIndicies) { item.Index = index; } item.transform.SetParent(Container, false); } /// /// Determines if item exists with the specified index. /// /// true if item exists with the specified index; otherwise, false. /// Index. public virtual bool IsValid(int index) { return (index >= 0) && (index < items.Count); } /// /// Gets the item. /// /// The item. /// Index. protected ListViewItem GetItem(int index) { return items.Find(x => x.Index==index); } /// /// Select item by the specified index. /// /// Index. public virtual void Select(int index) { if (index==-1) { return ; } if (!IsValid(index)) { var message = string.Format("Index must be between 0 and Items.Count ({0}). Gameobject {1}.", items.Count - 1, name); throw new IndexOutOfRangeException(message); } if (IsSelected(index) && Multiple) { return ; } if (!Multiple) { if ((selectedIndex!=-1) && (selectedIndex!=index)) { Deselect(selectedIndex); } selectedIndicies.Clear(); } selectedIndicies.Add(index); selectedIndex = index; SelectItem(index); OnSelect.Invoke(index, GetItem(index)); } /// /// Silents the deselect specified indicies. /// /// Indicies. protected void SilentDeselect(List indicies) { selectedIndicies = selectedIndicies.Except(indicies).ToList(); selectedIndex = (selectedIndicies.Count > 0) ? selectedIndicies[selectedIndicies.Count - 1] : -1; } /// /// Silents the select specified indicies. /// /// Indicies. protected void SilentSelect(List indicies) { if (indicies==null) { indicies = new List(); } selectedIndicies.AddRange(indicies.Except(selectedIndicies)); selectedIndex = (selectedIndicies.Count > 0) ? selectedIndicies[selectedIndicies.Count - 1] : -1; } /// /// Deselect item by the specified index. /// /// Index. public void Deselect(int index) { if (index==-1) { return ; } if (!IsSelected(index)) { return ; } selectedIndicies.Remove(index); selectedIndex = (selectedIndicies.Count > 0) ? selectedIndicies.Last() : - 1; if (IsValid(index)) { DeselectItem(index); OnDeselect.Invoke(index, GetItem(index)); } } /// /// Determines if item is selected with the specified index. /// /// true if item is selected with the specified index; otherwise, false. /// Index. public bool IsSelected(int index) { return selectedIndicies.Contains(index); } /// /// Toggle item by the specified index. /// /// Index. public void Toggle(int index) { if (IsSelected(index) && Multiple) { Deselect(index); } else { Select(index); } } /// /// Toggle the specified item. /// /// Item. void Toggle(ListViewItem item) { var shift_pressed = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); var have_selected = selectedIndicies.Count > 0; if (Multiple && shift_pressed && have_selected && selectedIndicies[0]!=item.Index) { // deselect all items except first selectedIndicies.GetRange(1, selectedIndicies.Count - 1).ForEach(Deselect); // find min and max indicies var min = Mathf.Min(selectedIndicies[0], item.Index); var max = Mathf.Max(selectedIndicies[0], item.Index); // select items from min to max Enumerable.Range(min, max - min + 1).ForEach(Select); return ; } Toggle(item.Index); } /// /// Gets the index of the component. /// /// The component index. /// Item. protected int GetComponentIndex(ListViewItem item) { return item.Index; } /// /// Move the component transform to the end of the local transform list. /// /// Item. protected void SetComponentAsLastSibling(ListViewItem item) { item.transform.SetAsLastSibling(); } /// /// Called when item selected. /// Use it for change visible style of selected item. /// /// Index. protected virtual void SelectItem(int index) { } /// /// Called when item deselected. /// Use it for change visible style of deselected item. /// /// Index. protected virtual void DeselectItem(int index) { } /// /// Coloring the specified component. /// /// Component. protected virtual void Coloring(ListViewItem component) { } /// /// Set highlights colors of specified component. /// /// Component. protected virtual void HighlightColoring(ListViewItem component) { } /// /// This function is called when the MonoBehaviour will be destroyed. /// protected virtual void OnDestroy() { RemoveCallbacks(); items.ForEach(Free); } /// /// Set EventSystem.current.SetSelectedGameObject with selected or first item. /// /// true, if component was selected, false otherwise. public virtual bool SelectComponent() { if (items.Count==0) { return false; } var index = (SelectedIndex!=-1) ? SelectedIndex : 0; SelectComponentByIndex(index); return true; } /// /// Selects the component by index. /// /// Index. protected void SelectComponentByIndex(int index) { ScrollTo(index); var ev = new ListViewItemEventData(EventSystem.current) { NewSelectedObject = GetItem(index).gameObject }; ExecuteEvents.Execute(ev.NewSelectedObject, ev, ExecuteEvents.selectHandler); } /// /// Raises the select event. /// /// Event data. void ISelectHandler.OnSelect(BaseEventData eventData) { if (!EventSystem.current.alreadySelecting) { EventSystem.current.SetSelectedGameObject(gameObject); } OnFocusIn.Invoke(eventData); } /// /// Raises the deselect event. /// /// Current event data. void IDeselectHandler.OnDeselect(BaseEventData eventData) { OnFocusOut.Invoke(eventData); } /// /// Raises the submit event. /// /// Event data. void ISubmitHandler.OnSubmit(BaseEventData eventData) { SelectComponent(); onSubmit.Invoke(); } /// /// Raises the cancel event. /// /// Event data. void ICancelHandler.OnCancel(BaseEventData eventData) { onCancel.Invoke(); } /// /// Calls specified function with each component. /// /// Func. public virtual void ForEachComponent(Action func) { items.ForEach(func); } #region ListViewPaginator support /// /// Gets the ScrollRect. /// /// The ScrollRect. public virtual ScrollRect GetScrollRect() { throw new NotImplementedException(); } /// /// Gets the items count. /// /// The items count. public virtual int GetItemsCount() { throw new NotImplementedException(); } /// /// Gets the items per block count. /// /// The items per block. public virtual int GetItemsPerBlock() { throw new NotImplementedException(); } /// /// Gets the item position by index. /// /// The item position. /// Index. public virtual float GetItemPosition(int index) { throw new NotImplementedException(); } /// /// Gets the index of the nearest item. /// /// The nearest item index. public virtual int GetNearestItemIndex() { throw new NotImplementedException(); } #endregion } }