// Copyright (c) Pixel Crushers. All rights reserved.
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;
namespace PixelCrushers.DialogueSystem
{
[AddComponentMenu("")] // Use wrapper.
public class StandardUIMenuPanel : UIPanel
{
#region Serialized Fields
[Tooltip("(Optional) Main response menu panel.")]
public UnityEngine.UI.Graphic panel;
[Tooltip("(Optional) Image to show PC portrait during response menu.")]
public UnityEngine.UI.Image pcImage;
[Tooltip("(Optional) Text element to show PC name during response menu.")]
public UITextField pcName;
[Tooltip("Set PC Image to actor portrait's native size. Image's Rect Transform can't use Stretch anchors.")]
public bool usePortraitNativeSize = false;
[Tooltip("(Optional) Slider for timed menus.")]
public UnityEngine.UI.Slider timerSlider;
[Tooltip("Assign design-time positioned buttons starting with first or last button.")]
public ResponseButtonAlignment buttonAlignment = ResponseButtonAlignment.ToFirst;
[Tooltip("Show buttons that aren't assigned to any responses. If using a 'dialogue wheel' for example, you'll want to show unused buttons so the entire wheel structure is visible.")]
public bool showUnusedButtons = false;
[Tooltip("Design-time positioned response buttons. (Optional if Button Template is assigned.)")]
public StandardUIResponseButton[] buttons;
[Tooltip("Template from which to instantiate response buttons. (Optional if using Buttons list above.)")]
public StandardUIResponseButton buttonTemplate;
[Tooltip("If using Button Template, instantiate buttons under this GameObject.")]
public UnityEngine.UI.Graphic buttonTemplateHolder;
[Tooltip("(Optional) Scrollbar to use if instantiated button holder is in a scroll rect.")]
public UnityEngine.UI.Scrollbar buttonTemplateScrollbar;
[Tooltip("(Optional) Component that enables or disables scrollbar as necessary for content.")]
public UIScrollbarEnabler scrollbarEnabler;
[Tooltip("Reset the scroll bar to this value when preparing response menu. To skip resetting the scrollbar, specify a negative value.")]
public float buttonTemplateScrollbarResetValue = 1;
[Tooltip("Automatically set up explicit joystick/keyboard navigation for instantiated template buttons instead of using Automatic navigation.")]
public bool explicitNavigationForTemplateButtons = true;
[Tooltip("If explicit navigation is enabled, loop around when navigating past end of menu.")]
public bool loopExplicitNavigation = false;
public UIAutonumberSettings autonumber = new UIAutonumberSettings();
[Tooltip("If non-zero, prevent input for this duration in seconds when opening menu.")]
public float blockInputDuration = 0;
[Tooltip("During block input duration, keep selected response button in selected visual state.")]
public bool showSelectionWhileInputBlocked = false;
[Tooltip("Log a warning if a response button text is blank.")]
public bool warnOnEmptyResponseText = false;
public UnityEvent onContentChanged = new UnityEvent();
[Tooltip("When focusing panel, set this animator trigger.")]
public string focusAnimationTrigger = string.Empty;
[Tooltip("When unfocusing panel, set this animator trigger.")]
public string unfocusAnimationTrigger = string.Empty;
[Tooltip("Wait for panels within this dialogue UI (not external) to close before showing menu.")]
public bool waitForClose = false;
///
/// Invoked when the subtitle panel gains focus.
///
public UnityEvent onFocus = new UnityEvent();
///
/// Invoked when the subtitle panel loses focus.
///
public UnityEvent onUnfocus = new UnityEvent();
#endregion
#region Public Properties
[SerializeField, Tooltip("Panel is currently in focused state.")]
private bool m_hasFocus = false;
public virtual bool hasFocus
{
get { return m_hasFocus; }
protected set { m_hasFocus = value; }
}
public override bool waitForShowAnimation { get { return true; } }
///
/// The instantiated buttons. These are only valid during a specific response menu,
/// and only if you're using templates. Each showing of the response menu clears
/// this list and re-populates it with new buttons.
///
public List instantiatedButtons { get { return m_instantiatedButtons; } }
private List m_instantiatedButtons = new List();
#endregion
#region Internal Fields
protected List instantiatedButtonPool { get { return m_instantiatedButtonPool; } }
private List m_instantiatedButtonPool = new List();
private string m_processedAutonumberFormat = string.Empty;
private Coroutine m_scrollbarCoroutine = null;
protected const float WaitForCloseTimeoutDuration = 8f;
protected StandardUITimer m_timer = null;
protected System.Action m_timeoutHandler = null;
protected CanvasGroup m_mainCanvasGroup = null;
protected static bool s_isInputDisabled = false;
private StandardDialogueUI m_dialogueUI = null;
protected StandardDialogueUI dialogueUI
{
get
{
if (m_dialogueUI == null) m_dialogueUI = GetComponentInParent();
return m_dialogueUI ?? DialogueManager.standardDialogueUI;
}
}
#endregion
#region Initialization
public virtual void Awake()
{
Tools.SetGameObjectActive(buttonTemplate, false);
}
#endregion
#region Show & Hide
protected override void Update()
{
if (s_isInputDisabled)
{
if (eventSystem != null) eventSystem.SetSelectedGameObject(null);
}
else
{
base.Update();
}
}
public override void CheckFocus()
{
if (s_isInputDisabled) return;
base.CheckFocus();
}
public virtual void SetPCPortrait(Sprite portraitSprite, string portraitName)
{
if (pcImage != null)
{
Tools.SetGameObjectActive(pcImage, portraitSprite != null);
pcImage.sprite = portraitSprite;
if (usePortraitNativeSize && portraitSprite != null)
{
pcImage.rectTransform.sizeDelta = portraitSprite.packed ?
new Vector2(portraitSprite.rect.width, portraitSprite.rect.height) :
new Vector2(portraitSprite.texture.width, portraitSprite.texture.height);
}
}
pcName.text = portraitName;
}
[System.Obsolete("Use SetPCPortrait(Sprite,string) instead.")]
public virtual void SetPCPortrait(Texture2D portraitTexture, string portraitName)
{
SetPCPortrait(UITools.CreateSprite(portraitTexture), portraitName);
}
public virtual void ShowResponses(Subtitle subtitle, Response[] responses, Transform target)
{
if (waitForClose && dialogueUI != null)
{
if (dialogueUI.AreAnyPanelsClosing())
{
DialogueManager.instance.StartCoroutine(ShowAfterPanelsClose(subtitle, responses, target));
return;
}
}
CheckForBlankResponses(responses);
ShowResponsesNow(subtitle, responses, target);
}
private void CheckForBlankResponses(Response[] responses)
{
if (!DialogueDebug.logWarnings) return;
if (responses == null) return;
foreach (Response response in responses)
{
if (string.IsNullOrEmpty(response.formattedText.text))
{
Debug.LogWarning($"Dialogue System: Response [{response.destinationEntry.conversationID}:{response.destinationEntry.id}] has no text for a response button.");
}
}
}
protected virtual void ShowResponsesNow(Subtitle subtitle, Response[] responses, Transform target)
{
if (responses == null || responses.Length == 0)
{
if (DialogueDebug.logWarnings) Debug.LogWarning("Dialogue System: StandardDialogueUI ShowResponses received an empty list of responses.", this);
return;
}
ClearResponseButtons();
SetResponseButtons(responses, target);
ActivateUIElements();
Open();
Focus();
RefreshSelectablesList();
if (blockInputDuration > 0)
{
DisableInput();
if (InputDeviceManager.autoFocus) SetFocus(firstSelected);
if (Mathf.Approximately(0, Time.timeScale))
{
StartCoroutine(EnableInputAfterDuration(blockInputDuration));
}
else
{
Invoke(nameof(EnableInput), blockInputDuration);
}
}
else
{
if (InputDeviceManager.autoFocus) SetFocus(firstSelected);
if (s_isInputDisabled) EnableInput();
}
#if TMP_PRESENT
DialogueManager.instance.StartCoroutine(CheckTMProAutoScroll());
#endif
}
private IEnumerator EnableInputAfterDuration(float duration)
{
yield return new WaitForSecondsRealtime(duration);
EnableInput();
}
#if TMP_PRESENT
// Handles edge case where TMPro uses autoscroll but entry ends before typing starts.
// In this case, this method updates the autoscroll size.
protected IEnumerator CheckTMProAutoScroll()
{
var ui = GetComponentInParent();
if (ui == null || ui.conversationUIElements.defaultNPCSubtitlePanel == null || ui.conversationUIElements.defaultNPCSubtitlePanel.subtitleText == null) yield break;
var tmp = ui.conversationUIElements.defaultNPCSubtitlePanel.subtitleText.textMeshProUGUI;
if (tmp == null) yield break;
var layoutElement = tmp.GetComponent();
if (layoutElement != null) layoutElement.preferredHeight = -1;
var uiScrollbarEnabler = GetComponentInParent();
if (uiScrollbarEnabler != null)
{
yield return null;
uiScrollbarEnabler.CheckScrollbarWithResetValue(buttonTemplateScrollbarResetValue);
}
}
#endif
protected virtual IEnumerator ShowAfterPanelsClose(Subtitle subtitle, Response[] responses, Transform target)
{
if (dialogueUI != null)
{
float safeguardTime = Time.realtimeSinceStartup + WaitForCloseTimeoutDuration;
while (dialogueUI.AreAnyPanelsClosing() && Time.realtimeSinceStartup < safeguardTime)
{
yield return null;
}
}
ShowResponsesNow(subtitle, responses, target);
}
public virtual void HideResponses()
{
StopTimer();
Unfocus();
Close();
}
public override void Close()
{
if (isOpen) base.Close();
}
public virtual void Focus()
{
if (hasFocus) return;
if (panelState == PanelState.Opening && enabled && gameObject.activeInHierarchy)
{
StartCoroutine(FocusWhenOpen());
}
else
{
FocusNow();
}
}
protected IEnumerator FocusWhenOpen()
{
float timeout = Time.realtimeSinceStartup + 5f;
while (panelState != PanelState.Open && Time.realtimeSinceStartup < timeout)
{
yield return null;
}
FocusNow();
}
protected virtual void FocusNow()
{
panelState = PanelState.Open;
animatorMonitor.SetTrigger(focusAnimationTrigger, null, false);
UITools.EnableInteractivity(gameObject);
if (hasFocus) return;
if (string.IsNullOrEmpty(focusAnimationTrigger))
{
OnFocused();
}
else
{
animatorMonitor.SetTrigger(focusAnimationTrigger, OnFocused, true);
}
onFocus.Invoke();
}
private void OnFocused()
{
hasFocus = true;
}
public virtual void Unfocus()
{
if (!hasFocus) return;
hasFocus = false;
animatorMonitor.SetTrigger(unfocusAnimationTrigger, null, false);
onUnfocus.Invoke();
}
protected void ActivateUIElements()
{
SetUIElementsActive(true);
}
protected void DeactivateUIElements()
{
SetUIElementsActive(false);
}
protected virtual void SetUIElementsActive(bool value)
{
Tools.SetGameObjectActive(panel, value);
Tools.SetGameObjectActive(pcImage, value && pcImage != null && pcImage.sprite != null);
pcName.SetActive(value);
Tools.SetGameObjectActive(timerSlider, false); // Let StartTimer activate if needed.
if (value == false) ClearResponseButtons();
}
public virtual void HideImmediate()
{
OnHidden();
}
protected virtual void ClearResponseButtons()
{
DestroyInstantiatedButtons();
if (buttons != null)
{
for (int i = 0; i < buttons.Length; i++)
{
if (buttons[i] == null) continue;
buttons[i].Reset();
buttons[i].isVisible = showUnusedButtons;
buttons[i].gameObject.SetActive(showUnusedButtons);
}
}
}
///
/// Sets the response buttons.
///
/// Responses.
/// Target that will receive OnClick events from the buttons.
protected virtual void SetResponseButtons(Response[] responses, Transform target)
{
firstSelected = null;
DestroyInstantiatedButtons();
var hasDisabledButton = false;
// Prep autonumber format:
if (autonumber.enabled)
{
m_processedAutonumberFormat = FormattedText.Parse(autonumber.format.Replace("\\t", "\t").Replace("\\n", "\n")).text;
}
if ((buttons != null) && (responses != null))
{
// Add explicitly-positioned buttons:
int buttonNumber = 0;
for (int i = 0; i < responses.Length; i++)
{
if (responses[i].formattedText.position != FormattedText.NoAssignedPosition)
{
int position = responses[i].formattedText.position;
if (0 <= position && position < buttons.Length && buttons[position] != null)
{
SetResponseButton(buttons[position], responses[i], target, buttonNumber++);
}
else
{
Debug.LogWarning("Dialogue System: Buttons list doesn't contain a button for position " + position + ".", this);
}
}
}
if ((buttonTemplate != null) && (buttonTemplateHolder != null))
{
if (scrollbarEnabler != null) CheckScrollbar();
// Instantiate buttons from template:
for (int i = 0; i < responses.Length; i++)
{
if (responses[i].formattedText.position != FormattedText.NoAssignedPosition) continue;
GameObject buttonGameObject = InstantiateButton();
if (buttonGameObject == null)
{
Debug.LogError("Dialogue System: Couldn't instantiate response button template.");
}
else
{
instantiatedButtons.Add(buttonGameObject);
buttonGameObject.transform.SetParent(buttonTemplateHolder.transform, false);
buttonGameObject.transform.SetAsLastSibling();
buttonGameObject.SetActive(true);
StandardUIResponseButton responseButton = buttonGameObject.GetComponent();
SetResponseButton(responseButton, responses[i], target, buttonNumber++);
if (responseButton != null)
{
buttonGameObject.name = "Response: " + responseButton.text;
if (explicitNavigationForTemplateButtons && !responseButton.isClickable) hasDisabledButton = true;
}
if (firstSelected == null) firstSelected = buttonGameObject;
}
}
}
else
{
// Auto-position remaining buttons:
if (buttonAlignment == ResponseButtonAlignment.ToFirst)
{
// Align to first, so add in order to front:
for (int i = 0; i < Mathf.Min(buttons.Length, responses.Length); i++)
{
if (responses[i].formattedText.position == FormattedText.NoAssignedPosition)
{
int position = Mathf.Clamp(GetNextAvailableResponseButtonPosition(0, 1), 0, buttons.Length - 1);
SetResponseButton(buttons[position], responses[i], target, buttonNumber++);
if (firstSelected == null) firstSelected = buttons[position].gameObject;
}
}
}
else
{
// Align to last, so add in reverse order to back:
for (int i = Mathf.Min(buttons.Length, responses.Length) - 1; i >= 0; i--)
{
if (responses[i].formattedText.position == FormattedText.NoAssignedPosition)
{
int position = Mathf.Clamp(GetNextAvailableResponseButtonPosition(buttons.Length - 1, -1), 0, buttons.Length - 1);
SetResponseButton(buttons[position], responses[i], target, buttonNumber++);
firstSelected = buttons[position].gameObject;
}
}
}
}
}
if (explicitNavigationForTemplateButtons) SetupTemplateButtonNavigation(hasDisabledButton);
NotifyContentChanged();
}
protected virtual void CheckScrollbar()
{
if (scrollbarEnabler == null) return;
if (m_scrollbarCoroutine != null) StopCoroutine(m_scrollbarCoroutine);
m_scrollbarCoroutine = dialogueUI.StartCoroutine(CheckScrollbarCoroutine());
}
protected IEnumerator CheckScrollbarCoroutine()
{
var timeout = Time.realtimeSinceStartup + UIAnimatorMonitor.MaxWaitDuration;
while (!isOpen && Time.realtimeSinceStartup < timeout)
{
yield return null;
}
if (buttonTemplateScrollbarResetValue >= 0)
{
if (buttonTemplateScrollbar != null) buttonTemplateScrollbar.value = buttonTemplateScrollbarResetValue;
if (scrollbarEnabler != null)
{
scrollbarEnabler.CheckScrollbarWithResetValue(buttonTemplateScrollbarResetValue);
}
}
else if (scrollbarEnabler != null)
{
scrollbarEnabler.CheckScrollbar();
}
}
protected virtual void SetResponseButton(StandardUIResponseButton button, Response response, Transform target, int buttonNumber)
{
if (button != null)
{
button.response = response;
button.gameObject.SetActive(true);
button.isVisible = true;
button.isClickable = response.enabled;
button.target = target;
if (response != null)
{
if (warnOnEmptyResponseText && DialogueDebug.logWarnings && string.IsNullOrEmpty(response.formattedText.text))
{
Debug.LogWarning($"Dialogue System: Response entry [{response.destinationEntry.id}] menu text is blank.", button);
}
button.SetFormattedText(response.formattedText);
}
// Auto-number:
if (autonumber.enabled)
{
button.text = string.Format(m_processedAutonumberFormat, buttonNumber + 1, button.text);
// Add UIButtonKeyTrigger(s) if needed:
var numKeyTriggersNeeded = 0;
if (autonumber.regularNumberHotkeys) numKeyTriggersNeeded++;
if (autonumber.numpadHotkeys) numKeyTriggersNeeded++;
var keyTriggers = button.GetComponents();
if (keyTriggers.Length < numKeyTriggersNeeded)
{
for (int i = keyTriggers.Length; i < numKeyTriggersNeeded; i++)
{
button.gameObject.AddComponent();
}
keyTriggers = button.GetComponents();
}
int index = 0;
if (autonumber.regularNumberHotkeys)
{
keyTriggers[index++].key = (KeyCode)((int)KeyCode.Alpha1 + buttonNumber);
}
if (autonumber.numpadHotkeys)
{
keyTriggers[index].key = (KeyCode)((int)KeyCode.Keypad1 + buttonNumber);
}
}
}
}
protected int GetNextAvailableResponseButtonPosition(int start, int direction)
{
if (buttons != null)
{
int position = start;
while ((0 <= position) && (position < buttons.Length))
{
if (buttons[position].isVisible && buttons[position].response != null)
{
position += direction;
}
else
{
return position;
}
}
}
return 5;
}
public virtual void SetupTemplateButtonNavigation(bool hasDisabledButton)
{
// Assumes buttons are active (since uses GetComponent), so call after activating panel.
if (instantiatedButtons == null || instantiatedButtons.Count == 0) return;
var buttons = new List();
if (hasDisabledButton)
{
// If some buttons are disabled, make a list of only the clickable ones:
buttons.AddRange(instantiatedButtons.FindAll(x => x.GetComponent().isClickable));
}
else
{
buttons.AddRange(instantiatedButtons);
}
for (int i = 0; i < buttons.Count; i++)
{
var button = buttons[i].GetComponent();
if (button == null) continue;
var above = (i == 0) ? (loopExplicitNavigation ? buttons[buttons.Count - 1].GetComponent() : null)
: buttons[i - 1].GetComponent();
var below = (i == buttons.Count - 1) ? (loopExplicitNavigation ? buttons[0].GetComponent() : null)
: buttons[i + 1].GetComponent();
var navigation = new UnityEngine.UI.Navigation();
navigation.mode = UnityEngine.UI.Navigation.Mode.Explicit;
navigation.selectOnUp = above;
navigation.selectOnLeft = above;
navigation.selectOnDown = below;
navigation.selectOnRight = below;
button.navigation = navigation;
}
}
protected virtual GameObject InstantiateButton()
{
// Try to pull from pool first:
if (m_instantiatedButtonPool.Count > 0)
{
var button = m_instantiatedButtonPool[0];
m_instantiatedButtonPool.RemoveAt(0);
return button;
}
else
{
return GameObject.Instantiate(buttonTemplate.gameObject) as GameObject;
}
}
public void DestroyInstantiatedButtons()
{
// Return buttons to pool:
for (int i = 0; i < instantiatedButtons.Count; i++)
{
instantiatedButtons[i].SetActive(false);
}
m_instantiatedButtonPool.AddRange(instantiatedButtons);
instantiatedButtons.Clear();
NotifyContentChanged();
}
///
/// Makes the panel's buttons non-clickable.
/// Typically called by the dialogue UI as soon as a button has been
/// clicked to make sure the player can't click another one while the
/// menu is playing its hide animation.
///
public virtual void MakeButtonsNonclickable()
{
for (int i = 0; i < instantiatedButtons.Count; i++)
{
var responseButton = (instantiatedButtons[i] != null) ? instantiatedButtons[i].GetComponent() : null;
if (responseButton != null) responseButton.isClickable = false;
}
for (int i = 0; i < buttons.Length; i++)
{
if (buttons[i] != null) buttons[i].isClickable = false;
}
}
protected void NotifyContentChanged()
{
onContentChanged.Invoke();
}
protected void DisableInput()
{
SetInput(false);
}
protected void EnableInput()
{
SetInput(true);
}
protected void SetInput(bool value)
{
s_isInputDisabled = (value == false);
if (m_mainCanvasGroup == null)
{
// Try to get dialogue UI's main panel:
var ui = GetComponentInParent();
if (ui != null && ui.conversationUIElements.mainPanel != null)
{
var mainPanel = ui.conversationUIElements.mainPanel;
m_mainCanvasGroup = mainPanel.GetComponent() ?? mainPanel.gameObject.AddComponent();
}
else
{
// Otherwise try the menu's panel:
var menuPanel = panel;
if (menuPanel == null) menuPanel = buttonTemplateHolder;
if (menuPanel != null)
{
m_mainCanvasGroup = menuPanel.GetComponent() ?? menuPanel.gameObject.AddComponent();
}
}
}
if (m_mainCanvasGroup != null) m_mainCanvasGroup.interactable = value;
if (value == false)
{
// If auto focus, show firstSelected in selected state:
if (InputDeviceManager.autoFocus && firstSelected != null)
{
var button = firstSelected.GetComponent();
MethodInfo methodInfo = typeof(UnityEngine.UI.Button).GetMethod("DoStateTransition", BindingFlags.Instance | BindingFlags.NonPublic);
methodInfo.Invoke(button, new object[] { 3, true }); // 3 = SelectionState.Selected
}
}
if (eventSystem != null)
{
var inputModule = eventSystem.GetComponent();
if (inputModule != null) inputModule.enabled = value;
}
UIButtonKeyTrigger.monitorInput = value;
if (value == true)
{
RefreshSelectablesList();
CheckFocus();
if (eventSystem != null && eventSystem.currentSelectedGameObject != null)
{ // Also show in focused/selected state:
UIUtility.Select(eventSystem.currentSelectedGameObject.GetComponent());
}
}
}
#endregion
#region Timer
///
/// Starts the timer.
///
/// Timeout duration in seconds.
/// Invoke this handler on timeout.
public virtual void StartTimer(float timeout, System.Action timeoutHandler)
{
if (m_timer == null)
{
if (timerSlider != null)
{
Tools.SetGameObjectActive(timerSlider, true);
m_timer = timerSlider.GetComponent();
if (m_timer == null) m_timer = timerSlider.gameObject.AddComponent();
}
else
{
m_timer = GetComponentInChildren();
if (m_timer == null) m_timer = gameObject.AddComponent();
}
}
Tools.SetGameObjectActive(m_timer, true);
m_timer.StartCountdown(timeout, timeoutHandler);
}
public virtual void StopTimer()
{
if (m_timer != null)
{
m_timer.StopCountdown();
Tools.SetGameObjectActive(m_timer, false);
}
}
#endregion
}
}