using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine.Pool;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
///
/// Virtual spatial keyboard.
///
public class XRKeyboard : MonoBehaviour
{
///
/// Layout this keyboard is able to switch to with the corresponding layout command.
///
///
[Serializable]
public struct SubsetMapping
{
[SerializeField, Tooltip("This drives what GameObject layout is displayed.")]
string m_LayoutString;
///
/// This drives what GameObject layout is displayed.
///
public string layoutString
{
get => m_LayoutString;
set => m_LayoutString = value;
}
[SerializeField, Tooltip("GameObject root of the layout which contains the set of keys.")]
XRKeyboardLayout m_LayoutRoot;
///
/// GameObject root of the layout which contains the set of keys.
///
public XRKeyboardLayout layoutRoot
{
get => m_LayoutRoot;
set => m_LayoutRoot = value;
}
[SerializeField, Tooltip("Config asset which contains the key definitions for the layout when this is turned on.")]
XRKeyboardConfig m_ToggleOnConfig;
///
/// Config asset which contains the key definitions for the layout when this is turned on.
///
public XRKeyboardConfig toggleOnConfig
{
get => m_ToggleOnConfig;
set => m_ToggleOnConfig = value;
}
[SerializeField, Tooltip("Config asset which is the default config when this is turned off.")]
XRKeyboardConfig m_ToggleOffConfig;
///
/// Config asset which is the default config when this is turned off.
///
public XRKeyboardConfig toggleOffConfig
{
get => m_ToggleOffConfig;
set => m_ToggleOffConfig = value;
}
}
[SerializeField, HideInInspector]
string m_Text = string.Empty;
///
/// String of text currently in the keyboard. Setter invokes when updated.
///
public string text
{
get => m_Text;
protected set
{
if (m_Text != value)
{
m_Text = value;
caretPosition = Math.Clamp(caretPosition, 0, m_Text.Length);
using (m_KeyboardTextEventArgs.Get(out var args))
{
args.keyboard = this;
args.keyboardText = text;
onTextUpdated?.Invoke(args);
}
}
}
}
[SerializeField, HideInInspector]
TMP_InputField m_CurrentInputField;
///
/// Current input field this keyboard is observing.
///
protected TMP_InputField currentInputField
{
get => m_CurrentInputField;
set
{
if (m_CurrentInputField == value)
return;
StopObservingInputField(m_CurrentInputField);
m_CurrentInputField = value;
StartObservingInputField(m_CurrentInputField);
using (m_KeyboardTextEventArgs.Get(out var args))
{
args.keyboard = this;
args.keyboardText = text;
onFocusChanged?.Invoke(args);
}
}
}
[SerializeField]
KeyboardTextEvent m_OnTextSubmitted = new KeyboardTextEvent();
///
/// Event invoked when keyboard submits text.
///
public KeyboardTextEvent onTextSubmitted
{
get => m_OnTextSubmitted;
set => m_OnTextSubmitted = value;
}
[SerializeField]
KeyboardTextEvent m_OnTextUpdated = new KeyboardTextEvent();
///
/// Event invoked when keyboard text is updated.
///
public KeyboardTextEvent onTextUpdated
{
get => m_OnTextUpdated;
set => m_OnTextUpdated = value;
}
[SerializeField]
KeyboardKeyEvent m_OnKeyPressed = new KeyboardKeyEvent();
///
/// Event invoked after a key is pressed.
///
public KeyboardKeyEvent onKeyPressed
{
get => m_OnKeyPressed;
set => m_OnKeyPressed = value;
}
[SerializeField]
KeyboardModifiersEvent m_OnShifted = new KeyboardModifiersEvent();
///
/// Event invoked after keyboard shift is changed. These event args also contain the value for the caps lock state.
///
public KeyboardModifiersEvent onShifted
{
get => m_OnShifted;
set => m_OnShifted = value;
}
[SerializeField]
KeyboardLayoutEvent m_OnLayoutChanged = new KeyboardLayoutEvent();
///
/// Event invoked when keyboard layout is changed.
///
public KeyboardLayoutEvent onLayoutChanged
{
get => m_OnLayoutChanged;
set => m_OnLayoutChanged = value;
}
[SerializeField]
KeyboardTextEvent m_OnOpened = new KeyboardTextEvent();
///
/// Event invoked when the keyboard is opened.
///
public KeyboardTextEvent onOpened
{
get => m_OnOpened;
set => m_OnOpened = value;
}
[SerializeField]
KeyboardTextEvent m_OnClosed;
///
/// Event invoked after the keyboard is closed.
///
public KeyboardTextEvent onClosed
{
get => m_OnClosed;
set => m_OnClosed = value;
}
[SerializeField]
KeyboardTextEvent m_OnFocusChanged = new KeyboardTextEvent();
///
/// Event invoked when the keyboard changes or gains input field focus.
///
public KeyboardTextEvent onFocusChanged
{
get => m_OnFocusChanged;
set => m_OnFocusChanged = value;
}
[SerializeField]
KeyboardEvent m_OnCharacterLimitReached = new KeyboardEvent();
///
/// Event invoked when the keyboard tries to update text, but the character of the input field is reached.
///
public KeyboardEvent onCharacterLimitReached
{
get => m_OnCharacterLimitReached;
set => m_OnCharacterLimitReached = value;
}
[SerializeField]
bool m_SubmitOnEnter = true;
///
/// If true, will be invoked when the keyboard receives a return or enter command. Otherwise,
/// it will treat return or enter as a newline.
///
public bool submitOnEnter
{
get => m_SubmitOnEnter;
set => m_SubmitOnEnter = value;
}
[SerializeField]
bool m_CloseOnSubmit;
///
/// If true, keyboard will close on enter or return command.
///
public bool closeOnSubmit
{
get => m_CloseOnSubmit;
set => m_CloseOnSubmit = value;
}
[SerializeField]
float m_DoubleClickInterval = 2f;
///
/// Interval in which a key pressed twice would be considered a double click.
///
public float doubleClickInterval
{
get => m_DoubleClickInterval;
set => m_DoubleClickInterval = value;
}
[SerializeField]
List m_SubsetLayout;
///
/// List of layouts this keyboard is able to switch between given the corresponding layout command.
///
/// This supports multiple layout roots updating with the same .
public List subsetLayout
{
get => m_SubsetLayout;
set => m_SubsetLayout = value;
}
///
/// List of keys associated with this keyboard.
///
public List keys { get; set; }
int m_CaretPosition;
///
/// Caret index of this keyboard.
///
public int caretPosition
{
get => m_CaretPosition;
protected set => m_CaretPosition = value;
}
bool m_Shifted;
///
/// (Read Only) Gets the shift state of the keyboard.
///
public bool shifted => m_Shifted;
bool m_CapsLocked;
///
/// (Read Only) Gets the caps lock state of the keyboard.
///
public bool capsLocked => m_CapsLocked;
bool m_IsOpen;
///
/// Returns true if the keyboard has been opened with the open function and the keyboard is active and enabled, otherwise returns false.
///
public bool isOpen => (m_IsOpen && isActiveAndEnabled);
Dictionary> m_SubsetLayoutMap;
HashSet m_KeyboardLayouts;
// Reusable event args
readonly LinkedPool m_KeyboardTextEventArgs = new LinkedPool(() => new KeyboardTextEventArgs(), collectionCheck: false);
readonly LinkedPool m_KeyboardLayoutEventArgs = new LinkedPool(() => new KeyboardLayoutEventArgs(), collectionCheck: false);
readonly LinkedPool m_KeyboardModifiersEventArgs = new LinkedPool(() => new KeyboardModifiersEventArgs(), collectionCheck: false);
readonly LinkedPool m_KeyboardKeyEventArgs = new LinkedPool(() => new KeyboardKeyEventArgs(), collectionCheck: false);
readonly LinkedPool m_KeyboardBaseEventArgs = new LinkedPool(() => new KeyboardBaseEventArgs(), collectionCheck: false);
int m_CharacterLimit = -1;
bool m_MonitorCharacterLimit;
///
/// See .
///
void Awake()
{
m_SubsetLayoutMap = new Dictionary>();
m_KeyboardLayouts = new HashSet();
foreach (var subsetMapping in m_SubsetLayout)
{
if (m_SubsetLayoutMap.TryGetValue(subsetMapping.layoutString, out var subsetMappings))
subsetMappings.Add(subsetMapping);
else
m_SubsetLayoutMap[subsetMapping.layoutString] = new List { subsetMapping };
m_KeyboardLayouts.Add(subsetMapping.layoutRoot);
}
keys = new List();
GetComponentsInChildren(true, keys);
keys.ForEach(key => key.keyboard = this);
}
///
/// See .
///
void OnDisable()
{
// Reset if this component is turned off without first calling close function
m_IsOpen = false;
}
///
/// Processes a .
///
/// Key code to process.
/// True on supported KeyCode.
///
/// Override this method to add support for additional .
///
public virtual bool ProcessKeyCode(KeyCode keyCode)
{
var success = true;
switch (keyCode)
{
case KeyCode.LeftShift:
case KeyCode.RightShift:
Shift(!m_Shifted);
break;
case KeyCode.CapsLock:
CapsLock(!m_CapsLocked);
break;
case KeyCode.Backspace:
Backspace();
break;
case KeyCode.Delete:
Delete();
break;
case KeyCode.Clear:
Clear();
break;
case KeyCode.Space:
UpdateText(" ");
break;
case KeyCode.Return:
case KeyCode.KeypadEnter:
if (submitOnEnter)
{
Submit();
}
else
{
UpdateText("\n");
}
break;
default:
success = false;
break;
}
return success;
}
///
/// Attempts to process the key based on the key's character. Used as a fallback when KeyFunction is
/// empty on the key.
///
/// Key to attempt to process
public virtual void TryProcessKeyPress(XRKeyboardKey key)
{
if (key == null || !ReferenceEquals(key.keyboard, this))
return;
// Process key stroke
if (onKeyPressed != null)
{
// Try to process key code
if (ProcessKeyCode(key.keyCode))
return;
var keyPress = key.GetEffectiveCharacter();
// Monitor for subset change
if (UpdateLayout(keyPress))
return;
switch (keyPress)
{
case "\\s":
// Shift
Shift(!m_Shifted);
break;
case "\\caps":
CapsLock(!m_CapsLocked);
break;
case "\\b":
// Backspace
Backspace();
break;
case "\\c":
// cancel
break;
case "\\r" when submitOnEnter:
{
Submit();
break;
}
case "\\cl":
// Clear
Clear();
break;
case "\\h":
// Hide
Close();
break;
default:
{
UpdateText(keyPress);
break;
}
}
}
}
///
/// Pre-process function when a key is pressed.
///
/// Key that is about to process.
public virtual void PreprocessKeyPress(XRKeyboardKey key)
{
}
///
/// Post-process function when a key is pressed.
///
/// Key that has just been processed.
public virtual void PostprocessKeyPress(XRKeyboardKey key)
{
using (m_KeyboardKeyEventArgs.Get(out var args))
{
args.keyboard = this;
args.key = key;
onKeyPressed.Invoke(args);
}
}
#region Process Key Functions
///
/// Updates the keyboard text by inserting the string into the existing .
///
/// The new text to insert into the current keyboard text.
/// If the keyboard is set to monitor the input field's character limit, the keyboard will ensure
/// the text does not exceed the .
public virtual void UpdateText(string newText)
{
// Attempt to add key press to current text
var updatedText = text;
updatedText = updatedText.Insert(caretPosition, newText);
var isUpdatedTextWithinLimits = !m_MonitorCharacterLimit || updatedText.Length <= m_CharacterLimit;
if (isUpdatedTextWithinLimits)
{
caretPosition += newText.Length;
text = updatedText;
}
else
{
using (m_KeyboardBaseEventArgs.Get(out var args))
{
args.keyboard = this;
onCharacterLimitReached?.Invoke(args);
}
}
// Turn off shift after typing a letter
if (m_Shifted && !m_CapsLocked)
Shift(!m_Shifted);
}
///
/// Process shift command for keyboard.
///
public virtual void Shift(bool shiftValue)
{
m_Shifted = shiftValue;
using (m_KeyboardModifiersEventArgs.Get(out var args))
{
args.keyboard = this;
args.shiftValue = m_Shifted;
args.capsLockValue = m_CapsLocked;
onShifted.Invoke(args);
}
}
///
/// Process caps lock command for keyboard.
///
public virtual void CapsLock(bool capsLockValue)
{
m_CapsLocked = capsLockValue;
Shift(capsLockValue);
}
///
/// Process backspace command for keyboard.
///
public virtual void Backspace()
{
if (caretPosition > 0)
{
--caretPosition;
text = text.Remove(caretPosition, 1);
}
}
///
/// Process delete command for keyboard and deletes one character.
///
public virtual void Delete()
{
if (caretPosition < text.Length)
{
text = text.Remove(caretPosition, 1);
}
}
///
/// Invokes event and closes keyboard if is true.
///
public virtual void Submit()
{
using (m_KeyboardTextEventArgs.Get(out var args))
{
args.keyboard = this;
args.keyboardText = text;
onTextSubmitted?.Invoke(args);
}
if (closeOnSubmit)
Close(false);
}
///
/// Clears text to an empty string.
///
public virtual void Clear()
{
text = string.Empty;
caretPosition = text.Length;
}
///
/// Looks up the associated with the and updates the
/// on the . If the
/// is already ,
/// will be set as the active key mapping.
///
/// The string of the new layout as it is registered in the .
/// Returns true if the layout was successfully found and changed.
/// By default, shift or caps lock will be turned off on layout change.
public virtual bool UpdateLayout(string layoutKey)
{
if (m_SubsetLayoutMap.TryGetValue(layoutKey, out var subsetMappings))
{
foreach (var subsetMapping in subsetMappings)
{
var layout = subsetMapping.layoutRoot;
layout.activeKeyMapping = layout.activeKeyMapping != subsetMapping.toggleOnConfig ? subsetMapping.toggleOnConfig : subsetMapping.toggleOffConfig;
}
if (m_Shifted || m_CapsLocked)
CapsLock(false);
using (m_KeyboardLayoutEventArgs.Get(out var args))
{
args.keyboard = this;
args.layout = layoutKey;
onLayoutChanged.Invoke(args);
}
return true;
}
return false;
}
#endregion
#region Open Functions
///
/// Opens the keyboard with a parameter as the active input field.
///
/// The input field opening this keyboard.
/// If true, keyboard will observe the character limit from the .
public virtual void Open(TMP_InputField inputField, bool observeCharacterLimit = false)
{
currentInputField = inputField;
m_MonitorCharacterLimit = observeCharacterLimit;
m_CharacterLimit = observeCharacterLimit ? currentInputField.characterLimit : -1;
Open(currentInputField.text);
}
///
/// Opens the keyboard with any existing text.
///
///
/// Shortcut for Open(text).
///
public void Open() => Open(text);
///
/// Opens the keyboard with an empty string and clear any existing text in the input field or keyboard.
///
///
/// Shortcut for Open(string.Empty).
///
public void OpenCleared() => Open(string.Empty);
///
/// Opens the keyboard with a given string to populate the keyboard text.
///
/// Text string to set the keyboard to.
/// The event is fired before the text is updating with
/// to give any observers that would be listening the opportunity to close and stop observing before the text is updated.
/// This is a common use case for any utilizing the global keyboard.
public virtual void Open(string newText)
{
if (!isActiveAndEnabled)
{
// Fire event before updating text because any displays observing keyboards will be listening to that text change
// This gives them the opportunity to close and stop observing before the text is updated.
using (m_KeyboardTextEventArgs.Get(out var args))
{
args.keyboard = this;
args.keyboardText = text;
onOpened?.Invoke(args);
}
}
caretPosition = newText.Length;
text = newText;
gameObject.SetActive(true);
m_IsOpen = true;
}
#endregion
#region Close Functions
///
/// Process close command for keyboard.
///
/// Stops observing active input field, resets variables, and hides this GameObject.
public virtual void Close()
{
// Clear any input field the keyboard is observing
currentInputField = null;
m_MonitorCharacterLimit = false;
m_CharacterLimit = -1;
if (m_Shifted || m_CapsLocked)
CapsLock(false);
using (m_KeyboardTextEventArgs.Get(out var args))
{
args.keyboard = this;
args.keyboardText = text;
onClosed?.Invoke(args);
}
gameObject.SetActive(false);
m_IsOpen = false;
}
///
/// Process close command for keyboard. Optional overload for clearing text and resetting layout on close.
///
/// If true, text will be cleared upon keyboard closing. This will happen after the
/// event is fired so the observers have time to stop listening.
/// If true, each will reset to the .
/// Please note, if is true, the text will be cleared and the
/// event will be fired. This means any observers will be notified of an empty string. To avoid unwanted behavior of
/// the text clearing, use the event to unsubscribe to the keyboard events before the text is cleared.
public virtual void Close(bool clearText, bool resetLayout = true)
{
Close();
if (clearText)
text = string.Empty;
// Reset keyboard layout on close
if (resetLayout)
{
// Loop through each layout root and reset to default layouts
foreach (var layoutRoot in m_KeyboardLayouts)
{
layoutRoot.SetDefaultLayout();
}
// Fire event of layout change to ensure highlighted buttons are reset
using (m_KeyboardLayoutEventArgs.Get(out var args))
{
args.keyboard = this;
args.layout = "default";
onLayoutChanged.Invoke(args);
}
}
}
#endregion
#region Input Field Handling
protected virtual void StopObservingInputField(TMP_InputField inputField)
{
if (inputField == null)
return;
currentInputField.onValueChanged.RemoveListener(OnInputFieldValueChange);
}
protected virtual void StartObservingInputField(TMP_InputField inputField)
{
if (inputField == null)
return;
currentInputField.onValueChanged.AddListener(OnInputFieldValueChange);
}
///
/// Callback method invoked when the input field's text value changes.
///
/// The text of the input field.
protected virtual void OnInputFieldValueChange(string updatedText)
{
caretPosition = updatedText.Length;
text = updatedText;
}
#endregion
}
}