//
// TextField.cs: single-line text editor with Emacs keybindings
//
// Authors:
// Miguel de Icaza (miguel@gnome.org)
//
using System;
using System.Collections.Generic;
using System.Linq;
using NStack;
namespace Terminal.Gui {
///
/// Text data entry widget
///
///
/// The Entry widget provides Emacs-like editing
/// functionality, and mouse support.
///
public class TextField : View {
List text;
int first, point;
bool used;
///
/// Changed event, raised when the text has clicked.
///
///
/// Client code can hook up to this event, it is
/// raised when the text in the entry changes.
///
public event EventHandler Changed;
///
/// Public constructor that creates a text field, with layout controlled with X, Y, Width and Height.
///
/// Initial text contents.
public TextField (string text) : this (ustring.Make (text))
{
}
///
/// Public constructor that creates a text field, with layout controlled with X, Y, Width and Height.
///
/// Initial text contents.
public TextField (ustring text)
{
if (text == null)
text = "";
this.text = TextModel.ToRunes (text);
point = text.Length;
CanFocus = true;
}
///
/// Public constructor that creates a text field at an absolute position and size.
///
/// The x coordinate.
/// The y coordinate.
/// The width.
/// Initial text contents.
public TextField (int x, int y, int w, ustring text) : base (new Rect (x, y, w, 1))
{
if (text == null)
text = "";
this.text = TextModel.ToRunes (text);
point = text.Length;
first = point > w ? point - w : 0;
CanFocus = true;
}
public override Rect Frame {
get => base.Frame;
set {
base.Frame = value;
var w = base.Frame.Width;
first = point > w ? point - w : 0;
}
}
///
/// Sets or gets the text in the entry.
///
///
///
public ustring Text {
get {
return ustring.Make (text);
}
set {
text = TextModel.ToRunes (value);
if (point > text.Count)
point = Math.Max (text.Count-1, 0);
// FIXME: this needs to be updated to use Rune.ColumnWidth
first = point > Frame.Width ? point - Frame.Width : 0;
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 int CursorPosition {
get { return point; }
set {
point = value;
Adjust ();
SetNeedsDisplay ();
}
}
///
/// Sets the cursor position.
///
public override void PositionCursor ()
{
var col = 0;
for (int idx = first; idx < text.Count; idx++) {
if (idx == point)
break;
var cols = Rune.ColumnWidth (text [idx]);
col += cols;
}
Move (col, 0);
}
public override void Redraw (Rect region)
{
Driver.SetAttribute (ColorScheme.Focus);
Move (0, 0);
int p = first;
int col = 0;
int width = Frame.Width;
var tcount = text.Count;
for (int idx = 0; idx < tcount; idx++){
var rune = text [idx];
if (idx < first)
continue;
var cols = Rune.ColumnWidth (rune);
if (col + cols < width)
Driver.AddRune ((Rune)(Secret ? '*' : rune));
col += cols;
}
for (int i = col; i < Frame.Width; i++)
Driver.AddRune (' ');
PositionCursor ();
}
// Returns the size of the string starting at position start
int DisplaySize (List t, int start)
{
int size = 0;
int tcount = text.Count;
for (int i = start; i < tcount; i++) {
var rune = text [i];
size += Rune.ColumnWidth (rune);
}
return size;
}
void Adjust ()
{
if (point < first)
first = point;
else if (first + point >= Frame.Width) {
first = point - (Frame.Width - 1);
}
SetNeedsDisplay ();
}
void SetText (List newText)
{
text = newText;
if (Changed != null)
Changed (this, EventArgs.Empty);
}
void SetText (IEnumerable newText)
{
SetText (newText.ToList ());
}
public override bool CanFocus {
get => true;
set { base.CanFocus = value; }
}
void SetClipboard (IEnumerable text)
{
if (!Secret)
Clipboard.Contents = ustring.Make (text.ToList ());
}
public override bool ProcessKey (KeyEvent kb)
{
switch (kb.Key) {
case Key.DeleteChar:
case Key.ControlD:
if (text.Count == 0 || text.Count== point)
return true;
SetText (text.GetRange (0, point).Concat (text.GetRange (point + 1, text.Count - (point + 1))));
Adjust ();
break;
case Key.Delete:
case Key.Backspace:
if (point == 0)
return true;
SetText (text.GetRange (0, point - 1).Concat (text.GetRange (point, text.Count - (point))));
point--;
Adjust ();
break;
// Home, C-A
case Key.Home:
case Key.ControlA:
point = 0;
Adjust ();
break;
case Key.CursorLeft:
case Key.ControlB:
if (point > 0) {
point--;
Adjust ();
}
break;
case Key.End:
case Key.ControlE: // End
point = text.Count;
Adjust ();
break;
case Key.CursorRight:
case Key.ControlF:
if (point == text.Count)
break;
point++;
Adjust ();
break;
case Key.ControlK: // kill-to-end
if (point >= text.Count)
return true;
SetClipboard (text.GetRange (point, text.Count - point));
SetText (text.GetRange (0, point));
Adjust ();
break;
case Key.ControlY: // Control-y, yank
if (Clipboard.Contents == null)
return true;
var clip = TextModel.ToRunes (Clipboard.Contents);
if (clip == null)
return true;
if (point == text.Count) {
SetText (text.Concat (clip).ToList ());
point = text.Count;
} else {
SetText (text.GetRange (0, point).Concat (clip).Concat (text.GetRange (point, text.Count - point)));
point += clip.Count;
}
Adjust ();
break;
case (Key)((int)'b' + Key.AltMask):
int bw = WordBackward (point);
if (bw != -1)
point = bw;
Adjust ();
break;
case (Key)((int)'f' + Key.AltMask):
int fw = WordForward (point);
if (fw != -1)
point = fw;
Adjust ();
break;
// MISSING:
// Alt-D, Alt-backspace
// Alt-Y
// Delete adding to kill buffer
default:
// Ignore other control characters.
if (kb.Key < Key.Space || kb.Key > Key.CharMask)
return false;
var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key));
if (used) {
if (point == text.Count) {
SetText (text.Concat (kbstr).ToList ());
} else {
SetText (text.GetRange (0, point).Concat (kbstr).Concat (text.GetRange (point, text.Count - point)));
}
point++;
} else {
SetText (kbstr);
first = 0;
point = 1;
}
used = true;
Adjust ();
return true;
}
used = true;
return true;
}
int WordForward (int p)
{
if (p >= text.Count)
return -1;
int i = p;
if (Rune.IsPunctuation (text [p]) || Rune.IsWhiteSpace(text [p])) {
for (; i < text.Count; i++) {
var r = text [i];
if (Rune.IsLetterOrDigit(r))
break;
}
for (; i < text.Count; i++) {
var r = text [i];
if (!Rune.IsLetterOrDigit (r))
break;
}
} else {
for (; i < text.Count; i++) {
var r = text [i];
if (!Rune.IsLetterOrDigit (r))
break;
}
}
if (i != p)
return i;
return -1;
}
int WordBackward (int p)
{
if (p == 0)
return -1;
int i = p - 1;
if (i == 0)
return 0;
var ti = text [i];
if (Rune.IsPunctuation (ti) || Rune.IsSymbol(ti) || Rune.IsWhiteSpace(ti)) {
for (; i >= 0; i--) {
if (Rune.IsLetterOrDigit (text [i]))
break;
}
for (; i >= 0; i--) {
if (!Rune.IsLetterOrDigit (text [i]))
break;
}
} else {
for (; i >= 0; i--) {
if (!Rune.IsLetterOrDigit (text [i]))
break;
}
}
i++;
if (i != p)
return i;
return -1;
}
public override bool MouseEvent (MouseEvent ev)
{
if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked))
return false;
if (!HasFocus)
SuperView.SetFocus (this);
// We could also set the cursor position.
point = first + ev.X;
if (point > text.Count)
point = text.Count;
if (point < first)
point = 0;
SetNeedsDisplay ();
return true;
}
}
}