Last active
November 16, 2023 01:48
-
-
Save thsbrown/32765b7aaad3c76340d26070bd9eec79 to your computer and use it in GitHub Desktop.
A relatively hacky way to work around TMP_InputField
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using DG.Tweening; | |
using Sirenix.OdinInspector; | |
using TMPro; | |
using UnityEngine; | |
using UnityEngine.UI; | |
/// <summary> | |
/// An input that allows for easy error reporting alongside it | |
/// </summary> | |
public class ErrorableInput : MonoBehaviour | |
{ | |
/// <summary> | |
/// The input to validate and use | |
/// </summary> | |
[Tooltip("The input to validate and use")] | |
public TMP_InputField input; | |
/// <summary> | |
/// The image that we utilize to display our input borders. | |
/// </summary> | |
[Tooltip("The image that we utilize to display our input borders.")] | |
public Image borderImage; | |
/// <summary> | |
/// Displays error message for the input | |
/// </summary> | |
[Tooltip("Displays error message for the input")] | |
public TextMeshProUGUI errorText; | |
/// <summary> | |
/// The inline character at the beginning of our input field. | |
/// </summary> | |
[Tooltip("The inline character at the beginning of our input field.")] | |
public TextMeshProUGUI inputPrefixCharacter; | |
/// <summary> | |
/// The inline character at the end of our input field. | |
/// </summary> | |
[Tooltip("The inline character at the end of our input field.")] | |
public TextMeshProUGUI inputSuffixCharacter; | |
/// <summary> | |
/// The color we will color our field in the case of an error. | |
/// </summary> | |
[Tooltip("The color we will color our field in the case of an error.")] | |
[ColorPalette("CCE")] | |
public Color errorColor; | |
/// <summary> | |
/// The color we will color when there is no error. | |
/// </summary> | |
[Tooltip("The color we will color when there is no error.")] | |
[ColorPalette("CCE")] | |
public Color normalColor; | |
/// <summary> | |
/// Fired when <see cref="Dirty"/> value has changed. The value passed along is the new value of dirty. | |
/// </summary> | |
public event Action<bool> OnDirtyChanged; | |
private bool dirty; | |
private const float ERROR_TRANSITION_DURATION = 0.2f; | |
private void Awake() | |
{ | |
input.onValueChanged.AddListener(ClearErrorsOnValueInputChangedHandler); | |
} | |
/// <summary> | |
/// Displays an error with the given error message. | |
/// </summary> | |
/// <param name="errorMessage">The error message to display</param> | |
public void DisplayError(string errorMessage) | |
{ | |
Dirty = true; | |
errorText.text = errorMessage; | |
inputPrefixCharacter.DOColor(errorColor, ERROR_TRANSITION_DURATION); | |
borderImage.DOColor(errorColor, ERROR_TRANSITION_DURATION); | |
errorText.gameObject.SetActive(true); | |
inputSuffixCharacter.gameObject.SetActive(true); | |
} | |
/// <summary> | |
/// Clears the input field of visual errors | |
/// </summary> | |
public void ClearError(bool clearInputText = false) | |
{ | |
if (clearInputText) | |
{ | |
input.text = ""; | |
} | |
Dirty = false; | |
errorText.text = ""; | |
inputPrefixCharacter.DOColor(normalColor, ERROR_TRANSITION_DURATION); | |
borderImage.DOColor(normalColor, ERROR_TRANSITION_DURATION); | |
errorText.gameObject.SetActive(false); | |
inputSuffixCharacter.gameObject.SetActive(false); | |
} | |
/// <summary> | |
/// Clears displayed errors when the input value has been modified and is marked dirty | |
/// </summary> | |
/// <param name="text">The incoming text</param> | |
private void ClearErrorsOnValueInputChangedHandler(string text) | |
{ | |
if (!dirty) | |
{ | |
return; | |
} | |
ClearError(); | |
} | |
/// <summary> | |
/// When true will display allow errors to be displayed to user onValueChanged | |
/// </summary> | |
public bool Dirty | |
{ | |
get => dirty; | |
private set | |
{ | |
dirty = value; | |
OnDirtyChanged?.Invoke(dirty); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using Cysharp.Threading.Tasks; | |
using UnityEngine; | |
using UnityEngine.EventSystems; | |
using UnityEngine.InputSystem; | |
using UnityEngine.InputSystem.Controls; | |
using UnityEngine.UI; | |
using UnityEngine.UI.ProceduralImage; | |
#if !DISABLESTEAMWORKS | |
using Steamworks; | |
#endif | |
namespace _Game_Assets.Scripts.Runtime.Input | |
{ | |
public class StandaloneInputFieldController : Selectable, ISubmitHandler | |
{ | |
/// <summary> | |
/// The errorable input associated with the input field we are controlling. | |
/// </summary> | |
[Tooltip("The errorable input associated with the input field we are controlling.")] | |
public ErrorableInput errorableInput; | |
/// <summary> | |
/// The image component that will be highlighted when our input field is "selected". | |
/// </summary> | |
[Tooltip("The image component that will be highlighted when our input field is \"selected\".")] | |
public ProceduralImage inputFieldSelectedHighlighter; | |
/// <summary> | |
/// The input action asset that is being utilized for our UI events. | |
/// </summary> | |
[Tooltip("The input action asset that is being utilized for our UI events.")] | |
public InputActionAsset uiInputActionAsset; | |
/// <summary> | |
/// Reference to the action invoker on the exit button that will close our canvas controller | |
/// </summary> | |
[Tooltip("Reference to the action invoker on the exit button that will close our canvas controller")] | |
public InputActionListener modalExitButtonInputActionListener; | |
/// <summary> | |
/// The button associated with this input field that will submit the input field when pressed. | |
/// </summary> | |
[Tooltip("The button associated with this input field that will submit the input field when pressed.") | |
public Button associatedSubmitButton; | |
/// <summary> | |
/// The keyboard that will be shown when the game is running in steam. Most commonly on the steam deck. | |
/// </summary> | |
[Tooltip("The keyboard that will be shown when the game is running in steam. Most commonly on the steam deck.")] | |
public SteamKeyboard steamKeyboardToUse; | |
private bool preventUiSubmitInputAction = true; | |
private bool preventUiCancelInputAction = true; | |
private const string UI_ACTION_MAP_NAME = "UI"; | |
private const string UI_SUBMIT_ACTION_NAME = "Submit"; | |
private const string UI_CANCEL_ACTION_NAME = "Cancel"; | |
#if UNITY_STANDALONE && !DISABLESTEAMWORKS | |
private Callback<GamepadTextInputDismissed_t> gamepadTextInputDismissedCallback; | |
#endif | |
protected override void OnEnable() | |
{ | |
base.OnEnable(); | |
errorableInput.input.onSelect.AddListener(OnInputFieldSelectHandler); | |
errorableInput.input.onDeselect.AddListener(OnInputFieldDeselectHandler); | |
uiInputActionAsset.FindActionMap(UI_ACTION_MAP_NAME).FindAction(UI_SUBMIT_ACTION_NAME).performed += OnUISubmitInputActionPerformed; | |
uiInputActionAsset.FindActionMap(UI_ACTION_MAP_NAME).FindAction(UI_CANCEL_ACTION_NAME).performed += OnUICancelInputActionPerformed; | |
errorableInput.OnDirtyChanged += OnDirtyChangedUpdateControllerHighlighterHandler; | |
#if UNITY_STANDALONE && !DISABLESTEAMWORKS | |
gamepadTextInputDismissedCallback = Callback<GamepadTextInputDismissed_t>.Create(OnGamepadTextInputDismissed); | |
#endif | |
} | |
protected override void OnDisable() | |
{ | |
base.OnDisable(); | |
errorableInput.input.onSelect.RemoveListener(OnInputFieldSelectHandler); | |
errorableInput.input.onDeselect.RemoveListener(OnInputFieldDeselectHandler); | |
uiInputActionAsset.FindActionMap(UI_ACTION_MAP_NAME).FindAction(UI_SUBMIT_ACTION_NAME).performed -= OnUISubmitInputActionPerformed; | |
uiInputActionAsset.FindActionMap(UI_ACTION_MAP_NAME).FindAction(UI_CANCEL_ACTION_NAME).performed -= OnUICancelInputActionPerformed; | |
errorableInput.OnDirtyChanged -= OnDirtyChangedUpdateControllerHighlighterHandler; | |
#if UNITY_STANDALONE && !DISABLESTEAMWORKS | |
gamepadTextInputDismissedCallback.Dispose(); | |
#endif | |
} | |
#region Standalone Input Field Controller Handlers | |
/// <summary> | |
/// When we are selected, colorize our input field highlighter image to show that our input field is selected. | |
/// </summary> | |
/// <param name="eventData"></param> | |
public override void OnSelect(BaseEventData eventData) | |
{ | |
base.OnSelect(eventData); | |
OnDirtyChangedUpdateControllerHighlighterHandler(errorableInput.Dirty); | |
} | |
/// <summary> | |
/// When we select something besides the input field we represent (another gameobject) disable our highlighting as we | |
/// are no longer "selecting" our input field | |
/// </summary> | |
/// <param name="eventData"></param> | |
public override async void OnDeselect(BaseEventData eventData) | |
{ | |
base.OnDeselect(eventData); | |
//wait one frame to give baseEventData.selectedObject a chance to update so we can see what we actually selected | |
await UniTask.NextFrame(); | |
//if we selected our input field we want to stay highlighted, otherwise we selected our submit button or something else so we want to unhighlight | |
if (eventData.selectedObject == errorableInput.input.gameObject) | |
{ | |
return; | |
} | |
inputFieldSelectedHighlighter.color = Color.clear; | |
} | |
/// <summary> | |
/// When our input field controller is submitted, we want to enter into edit mode of our input field so select it. | |
/// </summary> | |
/// <param name="eventData"></param> | |
public void OnSubmit(BaseEventData eventData) | |
{ | |
errorableInput.input.Select(); | |
} | |
#endregion | |
#region Input Field Handlers | |
/// <summary> | |
/// Our input field is now selected so ensure back input exits input field not the menu and submit input | |
/// executes the associated submit button on click behavior. | |
/// </summary> | |
/// <param name="text"></param> | |
private void OnInputFieldSelectHandler(string text) | |
{ | |
modalExitButtonInputActionListener.enabled = false; | |
preventUiSubmitInputAction = false; | |
preventUiCancelInputAction = false; | |
#if UNITY_STANDALONE && !DISABLESTEAMWORKS | |
if (SteamManager.Initialized) | |
{ | |
switch (steamKeyboardToUse) | |
{ | |
case SteamKeyboard.FloatingKeyboard: | |
//optimize this | |
var errorableInputRectTransform = errorableInput.gameObject.GetComponent<RectTransform>(); | |
var canvas = errorableInputRectTransform.GetComponentInParent<Canvas>(); | |
var rect = RectTransformUtility.PixelAdjustRect(errorableInputRectTransform, canvas); | |
//use multiline so keyboard wont dismiss when we press enter | |
SteamUtils.ShowFloatingGamepadTextInput( | |
EFloatingGamepadTextInputMode.k_EFloatingGamepadTextInputModeModeMultipleLines, (int)rect.x, | |
(int)rect.y, (int)rect.size.x, (int)rect.size.y); | |
break; | |
case SteamKeyboard.BigPictureKeyboard: | |
SteamUtils.ShowGamepadTextInput( | |
EGamepadTextInputMode.k_EGamepadTextInputModeNormal, | |
EGamepadTextInputLineMode.k_EGamepadTextInputLineModeSingleLine, "Enter Display Name", | |
(uint)errorableInput.input.characterLimit, errorableInput.input.text); | |
break; | |
default: | |
throw new ArgumentOutOfRangeException(); | |
} | |
} | |
#endif | |
} | |
/// <summary> | |
/// Our input field is no longer selected so ensure back input once again exits the menu and submit input does nothing. | |
/// </summary> | |
/// <param name="text"></param> | |
private void OnInputFieldDeselectHandler(string text) | |
{ | |
modalExitButtonInputActionListener.enabled = true; | |
preventUiSubmitInputAction = true; | |
preventUiCancelInputAction = true; | |
#if UNITY_STANDALONE && !DISABLESTEAMWORKS | |
if (SteamManager.Initialized) | |
{ | |
SteamUtils.DismissFloatingGamepadTextInput(); | |
} | |
#endif | |
} | |
#endregion | |
#region Input System UI Handlers | |
/// <summary> | |
/// When we have submit input activate our associated submit button (unless we are preventing it). | |
/// </summary> | |
/// <param name="context"></param> | |
private void OnUISubmitInputActionPerformed(InputAction.CallbackContext context) | |
{ | |
if (preventUiSubmitInputAction) | |
{ | |
return; | |
} | |
//bug fix that causes input field to become uneditable when we submit via enter key vs other input method (confirm button on gamepad - A) | |
//doing this, we can reverse the hard coded behavior for the enter key in the TMP_InputField.cs script | |
if (context.control is KeyControl keyControl && keyControl.keyCode == Key.Enter) | |
{ | |
errorableInput.input.ActivateInputField(); | |
} | |
associatedSubmitButton.onClick.Invoke(); | |
} | |
/// <summary> | |
/// When we have cancel input activate our associated cancel button (unless we are preventing it). | |
/// </summary> | |
/// <param name="context"></param> | |
private void OnUICancelInputActionPerformed(InputAction.CallbackContext context) | |
{ | |
if (preventUiCancelInputAction) | |
{ | |
return; | |
} | |
Select(); | |
} | |
#endregion | |
#region General Handlers | |
/// <summary> | |
/// Make our input field highlighter image red if our input field is dirty, and normal if it is not. | |
/// </summary> | |
/// <param name="isDirty"></param> | |
private void OnDirtyChangedUpdateControllerHighlighterHandler(bool isDirty) | |
{ | |
inputFieldSelectedHighlighter.color = isDirty ? errorableInput.errorColor : errorableInput.normalColor; | |
} | |
#if UNITY_STANDALONE && !DISABLESTEAMWORKS | |
private void OnGamepadTextInputDismissed(GamepadTextInputDismissed_t callback) | |
{ | |
//the user cancelled so do nothing | |
if (!callback.m_bSubmitted) | |
{ | |
return; | |
} | |
var length = SteamUtils.GetEnteredGamepadTextLength(); | |
//according to steam return should only ever happen if length is > MaxInputLength | |
if (!SteamUtils.GetEnteredGamepadTextInput(out var enteredText, length)) | |
{ | |
return; | |
} | |
errorableInput.input.text = enteredText; | |
} | |
#endif | |
#endregion | |
public enum SteamKeyboard | |
{ | |
/// <summary> | |
/// Keyboard that will float above game and stream text directly into the game. | |
/// </summary> | |
FloatingKeyboard, | |
/// <summary> | |
/// Keyboard that will take up the whole screen that requires a callback to get text | |
/// </summary> | |
BigPictureKeyboard | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment