namespace UIWidgets.Menu { using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using UIWidgets.Styles; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// /// Context menu. /// Contains menu items and reference to the menu template. /// public partial class ContextMenu : UIBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler { /// /// Menu instance. /// protected class MenuInstance : IMenuSubscriber, IStylable { ContextMenuView template; /// /// Template. /// public ContextMenuView Template { get { return template; } set { if (value == null) { throw new ArgumentNullException("value"); } if (template == value) { return; } if (template != null) { ResetMenu(); Template.Unsubscribe(this); } template = value; ResetMenu(); Template.Subscribe(this); if (IsOpened) { UpdateItems(); } if (submenu != null) { submenu.Template = Template; } } } /// /// Preferred menu position. /// protected Vector2 Position; /// /// Menu items. /// protected ReadOnlyCollection Items; /// /// Owner. /// protected ContextMenu Owner; /// /// The modal key. /// protected int? ModalKey; /// /// Is menu opened? /// public bool IsOpened { get; protected set; } /// /// Menu gameobject. /// protected ContextMenuView View; /// /// Menu items view. /// protected List ItemViews = new List(); /// /// Is pointer over view? /// protected bool PointerOver { get { return (View != null) ? View.IsPointerOver : false; } } MenuInstance parent; MenuInstance submenu; int submenuItemIndex; int depth = 0; Coroutine openCoroutine; Coroutine closeCoroutine; RectTransform parentCanvas; /// /// Parent canvas. /// public RectTransform ParentCanvas { get { return parentCanvas; } set { parentCanvas = value; if (submenu != null) { submenu.ParentCanvas = value; } } } /// /// Initializes a new instance of the class. /// /// Owner. /// Template. /// Items. /// Parent canvas. public MenuInstance(ContextMenu owner, ContextMenuView template, ReadOnlyCollection items, RectTransform parentCanvas) { Owner = owner; Template = template; Items = items; ParentCanvas = parentCanvas; } /// /// Process locale changes. /// public virtual void LocaleChanged() { for (int i = 0; i < ItemViews.Count; i++) { ItemViews[i].LocaleChanged(); } if (submenu != null) { submenu.LocaleChanged(); } } /// /// Update menu items. /// /// Menu items. public void UpdateItems(ReadOnlyCollection items) { ResetItems(); Items = items; UpdateItems(); } /// /// Update menu items. /// public void UpdateItems() { if (!IsOpened) { return; } ResetItems(); for (int index = 0; index < Items.Count; index++) { var item = Items[index]; if (!item.Visible) { continue; } var view = Template.GetItemView(item); view.transform.SetParent(View.RectTransform, false); AddListeners(view); SetData(view, index); ItemViews.Add(view); } View.RectTransform.SetParent(ParentCanvas, false); View.RectTransform.SetAsLastSibling(); View.RectTransform.anchorMin = new Vector2(0, 1); View.RectTransform.anchorMax = new Vector2(0, 1); View.RectTransform.localScale = Vector3.one; View.RectTransform.localPosition = Vector3.zero; LayoutRebuilder.ForceRebuildLayoutImmediate(View.RectTransform); var canvas_size = ParentCanvas.rect.size; var width = LayoutUtility.GetPreferredWidth(View.RectTransform); var height = LayoutUtility.GetPreferredHeight(View.RectTransform); if ((Position.x + width) > canvas_size.x) { Position.x = parent != null ? Position.x - parent.View.RectTransform.rect.width - width : canvas_size.x - width; } if ((-(Position.y - height)) > canvas_size.y) { Position.y = Position.y + height; } View.RectTransform.anchoredPosition = Position; } /// /// Open menu. /// /// Preferred position. public void Open(Vector2 position) { if (IsOpened) { return; } IsOpened = true; Position = position; if (parent == null) { ModalKey = ModalHelper.Open(Owner.RectTransform, null, Color.clear, Close, ParentCanvas); } CreateView(); UpdateItems(); if (depth == 0) { Owner.OnOpen.Invoke(Owner); } } /// /// Fully close menu. /// protected void AllClose() { if (parent != null) { parent.AllClose(); } else { Close(); } } /// /// Close. /// public void Close() { if (!IsOpened) { return; } if (ModalKey.HasValue) { ModalHelper.Close(ModalKey.Value); } if ((submenu != null) && submenu.IsOpened) { submenu.Close(); } ResetMenu(); IsOpened = false; if (depth == 0) { Owner.OnClose.Invoke(Owner); } } /// /// Add listeners to the MenuItem view. /// /// MenuItem view. protected virtual void AddListeners(MenuItemView view) { view.OnClick.AddListener(ItemClick); view.OnEnter.AddListener(ItemEnter); view.OnExit.AddListener(ItemExit); view.OnAxisMove.AddListener(ItemMove); } /// /// Remove listeners from the MenuItem view. /// /// MenuItem view. protected virtual void RemoveListeners(MenuItemView view) { view.OnClick.RemoveListener(ItemClick); view.OnEnter.RemoveListener(ItemEnter); view.OnExit.RemoveListener(ItemExit); view.OnAxisMove.RemoveListener(ItemMove); } /// /// Set data. /// /// MenuItem view. /// MenuItem index. protected virtual void SetData(MenuItemView view, int index) { view.Index = index; view.Item = Items[index]; } /// /// Reset menu. /// protected void ResetMenu() { ResetItems(); if (View != null) { Template.Return(View); View = null; } } /// /// Reset menu items. /// public void ResetItems() { foreach (var view in ItemViews) { RemoveListeners(view); if (!Items[view.Index].Visible) { continue; } Template.Return(Items[view.Index], view); } ItemViews.Clear(); } /// /// Process the item click event. /// /// Item. protected virtual void ItemClick(MenuItem item) { if (item.Interactable) { item.Action.Invoke(item); if (!item.HasVisibleItems) { AllClose(); } } } /// /// Process the item enter event. /// /// Index. /// Pointer event. protected virtual void ItemEnter(int index, bool pointerEvent) { var item = Items[index]; Owner.OnItemSelect.Invoke(item); if (submenu != null) { StartCloseSubmenu(); } if (item.Interactable && item.HasVisibleItems && pointerEvent) { StartOpenSubmenu(index); } } /// /// Start open sub-menu. /// /// Index. protected void StartOpenSubmenu(int index) { if (openCoroutine != null) { Owner.StopCoroutine(openCoroutine); } openCoroutine = Owner.StartCoroutine(OpenSubmenuCoroutine(index)); } /// /// Start close sub-menu coroutine. /// protected void StartCloseSubmenu() { if (closeCoroutine != null) { Owner.StopCoroutine(closeCoroutine); } closeCoroutine = Owner.StartCoroutine(CloseSubmenuCoroutine()); } /// /// Open sub-menu coroutine. /// /// Index of the MenuItem. /// IEnumerator. [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "HAA0401:Possible allocation of reference type enumerator", Justification = "Enumerator is reusable.")] protected IEnumerator OpenSubmenuCoroutine(int index) { yield return UtilitiesTime.Wait(Owner.SubmenuDelay, Owner.UnscaledTime); OpenSubmenu(index); } /// /// Close sub-menu coroutine. /// /// IEnumerator. [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "HAA0401:Possible allocation of reference type enumerator", Justification = "Enumerator is reusable.")] protected IEnumerator CloseSubmenuCoroutine() { yield return UtilitiesTime.Wait(Owner.SubmenuDelay, Owner.UnscaledTime); CloseSubmenu(); } /// /// Process the item exit event. /// /// Index. /// Pointer event. protected virtual void ItemExit(int index, bool pointerEvent) { var item = Items[index]; if (pointerEvent) { StartCloseSubmenu(); } Owner.OnItemDeselect.Invoke(item); } /// /// Create view. /// protected virtual void CreateView() { if (View == null) { View = Template.Instance(); View.name = string.Format("{0} {1}", Owner.name, depth.ToString()); } } /// /// Open sub menu. /// /// Index. /// Select the item after open. protected virtual void OpenSubmenu(int index, bool selectItem = false) { if (closeCoroutine != null) { Owner.StopCoroutine(closeCoroutine); closeCoroutine = null; } submenuItemIndex = index; if (submenu == null) { submenu = new MenuInstance(Owner, Template, Items[index].Items.AsReadOnly(), ParentCanvas); submenu.parent = this; submenu.depth = depth + 1; } submenu.UpdateItems(Items[index].Items.AsReadOnly()); var item_view = Index2View(index); var distance = DistanceTopLeft(item_view.RectTransform); distance.x += item_view.RectTransform.rect.width; var position = View.RectTransform.anchoredPosition + distance; submenu.Open(position); if (selectItem) { submenu.SelectFirst(); } } /// /// Get distance from top left corner of the target to the top left corner of the target's parent. /// /// Target. /// Distance. protected Vector2 DistanceTopLeft(RectTransform target) { var parent_size = (target.parent as RectTransform).rect.size; var size = target.rect.size; var pos = target.anchoredPosition; var pivot = target.pivot; pos.x -= size.x * pivot.x; pos.y += size.y * (1f - pivot.y); pos.x += parent_size.x * ((target.anchorMax.x + target.anchorMin.x) / 2f); pos.y -= parent_size.y * ((target.anchorMax.y + target.anchorMin.y) / 2f); return pos; } /// /// Close sub menu. /// /// Select the item after close. protected virtual void CloseSubmenu(bool selectItem = false) { if (submenu == null) { return; } if (!selectItem && submenu.PointerOver) { return; } submenu.Close(); if (selectItem) { SelectItemView(submenuItemIndex); submenuItemIndex = -1; } } /// /// Index to MenuItem view. /// /// Index. /// MenuItem view. protected MenuItemView Index2View(int index) { for (int i = 0; i < ItemViews.Count; i++) { var view = ItemViews[i]; if (view.Index == index) { return view; } } return null; } /// /// Select the first item. /// public void SelectFirst() { SelectItemView(FindNextItem(-1)); } /// /// Select the MenuItem view by index. /// /// Index. protected void SelectItemView(int index) { if ((index < 0) && (index >= Items.Count)) { return; } var view = Index2View(index); if (view != null) { EventSystem.current.SetSelectedGameObject(view.gameObject); } } /// /// Find next visible and interactable item after the specified index. /// /// Start index. /// Next item index. protected int FindNextItem(int start) { for (int index = start + 1; index < Items.Count; index++) { if (Items[index].Visible && Items[index].Interactable) { return index; } } return -1; } /// /// Find previous visible and interactable item before the specified index. /// /// Start index. /// Previous item index. protected int FindPreviousItem(int start) { for (int index = start - 1; index >= 0; index--) { if (Items[index].Visible && Items[index].Interactable) { return index; } } return -1; } /// /// Process the item move event. /// /// Index. /// Event data. protected virtual void ItemMove(int index, AxisEventData eventData) { if (!Owner.Navigation) { return; } switch (eventData.moveDir) { case MoveDirection.Left: if (parent != null) { parent.CloseSubmenu(true); } break; case MoveDirection.Right: if (Items[index].Interactable && Items[index].HasVisibleItems) { OpenSubmenu(index, true); } break; case MoveDirection.Up: var prev_index = FindPreviousItem(index); if (prev_index >= 0) { SelectItemView(prev_index); } break; case MoveDirection.Down: var next_index = FindNextItem(index); if (next_index < Items.Count) { SelectItemView(next_index); } break; } } /// /// Set widget properties from specified style. /// /// Style data. /// true, if children gameobjects was processed, false otherwise. public bool SetStyle(Style style) { if (View != null) { View.SetStyle(style); } for (int i = 0; i < ItemViews.Count; i++) { ItemViews[i].SetStyle(style); } if (submenu != null) { submenu.SetStyle(style); } return false; } /// /// Set style options from widget properties. /// /// Style data. /// true, if children gameobjects was processed, false otherwise. public bool GetStyle(Style style) { if (View != null) { View.GetStyle(style); } for (int i = 0; i < ItemViews.Count; i++) { ItemViews[i].GetStyle(style); } if (submenu != null) { submenu.GetStyle(style); } return false; } } } }