Add XR sample assets and update settings

This commit is contained in:
2026-01-21 14:18:30 +01:00
parent da213b4475
commit 0ae28bf32d
1990 changed files with 506411 additions and 76 deletions

View File

@@ -0,0 +1,243 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
using TMPro;
using UnityEngine.XR.Interaction.Toolkit.Utilities;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// Manages spawning and positioning of the global keyboard.
/// </summary>
public class GlobalNonNativeKeyboard : MonoBehaviour
{
public static GlobalNonNativeKeyboard instance { get; private set; }
[SerializeField, Tooltip("The prefab with the XR Keyboard component to automatically instantiate.")]
GameObject m_KeyboardPrefab;
/// <summary>
/// The prefab with the XR Keyboard component to automatically instantiate.
/// </summary>
public GameObject keyboardPrefab
{
get => m_KeyboardPrefab;
set => m_KeyboardPrefab = value;
}
[SerializeField, Tooltip("The parent Transform to instantiate the Keyboard Prefab under.")]
Transform m_PlayerRoot;
/// <summary>
/// The parent Transform to instantiate the Keyboard Prefab under.
/// </summary>
public Transform playerRoot
{
get => m_PlayerRoot;
set => m_PlayerRoot = value;
}
[HideInInspector]
[SerializeField]
XRKeyboard m_Keyboard;
/// <summary>
/// Global keyboard instance.
/// </summary>
public XRKeyboard keyboard
{
get => m_Keyboard;
set => m_Keyboard = value;
}
[SerializeField, Tooltip("Position offset from the camera to place the keyboard.")]
Vector3 m_KeyboardOffset;
/// <summary>
/// Position offset from the camera to place the keyboard.
/// </summary>
public Vector3 keyboardOffset
{
get => m_KeyboardOffset;
set => m_KeyboardOffset = value;
}
[SerializeField, Tooltip("Transform of the camera. If left empty, this will default to Camera.main.")]
Transform m_CameraTransform;
/// <summary>
/// Transform of the camera. If left empty, this will default to Camera.main.
/// </summary>
public Transform cameraTransform
{
get => m_CameraTransform;
set => m_CameraTransform = value;
}
[SerializeField, Tooltip("If true, the keyboard will be repositioned to the starting position if it is out of view when Show Keyboard is called.")]
bool m_RepositionOutOfViewKeyboardOnOpen = true;
/// <summary>
/// If true, the keyboard will be repositioned to the starting position if it is out of view when Show Keyboard is called.
/// </summary>
public bool repositionOutOfViewKeyboardOnOpen
{
get => m_RepositionOutOfViewKeyboardOnOpen;
set => m_RepositionOutOfViewKeyboardOnOpen = value;
}
[SerializeField, Tooltip("Threshold for the dot product when determining if the keyboard is out of view and should be repositioned. The lower the threshold, the wider the field of view."), Range(0f, 1f)]
float m_FacingKeyboardThreshold = 0.15f;
/// <summary>
/// Threshold for the dot product when determining if the keyboard is out of view and should be repositioned. The lower the threshold, the wider the field of view.
/// </summary>
public float facingKeyboardThreshold
{
get => m_FacingKeyboardThreshold;
set => m_FacingKeyboardThreshold = value;
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Awake()
{
if (instance != null && instance != this)
{
Destroy(this);
return;
}
instance = this;
if (m_CameraTransform == null)
{
var mainCamera = Camera.main;
if (mainCamera != null)
m_CameraTransform = mainCamera.transform;
else
Debug.LogWarning("Could not find main camera to assign the missing Camera Transform property.", this);
}
if (m_KeyboardPrefab != null)
{
keyboard = Instantiate(m_KeyboardPrefab, m_PlayerRoot).GetComponent<XRKeyboard>();
keyboard.gameObject.SetActive(false);
}
}
/// <summary>
/// Opens the global keyboard with a <see cref="TMP_InputField"/> to monitor.
/// </summary>
/// <remarks>This will update the keyboard with <see cref="TMP_InputField.text"/> as the existing string for the keyboard.</remarks>
/// <param name="inputField">The input field for the global keyboard to monitor</param>
/// <param name="observeCharacterLimit">If true, the global keyboard will respect the character limit of the
/// <see cref="inputField"/>. This is false by default.</param>
public virtual void ShowKeyboard(TMP_InputField inputField, bool observeCharacterLimit = false)
{
if (keyboard == null)
return;
// Check if keyboard is already open or should be repositioned
var shouldPositionKeyboard = !keyboard.isOpen || (m_RepositionOutOfViewKeyboardOnOpen && IsKeyboardOutOfView());
// Open keyboard
keyboard.Open(inputField, observeCharacterLimit);
// Position keyboard in front of user if the keyboard is closed
if (shouldPositionKeyboard)
PositionKeyboard(m_CameraTransform);
}
/// <summary>
/// Opens the global keyboard with the option to populate it with existing text.
/// </summary>
/// <remarks>This will update the keyboard with <see cref="text"/> as the existing string for the keyboard.</remarks>
/// <param name="text">The existing text string to populate the keyboard with on open.</param>
public virtual void ShowKeyboard(string text)
{
if (keyboard == null)
return;
// Check if keyboard is already open or should be repositioned
var shouldPositionKeyboard = !keyboard.isOpen || (m_RepositionOutOfViewKeyboardOnOpen && IsKeyboardOutOfView());
// Open keyboard
keyboard.Open(text);
// Position keyboard in front of user if the keyboard is closed
if (shouldPositionKeyboard)
PositionKeyboard(m_CameraTransform);
}
/// <summary>
/// Opens the global keyboard with the option to clear any existing keyboard text.
/// </summary>
/// <param name="clearKeyboardText">If true, the keyboard will open with no string populated in the keyboard. If false,
/// the existing text will be maintained. This is false by default.</param>
public void ShowKeyboard(bool clearKeyboardText = false)
{
if (keyboard == null)
return;
ShowKeyboard(clearKeyboardText ? string.Empty : keyboard.text);
}
/// <summary>
/// Closes the global keyboard.
/// </summary>
public virtual void HideKeyboard()
{
if (keyboard == null)
return;
keyboard.Close();
}
/// <summary>
/// Reposition <see cref="keyboard"/> to starting position if it is out of view. Keyboard will only reposition if is active and enabled.
/// </summary>
/// <remarks>
/// Field if view is defined by the <see cref="facingKeyboardThreshold"/>, and the starting position
/// is defined by the <see cref="keyboardOffset"/> in relation to the camera.
/// </remarks>
public void RepositionKeyboardIfOutOfView()
{
if (IsKeyboardOutOfView())
{
if (keyboard.isOpen)
PositionKeyboard(m_CameraTransform);
}
}
void PositionKeyboard(Transform target)
{
var position = target.position +
target.right * m_KeyboardOffset.x +
target.forward * m_KeyboardOffset.z +
Vector3.up * m_KeyboardOffset.y;
keyboard.transform.position = position;
FaceKeyboardAtTarget(m_CameraTransform);
}
void FaceKeyboardAtTarget(Transform target)
{
var forward = (keyboard.transform.position - target.position).normalized;
BurstMathUtility.OrthogonalLookRotation(forward, Vector3.up, out var newTarget);
keyboard.transform.rotation = newTarget;
}
bool IsKeyboardOutOfView()
{
if (m_CameraTransform == null || keyboard == null)
{
Debug.LogWarning("Camera or keyboard reference is null. Unable to determine if keyboard is out of view.", this);
return false;
}
var dotProduct = Vector3.Dot(m_CameraTransform.forward, (keyboard.transform.position - m_CameraTransform.position).normalized);
return dotProduct < m_FacingKeyboardThreshold;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 015b50343a7cf174ebb7703245f6ba72
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// Abstract class defining callbacks for key functionality. Allows users to extend
/// custom functionality of keys and keyboard.
/// </summary>
public abstract class KeyFunction : ScriptableObject
{
/// <summary>
/// Pre-process function when a key is pressed.
/// </summary>
/// <param name="keyboardContext">The current keyboard associated with <see cref="key"/>.</param>
/// <param name="key">The key that is being pressed.</param>
public virtual void PreprocessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext != null)
keyboardContext.PreprocessKeyPress(key);
}
/// <summary>
/// Primary function callback when a key is pressed. Use this function to interface directly with a keyboard
/// and process logic based on the current keyboard and key context.
/// </summary>
/// <param name="keyboardContext">The current keyboard associated with <see cref="key"/>.</param>
/// <param name="key">The key that is being pressed.</param>
public abstract void ProcessKey(XRKeyboard keyboardContext, XRKeyboardKey key);
/// <summary>
/// Post-process function when a key is pressed. This function calls <see cref="XRKeyboard.PostprocessKeyPress"/> on the keyboard.
/// </summary>
/// <param name="keyboardContext">The current keyboard associated with <see cref="key"/>.</param>
/// <param name="key">The key that is being pressed.</param>
public virtual void PostprocessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext != null)
keyboardContext.PostprocessKeyPress(key);
}
/// <summary>
/// Uses keyboard and key context to determine if this key function should override the key's display icon.
/// </summary>
/// <param name="keyboardContext">Current keyboard context.</param>
/// <param name="key">Current keyboard key.</param>
/// <returns>Returns true if this key function should override the display icon.</returns>
public virtual bool OverrideDisplayIcon(XRKeyboard keyboardContext, XRKeyboardKey key)
{
return false;
}
/// <summary>
/// Returns display icon for this key function based on the context of the key and keyboard.
/// </summary>
/// <param name="keyboardContext">Current keyboard context.</param>
/// <param name="key">Current keyboard key.</param>
/// <returns>Returns display icon for this key.</returns>
public virtual Sprite GetDisplayIcon(XRKeyboard keyboardContext, XRKeyboardKey key)
{
return null;
}
/// <summary>
/// Allows this key function to process when a key is refreshing its display.
/// </summary>
/// <param name="keyboardContext">The current keyboard associated with <see cref="key"/>.</param>
/// <param name="key">The key that is refreshing the display.</param>
public virtual void ProcessRefreshDisplay(XRKeyboard keyboardContext, XRKeyboardKey key)
{
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 18e905492db2a3e4e9fd486d79848865
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 89613eddc1ae20046a22fe8d45de2d0d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard.KeyFunctions
{
/// <summary>
/// Key function used to hide the keyboard.
/// </summary>
[CreateAssetMenu(fileName = "Hide Function", menuName = "XR/Spatial Keyboard/Hide Key Function", order = 1)]
public class HideFunction : KeyFunction
{
/// <inheritdoc />
public override void ProcessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext != null)
keyboardContext.Close(false);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 58528143acafcd44e93258c8e6a59dc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard.KeyFunctions
{
/// <summary>
/// Key function used to send a key code for the keyboard to process.
/// </summary>
[CreateAssetMenu(fileName = "Key Code Function", menuName = "XR/Spatial Keyboard/Key Code Key Function", order = 1)]
public class KeyCodeFunction : KeyFunction
{
/// <inheritdoc />
public override void ProcessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext != null)
keyboardContext.ProcessKeyCode(key.keyCode);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92861f72e977a244cb0be6d8410a8d99
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard.KeyFunctions
{
/// <summary>
/// Key function used to update the keyboard layout.
/// </summary>
[CreateAssetMenu(fileName = "Layout Function", menuName = "XR/Spatial Keyboard/Layout Key Function", order = 1)]
public class LayoutFunction : KeyFunction
{
/// <inheritdoc />
public override void ProcessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext != null)
keyboardContext.UpdateLayout(key.GetEffectiveCharacter());
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0f25471b9f361d64ba3aebe064d73092
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,73 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard.KeyFunctions
{
/// <summary>
/// Key function used to process shift and caps lock functionality.
/// </summary>
[CreateAssetMenu(fileName = "Shift Function", menuName = "XR/Spatial Keyboard/Shift Key Function", order = 1)]
public class ShiftFunction : KeyFunction
{
[SerializeField]
Sprite m_CapsLockDisplayIcon;
public Sprite capsLockDisplayIcon
{
get => m_CapsLockDisplayIcon;
set => m_CapsLockDisplayIcon = value;
}
/// <inheritdoc />
public override Sprite GetDisplayIcon(XRKeyboard keyboardContext, XRKeyboardKey key)
{
// This method won't be called unless OverrideDisplayIcon below returns true,
// so no need for logic to return a shift display icon, which is already set up
// as the default in the UI.
return m_CapsLockDisplayIcon;
}
/// <inheritdoc />
public override bool OverrideDisplayIcon(XRKeyboard keyboardContext, XRKeyboardKey key)
{
return keyboardContext != null && keyboardContext.capsLocked;
}
/// <inheritdoc />
public override void ProcessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext == null)
return;
var keyCode = KeyCode.LeftShift;
// Check the caps lock state of the keyboard. If they key is shifted, check if there is a double click.
if (keyboardContext.capsLocked || (keyboardContext.shifted && key.timeSinceLastClick < keyboardContext.doubleClickInterval))
keyCode = KeyCode.CapsLock;
keyboardContext.ProcessKeyCode(keyCode);
}
/// <inheritdoc />
public override void PostprocessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
base.PostprocessKey(keyboardContext, key);
RefreshKeyHighlight(keyboardContext, key);
}
/// <inheritdoc />
public override void ProcessRefreshDisplay(XRKeyboard keyboardContext, XRKeyboardKey key)
{
base.ProcessRefreshDisplay(keyboardContext, key);
RefreshKeyHighlight(keyboardContext, key);
}
protected void RefreshKeyHighlight(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext == null)
return;
var highlight = keyboardContext.capsLocked || keyboardContext.shifted;
key.EnableHighlight(highlight);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cad704687d7908f42b71e1e4516c590a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard.KeyFunctions
{
/// <summary>
/// Key function used to update the keyboard text with a string value.
/// </summary>
[CreateAssetMenu(fileName = "Value Key Function", menuName = "XR/Spatial Keyboard/Value Key Function", order = 1)]
public class ValueKeyFunction : KeyFunction
{
/// <inheritdoc />
public override void ProcessKey(XRKeyboard keyboardContext, XRKeyboardKey key)
{
if (keyboardContext != null)
keyboardContext.UpdateText(key.GetEffectiveCharacter());
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4857d519405a3874cb91aea6424e314d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// This component moves a set of transforms to the same local z-axis position as a poke follow transform.
/// This is useful for batchable objects that need to move together.
/// </summary>
public class KeyboardBatchFollow : MonoBehaviour
{
[Tooltip("The transform to follow.")]
[SerializeField]
Transform m_FollowTransform;
/// <summary>
/// The transform to follow.
/// </summary>
public Transform followTransform
{
get => m_FollowTransform;
set => m_FollowTransform = value;
}
[Tooltip("The transforms to move to the same local z-axis position as the poke follow transform.")]
[SerializeField]
Transform[] m_FollowerTransforms;
/// <summary>
/// The transforms to move to the same local z-axis position as the poke follow transform.
/// </summary>
public Transform[] followerTransforms
{
get => m_FollowerTransforms;
set => m_FollowerTransforms = value;
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void OnDisable()
{
if (m_FollowerTransforms == null || m_FollowerTransforms.Length == 0)
return;
for (var index = 0; index < m_FollowerTransforms.Length; ++index)
{
var follower = m_FollowerTransforms[index];
var localPosition = follower.localPosition;
localPosition.z = 0f;
follower.localPosition = localPosition;
}
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void LateUpdate()
{
if (m_FollowTransform == null || m_FollowerTransforms == null || m_FollowerTransforms.Length == 0)
return;
var followLocalZ = m_FollowTransform.localPosition.z;
for (var index = 0; index < m_FollowerTransforms.Length; ++index)
{
var follower = m_FollowerTransforms[index];
var localPosition = follower.localPosition;
localPosition.z = followLocalZ;
follower.localPosition = localPosition;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1a82a58ed0ea7024b86e87eac0be67c3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,299 @@
using System.Collections.Generic;
using Unity.XR.CoreUtils;
using UnityEngine.UI;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// This script is used to optimize the keyboard rendering performance by updating the canvas hierarchy
/// into separate parent transforms based on UI Grouping. This will greatly reduce the number of draw calls.
/// Optimization is only done at runtime to prevent breaking the prefab.
/// </summary>
public class KeyboardOptimizer : MonoBehaviour
{
[SerializeField]
bool m_OptimizeOnStart = true;
/// <summary>
/// If enabled, the optimization will be called on <see cref="Start"/>.
/// </summary>
public bool optimizeOnStart
{
get => m_OptimizeOnStart;
set => m_OptimizeOnStart = value;
}
[SerializeField]
Transform m_BatchGroupParentTransform;
/// <summary>
/// The parent transform for batch groups.
/// </summary>
public Transform batchGroupParentTransform
{
get => m_BatchGroupParentTransform;
set => m_BatchGroupParentTransform = value;
}
[SerializeField]
Transform m_ButtonParentTransform;
/// <summary>
/// The parent transform for buttons.
/// </summary>
public Transform buttonParentTransform
{
get => m_ButtonParentTransform;
set => m_ButtonParentTransform = value;
}
[SerializeField]
Transform m_ImageParentTransform;
/// <summary>
/// The parent transform for images.
/// </summary>
public Transform imageParentTransform
{
get => m_ImageParentTransform;
set => m_ImageParentTransform = value;
}
[SerializeField]
Transform m_TextParentTransform;
/// <summary>
/// The parent transform for text elements.
/// </summary>
public Transform textParentTransform
{
get => m_TextParentTransform;
set => m_TextParentTransform = value;
}
[SerializeField]
Transform m_IconParentTransform;
/// <summary>
/// The parent transform for icons.
/// </summary>
public Transform iconParentTransform
{
get => m_IconParentTransform;
set => m_IconParentTransform = value;
}
[SerializeField]
Transform m_HighlightParentTransform;
/// <summary>
/// The parent transform for highlights.
/// </summary>
public Transform highlightParentTransform
{
get => m_HighlightParentTransform;
set => m_HighlightParentTransform = value;
}
bool m_IsCurrentlyOptimized;
/// <summary>
/// Is the keyboard currently optimized?
/// </summary>
public bool isCurrentlyOptimized => m_IsCurrentlyOptimized;
/// <summary>
/// Horizontal layout groups need to be disabled when optimizing the keyboard
/// otherwise the input field will not position correctly.
/// </summary>
HorizontalLayoutGroup[] m_LayoutGroups;
/// <summary>
/// List of key data. This is used to store information that allows us
/// to revert the keyboard back to its original state (aka unoptimize).
/// </summary>
readonly List<KeyData> m_KeyData = new List<KeyData>();
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void Start()
{
CheckReferences();
Canvas.ForceUpdateCanvases();
if (m_OptimizeOnStart)
Optimize();
}
/// <summary>
/// Check all the references needed for optimization.
/// </summary>
void CheckReferences()
{
if (!TryGetOrCreateTransformReferences())
{
Debug.LogError("Failed to get or create transform references. Optimization will not be possible.", this);
return;
}
if (m_KeyData.Count == 0)
GetKeys();
if (m_LayoutGroups == null || m_LayoutGroups.Length == 0)
GetLayoutGroups();
}
bool TryGetOrCreateTransformReferences()
{
if (m_BatchGroupParentTransform == null)
{
var canvasComponent = GetComponentInChildren<Canvas>(true);
if (canvasComponent == null)
{
Debug.LogError("No Canvas component found in hierarchy. Optimization will not be possible.", this);
return false;
}
m_BatchGroupParentTransform = CreateTransformAndSetParent("BatchingGroup", canvasComponent.transform);
}
if (m_ButtonParentTransform == null)
m_ButtonParentTransform = CreateTransformAndSetParent("Buttons", m_BatchGroupParentTransform);
if (m_ImageParentTransform == null)
m_ImageParentTransform = CreateTransformAndSetParent("Images", m_BatchGroupParentTransform);
if (m_TextParentTransform == null)
m_TextParentTransform = CreateTransformAndSetParent("Text", m_BatchGroupParentTransform);
if (m_IconParentTransform == null)
m_IconParentTransform = CreateTransformAndSetParent("Icons", m_BatchGroupParentTransform);
if (m_HighlightParentTransform == null)
m_HighlightParentTransform = CreateTransformAndSetParent("Highlights", m_BatchGroupParentTransform);
return true;
}
void GetKeys()
{
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
XRKeyboardKey[] keys = GetComponentsInChildren<XRKeyboardKey>();
foreach (var keyboardKey in keys)
{
m_KeyData.Add(new KeyData
{
key = keyboardKey,
batchFollow = keyboardKey.GetComponent<KeyboardBatchFollow>(),
parent = keyboardKey.transform.parent,
childPosition = keyboardKey.transform.GetSiblingIndex(),
});
}
#endif
}
void GetLayoutGroups()
{
m_LayoutGroups = GetComponentsInChildren<HorizontalLayoutGroup>();
}
static Transform CreateTransformAndSetParent(string name, Transform parent)
{
var t = new GameObject(name).transform;
t.SetParent(parent);
t.SetLocalPose(Pose.identity);
t.localScale = Vector3.one;
return t;
}
/// <summary>
/// Optimize the keyboard. This will set all the different components of each keyboard key into separate parent transforms for batching.
/// </summary>
public void Optimize()
{
m_IsCurrentlyOptimized = true;
foreach (var layoutGroup in m_LayoutGroups)
{
layoutGroup.enabled = false;
}
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
foreach (var keyData in m_KeyData)
{
var key = keyData.key;
if (key == null)
continue;
key.transform.SetParent(m_ButtonParentTransform);
if (key.targetGraphic != null)
key.targetGraphic.transform.SetParent(m_ImageParentTransform);
if (key.textComponent != null)
key.textComponent.transform.SetParent(m_TextParentTransform);
if (key.iconComponent != null)
key.iconComponent.transform.SetParent(m_IconParentTransform);
if (key.highlightComponent != null)
key.highlightComponent.transform.SetParent(m_HighlightParentTransform);
if (keyData.batchFollow != null)
keyData.batchFollow.enabled = true;
}
#endif
}
/// <summary>
/// Unoptimize the keyboard. This will set the keyboard back to its original state.
/// </summary>
public void Unoptimize()
{
m_IsCurrentlyOptimized = false;
foreach (var layoutGroup in GetComponentsInChildren<HorizontalLayoutGroup>())
{
layoutGroup.enabled = true;
}
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
foreach (var keyData in m_KeyData)
{
var key = keyData.key;
if (key == null)
continue;
// NOTE: Order of objects setting their parent is important for sorting order.
key.transform.SetParent(keyData.parent);
key.transform.SetSiblingIndex(keyData.childPosition);
if (key.targetGraphic != null)
key.targetGraphic.transform.SetParent(key.transform);
if (key.textComponent != null)
key.textComponent.transform.SetParent(key.targetGraphic.transform);
if (key.iconComponent != null)
key.iconComponent.transform.SetParent(key.targetGraphic.transform);
if (key.highlightComponent != null)
key.highlightComponent.transform.SetParent(key.targetGraphic.transform);
if (keyData.batchFollow != null)
keyData.batchFollow.enabled = false;
}
#endif
}
struct KeyData
{
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
public XRKeyboardKey key;
#endif
public KeyboardBatchFollow batchFollow;
public Transform parent;
public int childPosition;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dfb2fb55e0f7aff4db346e2c43f69469
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,798 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
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
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f17a1b36fcb274449d030ef08f66941
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,177 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
using System;
using System.Collections.Generic;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// Scriptable object that defines key mappings to support swapping <see cref="XRKeyboardLayout"/>. There should be one
/// instance of the <see cref="XRKeyboardConfig"/> for each layout (i.e. alphanumeric, symbols, etc.).
/// </summary>
public class XRKeyboardConfig : ScriptableObject
{
/// <summary>
/// Class representing the data needed to populate keys.
/// </summary>
[Serializable]
public class KeyMapping
{
[SerializeField]
string m_Character;
/// <summary>
/// Character for this key in non-shifted state. This string will be passed to the keyboard and appended to the keyboard text string or processed as a keyboard command.
/// </summary>
public string character
{
get => m_Character;
set => m_Character = value;
}
[SerializeField]
string m_DisplayCharacter;
/// <summary>
/// Display character for this key in a non-shifted state. This string will be displayed on the key text field.
/// If empty, character will be used as a fallback.
/// </summary>
public string displayCharacter
{
get => m_DisplayCharacter;
set => m_DisplayCharacter = value;
}
[SerializeField]
Sprite m_DisplayIcon;
/// <summary>
/// Display icon for this key in a non-shifted state. This icon will be displayed on the key image field.
/// If empty, the display character or character will be used as a fallback.
/// </summary>
public Sprite displayIcon
{
get => m_DisplayIcon;
set => m_DisplayIcon = value;
}
[SerializeField]
string m_ShiftCharacter;
/// <summary>
/// Character for this key in a shifted state. This string will be passed to the keyboard and appended to
/// the keyboard text string or processed as a keyboard command.
/// </summary>
public string shiftCharacter
{
get => m_ShiftCharacter;
set => m_ShiftCharacter = value;
}
[SerializeField]
string m_ShiftDisplayCharacter;
/// <summary>
/// Display character for this key in a shifted state. This string will be displayed on the key
/// text field. If empty, shift character will be used as a fallback.
/// </summary>
public string shiftDisplayCharacter
{
get => m_ShiftDisplayCharacter;
set => m_ShiftDisplayCharacter = value;
}
[SerializeField]
Sprite m_ShiftDisplayIcon;
/// <summary>
/// Display icon for this key in a shifted state. This icon will be displayed on the key image field.
/// If empty, the shift display character or shift character will be used as a fallback.
/// </summary>
public Sprite shiftDisplayIcon
{
get => m_ShiftDisplayIcon;
set => m_ShiftDisplayIcon = value;
}
[SerializeField]
bool m_OverrideDefaultKeyFunction;
/// <summary>
/// If true, this will expose a key function property to override the default key function of this config.
/// </summary>
public bool overrideDefaultKeyFunction
{
get => m_OverrideDefaultKeyFunction;
set => m_OverrideDefaultKeyFunction = value;
}
[SerializeField]
KeyFunction m_KeyFunction;
/// <summary>
/// <see cref="KeyFunction"/> used for this key. The function callback will be called on key press
/// and used to communicate with the keyboard API.
/// </summary>
public KeyFunction keyFunction
{
get => m_KeyFunction;
set => m_KeyFunction = value;
}
[SerializeField]
KeyCode m_KeyCode;
/// <summary>
/// (Optional) <see cref="KeyCode"/> used for this key. Used with <see cref="keyFunction"/> to
/// support already defined KeyCode values.
/// </summary>
public KeyCode keyCode
{
get => m_KeyCode;
set => m_KeyCode = value;
}
[SerializeField]
bool m_Disabled;
/// <summary>
/// If true, the key button interactable property will be set to false.
/// </summary>
public bool disabled
{
get => m_Disabled;
set => m_Disabled = value;
}
}
[SerializeField, Tooltip("Default key function for each key in this mapping.")]
KeyFunction m_DefaultKeyFunction;
/// <summary>
/// Default key function for each key in this mapping.
/// </summary>
/// <remarks>This is a utility feature that reduces the authoring needed when most key mappings share the same
/// functionality (i.e. value keys that append characters).</remarks>
public KeyFunction defaultKeyFunction
{
get => m_DefaultKeyFunction;
set => m_DefaultKeyFunction = value;
}
/// <summary>
/// List of each key mapping in this layout.
/// </summary>
[SerializeField, Tooltip("List of each key mapping in this layout.")]
List<KeyMapping> m_KeyMappings;
/// <summary>
/// List of each key mapping in this layout.
/// </summary>
public List<KeyMapping> keyMappings
{
get => m_KeyMappings;
set => m_KeyMappings = value;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 944d8cf1c59c0e8468bcd7e2fd86fe4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,414 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
using TMPro;
using UnityEngine.Events;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// Utility class to help facilitate input field relationship with <see cref="XRKeyboard"/>
/// </summary>
public class XRKeyboardDisplay : MonoBehaviour
{
[SerializeField, Tooltip("Input field linked to this display.")]
TMP_InputField m_InputField;
/// <summary>
/// Input field linked to this display.
/// </summary>
public TMP_InputField inputField
{
get => m_InputField;
set
{
if (inputField != null)
m_InputField.onSelect.RemoveListener(OnInputFieldGainedFocus);
m_InputField = value;
if (inputField != null)
{
m_InputField.resetOnDeActivation = false;
m_InputField.onSelect.AddListener(OnInputFieldGainedFocus);
}
}
}
// The script requires setter property logic to be run, so disable when playing
[SerializeField, Tooltip("Keyboard for this display to monitor and interact with. If empty this will default to the GlobalNonNativeKeyboard keyboard.")]
XRKeyboard m_Keyboard;
/// <summary>
/// Keyboard for this display to monitor and interact with. If empty this will default to the <see cref="GlobalNonNativeKeyboard"/> keyboard.
/// </summary>
public XRKeyboard keyboard
{
get => m_Keyboard;
set => SetKeyboard(value);
}
[SerializeField, Tooltip("If true, this display will use the keyboard reference. If false or if the keyboard field is empty, this display will use global keyboard.")]
bool m_UseSceneKeyboard;
/// <summary>
/// If true, this display will use the keyboard reference. If false or if the keyboard field is empty,
/// this display will use global keyboard.
/// </summary>
public bool useSceneKeyboard
{
get => m_UseSceneKeyboard;
set => m_UseSceneKeyboard = value;
}
[SerializeField, Tooltip("If true, this display will update with each key press. If false, this display will update on OnTextSubmit.")]
bool m_UpdateOnKeyPress = true;
/// <summary>
/// If true, this display will update with each key press. If false, this display will update on OnTextSubmit.
/// </summary>
public bool updateOnKeyPress
{
get => m_UpdateOnKeyPress;
set => m_UpdateOnKeyPress = value;
}
[SerializeField, Tooltip("If true, this display will always subscribe to the keyboard updates. If false, this display will subscribe to keyboard when the input field gains focus.")]
bool m_AlwaysObserveKeyboard;
/// <summary>
/// If true, this display will always subscribe to the keyboard updates. If false, this display will subscribe
/// to keyboard when the input field gains focus.
/// </summary>
public bool alwaysObserveKeyboard
{
get => m_AlwaysObserveKeyboard;
set => m_AlwaysObserveKeyboard = value;
}
[SerializeField, Tooltip("If true, this display will use the input field's character limit to limit the update text from the keyboard and will pass this into the keyboard when opening.")]
public bool m_MonitorInputFieldCharacterLimit;
/// <summary>
/// If true, this display will use the input field's character limit to limit the update text from the keyboard
/// and will pass this into the keyboard when opening if.
/// </summary>
public bool monitorInputFieldCharacterLimit
{
get => m_MonitorInputFieldCharacterLimit;
set => m_MonitorInputFieldCharacterLimit = value;
}
[SerializeField, Tooltip("If true, this display will clear the input field text on text submit from the keyboard.")]
public bool m_ClearTextOnSubmit;
/// <summary>
/// If true, this display will clear the input field text on text submit from the keyboard.
/// </summary>
public bool clearTextOnSubmit
{
get => m_ClearTextOnSubmit;
set => m_ClearTextOnSubmit = value;
}
[SerializeField, Tooltip("If true, this display will clear the input field text when the keyboard opens.")]
public bool m_ClearTextOnOpen;
/// <summary>
/// If true, this display will clear the input field text on text submit from the keyboard.
/// </summary>
public bool clearTextOnOpen
{
get => m_ClearTextOnOpen;
set => m_ClearTextOnOpen = value;
}
[SerializeField, Tooltip("If true, this display will close the keyboard it is observing when this GameObject is disabled.")]
public bool m_HideKeyboardOnDisable = true;
/// <summary>
/// If true, this display will close the keyboard it is observing when this GameObject is disabled.
/// </summary>
/// <remarks>If this display is not observing a keyboard when disabled, this will have not effect on open keyboards.</remarks>
public bool hideKeyboardOnDisable
{
get => m_HideKeyboardOnDisable;
set => m_HideKeyboardOnDisable = value;
}
[SerializeField, Tooltip("The event that is called when this display receives a text submitted event from the keyboard. Invoked with the keyboard text as a parameter.")]
UnityEvent<string> m_OnTextSubmitted = new UnityEvent<string>();
/// <summary>
/// The event that is called when this display receives a text submitted event from the keyboard.
/// </summary>
public UnityEvent<string> onTextSubmitted
{
get => m_OnTextSubmitted;
set => m_OnTextSubmitted = value;
}
[SerializeField, Tooltip("The event that is called when this display opens a keyboard.")]
UnityEvent m_OnKeyboardOpened = new UnityEvent();
/// <summary>
/// The event that is called when this display opens a keyboard.
/// </summary>
public UnityEvent onKeyboardOpened
{
get => m_OnKeyboardOpened;
set => m_OnKeyboardOpened = value;
}
[SerializeField, Tooltip("The event that is called when the keyboard this display is observing is closed.")]
UnityEvent m_OnKeyboardClosed = new UnityEvent();
/// <summary>
/// The event that is called when the keyboard this display is observing is closed.
/// </summary>
public UnityEvent onKeyboardClosed
{
get => m_OnKeyboardClosed;
set => m_OnKeyboardClosed = value;
}
[SerializeField, Tooltip("The event that is called when the keyboard changes focus and this display is not focused.")]
UnityEvent m_OnKeyboardFocusChanged = new UnityEvent();
/// <summary>
/// The event that is called when the keyboard changes focus and this display is not focused.
/// </summary>
public UnityEvent onKeyboardFocusChanged
{
get => m_OnKeyboardFocusChanged;
set => m_OnKeyboardFocusChanged = value;
}
// Active keyboard for this display
XRKeyboard m_ActiveKeyboard;
bool m_IsActivelyObservingKeyboard;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Awake()
{
// Set active keyboard to any serialized keyboard
m_ActiveKeyboard = m_Keyboard;
if (m_InputField != null)
{
// resetOnDeActivation should be false so the caret position does not break with the keyboard interaction
m_InputField.resetOnDeActivation = false;
// shouldHideSoftKeyboard should be true so there is no conflict with the spatial keyboard and the system keyboard
m_InputField.shouldHideSoftKeyboard = true;
}
if (m_AlwaysObserveKeyboard && m_ActiveKeyboard != null)
StartObservingKeyboard(m_ActiveKeyboard);
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnEnable()
{
if (m_InputField != null)
m_InputField.onSelect.AddListener(OnInputFieldGainedFocus);
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDisable()
{
if (m_InputField != null)
m_InputField.onSelect.RemoveListener(OnInputFieldGainedFocus);
// Close the keyboard this display is observing
var isObservingKeyboard = m_ActiveKeyboard != null && m_ActiveKeyboard.gameObject.activeInHierarchy && m_IsActivelyObservingKeyboard;
if (m_HideKeyboardOnDisable && isObservingKeyboard && m_ActiveKeyboard.isOpen)
m_ActiveKeyboard.Close();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDestroy()
{
StopObservingKeyboard(m_ActiveKeyboard);
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Start()
{
// Set active keyboard to global keyboard if needed
if (m_ActiveKeyboard == null || !m_UseSceneKeyboard)
m_ActiveKeyboard = GlobalNonNativeKeyboard.instance.keyboard;
// Observe keyboard if always observe is true
var observeOnStart = m_AlwaysObserveKeyboard && m_ActiveKeyboard != null & !m_IsActivelyObservingKeyboard;
if (observeOnStart)
StartObservingKeyboard(m_ActiveKeyboard);
}
void SetKeyboard(XRKeyboard updateKeyboard, bool observeKeyboard = true)
{
if (ReferenceEquals(updateKeyboard, m_Keyboard))
return;
StopObservingKeyboard(m_ActiveKeyboard);
// Update serialized referenced
m_Keyboard = updateKeyboard;
// Update private keyboard
m_ActiveKeyboard = m_Keyboard;
if (m_ActiveKeyboard != null && (observeKeyboard || m_AlwaysObserveKeyboard))
StartObservingKeyboard(m_ActiveKeyboard);
}
void StartObservingKeyboard(XRKeyboard activeKeyboard)
{
if (activeKeyboard == null || m_IsActivelyObservingKeyboard)
return;
activeKeyboard.onTextUpdated.AddListener(OnTextUpdate);
activeKeyboard.onTextSubmitted.AddListener(OnTextSubmit);
activeKeyboard.onClosed.AddListener(KeyboardClosing);
activeKeyboard.onOpened.AddListener(KeyboardOpening);
activeKeyboard.onFocusChanged.AddListener(KeyboardFocusChanged);
m_IsActivelyObservingKeyboard = true;
}
void StopObservingKeyboard(XRKeyboard activeKeyboard)
{
if (activeKeyboard == null)
return;
activeKeyboard.onTextUpdated.RemoveListener(OnTextUpdate);
activeKeyboard.onTextSubmitted.RemoveListener(OnTextSubmit);
activeKeyboard.onClosed.RemoveListener(KeyboardClosing);
activeKeyboard.onOpened.RemoveListener(KeyboardOpening);
activeKeyboard.onFocusChanged.RemoveListener(KeyboardFocusChanged);
m_IsActivelyObservingKeyboard = false;
}
void OnInputFieldGainedFocus(string text)
{
// If this display is already observing keyboard, sync, attempt to reposition, and early out
// Displays that are always observing keyboards call open to ensure they sync with the keyboard
if (m_IsActivelyObservingKeyboard && !alwaysObserveKeyboard)
{
if (!m_UseSceneKeyboard || m_Keyboard == null)
GlobalNonNativeKeyboard.instance.RepositionKeyboardIfOutOfView();
// Sync input field caret position with keyboard caret position
if (m_InputField.stringPosition != m_ActiveKeyboard.caretPosition)
m_InputField.stringPosition = m_ActiveKeyboard.caretPosition;
return;
}
if (m_ClearTextOnOpen)
m_InputField.text = string.Empty;
// If not using a scene keyboard, use global keyboard.
if (!m_UseSceneKeyboard || m_Keyboard == null)
{
GlobalNonNativeKeyboard.instance.ShowKeyboard(m_InputField, m_MonitorInputFieldCharacterLimit);
}
else
{
m_ActiveKeyboard.Open(m_InputField, m_MonitorInputFieldCharacterLimit);
}
// Sync input field caret position with keyboard caret position
if (m_InputField.stringPosition != m_ActiveKeyboard.caretPosition)
m_InputField.stringPosition = m_ActiveKeyboard.caretPosition;
// This display is opening the keyboard
m_OnKeyboardOpened.Invoke();
StartObservingKeyboard(m_ActiveKeyboard);
}
void OnTextSubmit(KeyboardTextEventArgs args)
{
UpdateText(args.keyboardText);
m_OnTextSubmitted?.Invoke(args.keyboardText);
if (m_ClearTextOnSubmit)
{
inputField.text = string.Empty;
}
}
void OnTextUpdate(KeyboardTextEventArgs args)
{
if (!m_UpdateOnKeyPress)
return;
UpdateText(args.keyboardText);
}
void UpdateText(string text)
{
var updatedText = text;
// Clip updated text to substring
if (m_MonitorInputFieldCharacterLimit && updatedText.Length >= m_InputField.characterLimit)
updatedText = updatedText.Substring(0, m_InputField.characterLimit);
m_InputField.text = updatedText;
// Update input field caret position with keyboard caret position
if (m_InputField.stringPosition != m_ActiveKeyboard.caretPosition)
m_InputField.stringPosition = m_ActiveKeyboard.caretPosition;
}
void KeyboardOpening(KeyboardTextEventArgs args)
{
Debug.Assert(args.keyboard == m_ActiveKeyboard);
if (args.keyboard != m_ActiveKeyboard)
return;
if (!m_InputField.isFocused && !m_AlwaysObserveKeyboard)
StopObservingKeyboard(m_ActiveKeyboard);
}
void KeyboardClosing(KeyboardTextEventArgs args)
{
Debug.Assert(args.keyboard == m_ActiveKeyboard);
if (args.keyboard != m_ActiveKeyboard)
return;
if (!m_AlwaysObserveKeyboard)
StopObservingKeyboard(m_ActiveKeyboard);
m_OnKeyboardClosed.Invoke();
}
void KeyboardFocusChanged(KeyboardTextEventArgs args)
{
Debug.Assert(args.keyboard == m_ActiveKeyboard);
if (args.keyboard != m_ActiveKeyboard)
return;
if (!m_InputField.isFocused && !m_AlwaysObserveKeyboard)
StopObservingKeyboard(m_ActiveKeyboard);
// The keyboard changed focus and this input field is no longer in focus
if (!m_InputField.isFocused)
m_OnKeyboardFocusChanged.Invoke();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2317ceab4d3d38419fe16eebc1c0cd3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,135 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
using System;
using UnityEngine.Events;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
#region EventArgs
/// <summary>
/// Event data associated with a keyboard event.
/// </summary>
public class KeyboardBaseEventArgs
{
/// <summary>
/// The XR Keyboard associated with this keyboard event.
/// </summary>
public XRKeyboard keyboard { get; set; }
}
/// <summary>
/// Event data associated with a keyboard event that includes text.
/// </summary>
public class KeyboardTextEventArgs : KeyboardBaseEventArgs
{
/// <summary>
/// The current keyboard text when this event is fired.
/// </summary>
public string keyboardText { get; set; }
}
/// <summary>
/// Event data associated with a keyboard event that includes a keyboard key.
/// </summary>
public class KeyboardKeyEventArgs : KeyboardBaseEventArgs
{
/// <summary>
/// The key associated with this event.
/// </summary>
public XRKeyboardKey key { get; set; }
}
/// <summary>
/// Event data associated with a keyboard event that includes a bool value.
/// </summary>
public class KeyboardBoolEventArgs : KeyboardBaseEventArgs
{
/// <summary>
/// The bool value associated with this event.
/// </summary>
public bool value { get; set; }
}
/// <summary>
/// Event data associated with a keyboard event that includes a layout string.
/// </summary>
public class KeyboardLayoutEventArgs : KeyboardBaseEventArgs
{
/// <summary>
/// The layout string associated with this event.
/// </summary>
public string layout { get; set; }
}
/// <summary>
/// Event data associated with modifiers of the keyboard.
/// </summary>
public class KeyboardModifiersEventArgs : KeyboardBaseEventArgs
{
/// <summary>
/// The shift value associated with this event.
/// </summary>
public bool shiftValue { get; set; }
/// <summary>
/// The caps lock value associated with this event.
/// </summary>
public bool capsLockValue { get; set; }
}
#endregion
#region Events
/// <summary>
/// <see cref="UnityEvent"/> that Unity invokes on a keyboard.
/// </summary>
[Serializable]
public sealed class KeyboardEvent : UnityEvent<KeyboardBaseEventArgs>
{
}
/// <summary>
/// <see cref="UnityEvent"/> that includes text that Unity invokes on a keyboard.
/// </summary>
[Serializable]
public sealed class KeyboardTextEvent : UnityEvent<KeyboardTextEventArgs>
{
}
/// <summary>
/// <see cref="UnityEvent"/> that includes a key that Unity invokes on a keyboard.
/// </summary>
[Serializable]
public sealed class KeyboardKeyEvent : UnityEvent<KeyboardKeyEventArgs>
{
}
/// <summary>
/// <see cref="UnityEvent"/> that includes a bool value that Unity invokes on a keyboard.
/// </summary>
[Serializable]
public sealed class KeyboardBoolEvent : UnityEvent<KeyboardBoolEventArgs>
{
}
/// <summary>
/// <see cref="UnityEvent"/> that includes a layout string that Unity invokes on a keyboard.
/// </summary>
[Serializable]
public sealed class KeyboardLayoutEvent : UnityEvent<KeyboardLayoutEventArgs>
{
}
/// <summary>
/// <see cref="UnityEvent"/> that includes supported keyboard modifiers.
/// </summary>
/// <remarks>Currently supported keyboard modifiers include shift and caps lock.</remarks>
[Serializable]
public sealed class KeyboardModifiersEvent : UnityEvent<KeyboardModifiersEventArgs>
{
}
#endregion
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f1f37871f6d8da54babd815aa4184559
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,453 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
using TMPro;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// Keyboard key used to interface with <see cref="XRKeyboard"/>.
/// </summary>
public class XRKeyboardKey : Button
{
[SerializeField, Tooltip("KeyFunction used for this key which is called when key is pressed. Used to communicate with the Keyboard.")]
KeyFunction m_KeyFunction;
/// <summary>
/// <see cref="KeyFunction"/> used for this key which is called when key is pressed. Used to communicate with
/// the Keyboard.
/// </summary>
public KeyFunction keyFunction
{
get => m_KeyFunction;
set => m_KeyFunction = value;
}
[SerializeField, Tooltip("(Optional) KeyCode used for this key. Used in conjunction with Key Function or as a fallback for standard commands.")]
KeyCode m_KeyCode;
/// <summary>
/// (Optional) <see cref="KeyCode"/> used for this key. Used in conjunction with Key Function or as a fallback for standard commands.
/// </summary>
public KeyCode keyCode
{
get => m_KeyCode;
set => m_KeyCode = value;
}
[SerializeField, Tooltip("Character for this key in non-shifted state. This string will be passed to the keyboard and appended to the keyboard text string or processed as a keyboard command.")]
string m_Character;
/// <summary>
/// Character for this key in non-shifted state. This string will be passed to the keyboard and appended
/// to the keyboard text string or processed as a keyboard command (i.e. '\s' for shift)
/// </summary>
public string character
{
get => m_Character;
set => m_Character = value;
}
[SerializeField, Tooltip("(Optional) Display character for this key in a non-shifted state. This string will be displayed on the key text field. If empty, character will be used as a fall back.")]
string m_DisplayCharacter;
/// <summary>
/// (Optional) Display character for this key in a non-shifted state. This string will be displayed on the
/// key text field. If left empty, <see cref="character"/> will be used instead.
/// </summary>
public string displayCharacter
{
get => m_DisplayCharacter;
set
{
m_DisplayCharacter = value;
RefreshDisplayCharacter();
}
}
[SerializeField, Tooltip("(Optional) Display icon for this key in a non-shifted state. This icon will be displayed on the key icon image. If empty, the display character or character will be used as a fall back.")]
Sprite m_DisplayIcon;
/// <summary>
/// (Optional) Display icon for this key in a non-shifted state. This icon will be displayed on the key icon image.
/// If empty, the display character or character will be used instead.
/// </summary>
public Sprite displayIcon
{
get => m_DisplayIcon;
set
{
m_DisplayIcon = value;
if (m_IconComponent != null)
{
m_IconComponent.sprite = m_DisplayIcon;
m_IconComponent.enabled = m_DisplayIcon != null;
}
RefreshDisplayCharacter();
}
}
[SerializeField, Tooltip("Character for this key in a shifted state. This string will be passed to the keyboard and appended to the keyboard text string or processed as a keyboard command.")]
string m_ShiftCharacter;
/// <summary>
/// Character for this key in a shifted state. This string will be passed to the keyboard and appended
/// to the keyboard text string or processed as a keyboard command (i.e. '\s' for shift).
/// </summary>
public string shiftCharacter
{
get => m_ShiftCharacter;
set => m_ShiftCharacter = value;
}
[SerializeField, Tooltip("(Optional) Display character for this key in a shifted state. This string will be displayed on the key text field. If empty, shift character will be used as a fall back.")]
string m_ShiftDisplayCharacter;
/// <summary>
/// (Optional) Display character for this key in a shifted state. This string will be displayed on the key text field.
/// If empty, <see cref="shiftCharacter"/> will be used instead, and finally <see cref="character"/> will
/// be capitalized and used if <see cref="shiftCharacter"/> is empty.
/// </summary>
public string shiftDisplayCharacter
{
get => m_ShiftDisplayCharacter;
set
{
m_ShiftDisplayCharacter = value;
RefreshDisplayCharacter();
}
}
[SerializeField, Tooltip("(Optional) Display icon for this key in a shifted state. This icon will be displayed on the key icon image. If empty, the shift display character or shift character will be used as a fall back.")]
Sprite m_ShiftDisplayIcon;
/// <summary>
/// (Optional) Display icon for this key in a shifted state. This icon will be displayed on the key icon image.
/// If empty, the shift display character or shift character will be used instead.
/// </summary>
public Sprite shiftDisplayIcon
{
get => m_ShiftDisplayIcon;
set
{
m_ShiftDisplayIcon = value;
if (m_IconComponent != null)
{
m_IconComponent.sprite = shiftDisplayIcon;
m_IconComponent.enabled = shiftDisplayIcon != null;
}
RefreshDisplayCharacter();
}
}
[SerializeField, Tooltip("If true, the key pressed event will fire on button down. If false, the key pressed event will fire on On Click.")]
bool m_UpdateOnKeyDown;
/// <summary>
/// If true, key pressed will fire on button down instead of on button up.
/// </summary>
public bool updateOnKeyDown
{
get => m_UpdateOnKeyDown;
set => m_UpdateOnKeyDown = value;
}
[SerializeField, Tooltip("Text field used to display key character.")]
TMP_Text m_TextComponent;
/// <summary>
/// Text field used to display key character.
/// </summary>
public TMP_Text textComponent
{
get => m_TextComponent;
set => m_TextComponent = value;
}
[SerializeField, Tooltip("Image component used to display icons for key.")]
Image m_IconComponent;
/// <summary>
/// Image component used to display icons for key.
/// </summary>
public Image iconComponent
{
get => m_IconComponent;
set => m_IconComponent = value;
}
[SerializeField, Tooltip("Image component used to highlight key indicating an active state.")]
Image m_HighlightComponent;
/// <summary>
/// Image component used to highlight key indicating an active state.
/// </summary>
public Image highlightComponent
{
get => m_HighlightComponent;
set => m_HighlightComponent = value;
}
[SerializeField, Tooltip("(Optional) Audio source played when key is pressed.")]
AudioSource m_AudioSource;
/// <summary>
/// (Optional) Audio source played when key is pressed.
/// </summary>
public AudioSource audioSource
{
get => m_AudioSource;
set => m_AudioSource = value;
}
XRKeyboard m_Keyboard;
float m_LastClickTime;
bool m_Shifted;
/// <summary>
/// True if this key is in a shifted state, otherwise returns false.
/// </summary>
public bool shifted
{
get => m_Shifted;
set
{
m_Shifted = value;
RefreshDisplayCharacter();
}
}
/// <summary>
/// Time the key was last pressed.
/// </summary>
public float lastClickTime => m_LastClickTime;
/// <summary>
/// Time since the key was last pressed.
/// </summary>
public float timeSinceLastClick => Time.time - m_LastClickTime;
/// <summary>
/// The keyboard associated with this key.
/// </summary>
public XRKeyboard keyboard
{
get => m_Keyboard;
set
{
if (m_Keyboard != null)
{
m_Keyboard.onShifted.RemoveListener(OnKeyboardShift);
m_Keyboard.onLayoutChanged.RemoveListener(OnKeyboardLayoutChange);
}
m_Keyboard = value;
if (m_Keyboard != null)
{
m_Keyboard.onShifted.AddListener(OnKeyboardShift);
m_Keyboard.onLayoutChanged.AddListener(OnKeyboardLayoutChange);
}
}
}
/// <inheritdoc />
protected override void Start()
{
base.Start();
RefreshDisplayCharacter();
}
/// <inheritdoc />
protected override void OnDestroy()
{
base.OnDestroy();
if (m_Keyboard != null)
{
m_Keyboard.onShifted.RemoveListener(OnKeyboardShift);
m_Keyboard.onLayoutChanged.RemoveListener(OnKeyboardLayoutChange);
}
}
/// <inheritdoc />
public override void OnPointerDown(PointerEventData eventData)
{
base.OnPointerDown(eventData);
if (m_UpdateOnKeyDown && interactable)
KeyClick();
}
/// <inheritdoc />
public override void OnPointerClick(PointerEventData eventData)
{
base.OnPointerClick(eventData);
if (!m_UpdateOnKeyDown)
KeyClick();
}
protected virtual void KeyClick()
{
// Local function of things to do to the key when pressed (Audio, etc.)
KeyPressed();
if (m_KeyFunction != null)
{
m_KeyFunction.PreprocessKey(m_Keyboard, this);
m_KeyFunction.ProcessKey(m_Keyboard, this);
m_KeyFunction.PostprocessKey(m_Keyboard, this);
}
else
{
// Fallback if key function is null
m_Keyboard.PreprocessKeyPress(this);
m_Keyboard.TryProcessKeyPress(this);
m_Keyboard.PostprocessKeyPress(this);
}
m_LastClickTime = Time.time;
}
/// <summary>
/// Local handling of this key being pressed.
/// </summary>
protected virtual void KeyPressed()
{
if (m_AudioSource != null)
{
if (m_AudioSource.isPlaying)
m_AudioSource.Stop();
float pitchVariance = Random.Range(0.95f, 1.05f);
m_AudioSource.pitch = pitchVariance;
m_AudioSource.Play();
}
}
protected virtual void OnKeyboardShift(KeyboardModifiersEventArgs args)
{
shifted = args.shiftValue;
}
protected virtual void OnKeyboardLayoutChange(KeyboardLayoutEventArgs args)
{
var enableHighlight = args.layout == m_Character && !m_HighlightComponent.enabled;
EnableHighlight(enableHighlight);
}
/// <summary>
/// Enables or disables the key highlight image.
/// </summary>
/// <param name="enable">If true, the highlight image is enabled. If false, the highlight image is disabled.</param>
public void EnableHighlight(bool enable)
{
if (m_HighlightComponent != null)
{
m_HighlightComponent.enabled = enable;
}
}
// Helper functions
protected void RefreshDisplayCharacter()
{
if (m_KeyFunction != null && m_Keyboard != null)
m_KeyFunction.ProcessRefreshDisplay(m_Keyboard, this);
if (m_IconComponent != null)
{
m_IconComponent.sprite = GetEffectiveDisplayIcon();
if (m_IconComponent.sprite != null)
{
m_TextComponent.enabled = false;
m_IconComponent.enabled = true;
return;
}
}
if (m_TextComponent != null)
{
m_TextComponent.text = GetEffectiveDisplayCharacter();
m_TextComponent.enabled = true;
m_IconComponent.enabled = false;
}
}
protected virtual string GetEffectiveDisplayCharacter()
{
// If we've got a display character, prioritize that.
string value;
if (!string.IsNullOrEmpty(m_DisplayCharacter))
value = m_DisplayCharacter;
else if (!string.IsNullOrEmpty(m_Character))
value = m_Character;
else
value = string.Empty;
// If we're in shift mode, check our shift overrides.
if (m_Shifted)
{
if (!string.IsNullOrEmpty(m_ShiftDisplayCharacter))
value = m_ShiftDisplayCharacter;
else if (!string.IsNullOrEmpty(m_ShiftCharacter))
value = m_ShiftCharacter;
else
value = value.ToUpper();
}
return value;
}
protected virtual Sprite GetEffectiveDisplayIcon()
{
if (m_KeyFunction != null && m_Keyboard != null && m_KeyFunction.OverrideDisplayIcon(m_Keyboard, this))
return m_KeyFunction.GetDisplayIcon(m_Keyboard, this);
return m_Shifted ? m_ShiftDisplayIcon : m_DisplayIcon;
}
/// <summary>
/// Helper function that returns the current effective character for this key based on shifted state.
/// </summary>
/// <returns>Returns the <see cref="shiftCharacter"/> when this key is in the shifted state or <see cref="character"/>
/// when this key is not shifted.</returns>
public virtual string GetEffectiveCharacter()
{
if (m_Shifted)
{
if (!string.IsNullOrEmpty(m_ShiftCharacter))
return m_ShiftCharacter;
return m_Character.ToUpper();
}
return m_Character;
}
/// <summary>
/// Enables or disables the key button being interactable. The icon and text alpha will be adjusted to reflect
/// the state of the button.
/// </summary>
/// <param name="enable">The desired interactable state of the key.</param>
public virtual void SetButtonInteractable(bool enable)
{
const float enabledAlpha = 1f;
const float disabledAlpha = 0.25f;
interactable = enable;
if (m_TextComponent != null)
{
m_TextComponent.alpha = enable ? enabledAlpha : disabledAlpha;
}
if (m_IconComponent != null)
{
var c = m_IconComponent.color;
c.a = enable ? enabledAlpha : disabledAlpha;
m_IconComponent.color = c;
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84bf39f7245577d4491b0aa8ea6016ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,112 @@
#if TEXT_MESH_PRO_PRESENT || (UGUI_2_0_PRESENT && UNITY_6000_0_OR_NEWER)
namespace UnityEngine.XR.Interaction.Toolkit.Samples.SpatialKeyboard
{
/// <summary>
/// Manage the reuse and updating of data for each child <see cref="XRKeyboardKey"/> button.
/// </summary>
public class XRKeyboardLayout : MonoBehaviour
{
[SerializeField]
XRKeyboardConfig m_DefaultKeyMapping;
/// <summary>
/// Default key mapping for resetting key layout.
/// </summary>
public XRKeyboardConfig defaultKeyMapping
{
get => m_DefaultKeyMapping;
set => m_DefaultKeyMapping = value;
}
[SerializeField]
XRKeyboardConfig m_ActiveKeyMapping;
/// <summary>
/// Active <see cref="XRKeyboardConfig"/> which data is populated in these keys.
/// </summary>
public XRKeyboardConfig activeKeyMapping
{
get => m_ActiveKeyMapping;
set
{
m_ActiveKeyMapping = value;
PopulateKeys();
}
}
XRKeyboardKey [] m_Keys;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Awake()
{
m_Keys = GetComponentsInChildren<XRKeyboardKey>();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Start()
{
PopulateKeys();
}
/// <summary>
/// Sets the active key mapping to the default key mapping.
/// </summary>
/// <seealso cref="activeKeyMapping"/>
/// <seealso cref="defaultKeyMapping"/>
public void SetDefaultLayout()
{
if (m_DefaultKeyMapping != null)
activeKeyMapping = m_DefaultKeyMapping;
}
/// <summary>
/// Updates all child <see cref="XRKeyboardKey"/> buttons with the data from the <see cref="activeKeyMapping"/>.
/// </summary>
/// <remarks>
/// This function returns without changing the keys if the number of child <see cref="XRKeyboardKey"/> buttons is less than
/// the number of mappings in the <see cref="activeKeyMapping"/>.
/// </remarks>
public void PopulateKeys()
{
if (m_ActiveKeyMapping == null)
return;
var keyMappings = m_ActiveKeyMapping.keyMappings;
if (m_Keys == null || m_Keys.Length == 0)
m_Keys = GetComponentsInChildren<XRKeyboardKey>();
if (m_Keys.Length < keyMappings.Count)
{
Debug.LogWarning("Keyboard layout update failed: There are fewer keys than key mappings in the current config. Ensure there is a correct number of keys and key mappings.", this);
return;
}
for (var i = 0; i < keyMappings.Count; ++i)
{
var mapping = keyMappings[i];
var key = m_Keys[i];
key.character = mapping.character;
key.displayCharacter = mapping.displayCharacter;
key.shiftCharacter = mapping.shiftCharacter;
key.shiftDisplayCharacter = mapping.shiftDisplayCharacter;
key.keyFunction = mapping.overrideDefaultKeyFunction ? mapping.keyFunction : m_ActiveKeyMapping.defaultKeyFunction;
key.keyCode = mapping.keyCode;
key.displayIcon = mapping.displayIcon;
key.shiftDisplayIcon = mapping.shiftDisplayIcon;
key.SetButtonInteractable(!mapping.disabled);
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84662ebe3530d154a80c5c813e8ce186
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: