using System; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using MonoGame.Extended.Input; using MonoGame.Extended.Input.InputListeners; namespace MonoGame.Extended.NuclexGui.Controls.Desktop { /// Control through which the user can enter text /// /// /// Through this control, users can be asked to enter an arbitrary string /// of characters, their name for example. Desktop users can enter text through /// their normal keyboard where Windows' own key translation is used to /// support regional settings and custom keyboard layouts. /// /// /// XBox 360 users will open the virtual keyboard when the input box gets /// the input focus and can add characters by selecting them from the virtual /// keyboard's character matrix. /// /// public class GuiInputControl : GuiControl, IWritable { /// Position of the cursor within the text private int _caretPosition; /// Tick count at the time the caret was last moved private int _lastCaretMovementTicks; /// X coordinate of the last known mouse position private float _mouseX; /// Y coordinate of the last known mouse position private float _mouseY; /// Array used to store characters before they are appended private readonly char[ /*1*/] _singleCharArray; /// Text the user has entered into the text input control private readonly StringBuilder _text; /// Whether user interaction with the control is allowed public bool Enabled; /// Description to be displayed in the on-screen keyboard public string GuideDescription; /// Title to be displayed in the on-screen keyboard public string GuideTitle; /// /// Can be set by renderers to enable cursor positioning by the mouse /// public IOpeningLocator OpeningLocator; /// Initializes a new text input control public GuiInputControl() { _singleCharArray = new char[1]; _text = new StringBuilder(64); Enabled = true; GuideTitle = "Text Entry"; GuideDescription = "Please enter the text for this input field"; } /// Position of the cursor within the text public int CaretPosition { get { return _caretPosition; } set { if ((value < 0) || (value > Text.Length)) throw new ArgumentException("Invalid caret position", "CaretPosition"); _caretPosition = value; } } /// Whether the control currently has the input focus public bool HasFocus => (Screen != null) && ReferenceEquals(Screen.FocusedControl, this); /// Elapsed milliseconds since the user last moved the caret /// /// This is an unusual property for an input box to have. It is retrieved by /// the renderer and could be used for several purposes, such as lighting up /// a control when text is entered to provide better visual tracking or /// preventing the cursor from blinking whilst the user is typing. /// public int MillisecondsSinceLastCaretMovement => Environment.TickCount - _lastCaretMovementTicks; /// Text that is being displayed on the control public string Text { get { return _text.ToString(); } set { _text.Remove(0, _text.Length); _text.Append(value); // Cursor index is in openings between letters, including before first // and after last letter, so text.Length is a valid position. if (_caretPosition > _text.Length) _caretPosition = _text.Length; } } /// Called when the user has entered a character /// Character that has been entered void IWritable.OnCharacterEntered(char character) { OnCharacterEntered(character); } /// Whether the control can currently obtain the input focus bool IFocusable.CanGetFocus => Enabled; /// Title to be displayed in the on-screen keyboard string IWritable.GuideTitle => GuideTitle; /// Description to be displayed in the on-screen keyboard string IWritable.GuideDescription => GuideDescription; /// Called when the user has entered a character /// Character that has been entered protected virtual void OnCharacterEntered(char character) { // For some reason, Windows translates Backspace to a character :) if (character != '\b') { UpdateLastCaretMovementTicks(); // There's no single-character overload on the XBox 360... _singleCharArray[0] = character; _text.Insert(_caretPosition, _singleCharArray); ++_caretPosition; } } /// Called when a key on the keyboard has been pressed down /// Code of the key that was pressed /// /// True if the key press was handles by the control, otherwise false. /// /// /// If the control indicates that it didn't handle the key press, it will not /// receive the associated key release notification. /// protected override bool OnKeyPressed(Keys keyCode) { // We only accept keys if we have the focus. If the notification is sent in search // for a key handler without the input box being focused, we will not respond to // the key press in order to not sabotage shortcut keys for other controls. if (!HasFocus) return false; switch (keyCode) { // Backspace: erase the character left of the caret case Keys.Back: { if (_caretPosition > 0) { UpdateLastCaretMovementTicks(); _text.Remove(_caretPosition - 1, 1); --_caretPosition; } break; } // Delete: erase the character right of the caret case Keys.Delete: { if (_caretPosition < _text.Length) { UpdateLastCaretMovementTicks(); _text.Remove(_caretPosition, 1); } break; } // Cursor left: move the caret to the left by one character case Keys.Left: { if (_caretPosition > 0) { UpdateLastCaretMovementTicks(); --_caretPosition; } break; } // Cursor right: move the caret to the right by one character case Keys.Right: { if (_caretPosition < _text.Length) { UpdateLastCaretMovementTicks(); ++_caretPosition; } break; } // Home: place the caret before the first character case Keys.Home: { UpdateLastCaretMovementTicks(); _caretPosition = 0; break; } // Home: place the caret behind the last character case Keys.End: { UpdateLastCaretMovementTicks(); _caretPosition = _text.Length; break; } // Keys that can be used to navigate the dialog case Keys.Tab: case Keys.Up: case Keys.Down: case Keys.Enter: { return false; } } return true; } /// Called when the mouse position is updated /// X coordinate of the mouse cursor on the control /// Y coordinate of the mouse cursor on the control protected override void OnMouseMoved(float x, float y) { _mouseX = x; _mouseY = y; } /// Called when a mouse button has been pressed down /// Index of the button that has been pressed protected override void OnMousePressed(MouseButton button) { if (button == MouseButton.Left) { if (OpeningLocator != null) { var absoluteBounds = GetAbsoluteBounds(); var absolutePosition = new Vector2(absoluteBounds.X + _mouseX, absoluteBounds.Y + _mouseY); _caretPosition = OpeningLocator.GetClosestOpening(absoluteBounds, Text, absolutePosition); } else { // Nope, our renderer is being secretive MoveCaretToEnd(); } } } /// Handles user text input by a physical keyboard /// Character that has been entered internal void ProcessCharacter(char character) { // This notifications always concerns ourselves because it is only sent // to the focused control OnCharacterEntered(character); } /// Moves the caret to the end of the text private void MoveCaretToEnd() { UpdateLastCaretMovementTicks(); _caretPosition = _text.Length; } /// Updates the tick count when the caret was last moved /// /// Used to prevent the caret from blinking when /// private void UpdateLastCaretMovementTicks() { _lastCaretMovementTicks = Environment.TickCount; } } }