using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using Terminal.Gui.Resources;
namespace Terminal.Gui;
///
/// Single-line text entry
///
///
/// The provides editing functionality and mouse support.
///
public class TextField : View {
CultureInfo _currentCulture;
CursorVisibility _desiredCursorVisibility = CursorVisibility.Default;
int _cursorPosition;
readonly HistoryText _historyText = new ();
bool _isButtonPressed;
bool _isButtonReleased = true;
bool _isDrawing;
int _preTextChangedCursorPos;
CursorVisibility _savedCursorVisibility;
int _selectedStart = -1; // -1 represents there is no text selection.
string _selectedText;
int _start;
List _text;
CursorVisibility _visibility;
///
/// Initializes a new instance of the class using positioning.
///
public TextField () : this (string.Empty) { }
///
/// Initializes a new instance of the class using positioning.
///
/// Initial text contents.
public TextField (string text) : base (text) => SetInitialProperties (text, text.GetRuneCount () + 1);
///
/// Initializes a new instance of the class using positioning.
///
/// The x coordinate.
/// The y coordinate.
/// The width.
/// Initial text contents.
public TextField (int x, int y, int w, string text) : base (new Rect (x, y, w, 1)) => SetInitialProperties (text, w);
///
/// Gets or sets the text to render in control when no value has
/// been entered yet and the does not yet have
/// input focus.
///
public string Caption { get; set; }
///
/// Gets or sets the foreground to use when
/// rendering .
///
public Color CaptionColor { get; set; } = new (Color.DarkGray);
///
/// Tracks whether the text field should be considered "used", that is, that the user has moved in the entry, so new input
/// should be appended at the cursor position, rather than clearing the entry
///
public bool Used { get; set; }
///
/// If set to true its not allow any changes in the text.
///
public bool ReadOnly { get; set; } = false;
///
/// Provides autocomplete context menu based on suggestions at the current cursor
/// position. Configure to enable this feature.
///
public IAutocomplete Autocomplete { get; set; } = new TextFieldAutocomplete ();
///
/// Sets or gets the text held by the view.
///
public new string Text {
get => StringExtensions.ToString (_text);
set {
var oldText = StringExtensions.ToString (_text);
if (oldText == value) {
return;
}
var newText = OnTextChanging (value.Replace ("\t", "").Split ("\n") [0]);
if (newText.Cancel) {
if (_cursorPosition > _text.Count) {
_cursorPosition = _text.Count;
}
return;
}
ClearAllSelection ();
_text = newText.NewText.EnumerateRunes ().ToList ();
if (!Secret && !_historyText.IsFromHistory) {
_historyText.Add (new List> { TextModel.ToRuneCellList (oldText) },
new Point (_cursorPosition, 0));
_historyText.Add (new List> { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0)
, HistoryText.LineStatus.Replaced);
}
TextChanged?.Invoke (this, new TextChangedEventArgs (oldText));
ProcessAutocomplete ();
if (_cursorPosition > _text.Count) {
_cursorPosition = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0);
}
Adjust ();
SetNeedsDisplay ();
}
}
///
/// Sets the secret property.
///
/// This makes the text entry suitable for entering passwords.
///
///
public bool Secret { get; set; }
///
/// Sets or gets the current cursor position.
///
public virtual int CursorPosition {
get => _cursorPosition;
set {
if (value < 0) {
_cursorPosition = 0;
} else if (value > _text.Count) {
_cursorPosition = _text.Count;
} else {
_cursorPosition = value;
}
PrepareSelection (_selectedStart, _cursorPosition - _selectedStart);
}
}
///
/// Gets the left offset position.
///
public int ScrollOffset { get; private set; }
///
/// Indicates whatever the text was changed or not.
/// if the text was changed otherwise.
///
public bool IsDirty => _historyText.IsDirty (Text);
///
/// Indicates whatever the text has history changes or not.
/// if the text has history changes otherwise.
///
public bool HasHistoryChanges => _historyText.HasHistoryChanges;
///
/// Get the for this view.
///
public ContextMenu ContextMenu { get; private set; }
///
public override bool CanFocus {
get => base.CanFocus;
set => base.CanFocus = value;
}
///
/// Start position of the selected text.
///
public int SelectedStart {
get => _selectedStart;
set {
if (value < -1) {
_selectedStart = -1;
} else if (value > _text.Count) {
_selectedStart = _text.Count;
} else {
_selectedStart = value;
}
PrepareSelection (_selectedStart, _cursorPosition - _selectedStart);
}
}
///
/// Length of the selected text.
///
public int SelectedLength { get; private set; }
///
/// The selected text.
///
public string SelectedText {
get => Secret ? null : _selectedText;
private set => _selectedText = value;
}
///
/// Get / Set the wished cursor when the field is focused
///
public CursorVisibility DesiredCursorVisibility {
get => _desiredCursorVisibility;
set {
if ((_desiredCursorVisibility != value || _visibility != value) && HasFocus) {
Application.Driver.SetCursorVisibility (value);
}
_desiredCursorVisibility = _visibility = value;
}
}
///
/// Changing event, raised before the changes and can be canceled or changing the new text.
///
public event EventHandler TextChanging;
///
/// Changed event, raised when the text has changed.
///
/// This event is raised when the changes.
/// The passed is a containing the old value.
///
///
public event EventHandler TextChanged;
void SetInitialProperties (string text, int w)
{
Height = 1;
if (text == null) {
text = "";
}
_text = text.Split ("\n") [0].EnumerateRunes ().ToList ();
_cursorPosition = text.GetRuneCount ();
ScrollOffset = _cursorPosition > w + 1 ? _cursorPosition - w + 1 : 0;
CanFocus = true;
Used = true;
WantMousePositionReports = true;
_savedCursorVisibility = _desiredCursorVisibility;
_historyText.ChangeText += HistoryText_ChangeText;
Initialized += TextField_Initialized;
LayoutComplete += TextField_LayoutComplete;
// Things this view knows how to do
AddCommand (Command.DeleteCharRight, () => {
DeleteCharRight ();
return true;
});
AddCommand (Command.DeleteCharLeft, () => {
DeleteCharLeft (false);
return true;
});
AddCommand (Command.LeftHomeExtend, () => {
MoveHomeExtend ();
return true;
});
AddCommand (Command.RightEndExtend, () => {
MoveEndExtend ();
return true;
});
AddCommand (Command.LeftHome, () => {
MoveHome ();
return true;
});
AddCommand (Command.LeftExtend, () => {
MoveLeftExtend ();
return true;
});
AddCommand (Command.RightExtend, () => {
MoveRightExtend ();
return true;
});
AddCommand (Command.WordLeftExtend, () => {
MoveWordLeftExtend ();
return true;
});
AddCommand (Command.WordRightExtend, () => {
MoveWordRightExtend ();
return true;
});
AddCommand (Command.Left, () => {
MoveLeft ();
return true;
});
AddCommand (Command.RightEnd, () => {
MoveEnd ();
return true;
});
AddCommand (Command.Right, () => {
MoveRight ();
return true;
});
AddCommand (Command.CutToEndLine, () => {
KillToEnd ();
return true;
});
AddCommand (Command.CutToStartLine, () => {
KillToStart ();
return true;
});
AddCommand (Command.Undo, () => {
Undo ();
return true;
});
AddCommand (Command.Redo, () => {
Redo ();
return true;
});
AddCommand (Command.WordLeft, () => {
MoveWordLeft ();
return true;
});
AddCommand (Command.WordRight, () => {
MoveWordRight ();
return true;
});
AddCommand (Command.KillWordForwards, () => {
KillWordForwards ();
return true;
});
AddCommand (Command.KillWordBackwards, () => {
KillWordBackwards ();
return true;
});
AddCommand (Command.ToggleOverwrite, () => {
SetOverwrite (!Used);
return true;
});
AddCommand (Command.EnableOverwrite, () => {
SetOverwrite (true);
return true;
});
AddCommand (Command.DisableOverwrite, () => {
SetOverwrite (false);
return true;
});
AddCommand (Command.Copy, () => {
Copy ();
return true;
});
AddCommand (Command.Cut, () => {
Cut ();
return true;
});
AddCommand (Command.Paste, () => {
Paste ();
return true;
});
AddCommand (Command.SelectAll, () => {
SelectAll ();
return true;
});
AddCommand (Command.DeleteAll, () => {
DeleteAll ();
return true;
});
AddCommand (Command.ShowContextMenu, () => {
ShowContextMenu ();
return true;
});
// Default keybindings for this view
// We follow this as closely as possible: https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts
KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight);
KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask, Command.DeleteCharRight);
KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft);
KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask, Command.LeftHomeExtend);
KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.LeftHomeExtend);
KeyBindings.Add (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.LeftHomeExtend);
KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask, Command.RightEndExtend);
KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.RightEndExtend);
KeyBindings.Add (KeyCode.E | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.RightEndExtend);
KeyBindings.Add (KeyCode.Home, Command.LeftHome);
KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask, Command.LeftHome);
KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.LeftHome);
KeyBindings.Add (KeyCode.CursorLeft | KeyCode.ShiftMask, Command.LeftExtend);
KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LeftExtend);
KeyBindings.Add (KeyCode.CursorRight | KeyCode.ShiftMask, Command.RightExtend);
KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.RightExtend);
KeyBindings.Add (KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordLeftExtend);
KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordLeftExtend);
KeyBindings.Add ('B' + KeyCode.ShiftMask | KeyCode.AltMask, Command.WordLeftExtend);
KeyBindings.Add (KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordRightExtend);
KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordRightExtend);
KeyBindings.Add ('F' + KeyCode.ShiftMask | KeyCode.AltMask, Command.WordRightExtend);
KeyBindings.Add (KeyCode.CursorLeft, Command.Left);
KeyBindings.Add (KeyCode.B | KeyCode.CtrlMask, Command.Left);
KeyBindings.Add (KeyCode.End, Command.RightEnd);
KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask, Command.RightEnd);
KeyBindings.Add (KeyCode.E | KeyCode.CtrlMask, Command.RightEnd);
KeyBindings.Add (KeyCode.CursorRight, Command.Right);
KeyBindings.Add (KeyCode.F | KeyCode.CtrlMask, Command.Right);
KeyBindings.Add (KeyCode.K | KeyCode.CtrlMask, Command.CutToEndLine);
KeyBindings.Add (KeyCode.K | KeyCode.AltMask, Command.CutToStartLine);
KeyBindings.Add (KeyCode.Z | KeyCode.CtrlMask, Command.Undo);
KeyBindings.Add (KeyCode.Backspace | KeyCode.AltMask, Command.Undo);
KeyBindings.Add (KeyCode.Y | KeyCode.CtrlMask, Command.Redo);
KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.WordLeft);
KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.WordLeft);
KeyBindings.Add ('B' + KeyCode.AltMask, Command.WordLeft);
KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.WordRight);
KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.WordRight);
KeyBindings.Add ('F' + KeyCode.AltMask, Command.WordRight);
KeyBindings.Add (KeyCode.Delete | KeyCode.CtrlMask, Command.KillWordForwards);
KeyBindings.Add (KeyCode.Backspace | KeyCode.CtrlMask, Command.KillWordBackwards);
KeyBindings.Add (KeyCode.Insert, Command.ToggleOverwrite);
KeyBindings.Add (KeyCode.C | KeyCode.CtrlMask, Command.Copy);
KeyBindings.Add (KeyCode.X | KeyCode.CtrlMask, Command.Cut);
KeyBindings.Add (KeyCode.V | KeyCode.CtrlMask, Command.Paste);
KeyBindings.Add (KeyCode.T | KeyCode.CtrlMask, Command.SelectAll);
KeyBindings.Add (KeyCode.R | KeyCode.CtrlMask, Command.DeleteAll);
KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.DeleteAll);
_currentCulture = Thread.CurrentThread.CurrentUICulture;
ContextMenu = new ContextMenu (this, BuildContextMenuBarItem ());
ContextMenu.KeyChanged += ContextMenu_KeyChanged;
KeyBindings.Add (ContextMenu.Key.KeyCode, KeyBindingScope.HotKey, Command.ShowContextMenu);
}
void TextField_LayoutComplete (object sender, LayoutEventArgs e)
{
// Don't let height > 1
if (Frame.Height > 1) {
Height = 1;
}
}
MenuBarItem BuildContextMenuBarItem () => new (new MenuItem [] {
new (Strings.ctxSelectAll, "", () => SelectAll (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll)),
new (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll)),
new (Strings.ctxCopy, "", () => Copy (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy)),
new (Strings.ctxCut, "", () => Cut (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut)),
new (Strings.ctxPaste, "", () => Paste (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste)),
new (Strings.ctxUndo, "", () => Undo (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo)),
new (Strings.ctxRedo, "", () => Redo (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo))
});
void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) => KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode);
void HistoryText_ChangeText (object sender, HistoryText.HistoryTextItem obj)
{
if (obj == null) {
return;
}
Text = TextModel.ToString (obj?.Lines [obj.CursorPosition.Y]);
CursorPosition = obj.CursorPosition.X;
Adjust ();
}
void TextField_Initialized (object sender, EventArgs e)
{
Autocomplete.HostControl = this;
Autocomplete.PopupInsideContainer = false;
}
///
public override bool OnEnter (View view)
{
if (IsInitialized) {
Application.Driver.SetCursorVisibility (DesiredCursorVisibility);
}
return base.OnEnter (view);
}
///
public override bool OnLeave (View view)
{
if (Application.MouseGrabView != null && Application.MouseGrabView == this) {
Application.UngrabMouse ();
}
//if (SelectedLength != 0 && !(Application.MouseGrabView is MenuBar))
// ClearAllSelection ();
return base.OnLeave (view);
}
///
/// Sets the cursor position.
///
public override void PositionCursor ()
{
if (!IsInitialized) {
return;
}
ProcessAutocomplete ();
var col = 0;
for (var idx = ScrollOffset < 0 ? 0 : ScrollOffset; idx < _text.Count; idx++) {
if (idx == _cursorPosition) {
break;
}
var cols = _text [idx].GetColumns ();
TextModel.SetCol (ref col, Frame.Width - 1, cols);
}
var pos = _cursorPosition - ScrollOffset + Math.Min (Frame.X, 0);
var offB = OffSetBackground ();
var containerFrame = SuperView?.BoundsToScreen (SuperView.Bounds) ?? default;
var thisFrame = BoundsToScreen (Bounds);
if (pos > -1 && col >= pos && pos < Frame.Width + offB
&& containerFrame.IntersectsWith (thisFrame)) {
RestoreCursorVisibility ();
Move (col, 0);
} else {
HideCursorVisibility ();
if (pos < 0) {
Move (pos, 0);
} else {
Move (pos - offB, 0);
}
}
}
void HideCursorVisibility ()
{
if (_desiredCursorVisibility != CursorVisibility.Invisible) {
DesiredCursorVisibility = CursorVisibility.Invisible;
}
}
void RestoreCursorVisibility ()
{
Application.Driver.GetCursorVisibility (out _visibility);
if (_desiredCursorVisibility != _savedCursorVisibility || _visibility != _savedCursorVisibility) {
DesiredCursorVisibility = _savedCursorVisibility;
}
}
///
public override void OnDrawContent (Rect contentArea)
{
_isDrawing = true;
var selColor = new Attribute (ColorScheme.Focus.Background, ColorScheme.Focus.Foreground);
SetSelectedStartSelectedLength ();
Driver.SetAttribute (GetNormalColor ());
Move (0, 0);
var p = ScrollOffset;
var col = 0;
var width = Frame.Width + OffSetBackground ();
var tcount = _text.Count;
var roc = GetReadOnlyColor ();
for (var idx = p; idx < tcount; idx++) {
var rune = _text [idx];
var cols = rune.GetColumns ();
if (idx == _cursorPosition && HasFocus && !Used && SelectedLength == 0 && !ReadOnly) {
Driver.SetAttribute (selColor);
} else if (ReadOnly) {
Driver.SetAttribute (idx >= _start && SelectedLength > 0 && idx < _start + SelectedLength ? selColor : roc);
} else if (!HasFocus && Enabled) {
Driver.SetAttribute (ColorScheme.Focus);
} else if (!Enabled) {
Driver.SetAttribute (roc);
} else {
Driver.SetAttribute (idx >= _start && SelectedLength > 0 && idx < _start + SelectedLength ? selColor : ColorScheme.Focus);
}
if (col + cols <= width) {
Driver.AddRune (Secret ? Glyphs.Dot : rune);
}
if (!TextModel.SetCol (ref col, width, cols)) {
break;
}
if (idx + 1 < tcount && col + _text [idx + 1].GetColumns () > width) {
break;
}
}
Driver.SetAttribute (ColorScheme.Focus);
for (var i = col; i < width; i++) {
Driver.AddRune ((Rune)' ');
}
PositionCursor ();
RenderCaption ();
ProcessAutocomplete ();
_isDrawing = false;
}
void ProcessAutocomplete ()
{
if (_isDrawing) {
return;
}
if (SelectedLength > 0) {
return;
}
// draw autocomplete
GenerateSuggestions ();
var renderAt = new Point (
Autocomplete.Context.CursorPosition, 0);
Autocomplete.RenderOverlay (renderAt);
}
void RenderCaption ()
{
if (HasFocus || Caption == null || Caption.Length == 0
|| Text?.Length > 0) {
return;
}
var color = new Attribute (CaptionColor, GetNormalColor ().Background);
Driver.SetAttribute (color);
Move (0, 0);
var render = Caption;
if (render.GetColumns () > Bounds.Width) {
render = render [..Bounds.Width];
}
Driver.AddStr (render);
}
void GenerateSuggestions ()
{
var currentLine = TextModel.ToRuneCellList (Text);
var cursorPosition = Math.Min (CursorPosition, currentLine.Count);
Autocomplete.Context = new AutocompleteContext (currentLine, cursorPosition,
Autocomplete.Context != null ? Autocomplete.Context.Canceled : false);
Autocomplete.GenerateSuggestions (
Autocomplete.Context);
}
///
public override Attribute GetNormalColor () => Enabled ? ColorScheme.Focus : ColorScheme.Disabled;
Attribute GetReadOnlyColor ()
{
if (ColorScheme.Disabled.Foreground == ColorScheme.Focus.Background) {
return new Attribute (ColorScheme.Focus.Foreground, ColorScheme.Focus.Background);
}
return new Attribute (ColorScheme.Disabled.Foreground, ColorScheme.Focus.Background);
}
void Adjust ()
{
if (!IsAdded) {
return;
}
var offB = OffSetBackground ();
var need = NeedsDisplay || !Used;
if (_cursorPosition < ScrollOffset) {
ScrollOffset = _cursorPosition;
need = true;
} else if (Frame.Width > 0 && (ScrollOffset + _cursorPosition - (Frame.Width + offB) == 0 ||
TextModel.DisplaySize (_text, ScrollOffset, _cursorPosition).size >= Frame.Width + offB)) {
ScrollOffset = Math.Max (TextModel.CalculateLeftColumn (_text, ScrollOffset,
_cursorPosition, Frame.Width + offB), 0);
need = true;
}
if (need) {
SetNeedsDisplay ();
} else {
PositionCursor ();
}
}
int OffSetBackground ()
{
var offB = 0;
if (SuperView?.Frame.Right - Frame.Right < 0) {
offB = SuperView.Frame.Right - Frame.Right - 1;
}
return offB;
}
void SetText (List newText) => Text = StringExtensions.ToString (newText);
void SetText (IEnumerable newText) => SetText (newText.ToList ());
void SetClipboard (IEnumerable text)
{
if (!Secret) {
Clipboard.Contents = StringExtensions.ToString (text.ToList ());
}
}
///
public override bool? OnInvokingKeyBindings (Key a)
{
// Give autocomplete first opportunity to respond to key presses
if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (a)) {
return true;
}
return base.OnInvokingKeyBindings (a);
}
/// TODO: Flush out these docs
///
/// Processes key presses for the .
///
/// The control responds to the following keys:
///
///
/// Keys
/// Function
///
/// -
/// ,
/// Deletes the character before cursor.
///
///
///
///
///
///
public override bool OnProcessKeyDown (Key a)
{
// Remember the cursor position because the new calculated cursor position is needed
// to be set BEFORE the TextChanged event is triggered.
// Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2
_preTextChangedCursorPos = _cursorPosition;
// Ignore other control characters.
if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask)) {
return false;
}
if (ReadOnly) {
return true;
}
InsertText (a, true);
return true;
}
void InsertText (Key a, bool usePreTextChangedCursorPos)
{
_historyText.Add (new List> { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0));
var newText = _text;
if (SelectedLength > 0) {
newText = DeleteSelectedText ();
_preTextChangedCursorPos = _cursorPosition;
}
if (!usePreTextChangedCursorPos) {
_preTextChangedCursorPos = _cursorPosition;
}
var kbstr = a.AsRune.ToString ().EnumerateRunes ();
if (Used) {
_cursorPosition++;
if (_cursorPosition == newText.Count + 1) {
SetText (newText.Concat (kbstr).ToList ());
} else {
if (_preTextChangedCursorPos > newText.Count) {
_preTextChangedCursorPos = newText.Count;
}
SetText (newText.GetRange (0, _preTextChangedCursorPos).Concat (kbstr).Concat (newText.GetRange (_preTextChangedCursorPos, Math.Min (newText.Count - _preTextChangedCursorPos, newText.Count))));
}
} else {
SetText (newText.GetRange (0, _preTextChangedCursorPos).Concat (kbstr).Concat (newText.GetRange (Math.Min (_preTextChangedCursorPos + 1, newText.Count), Math.Max (newText.Count - _preTextChangedCursorPos - 1, 0))));
_cursorPosition++;
}
Adjust ();
}
void SetOverwrite (bool overwrite)
{
Used = overwrite;
SetNeedsDisplay ();
}
TextModel GetModel ()
{
var model = new TextModel ();
model.LoadString (Text);
return model;
}
///
/// Deletes word backwards.
///
public virtual void KillWordBackwards ()
{
ClearAllSelection ();
var newPos = GetModel ().WordBackward (_cursorPosition, 0);
if (newPos == null) {
return;
}
if (newPos.Value.col != -1) {
SetText (_text.GetRange (0, newPos.Value.col).Concat (_text.GetRange (_cursorPosition, _text.Count - _cursorPosition)));
_cursorPosition = newPos.Value.col;
}
Adjust ();
}
///
/// Deletes word forwards.
///
public virtual void KillWordForwards ()
{
ClearAllSelection ();
var newPos = GetModel ().WordForward (_cursorPosition, 0);
if (newPos == null) {
return;
}
if (newPos.Value.col != -1) {
SetText (_text.GetRange (0, _cursorPosition).Concat (_text.GetRange (newPos.Value.col, _text.Count - newPos.Value.col)));
}
Adjust ();
}
void MoveWordRight ()
{
ClearAllSelection ();
var newPos = GetModel ().WordForward (_cursorPosition, 0);
if (newPos == null) {
return;
}
if (newPos.Value.col != -1) {
_cursorPosition = newPos.Value.col;
}
Adjust ();
}
void MoveWordLeft ()
{
ClearAllSelection ();
var newPos = GetModel ().WordBackward (_cursorPosition, 0);
if (newPos == null) {
return;
}
if (newPos.Value.col != -1) {
_cursorPosition = newPos.Value.col;
}
Adjust ();
}
///
/// Redoes the latest changes.
///
public void Redo ()
{
if (ReadOnly) {
return;
}
_historyText.Redo ();
//if (string.IsNullOrEmpty (Clipboard.Contents))
// return true;
//var clip = TextModel.ToRunes (Clipboard.Contents);
//if (clip == null)
// return true;
//if (point == text.Count) {
// point = text.Count;
// SetText(text.Concat(clip).ToList());
//} else {
// point += clip.Count;
// SetText(text.GetRange(0, oldCursorPos).Concat(clip).Concat(text.GetRange(oldCursorPos, text.Count - oldCursorPos)));
//}
//Adjust ();
}
///
/// Undoes the latest changes.
///
public void Undo ()
{
if (ReadOnly) {
return;
}
_historyText.Undo ();
}
void KillToStart ()
{
if (ReadOnly) {
return;
}
ClearAllSelection ();
if (_cursorPosition == 0) {
return;
}
SetClipboard (_text.GetRange (0, _cursorPosition));
SetText (_text.GetRange (_cursorPosition, _text.Count - _cursorPosition));
_cursorPosition = 0;
Adjust ();
}
void KillToEnd ()
{
if (ReadOnly) {
return;
}
ClearAllSelection ();
if (_cursorPosition >= _text.Count) {
return;
}
SetClipboard (_text.GetRange (_cursorPosition, _text.Count - _cursorPosition));
SetText (_text.GetRange (0, _cursorPosition));
Adjust ();
}
void MoveRight ()
{
ClearAllSelection ();
if (_cursorPosition == _text.Count) {
return;
}
_cursorPosition++;
Adjust ();
}
///
/// Moves cursor to the end of the typed text.
///
public void MoveEnd ()
{
ClearAllSelection ();
_cursorPosition = _text.Count;
Adjust ();
}
void MoveLeft ()
{
ClearAllSelection ();
if (_cursorPosition > 0) {
_cursorPosition--;
Adjust ();
}
}
void MoveWordRightExtend ()
{
if (_cursorPosition < _text.Count) {
var x = _start > -1 && _start > _cursorPosition ? _start : _cursorPosition;
var newPos = GetModel ().WordForward (x, 0);
if (newPos == null) {
return;
}
if (newPos.Value.col != -1) {
_cursorPosition = newPos.Value.col;
}
PrepareSelection (x, newPos.Value.col - x);
}
}
void MoveWordLeftExtend ()
{
if (_cursorPosition > 0) {
var x = Math.Min (_start > -1 && _start > _cursorPosition ? _start : _cursorPosition, _text.Count);
if (x > 0) {
var newPos = GetModel ().WordBackward (x, 0);
if (newPos == null) {
return;
}
if (newPos.Value.col != -1) {
_cursorPosition = newPos.Value.col;
}
PrepareSelection (x, newPos.Value.col - x);
}
}
}
void MoveRightExtend ()
{
if (_cursorPosition < _text.Count) {
PrepareSelection (_cursorPosition++, 1);
}
}
void MoveLeftExtend ()
{
if (_cursorPosition > 0) {
PrepareSelection (_cursorPosition--, -1);
}
}
void MoveHome ()
{
ClearAllSelection ();
_cursorPosition = 0;
Adjust ();
}
void MoveEndExtend ()
{
if (_cursorPosition <= _text.Count) {
var x = _cursorPosition;
_cursorPosition = _text.Count;
PrepareSelection (x, _cursorPosition - x);
}
}
void MoveHomeExtend ()
{
if (_cursorPosition > 0) {
var x = _cursorPosition;
_cursorPosition = 0;
PrepareSelection (x, _cursorPosition - x);
}
}
///
/// Deletes the character to the left.
///
///
/// If set to true use the cursor position cached
/// ; otherwise use .
/// use .
///
public virtual void DeleteCharLeft (bool usePreTextChangedCursorPos)
{
if (ReadOnly) {
return;
}
_historyText.Add (new List> { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0));
if (SelectedLength == 0) {
if (_cursorPosition == 0) {
return;
}
if (!usePreTextChangedCursorPos) {
_preTextChangedCursorPos = _cursorPosition;
}
_cursorPosition--;
if (_preTextChangedCursorPos < _text.Count) {
SetText (_text.GetRange (0, _preTextChangedCursorPos - 1).Concat (_text.GetRange (_preTextChangedCursorPos, _text.Count - _preTextChangedCursorPos)));
} else {
SetText (_text.GetRange (0, _preTextChangedCursorPos - 1));
}
Adjust ();
} else {
var newText = DeleteSelectedText ();
Text = StringExtensions.ToString (newText);
Adjust ();
}
}
///
/// Deletes the character to the right.
///
public virtual void DeleteCharRight ()
{
if (ReadOnly) {
return;
}
_historyText.Add (new List> { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0));
if (SelectedLength == 0) {
if (_text.Count == 0 || _text.Count == _cursorPosition) {
return;
}
SetText (_text.GetRange (0, _cursorPosition).Concat (_text.GetRange (_cursorPosition + 1, _text.Count - (_cursorPosition + 1))));
Adjust ();
} else {
var newText = DeleteSelectedText ();
Text = StringExtensions.ToString (newText);
Adjust ();
}
}
void ShowContextMenu ()
{
if (_currentCulture != Thread.CurrentThread.CurrentUICulture) {
_currentCulture = Thread.CurrentThread.CurrentUICulture;
ContextMenu.MenuItems = BuildContextMenuBarItem ();
}
ContextMenu.Show ();
}
///
/// Selects all text.
///
public void SelectAll ()
{
if (_text.Count == 0) {
return;
}
_selectedStart = 0;
MoveEndExtend ();
SetNeedsDisplay ();
}
///
/// Deletes all text.
///
public void DeleteAll ()
{
if (_text.Count == 0) {
return;
}
_selectedStart = 0;
MoveEndExtend ();
DeleteCharLeft (false);
SetNeedsDisplay ();
}
///
public override bool MouseEvent (MouseEvent ev)
{
if (!ev.Flags.HasFlag (MouseFlags.Button1Pressed) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) &&
!ev.Flags.HasFlag (MouseFlags.Button1Released) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
!ev.Flags.HasFlag (MouseFlags.Button1TripleClicked) && !ev.Flags.HasFlag (ContextMenu.MouseFlags)) {
return false;
}
if (!CanFocus) {
return true;
}
if (!HasFocus && ev.Flags != MouseFlags.ReportMousePosition) {
SetFocus ();
}
// Give autocomplete first opportunity to respond to mouse clicks
if (SelectedLength == 0 && Autocomplete.MouseEvent (ev, true)) {
return true;
}
if (ev.Flags == MouseFlags.Button1Pressed) {
EnsureHasFocus ();
PositionCursor (ev);
if (_isButtonReleased) {
ClearAllSelection ();
}
_isButtonReleased = true;
_isButtonPressed = true;
} else if (ev.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && _isButtonPressed) {
var x = PositionCursor (ev);
_isButtonReleased = false;
PrepareSelection (x);
if (Application.MouseGrabView == null) {
Application.GrabMouse (this);
}
} else if (ev.Flags == MouseFlags.Button1Released) {
_isButtonReleased = true;
_isButtonPressed = false;
Application.UngrabMouse ();
} else if (ev.Flags == MouseFlags.Button1DoubleClicked) {
EnsureHasFocus ();
var x = PositionCursor (ev);
var sbw = x;
if (x == _text.Count || x > 0 && (char)_text [x - 1].Value != ' '
|| x > 0 && (char)_text [x].Value == ' ') {
var newPosBw = GetModel ().WordBackward (x, 0);
if (newPosBw == null) {
return true;
}
sbw = newPosBw.Value.col;
}
if (sbw != -1) {
x = sbw;
PositionCursor (x);
}
var newPosFw = GetModel ().WordForward (x, 0);
if (newPosFw == null) {
return true;
}
ClearAllSelection ();
if (newPosFw.Value.col != -1 && sbw != -1) {
_cursorPosition = newPosFw.Value.col;
}
PrepareSelection (sbw, newPosFw.Value.col - sbw);
} else if (ev.Flags == MouseFlags.Button1TripleClicked) {
EnsureHasFocus ();
PositionCursor (0);
ClearAllSelection ();
PrepareSelection (0, _text.Count);
} else if (ev.Flags == ContextMenu.MouseFlags) {
ShowContextMenu ();
}
SetNeedsDisplay ();
return true;
void EnsureHasFocus ()
{
if (!HasFocus) {
SetFocus ();
}
}
}
int PositionCursor (MouseEvent ev)
{
// We could also set the cursor position.
int x;
var pX = TextModel.GetColFromX (_text, ScrollOffset, ev.X);
if (_text.Count == 0) {
x = pX - ev.OfX;
} else {
x = pX;
}
return PositionCursor (x, false);
}
int PositionCursor (int x, bool getX = true)
{
var pX = x;
if (getX) {
pX = TextModel.GetColFromX (_text, ScrollOffset, x);
}
if (ScrollOffset + pX > _text.Count) {
_cursorPosition = _text.Count;
} else if (ScrollOffset + pX < ScrollOffset) {
_cursorPosition = 0;
} else {
_cursorPosition = ScrollOffset + pX;
}
return _cursorPosition;
}
void PrepareSelection (int x, int direction = 0)
{
x = x + ScrollOffset < -1 ? 0 : x;
_selectedStart = _selectedStart == -1 && _text.Count > 0 && x >= 0 && x <= _text.Count ? x : _selectedStart;
if (_selectedStart > -1) {
SelectedLength = Math.Abs (x + direction <= _text.Count ? x + direction - _selectedStart : _text.Count - _selectedStart);
SetSelectedStartSelectedLength ();
if (_start > -1 && SelectedLength > 0) {
_selectedText = SelectedLength > 0 ? StringExtensions.ToString (_text.GetRange (
_start < 0 ? 0 : _start, SelectedLength > _text.Count ? _text.Count : SelectedLength)) : "";
if (ScrollOffset > _start) {
ScrollOffset = _start;
}
} else if (_start > -1 && SelectedLength == 0) {
_selectedText = null;
}
SetNeedsDisplay ();
} else if (SelectedLength > 0 || _selectedText != null) {
ClearAllSelection ();
}
Adjust ();
}
///
/// Clear the selected text.
///
public void ClearAllSelection ()
{
if (_selectedStart == -1 && SelectedLength == 0 && string.IsNullOrEmpty (_selectedText)) {
return;
}
_selectedStart = -1;
SelectedLength = 0;
_selectedText = null;
_start = 0;
SelectedLength = 0;
SetNeedsDisplay ();
}
void SetSelectedStartSelectedLength ()
{
if (SelectedStart > -1 && _cursorPosition < SelectedStart) {
_start = _cursorPosition;
} else {
_start = SelectedStart;
}
}
///
/// Copy the selected text to the clipboard.
///
public virtual void Copy ()
{
if (Secret || SelectedLength == 0) {
return;
}
Clipboard.Contents = SelectedText;
}
///
/// Cut the selected text to the clipboard.
///
public virtual void Cut ()
{
if (ReadOnly || Secret || SelectedLength == 0) {
return;
}
Clipboard.Contents = SelectedText;
var newText = DeleteSelectedText ();
Text = StringExtensions.ToString (newText);
Adjust ();
}
List DeleteSelectedText ()
{
SetSelectedStartSelectedLength ();
var selStart = SelectedStart > -1 ? _start : _cursorPosition;
var newText = StringExtensions.ToString (_text.GetRange (0, selStart)) +
StringExtensions.ToString (_text.GetRange (selStart + SelectedLength, _text.Count - (selStart + SelectedLength)));
ClearAllSelection ();
_cursorPosition = selStart >= newText.GetRuneCount () ? newText.GetRuneCount () : selStart;
return newText.ToRuneList ();
}
///
/// Paste the selected text from the clipboard.
///
public virtual void Paste ()
{
if (ReadOnly || string.IsNullOrEmpty (Clipboard.Contents)) {
return;
}
SetSelectedStartSelectedLength ();
var selStart = _start == -1 ? CursorPosition : _start;
var cbTxt = Clipboard.Contents.Split ("\n") [0] ?? "";
Text = StringExtensions.ToString (_text.GetRange (0, selStart)) +
cbTxt +
StringExtensions.ToString (_text.GetRange (selStart + SelectedLength, _text.Count - (selStart + SelectedLength)));
_cursorPosition = Math.Min (selStart + cbTxt.GetRuneCount (), _text.Count);
ClearAllSelection ();
SetNeedsDisplay ();
Adjust ();
}
///
/// Virtual method that invoke the event if it's defined.
///
/// The new text to be replaced.
/// Returns the
public virtual TextChangingEventArgs OnTextChanging (string newText)
{
var ev = new TextChangingEventArgs (newText);
TextChanging?.Invoke (this, ev);
return ev;
}
///
/// Inserts the given text at the current cursor position
/// exactly as if the user had just typed it
///
/// Text to add
/// Use the previous cursor position.
public void InsertText (string toAdd, bool useOldCursorPos = true)
{
foreach (var ch in toAdd) {
KeyCode key;
try {
key = (KeyCode)ch;
} catch (Exception) {
throw new ArgumentException ($"Cannot insert character '{ch}' because it does not map to a Key");
}
InsertText (new Key { KeyCode = key }, useOldCursorPos);
}
}
///
/// Allows clearing the items updating the original text.
///
public void ClearHistoryChanges () => _historyText.Clear (Text);
///
/// Returns if the current cursor position is
/// at the end of the . This includes when it is empty.
///
///
internal bool CursorIsAtEnd () => CursorPosition == Text.Length;
///
/// Returns if the current cursor position is
/// at the start of the .
///
///
internal bool CursorIsAtStart () => CursorPosition <= 0;
}
///
/// Renders an overlay on another view at a given point that allows selecting
/// from a range of 'autocomplete' options.
/// An implementation on a TextField.
///
public class TextFieldAutocomplete : PopupAutocomplete {
///
protected override void DeleteTextBackwards () => ((TextField)HostControl).DeleteCharLeft (false);
///
protected override void InsertText (string accepted) => ((TextField)HostControl).InsertText (accepted, false);
///
protected override void SetCursorPosition (int column) => ((TextField)HostControl).CursorPosition = column;
}