mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
* Fixes right click menu stopping input Removed a flag change when the modal stack was showing, which preventing the click from getting passed to the next function. * Added parsing for mod2 and mod3 from Keybind YAML * Fixes a crash on context change when a keybind is not defined in keybinds.yml * Implemented ShowDebugConsole Hotkey * Refactored input system Refactored input system to run Key and Mouse input through the InputManager before doing stuff. * Upgraded LineEdit and classes that use it. Fixed input while KeyboardFocused. Upgraded LineEdit to use Keybinds. Upgraded DebugConsole to use Keybinds. Replaced all references to MouseDown, MouseUp, KeyDown, and KeyUp with KeyBindUp and KeyBindDown. Moved UserInterfaceManager call from GameController.Input to InputManager event. Stopped input going to simulation while a control has focus in UserInterfaceManager. * Some fixes for input system Fixed keybinds getting stuck when selecting a LineEdit. Changed MenuBar to not error. Fixed a few cases where LineEdit would eat input if you hovered over it and where mouse input got eaten when clicking in the world while a LineEdit was selected. * Removed extra dependencies * Added GUIBoundKeyEventArgs to ButtonEventArgs * Fixes for input with LineEdit selected Fixed multiple keybinds mapped to the same key not triggering. Fixed keybind input not getting to LineEdit when hovering over a control. * Implemented Key Repeat for LineEdit * Fix for input on Robust.Lite Launcher * Renames NonFocusKeybinds to EnableAllKeybinds Renamed NonFocusKeybinds to EnableAllKeybinds and added comment to clarify usage * Adds repeating keybinds Used for TextBackspace, TextCursorLeft, and TextCursorRight Reverts a change to LineEdit that implemented repeating keys Adds some documentation comments
440 lines
13 KiB
C#
440 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using JetBrains.Annotations;
|
|
using Robust.Client.GameObjects.EntitySystems;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Graphics.Drawing;
|
|
using Robust.Client.Input;
|
|
using Robust.Client.Interfaces.Input;
|
|
using Robust.Client.Interfaces.UserInterface;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.Interfaces.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Timing;
|
|
|
|
namespace Robust.Client.UserInterface.Controls
|
|
{
|
|
/// <summary>
|
|
/// Allows the user to input and modify a line of text.
|
|
/// </summary>
|
|
public class LineEdit : Control
|
|
{
|
|
public const string StylePropertyStyleBox = "stylebox";
|
|
public const string StyleClassLineEditNotEditable = "notEditable";
|
|
public const string StylePseudoClassPlaceholder = "placeholder";
|
|
|
|
[NotNull] private string _text = "";
|
|
private bool _editable = true;
|
|
[CanBeNull] private string _placeHolder;
|
|
private int _cursorPosition;
|
|
private float _cursorBlinkTimer;
|
|
private bool _cursorCurrentlyLit;
|
|
private const float BlinkTime = 0.5f;
|
|
|
|
private bool IsPlaceHolderVisible => string.IsNullOrEmpty(_text) && _placeHolder != null;
|
|
|
|
public LineEdit()
|
|
{
|
|
MouseFilter = MouseFilterMode.Stop;
|
|
CanKeyboardFocus = true;
|
|
KeyboardFocusOnClick = true;
|
|
}
|
|
|
|
public AlignMode TextAlign { get; set; }
|
|
|
|
public string Text
|
|
{
|
|
get => _text;
|
|
set
|
|
{
|
|
if (value == null)
|
|
{
|
|
value = "";
|
|
}
|
|
|
|
_text = value;
|
|
_cursorPosition = 0;
|
|
_updatePseudoClass();
|
|
}
|
|
}
|
|
|
|
public bool Editable
|
|
{
|
|
get => _editable;
|
|
set
|
|
{
|
|
_editable = value;
|
|
if (!_editable)
|
|
{
|
|
AddStyleClass(StyleClassLineEditNotEditable);
|
|
}
|
|
else
|
|
{
|
|
RemoveStyleClass(StyleClassLineEditNotEditable);
|
|
}
|
|
}
|
|
}
|
|
|
|
public string PlaceHolder
|
|
{
|
|
get => _placeHolder;
|
|
set
|
|
{
|
|
_placeHolder = value;
|
|
_updatePseudoClass();
|
|
}
|
|
}
|
|
|
|
public int CursorPos
|
|
{
|
|
set
|
|
{
|
|
if (value < 0)
|
|
{
|
|
value = 0;
|
|
}
|
|
else if (value > _text.Length)
|
|
{
|
|
value = _text.Length;
|
|
}
|
|
_cursorPosition = value;
|
|
}
|
|
}
|
|
|
|
public bool IgnoreNext { get; set; }
|
|
|
|
// TODO:
|
|
// I decided to not implement the entire LineEdit API yet,
|
|
// since most of it won't be used yet (if at all).
|
|
// Feel free to implement wrappers for all the other properties!
|
|
// Future me reporting, thanks past me.
|
|
// Second future me reporting, thanks again.
|
|
// Third future me is here to say thanks.
|
|
// Fourth future me is here to continue the tradition.
|
|
|
|
public void Clear()
|
|
{
|
|
Text = "";
|
|
}
|
|
|
|
public void Select(int from = 0, int to = -1)
|
|
{
|
|
}
|
|
|
|
public void SelectAll()
|
|
{
|
|
}
|
|
|
|
public event Action<LineEditEventArgs> OnTextChanged;
|
|
public event Action<LineEditEventArgs> OnTextEntered;
|
|
|
|
public void InsertAtCursor(string text)
|
|
{
|
|
// Strip newlines.
|
|
var chars = new List<char>(text.Length);
|
|
foreach (var chr in text)
|
|
{
|
|
if (chr == '\n')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
chars.Add(chr);
|
|
}
|
|
|
|
if (chars.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_text = _text.Insert(_cursorPosition, new string(chars.ToArray()));
|
|
_cursorPosition += chars.Count;
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_updatePseudoClass();
|
|
}
|
|
|
|
protected internal override void Draw(DrawingHandleScreen handle)
|
|
{
|
|
var styleBox = _getStyleBox();
|
|
var drawBox = PixelSizeBox;
|
|
var contentBox = styleBox.GetContentBox(drawBox);
|
|
styleBox.Draw(handle, drawBox);
|
|
var font = _getFont();
|
|
var renderedTextColor = _getFontColor();
|
|
|
|
var offsetY = (int) (contentBox.Height - font.GetHeight(UIScale)) / 2;
|
|
var baseLine = new Vector2i(0, offsetY + font.GetAscent(UIScale)) + contentBox.TopLeft;
|
|
|
|
string renderedText;
|
|
|
|
if (IsPlaceHolderVisible)
|
|
{
|
|
renderedText = _placeHolder;
|
|
}
|
|
else
|
|
{
|
|
renderedText = _text;
|
|
}
|
|
|
|
float? actualCursorPosition = null;
|
|
|
|
if (_cursorPosition == 0)
|
|
{
|
|
actualCursorPosition = contentBox.Left;
|
|
}
|
|
|
|
var count = 0;
|
|
foreach (var chr in renderedText)
|
|
{
|
|
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
|
{
|
|
count += 1;
|
|
continue;
|
|
}
|
|
|
|
// Glyph would be outside the bounding box, abort.
|
|
if (baseLine.X + metrics.Width + metrics.BearingX > contentBox.Right)
|
|
{
|
|
break;
|
|
}
|
|
|
|
font.DrawChar(handle, chr, baseLine, UIScale, renderedTextColor);
|
|
baseLine += new Vector2(metrics.Advance, 0);
|
|
count += 1;
|
|
if (count == _cursorPosition)
|
|
{
|
|
actualCursorPosition = baseLine.X;
|
|
}
|
|
}
|
|
|
|
if (_cursorCurrentlyLit && actualCursorPosition.HasValue && HasKeyboardFocus())
|
|
{
|
|
handle.DrawRect(
|
|
new UIBox2(actualCursorPosition.Value, contentBox.Top, actualCursorPosition.Value + 1,
|
|
contentBox.Bottom), Color.White);
|
|
}
|
|
}
|
|
|
|
protected override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
|
|
_cursorBlinkTimer -= args.DeltaSeconds;
|
|
if (_cursorBlinkTimer <= 0)
|
|
{
|
|
_cursorBlinkTimer += BlinkTime;
|
|
_cursorCurrentlyLit = !_cursorCurrentlyLit;
|
|
}
|
|
}
|
|
|
|
protected override Vector2 CalculateMinimumSize()
|
|
{
|
|
var font = _getFont();
|
|
var style = _getStyleBox();
|
|
return new Vector2(0, font.GetHeight(UIScale) / UIScale) + style.MinimumSize / UIScale;
|
|
}
|
|
|
|
protected internal override void TextEntered(GUITextEventArgs args)
|
|
{
|
|
base.TextEntered(args);
|
|
|
|
if (!Editable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (IgnoreNext)
|
|
{
|
|
IgnoreNext = false;
|
|
return;
|
|
}
|
|
|
|
_text = _text.Insert(_cursorPosition, ((char) args.CodePoint).ToString());
|
|
_cursorPosition += 1;
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_updatePseudoClass();
|
|
}
|
|
|
|
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
|
{
|
|
base.KeyBindDown(args);
|
|
|
|
if (!args.CanFocus)
|
|
{
|
|
if (args.Function == EngineKeyFunctions.TextBackspace)
|
|
{
|
|
if (_cursorPosition == 0 || !Editable)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_text = _text.Remove(_cursorPosition - 1, 1);
|
|
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
|
_cursorPosition -= 1;
|
|
_updatePseudoClass();
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextCursorLeft)
|
|
{
|
|
if (_cursorPosition == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cursorPosition -= 1;
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextCursorRight)
|
|
{
|
|
if (_cursorPosition == _text.Length)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_cursorPosition += 1;
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextSubmit)
|
|
{
|
|
if (Editable)
|
|
{
|
|
OnTextEntered?.Invoke(new LineEditEventArgs(this, _text));
|
|
}
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.TextPaste)
|
|
{
|
|
if (Editable)
|
|
{
|
|
var clipboard = IoCManager.Resolve<IClipboardManager>();
|
|
if (!clipboard.Available)
|
|
{
|
|
UserInterfaceManager.Popup(clipboard.NotAvailableReason, "Clipboard not available!");
|
|
return;
|
|
}
|
|
|
|
InsertAtCursor(clipboard.GetText());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Find closest cursor position under mouse.
|
|
var style = _getStyleBox();
|
|
var contentBox = style.GetContentBox(PixelSizeBox);
|
|
|
|
var clickPosX = args.RelativePosition.X * UIScale;
|
|
|
|
var font = _getFont();
|
|
var index = 0;
|
|
var chrPosX = contentBox.Left;
|
|
var lastChrPostX = contentBox.Left;
|
|
foreach (var chr in _text)
|
|
{
|
|
if (!font.TryGetCharMetrics(chr, UIScale, out var metrics))
|
|
{
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if (chrPosX > clickPosX)
|
|
{
|
|
break;
|
|
}
|
|
|
|
lastChrPostX = chrPosX;
|
|
chrPosX += metrics.Advance;
|
|
index += 1;
|
|
|
|
if (chrPosX > contentBox.Right)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Distance between the right side of the glyph overlapping the mouse and the mouse.
|
|
var distanceRight = chrPosX - clickPosX;
|
|
// Same but left side.
|
|
var distanceLeft = clickPosX - lastChrPostX;
|
|
// If the mouse is closer to the left of the glyph we lower the index one, so we select before that glyph.
|
|
if (distanceRight > distanceLeft)
|
|
{
|
|
index -= 1;
|
|
}
|
|
|
|
_cursorPosition = index;
|
|
}
|
|
// Reset this so the cursor is always visible immediately after a keybind is pressed.
|
|
_resetCursorBlink();
|
|
}
|
|
|
|
protected internal override void FocusEntered()
|
|
{
|
|
base.FocusEntered();
|
|
|
|
// Reset this so the cursor is always visible immediately after gaining focus..
|
|
_resetCursorBlink();
|
|
}
|
|
|
|
[Pure]
|
|
private Font _getFont()
|
|
{
|
|
if (TryGetStyleProperty("font", out Font font))
|
|
{
|
|
return font;
|
|
}
|
|
|
|
return UserInterfaceManager.ThemeDefaults.DefaultFont;
|
|
}
|
|
|
|
[Pure]
|
|
private StyleBox _getStyleBox()
|
|
{
|
|
if (TryGetStyleProperty(StylePropertyStyleBox, out StyleBox box))
|
|
{
|
|
return box;
|
|
}
|
|
|
|
return UserInterfaceManager.ThemeDefaults.LineEditBox;
|
|
}
|
|
|
|
[Pure]
|
|
private Color _getFontColor()
|
|
{
|
|
if (TryGetStyleProperty("font-color", out Color color))
|
|
{
|
|
return color;
|
|
}
|
|
|
|
return Color.White;
|
|
}
|
|
|
|
private void _updatePseudoClass()
|
|
{
|
|
SetOnlyStylePseudoClass(IsPlaceHolderVisible ? StylePseudoClassPlaceholder : null);
|
|
}
|
|
|
|
public enum AlignMode
|
|
{
|
|
Left = 0,
|
|
Center = 1,
|
|
Right = 2
|
|
}
|
|
|
|
public class LineEditEventArgs : EventArgs
|
|
{
|
|
public LineEdit Control { get; }
|
|
public string Text { get; }
|
|
|
|
public LineEditEventArgs(LineEdit control, string text)
|
|
{
|
|
Control = control;
|
|
Text = text;
|
|
}
|
|
}
|
|
|
|
private void _resetCursorBlink()
|
|
{
|
|
_cursorCurrentlyLit = true;
|
|
_cursorBlinkTimer = BlinkTime;
|
|
}
|
|
}
|
|
}
|