//
// 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 {
ustring 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 (ustring text)
{
if (text == null)
text = "";
this.text = 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 = 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 text;
}
set {
text = value;
if (point > text.Length)
point = text.Length;
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; }
///
/// The current cursor position.
///
public int CursorPosition { get { return point; } }
///
/// Sets the cursor position.
///
public override void PositionCursor ()
{
Move (point - first, 0);
}
public override void Redraw (Rect region)
{
Driver.SetAttribute (ColorScheme.Focus);
Move (0, 0);
for (int i = 0; i < Frame.Width; i++) {
int p = first + i;
if (p < text.Length) {
Driver.AddRune (Secret ? (Rune)'*' : text [p]);
} else
Driver.AddRune (' ');
}
PositionCursor ();
}
void Adjust ()
{
if (point < first)
first = point;
else if (first + point >= Frame.Width)
first = point - (Frame.Width / 3);
SetNeedsDisplay ();
}
void SetText (ustring new_text)
{
text = new_text;
if (Changed != null)
Changed (this, EventArgs.Empty);
}
public override bool CanFocus {
get => true;
set { base.CanFocus = value; }
}
void SetClipboard (ustring text)
{
if (!Secret)
Clipboard.Contents = text;
}
public override bool ProcessKey (KeyEvent kb)
{
switch (kb.Key) {
case Key.Delete:
#if false
case Key.DeleteChar:
if (text.Length == 0 || text.Length == point)
return true;
SetText (text [0, point] + text [point + 1, null]);
Adjust ();
break;
#endif
case Key.Backspace:
if (point == 0)
return true;
SetText (text [0, point - 1] + text [point, null]);
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.ControlD: // Delete
if (point == text.Length)
break;
SetText (text [0, point] + text [point + 1, null]);
Adjust ();
break;
case Key.ControlE: // End
point = text.Length;
Adjust ();
break;
case Key.CursorRight:
case Key.ControlF:
if (point == text.Length)
break;
point++;
Adjust ();
break;
case Key.ControlK: // kill-to-end
SetClipboard (text.Substring (point));
SetText (text [0, point]);
Adjust ();
break;
case Key.ControlY: // Control-y, yank
var clip = Clipboard.Contents;
if (clip== null)
return true;
if (point == text.Length) {
SetText (text + clip);
point = text.Length;
} else {
SetText (text [0, point] + clip + text.Substring (point));
point += clip.RuneCount;
}
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 = ustring.Make ((uint)kb.Key);
if (used) {
if (point == text.Length) {
SetText (text + kbstr);
} else {
SetText (text [0, point] + kbstr + text [point, null]);
}
point++;
} else {
SetText (kbstr);
first = 0;
point = 1;
}
used = true;
Adjust ();
return true;
}
used = true;
return true;
}
int WordForward (int p)
{
if (p >= text.Length)
return -1;
int i = p;
if (Rune.IsPunctuation (text [p]) || Rune.IsWhiteSpace(text [p])) {
for (; i < text.Length; i++) {
var r = text [i];
if (Rune.IsLetterOrDigit(r))
break;
}
for (; i < text.Length; i++) {
var r = text [i];
if (!Rune.IsLetterOrDigit (r))
break;
}
} else {
for (; i < text.Length; 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.Length)
point = text.Length;
if (point < first)
point = 0;
SetNeedsDisplay ();
return true;
}
}
}