wirm/Game/Assets/Samples/XR Interaction Toolkit/3.0.3/Spatial Keyboard/Scripts/XRKeyboard.cs
immaunicorn107 be4c63ba7c auth manager and log in scene (under start)
pls double cfm ui on your end
2025-02-20 00:58:27 +08:00

796 lines
27 KiB
C#

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