namespace UIWidgets.Menu { using System; using System.Collections.Generic; using UIWidgets.Attributes; using UIWidgets.l10n; using UIWidgets.Styles; using UnityEngine; using UnityEngine.EventSystems; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif using UnityEngine.Serialization; /// /// Context menu. /// Contains menu items and reference to the menu template. /// [RequireComponent(typeof(RectTransform))] [DisallowMultipleComponent] [AddComponentMenu("UI/New UI Widgets/ContextMenu")] [DataBindSupport] public partial class ContextMenu : UIBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler, IStylable #if !ENABLE_INPUT_SYSTEM , IUpdatable #endif { /// /// Serialized menu item. /// [Serializable] protected class MenuItemSerialized { /// /// Depth. /// [SerializeField] public int Depth; /// /// Icon. /// [SerializeField] public Sprite Icon; /// /// Checked. /// [SerializeField] public bool Checked; /// /// Name. /// [SerializeField] public string Name; /// /// Template. /// [SerializeField] public string Template; /// /// Name. /// [SerializeField] public HotKey HotKey; /// /// Is item visible? /// [SerializeField] public bool Visible; /// /// Is item interactable. /// [SerializeField] public bool Interactable; /// /// For the editor use only. /// [SerializeField] [HideInInspector] #pragma warning disable 0414 bool showAction = false; #pragma warning restore 0414 /// /// Action. /// [SerializeField] public MenuItemAction Action = new MenuItemAction(); /// /// Convert specified instance to the menu item. /// /// Serialized instance. /// Menu item. public static MenuItem ToMenuItem(MenuItemSerialized serialized) { return serialized; } /// /// Convert specified instance to the menu item. /// /// Serialized instance. public static implicit operator MenuItem(MenuItemSerialized serialized) { return new MenuItem() { Icon = serialized.Icon, Checked = serialized.Checked, Name = serialized.Name, Template = serialized.Template, HotKey = serialized.HotKey, Visible = serialized.Visible, Interactable = serialized.Interactable, Action = serialized.Action, }; } } #region Interactable [SerializeField] bool interactable = true; /// /// Is widget interactable. /// /// true if interactable; otherwise, false. [DataBindField] public bool Interactable { get { return interactable; } set { if (interactable != value) { interactable = value; InteractableChanged(); } } } /// /// If the canvas groups allow interaction. /// protected bool GroupsAllowInteraction = true; /// /// The CanvasGroup cache. /// protected List CanvasGroupCache = new List(); /// /// Process the CanvasGroupChanged event. /// protected override void OnCanvasGroupChanged() { var groupAllowInteraction = true; var t = transform; while (t != null) { t.GetComponents(CanvasGroupCache); var shouldBreak = false; foreach (var canvas_group in CanvasGroupCache) { if (!canvas_group.interactable) { groupAllowInteraction = false; shouldBreak = true; } shouldBreak |= canvas_group.ignoreParentGroups; } if (shouldBreak) { break; } t = t.parent; } if (groupAllowInteraction != GroupsAllowInteraction) { GroupsAllowInteraction = groupAllowInteraction; InteractableChanged(); } } /// /// Returns true if the GameObject and the Component are active. /// /// true if the GameObject and the Component are active; otherwise false. public override bool IsActive() { return base.IsActive() && GroupsAllowInteraction && Interactable && isInited; } /// /// Process interactable change. /// protected virtual void InteractableChanged() { if (!base.IsActive() || !isInited) { return; } OnInteractableChange(GroupsAllowInteraction && Interactable); } /// /// Process interactable change. /// /// Current interactable state. protected virtual void OnInteractableChange(bool interactableState) { } #endregion [SerializeField] ContextMenuView template; /// /// Menu template. /// public ContextMenuView Template { get { return template; } set { template = value; if (Instance != null) { Instance.Template = template; } } } /// /// Serialized menu items. /// [SerializeField] [HideInInspector] protected List MenuItemsSerialized = new List(); ObservableList menuItems; /// /// Menu items. /// public ObservableList MenuItems { get { if (menuItems == null) { menuItems = Deserialize(MenuItemsSerialized); menuItems.OnCollectionChange += UpdateMenuItems; } return menuItems; } set { if (value == null) { throw new ArgumentNullException("value"); } if (menuItems != null) { menuItems.OnCollectionChange -= UpdateMenuItems; DisableHotKeys(menuItems); Instance.ResetItems(); } menuItems = value; menuItems.OnCollectionChange += UpdateMenuItems; UpdateMenuItems(); } } /// /// Is default menu? /// [SerializeField] public bool IsDefault = false; /// /// Allow navigation. /// [SerializeField] public bool Navigation = true; /// /// Open menu on pointer right button click. /// [SerializeField] public bool OpenOnRightButtonClick = true; /// /// Open menu on context menu key. /// [SerializeField] public bool OpenOnContextMenuKey = true; /// /// Time to wait before open or close sub-menu. /// [SerializeField] public float SubmenuDelay = 0.3f; /// /// Use unscaled time. /// [SerializeField] public bool UnscaledTime = true; [SerializeField] [FormerlySerializedAs("ParentCanvas")] RectTransform parentCanvas; /// /// Parent canvas. /// public RectTransform ParentCanvas { get { return parentCanvas; } set { parentCanvas = value; if (instance != null) { instance.ParentCanvas = value; } } } /// /// Event on menu open. /// [SerializeField] [DataBindEvent] public MenuEvent OnOpen = new MenuEvent(); /// /// Event on menu close. /// [SerializeField] [DataBindEvent] public MenuEvent OnClose = new MenuEvent(); /// /// Event on pointer over menu item. /// [SerializeField] [DataBindEvent] public MenuItemEvent OnItemSelect = new MenuItemEvent(); /// /// Event on pointer out menu item. /// [SerializeField] [DataBindEvent] public MenuItemEvent OnItemDeselect = new MenuItemEvent(); MenuInstance instance; /// /// Menu instance. /// protected MenuInstance Instance { get { if (instance == null) { if (ParentCanvas == null) { ParentCanvas = UtilitiesUI.FindTopmostCanvas(transform); } instance = new MenuInstance(this, Template, MenuItems.AsReadOnly(), ParentCanvas); } return instance; } } RectTransform rectTransform; /// /// RectTransform. /// public RectTransform RectTransform { get { if (rectTransform == null) { rectTransform = transform as RectTransform; } return rectTransform; } } /// /// Active menu. /// protected static readonly List ActiveMenu = new List(); bool isInited; /// /// Process the start event. /// protected override void Start() { base.Start(); Init(); } /// /// Init this instance. /// public virtual void Init() { if (isInited) { return; } if (ParentCanvas == null) { ParentCanvas = UtilitiesUI.FindTopmostCanvas(transform); } Template.Init(); isInited = true; CreateToggleAction(); UpdateMenuItems(); Localization.OnLocaleChanged += LocaleChanged; } /// /// Process locale changes. /// public virtual void LocaleChanged() { if (instance != null) { instance.LocaleChanged(); } } /// /// Process the enable event. /// protected override void OnEnable() { base.OnEnable(); if (IsDefault) { for (int i = 0; i < ActiveMenu.Count; i++) { ActiveMenu[i].IsDefault = false; } } ActiveMenu.Add(this); #if !ENABLE_INPUT_SYSTEM Updater.Add(this); #endif } /// /// Process the disable event. /// protected override void OnDisable() { base.OnDisable(); ActiveMenu.Remove(this); #if !ENABLE_INPUT_SYSTEM Updater.Remove(this); #endif } /// protected override void OnDestroy() { base.OnDestroy(); if (menuItems != null) { DisableHotKeys(menuItems); Instance.ResetItems(); menuItems.OnCollectionChange -= UpdateMenuItems; menuItems = null; } Localization.OnLocaleChanged -= LocaleChanged; } /// /// Update menu items. /// protected void UpdateMenuItems() { EnableHotKeys(MenuItems); if (Instance.IsOpened) { Instance.UpdateItems(MenuItems.AsReadOnly()); } } /// /// Enable hot keys. /// Supported for the InputSystem only. /// /// Items. protected virtual void EnableHotKeys(ObservableList items) { for (int i = 0; i < items.Count; i++) { items[i].EnableHotKey(); if (items[i].Items != null) { EnableHotKeys(items[i].Items); } } } /// /// Disable hot keys. /// Supported for the InputSystem only. /// /// Items. protected virtual void DisableHotKeys(ObservableList items) { for (int i = 0; i < items.Count; i++) { items[i].DisableHotKey(); if (items[i].Items != null) { DisableHotKeys(items[i].Items); } } } /// /// Enable hot keys. /// Supported for the InputSystem only. /// public virtual void EnableHotKeys() { EnableHotKeys(MenuItems); } /// /// Disable hot keys. /// Supported for the InputSystem only. /// public virtual void DisableHotKeys() { EnableHotKeys(MenuItems); } /// /// Open. /// /// Event data. public virtual void Open(PointerEventData eventData) { var canvas = UtilitiesUI.FindTopmostCanvas(RectTransform); Vector2 position; RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas, eventData.position, eventData.pressEventCamera, out position); var size = canvas.rect.size; var pivot = canvas.pivot; position.x += size.x * pivot.x; position.y -= size.y * pivot.y; Open(position); } /// /// Open menu in the specified position. /// /// Position. public virtual void Open(Vector2 position) { Instance.Open(position); CurrentMenu = this; } /// /// Close menu. /// public virtual void Close() { Instance.Close(); CurrentMenu = null; } /// /// Get default position. /// /// Position. protected virtual Vector2 GetDefaultPosition() { var canvas = UtilitiesUI.FindTopmostCanvas(RectTransform); var canvas_position = UtilitiesRectTransform.GetTopLeftCornerGlobalPosition(canvas); var rt_position = UtilitiesRectTransform.GetTopLeftCornerGlobalPosition(RectTransform); return rt_position - canvas_position; } /// /// Toggle menu. /// protected virtual void Toggle() { if (Instance.IsOpened) { Instance.Close(); } else { Instance.Open(GetDefaultPosition()); Instance.SelectFirst(); } } /// /// Process the pointer down event. /// /// Event data. public virtual void OnPointerDown(PointerEventData eventData) { // required by OnPointerClick } /// /// Process the pointer up event. /// /// Event data. public virtual void OnPointerUp(PointerEventData eventData) { // required by OnPointerClick } /// /// Process the pointer click event. /// /// Event data. public virtual void OnPointerClick(PointerEventData eventData) { if (!IsActive() || !OpenOnRightButtonClick) { return; } if (eventData.button == PointerEventData.InputButton.Right) { Open(eventData); } } /// /// Deserialize menu items. /// /// Serializes items. /// Items. protected static ObservableList Deserialize(List serialized) { var result = new ObservableList(); Stack current_branch = new Stack(); var prev_depth = 0; for (int i = 0; i < serialized.Count; i++) { var current = serialized[i]; if (current.Depth == 0) { current_branch.Clear(); current_branch.Push(current); result.Add(current_branch.Peek()); } else if (current.Depth == (prev_depth + 1)) { MenuItem item = current; current_branch.Peek().Items.Add(item); current_branch.Push(item); } else if (current.Depth <= prev_depth) { var n = prev_depth + 1 - current.Depth; for (int j = 0; j < n; j++) { current_branch.Pop(); } MenuItem item = current; current_branch.Peek().Items.Add(item); current_branch.Push(item); } else { // Debug.LogWarning("Unknown case"); } prev_depth = current.Depth; } return result; } /// /// Is current menu active? /// /// true if current menu active; otherwise false. protected virtual bool IsActiveMenu() { if (EventSystem.current.currentSelectedGameObject == null) { return false; } var t = EventSystem.current.currentSelectedGameObject.transform; return (t == transform) || t.IsChildOf(transform); } /// /// Last frame when toggle was called. /// protected static int LastToggledByKey = -2; /// /// Current menu. /// protected static ContextMenu CurrentMenu; /// /// Nested menu. /// protected static List NestedMenu = new List(); /// /// Toggle active menu. /// protected static void ToggleActiveMenuByKey() { if (LastToggledByKey == UtilitiesTime.GetFrameCount()) { return; } if ((CurrentMenu != null) && CurrentMenu.OpenOnContextMenuKey) { CurrentMenu.Toggle(); } else { var menu = FindActiveMenu(); if (menu != null) { menu.Toggle(); } } LastToggledByKey = UtilitiesTime.GetFrameCount(); } /// /// Find active menu. /// /// Active menu. protected static ContextMenu FindActiveMenu() { for (int i = 0; i < ActiveMenu.Count; i++) { if (ActiveMenu[i].IsActiveMenu() && ActiveMenu[i].OpenOnContextMenuKey) { NestedMenu.Add(ActiveMenu[i]); } } if (NestedMenu.Count > 0) { NestedMenu.Sort(MenuComparison); var result = NestedMenu[0]; NestedMenu.Clear(); return result; } for (int i = 0; i < ActiveMenu.Count; i++) { if (ActiveMenu[i].IsDefault && ActiveMenu[i].OpenOnContextMenuKey) { return ActiveMenu[i]; } } return null; } /// /// Menu comparison. /// /// X. /// Y. /// Comparison result. protected static int MenuComparison(ContextMenu x, ContextMenu y) { var x_depth = Utilities.GetDepth(x.transform); var y_depth = Utilities.GetDepth(y.transform); return -x_depth.CompareTo(y_depth); } #if UNITY_2019_3_OR_NEWER [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void StaticInit() { LastToggledByKey = -2; NestedMenu.Clear(); #if ENABLE_INPUT_SYSTEM ToggleAction = null; #endif } #endif #if ENABLE_INPUT_SYSTEM /// /// Input action. /// protected static InputAction ToggleAction; /// /// Create toggle action. /// protected virtual void CreateToggleAction() { if (ToggleAction == null) { ToggleAction = new InputAction(name); ToggleAction.AddBinding(string.Format("/{0}", EnumHelper.ToString(Key.ContextMenu))); ToggleAction.performed += Toggle; ToggleAction.Enable(); } } /// /// Toggle active menu. /// /// Context. protected static void Toggle(InputAction.CallbackContext context) { ToggleActiveMenuByKey(); } #else /// /// Create toggle action. /// protected virtual void CreateToggleAction() { } /// /// Process the update event. /// public virtual void RunUpdate() { if (CompatibilityInput.ContextMenuUp) { ToggleActiveMenuByKey(); } CheckHotKeys(MenuItems); } /// /// Check hot keys status. /// /// Items. protected virtual void CheckHotKeys(ObservableList items) { for (int i = 0; i < items.Count; i++) { var item = items[i]; if (item.Visible && item.Interactable && item.HotKey.IsUp) { item.Action.Invoke(item); Close(); } if (item.Items != null) { CheckHotKeys(item.Items); } } } #endif #if UNITY_EDITOR /// /// Validate this instance. /// protected override void OnValidate() { base.OnValidate(); if (ParentCanvas == null) { ParentCanvas = UtilitiesUI.FindTopmostCanvas(transform); } } /// /// Reset this instance. /// protected override void Reset() { base.Reset(); if (ParentCanvas == null) { ParentCanvas = UtilitiesUI.FindTopmostCanvas(transform); } } #endif #region IStyleable /// public bool SetStyle(Style style) { Template.SetStyle(style); if (instance != null) { instance.SetStyle(style); } return false; } /// public bool GetStyle(Style style) { Template.GetStyle(style); return false; } #endregion } }