// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System; using System.Collections.Generic; using UltimateXR.Core.Components; using UltimateXR.Extensions.System; using UltimateXR.UI.UnityInputModule.Controls; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace UltimateXR.UI.Helpers.Keyboard { /// /// Component that handles a keyboard in VR for user input /// public class UxrKeyboardUI : UxrComponent { #region Inspector Properties/Serialized Fields [SerializeField] private bool _multiline = true; [SerializeField] private int _maxLineLength; [SerializeField] private int _maxLineCount; [SerializeField] private Text _consoleDisplay; [SerializeField] private Text _currentLineDisplay; [SerializeField] private bool _consoleDisplayUsesCursor = true; [SerializeField] private bool _lineDisplayUsesCursor = true; [SerializeField] private GameObject _capsLockEnabledObject; [SerializeField] private bool _capsLockEnabled; [SerializeField] private bool _previewCaps; [SerializeField] private GameObject _passwordPreviewRootObject; [SerializeField] private GameObject _passwordPreviewEnabledObject; [SerializeField] private bool _isPassword; [SerializeField] private bool _hidePassword = true; #endregion #region Public Types & Data /// /// Event called on key presses/releases. /// public event EventHandler KeyPressed; /// /// Event called on key presses/releases when the input is disabled using . /// public event EventHandler DisallowedKeyPressed; /// /// Event we can subscribe to if we want notifications whenever the current line /// being typed in using the keyboard changed. /// public event EventHandler CurrentLineChanged; /// /// Contains information about the key in our internal dictionary. /// public class KeyInfo { } /// /// Gets whether a shift key is being pressed. /// public bool ShiftEnabled => _shiftEnabled > 0; /// /// Gets whether a Control key is pressed. /// public bool ControlEnabled => _controlEnabled > 0; /// /// Gets the current console text content including the cursor. /// public string ConsoleContentWithCursor => ConsoleContent + CurrentCursor; /// /// Gets the current console line including the cursor. /// public string CurrentLineWithCursor => CurrentLine + CurrentCursor; /// /// Gets the current console cursor (can be empty or the cursor character as a string). /// public string CurrentCursor => AllowInput && Mathf.RoundToInt(Time.time * 1000) / 200 % 2 == 0 ? "_" : string.Empty; /// /// Gets whether caps lock is enabled. /// public bool CapsLockEnabled { get => _capsLockEnabled; set => _capsLockEnabled = value; } /// /// Gets whether the Alt key is pressed. /// public bool AltEnabled { get; private set; } /// /// Gets whether the Alt GR key is pressed. /// public bool AltGrEnabled { get; private set; } /// /// Gets the current console text content. /// public string ConsoleContent { get; private set; } /// /// Gets the current console line without the cursor. /// public string CurrentLine { get => _currentLine; private set { if (value != _currentLine) { _currentLine = value; OnCurrentLineChanged(value); } } } /// /// Gets or sets whether keyboard input is allowed. /// public bool AllowInput { get; set; } /// /// Gets or sets whether the key labels casing changes when the shift of caps lock key is pressed. /// public bool PreviewCaps { get => _previewCaps; set { _previewCaps = value; UpdateLabelsCase(); } } /// /// Gets or sets whether the keyboard is being used to type in a password. This can be used to hide the content behind /// asterisk characters. /// public bool IsPassword { get => _isPassword; set => _isPassword = value; } /// /// Gets or sets whether to hide password characters when is used. /// public bool HidePassword { get => _hidePassword; set => _hidePassword = value; } #endregion #region Public Methods /// /// Clears the console content. /// public void Clear() { _currentLineCount = 1; ConsoleContent = string.Empty; CurrentLine = string.Empty; } /// /// If different symbols are present (through a ToggleSymbols keyboard key), sets the default symbols /// as the currently enabled. Usually the default symbols are the regular alphabet letters. /// public void EnableDefaultSymbols() { if (_keyToggleSymbols != null) { _keyToggleSymbols.SetDefaultSymbols(); } } /// /// Adds content to the console. This method should be used instead of the property since /// will not process lines. /// /// Text content to append public void AddConsoleContent(string newContent) { if (string.IsNullOrEmpty(newContent)) { return; } // Count the number of lines we are adding: int newLineCount = newContent.GetOccurrenceCount("\n", false); ConsoleContent += newContent; _currentLineCount += newLineCount; // Check if we exceeded the maximum line amount CheckMaxLines(); } /// /// Called to register a new key in the keyboard. /// /// Key to register public void RegisterKey(UxrKeyboardKeyUI key) { Debug.Assert(key != null, "Keyboard key is null"); Debug.Assert(key.ControlInput != null, "Keyboard key's ControlInput is null"); if (!_keys.ContainsKey(key)) { _keys.Add(key, new KeyInfo()); key.ControlInput.Pressed += KeyButton_KeyDown; key.ControlInput.Released += KeyButton_KeyUp; if (key.KeyType == UxrKeyType.ToggleSymbols) { _keyToggleSymbols = key; } } } /// /// Called to unregister a key from the keyboard. /// /// Key to unregister public void UnregisterKey(UxrKeyboardKeyUI key) { Debug.Assert(key != null, "Keyboard key is null"); if (_keys.ContainsKey(key)) { _keys.Remove(key); key.ControlInput.Pressed -= KeyButton_KeyDown; key.ControlInput.Released -= KeyButton_KeyUp; if (key == _keyToggleSymbols) { _keyToggleSymbols = null; } } } #endregion #region Unity /// /// Initializes the keyboard and clears the content. /// protected override void Awake() { base.Awake(); AllowInput = true; Clear(); if (_previewCaps) { UpdateLabelsCase(); } } /// /// If there is a console display Text component specified, it becomes updated with the content plus the cursor. /// If there is a caps lock GameObject specified it is updated to reflect the caps lock state as well. /// private void Update() { if (_consoleDisplay != null) { _consoleDisplay.text = FormatStringOutput(_consoleDisplayUsesCursor ? ConsoleContentWithCursor : ConsoleContent, _consoleDisplayUsesCursor); } if (_currentLineDisplay != null) { _currentLineDisplay.text = FormatStringOutput(_lineDisplayUsesCursor ? CurrentLineWithCursor : CurrentLine, _consoleDisplayUsesCursor); } if (_capsLockEnabledObject != null) { _capsLockEnabledObject.SetActive(_capsLockEnabled); } if (_passwordPreviewRootObject != null) { _passwordPreviewRootObject.SetActive(IsPassword); } if (_passwordPreviewEnabledObject != null) { _passwordPreviewEnabledObject.SetActive(!_hidePassword && _isPassword); } } #endregion #region Event Handling Methods /// /// Called when a keyboard key was pressed. /// /// The control that was pressed /// Event data private void KeyButton_KeyDown(UxrControlInput controlInput, PointerEventData eventData) { UxrKeyboardKeyUI key = controlInput.GetComponent(); if (!AllowInput) { // Event notification DisallowedKeyPressed?.Invoke(this, new UxrKeyboardKeyEventArgs(key, true, null)); return; } string lastLine = string.Empty; if (key.KeyType == UxrKeyType.Printable) { if (!(_maxLineLength > 0 && CurrentLine.Length >= _maxLineLength)) { if (key.KeyLayoutType == UxrKeyLayoutType.SingleChar) { if (!string.IsNullOrEmpty(key.ForceLabel)) { ConsoleContent += key.GetSingleLayoutValueNoForceLabel(_capsLockEnabled || _shiftEnabled > 0, AltGrEnabled); CurrentLine += key.GetSingleLayoutValueNoForceLabel(_capsLockEnabled || _shiftEnabled > 0, AltGrEnabled); } else { if (char.IsLetter(key.SingleLayoutValue)) { char newCar = _capsLockEnabled || _shiftEnabled > 0 ? char.ToUpper(key.SingleLayoutValue) : char.ToLower(key.SingleLayoutValue); ConsoleContent += newCar; CurrentLine += newCar; } else { char newCar = key.GetSingleLayoutValueNoForceLabel(_shiftEnabled > 0 || _capsLockEnabled, AltGrEnabled); ConsoleContent += newCar; CurrentLine += newCar; } } } else if (key.KeyLayoutType == UxrKeyLayoutType.MultipleChar) { if (_shiftEnabled > 0) { ConsoleContent += key.MultipleLayoutValueTopLeft; CurrentLine += key.MultipleLayoutValueTopLeft; } else if (AltGrEnabled) { if (key.HasMultipleLayoutValueBottomRight) { ConsoleContent += key.MultipleLayoutValueBottomRight; CurrentLine += key.MultipleLayoutValueBottomRight; } } else { ConsoleContent += key.MultipleLayoutValueBottomLeft; CurrentLine += key.MultipleLayoutValueBottomLeft; } } } } else if (key.KeyType == UxrKeyType.Tab) { string tab = " "; int charsAddedCount = _maxLineLength > 0 ? CurrentLine.Length + tab.Length > _maxLineLength ? _maxLineLength - CurrentLine.Length : tab.Length : tab.Length; ConsoleContent += tab.Substring(0, charsAddedCount); CurrentLine += tab.Substring(0, charsAddedCount); } else if (key.KeyType == UxrKeyType.Shift) { _shiftEnabled++; if (_previewCaps) { UpdateLabelsCase(); } } else if (key.KeyType == UxrKeyType.CapsLock) { _capsLockEnabled = !_capsLockEnabled; if (_previewCaps) { UpdateLabelsCase(); } } else if (key.KeyType == UxrKeyType.Control) { _controlEnabled++; } else if (key.KeyType == UxrKeyType.Alt) { AltEnabled = true; } else if (key.KeyType == UxrKeyType.AltGr) { AltGrEnabled = true; } else if (key.KeyType == UxrKeyType.Enter) { #if !UNITY_WSA lastLine = string.Copy(CurrentLine); #else lastLine = string.Empty + CurrentLine; #endif if (_multiline) { ConsoleContent += "\n"; CurrentLine = string.Empty; _currentLineCount++; CheckMaxLines(); } } else if (key.KeyType == UxrKeyType.Backspace) { if (CurrentLine.Length > 0) { ConsoleContent = ConsoleContent.Substring(0, ConsoleContent.Length - 1); CurrentLine = CurrentLine.Substring(0, CurrentLine.Length - 1); } } else if (key.KeyType == UxrKeyType.Del) { } else if (key.KeyType == UxrKeyType.ToggleSymbols) { key.ToggleSymbols(); } else if (key.KeyType == UxrKeyType.ToggleViewPassword) { _hidePassword = !_hidePassword; } else if (key.KeyType == UxrKeyType.Escape) { } // Event notification KeyPressed?.Invoke(this, new UxrKeyboardKeyEventArgs(key, true, key.KeyType == UxrKeyType.Enter ? lastLine : CurrentLine)); } /// /// Called when a keyboard keypress was released. /// /// The control that was released /// Event data private void KeyButton_KeyUp(UxrControlInput controlInput, PointerEventData eventData) { UxrKeyboardKeyUI key = controlInput.GetComponent(); if (!AllowInput) { // Event notification DisallowedKeyPressed?.Invoke(this, new UxrKeyboardKeyEventArgs(key, false, null)); return; } if (key.KeyType == UxrKeyType.Printable) { } else if (key.KeyType == UxrKeyType.Tab) { } else if (key.KeyType == UxrKeyType.Shift) { _shiftEnabled--; } else if (key.KeyType == UxrKeyType.CapsLock) { } else if (key.KeyType == UxrKeyType.Control) { _controlEnabled--; } else if (key.KeyType == UxrKeyType.Alt) { AltEnabled = false; } else if (key.KeyType == UxrKeyType.AltGr) { AltGrEnabled = false; } else if (key.KeyType == UxrKeyType.Enter) { } else if (key.KeyType == UxrKeyType.Backspace) { } else if (key.KeyType == UxrKeyType.Del) { } else if (key.KeyType == UxrKeyType.Escape) { } // Event notification KeyPressed?.Invoke(this, new UxrKeyboardKeyEventArgs(key, false, CurrentLine)); } #endregion #region Event Trigger Methods /// /// Event trigger for the event. /// /// New line value protected virtual void OnCurrentLineChanged(string value) { CurrentLineChanged?.Invoke(this, value); } #endregion #region Private Methods /// /// Formats the given string to show it to the user. This is mainly used to make sure that passwords are hidden behind /// asterisk characters. /// /// Content to format using the current settings /// /// Tells whether content is a string that may have a cursor appended /// /// Formatted string ready to show to the user private string FormatStringOutput(string content, bool isUsingCursor) { if (string.IsNullOrEmpty(content)) { return string.Empty; } return _isPassword && _hidePassword ? new string('*', content.Length - CurrentCursor.Length) + CurrentCursor : content; } /// /// Checks if the maximum number of lines was reached in the console and if so removes lines from the beginning. /// private void CheckMaxLines() { if (_maxLineCount > 0 && _currentLineCount > _maxLineCount) { int linesCounted = 0; for (int i = 0; i < ConsoleContent.Length; ++i) { if (ConsoleContent[i] == '\n') { linesCounted++; if (linesCounted == _currentLineCount - _maxLineCount) { ConsoleContent = ConsoleContent.Remove(0, i + 1); _currentLineCount -= linesCounted; break; } } } } } /// /// Updates uppercase/lowercase labels depending on the shift and caps lock state. /// private void UpdateLabelsCase() { if (_keys == null) { return; } foreach (KeyValuePair keyPair in _keys) { if (keyPair.Key.IsLetterKey) { keyPair.Key.UpdateLetterKeyLabel(ShiftEnabled || CapsLockEnabled); } } } #endregion #region Private Types & Data private readonly Dictionary _keys = new Dictionary(); private string _currentLine; private int _currentLineCount; private int _shiftEnabled; private int _controlEnabled; private UxrKeyboardKeyUI _keyToggleSymbols; #endregion } }