2
0

TextField.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. //
  2. // TextField.cs: single-line text editor with Emacs keybindings
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. using System;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using NStack;
  11. namespace Terminal.Gui {
  12. /// <summary>
  13. /// Text data entry widget
  14. /// </summary>
  15. /// <remarks>
  16. /// The Entry widget provides Emacs-like editing
  17. /// functionality, and mouse support.
  18. /// </remarks>
  19. public class TextField : View {
  20. List<Rune> text;
  21. int first, point;
  22. bool used;
  23. /// <summary>
  24. /// 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
  25. /// </summary>
  26. public bool Used { get => used; set { used = value; } }
  27. /// <summary>
  28. /// Changed event, raised when the text has clicked.
  29. /// </summary>
  30. /// <remarks>
  31. /// Client code can hook up to this event, it is
  32. /// raised when the text in the entry changes.
  33. /// </remarks>
  34. public event EventHandler<ustring> Changed;
  35. /// <summary>
  36. /// Public constructor that creates a text field, with layout controlled with X, Y, Width and Height.
  37. /// </summary>
  38. /// <param name="text">Initial text contents.</param>
  39. public TextField (string text) : this (ustring.Make (text))
  40. {
  41. Height = 1;
  42. }
  43. /// <summary>
  44. /// Public constructor that creates a text field, with layout controlled with X, Y, Width and Height.
  45. /// </summary>
  46. /// <param name="text">Initial text contents.</param>
  47. public TextField (ustring text)
  48. {
  49. Initialize (text, Frame.Width);
  50. }
  51. /// <summary>
  52. /// Public constructor that creates a text field at an absolute position and size.
  53. /// </summary>
  54. /// <param name="x">The x coordinate.</param>
  55. /// <param name="y">The y coordinate.</param>
  56. /// <param name="w">The width.</param>
  57. /// <param name="text">Initial text contents.</param>
  58. public TextField (int x, int y, int w, ustring text) : base (new Rect (x, y, w, 1))
  59. {
  60. Initialize (text, w);
  61. }
  62. void Initialize (ustring text, int w)
  63. {
  64. if (text == null)
  65. text = "";
  66. this.text = TextModel.ToRunes (text);
  67. point = text.Length;
  68. first = point > w ? point - w : 0;
  69. CanFocus = true;
  70. Used = true;
  71. WantMousePositionReports = true;
  72. OnLeave += TextField_OnLeave;
  73. }
  74. void TextField_OnLeave (object sender, EventArgs e)
  75. {
  76. if (Application.mouseGrabView != null && Application.mouseGrabView == this)
  77. Application.UngrabMouse ();
  78. if (SelectedLength != 0 && !(Application.mouseGrabView is MenuBar))
  79. ClearAllSelection ();
  80. }
  81. public override Rect Frame {
  82. get => base.Frame;
  83. set {
  84. base.Frame = value;
  85. var w = base.Frame.Width;
  86. first = point > w ? point - w : 0;
  87. }
  88. }
  89. /// <summary>
  90. /// Sets or gets the text in the entry.
  91. /// </summary>
  92. /// <remarks>
  93. /// </remarks>
  94. public ustring Text {
  95. get {
  96. return ustring.Make (text);
  97. }
  98. set {
  99. ustring oldText = ustring.Make (text);
  100. text = TextModel.ToRunes (value);
  101. Changed?.Invoke (this, oldText);
  102. if (point > text.Count)
  103. point = Math.Max (text.Count-1, 0);
  104. // FIXME: this needs to be updated to use Rune.ColumnWidth
  105. first = point > Frame.Width ? point - Frame.Width : 0;
  106. SetNeedsDisplay ();
  107. }
  108. }
  109. /// <summary>
  110. /// Sets the secret property.
  111. /// </summary>
  112. /// <remarks>
  113. /// This makes the text entry suitable for entering passwords.
  114. /// </remarks>
  115. public bool Secret { get; set; }
  116. /// <summary>
  117. /// Sets or gets the current cursor position.
  118. /// </summary>
  119. public int CursorPosition {
  120. get { return point; }
  121. set {
  122. point = value;
  123. Adjust ();
  124. SetNeedsDisplay ();
  125. }
  126. }
  127. /// <summary>
  128. /// Sets the cursor position.
  129. /// </summary>
  130. public override void PositionCursor ()
  131. {
  132. var col = 0;
  133. for (int idx = first < 0 ? 0 : first; idx < text.Count; idx++) {
  134. if (idx == point)
  135. break;
  136. var cols = Rune.ColumnWidth (text [idx]);
  137. col += cols;
  138. }
  139. Move (col, 0);
  140. }
  141. public override void Redraw (Rect region)
  142. {
  143. ColorScheme color = Colors.Menu;
  144. SetSelectedStartSelectedLength ();
  145. Driver.SetAttribute (ColorScheme.Focus);
  146. Move (0, 0);
  147. int p = first;
  148. int col = 0;
  149. int width = Frame.Width;
  150. var tcount = text.Count;
  151. for (int idx = 0; idx < tcount; idx++){
  152. var rune = text [idx];
  153. if (idx < first)
  154. continue;
  155. var cols = Rune.ColumnWidth (rune);
  156. if (col == point && HasFocus && !Used && SelectedLength == 0)
  157. Driver.SetAttribute (Colors.Menu.HotFocus);
  158. else
  159. Driver.SetAttribute (idx >= start && length > 0 && idx < start + length ? color.Focus : ColorScheme.Focus);
  160. if (col + cols < width)
  161. Driver.AddRune ((Rune)(Secret ? '*' : rune));
  162. col += cols;
  163. }
  164. Driver.SetAttribute (ColorScheme.Focus);
  165. for (int i = col; i < Frame.Width; i++)
  166. Driver.AddRune (' ');
  167. PositionCursor ();
  168. }
  169. // Returns the size of the string starting at position start
  170. int DisplaySize (List<Rune> t, int start)
  171. {
  172. int size = 0;
  173. int tcount = t.Count;
  174. for (int i = start; i < tcount; i++) {
  175. var rune = t [i];
  176. size += Rune.ColumnWidth (rune);
  177. }
  178. return size;
  179. }
  180. void Adjust ()
  181. {
  182. if (point < first)
  183. first = point;
  184. else if (first + point >= Frame.Width) {
  185. first = point - (Frame.Width - 1);
  186. }
  187. SetNeedsDisplay ();
  188. }
  189. void SetText (List<Rune> newText)
  190. {
  191. Text = ustring.Make (newText);
  192. }
  193. void SetText (IEnumerable<Rune> newText)
  194. {
  195. SetText (newText.ToList ());
  196. }
  197. public override bool CanFocus {
  198. get => true;
  199. set { base.CanFocus = value; }
  200. }
  201. void SetClipboard (IEnumerable<Rune> text)
  202. {
  203. if (!Secret)
  204. Clipboard.Contents = ustring.Make (text.ToList ());
  205. }
  206. public override bool ProcessKey (KeyEvent kb)
  207. {
  208. // remember current cursor position
  209. // because the new calculated cursor position is needed to be set BEFORE the change event is triggest
  210. // Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2
  211. var oldCursorPos = point;
  212. switch (kb.Key) {
  213. case Key.DeleteChar:
  214. case Key.ControlD:
  215. if (SelectedLength == 0) {
  216. if (text.Count == 0 || text.Count == point)
  217. return true;
  218. SetText (text.GetRange (0, point).Concat (text.GetRange (point + 1, text.Count - (point + 1))));
  219. Adjust ();
  220. } else {
  221. DeleteSelectedText ();
  222. }
  223. break;
  224. case Key.Delete:
  225. case Key.Backspace:
  226. if (SelectedLength == 0) {
  227. if (point == 0)
  228. return true;
  229. point--;
  230. SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - (oldCursorPos))));
  231. Adjust ();
  232. } else {
  233. DeleteSelectedText ();
  234. }
  235. break;
  236. // Home, C-A
  237. case Key.Home:
  238. case Key.ControlA:
  239. ClearAllSelection ();
  240. point = 0;
  241. Adjust ();
  242. break;
  243. case Key.CursorLeft:
  244. case Key.ControlB:
  245. ClearAllSelection ();
  246. if (point > 0) {
  247. point--;
  248. Adjust ();
  249. }
  250. break;
  251. case Key.End:
  252. case Key.ControlE: // End
  253. ClearAllSelection ();
  254. point = text.Count;
  255. Adjust ();
  256. break;
  257. case Key.CursorRight:
  258. case Key.ControlF:
  259. ClearAllSelection ();
  260. if (point == text.Count)
  261. break;
  262. point++;
  263. Adjust ();
  264. break;
  265. case Key.ControlK: // kill-to-end
  266. ClearAllSelection ();
  267. if (point >= text.Count)
  268. return true;
  269. SetClipboard (text.GetRange (point, text.Count - point));
  270. SetText (text.GetRange (0, point));
  271. Adjust ();
  272. break;
  273. case Key.ControlY: // Control-y, yank
  274. if (Clipboard.Contents == null)
  275. return true;
  276. var clip = TextModel.ToRunes (Clipboard.Contents);
  277. if (clip == null)
  278. return true;
  279. if (point == text.Count) {
  280. point = text.Count;
  281. SetText(text.Concat(clip).ToList());
  282. } else {
  283. point += clip.Count;
  284. SetText(text.GetRange(0, oldCursorPos).Concat(clip).Concat(text.GetRange(oldCursorPos, text.Count - oldCursorPos)));
  285. }
  286. Adjust ();
  287. break;
  288. case (Key)((int)'b' + Key.AltMask):
  289. ClearAllSelection ();
  290. int bw = WordBackward (point);
  291. if (bw != -1)
  292. point = bw;
  293. Adjust ();
  294. break;
  295. case (Key)((int)'f' + Key.AltMask):
  296. ClearAllSelection ();
  297. int fw = WordForward (point);
  298. if (fw != -1)
  299. point = fw;
  300. Adjust ();
  301. break;
  302. case Key.AltMask | Key.ControlI:
  303. Used = !Used;
  304. SetNeedsDisplay ();
  305. break;
  306. case Key.AltMask | Key.ControlC:
  307. Copy ();
  308. break;
  309. case Key.AltMask | Key.ControlX:
  310. Cut ();
  311. break;
  312. case Key.AltMask | Key.ControlV:
  313. Paste ();
  314. break;
  315. // MISSING:
  316. // Alt-D, Alt-backspace
  317. // Alt-Y
  318. // Delete adding to kill buffer
  319. default:
  320. // Ignore other control characters.
  321. if (kb.Key < Key.Space || kb.Key > Key.CharMask)
  322. return false;
  323. if (SelectedLength != 0) {
  324. DeleteSelectedText ();
  325. oldCursorPos = point;
  326. }
  327. var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key));
  328. if (used) {
  329. point++;
  330. if (point == text.Count) {
  331. SetText (text.Concat (kbstr).ToList ());
  332. } else {
  333. SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.Count))));
  334. }
  335. } else {
  336. SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (Math.Min (oldCursorPos + 1, text.Count), Math.Max (text.Count - oldCursorPos - 1, 0))));
  337. point++;
  338. }
  339. Adjust ();
  340. return true;
  341. }
  342. return true;
  343. }
  344. int WordForward (int p)
  345. {
  346. if (p >= text.Count)
  347. return -1;
  348. int i = p;
  349. if (Rune.IsPunctuation (text [p]) || Rune.IsWhiteSpace(text [p])) {
  350. for (; i < text.Count; i++) {
  351. var r = text [i];
  352. if (Rune.IsLetterOrDigit(r))
  353. break;
  354. }
  355. for (; i < text.Count; i++) {
  356. var r = text [i];
  357. if (!Rune.IsLetterOrDigit (r))
  358. break;
  359. }
  360. } else {
  361. for (; i < text.Count; i++) {
  362. var r = text [i];
  363. if (!Rune.IsLetterOrDigit (r))
  364. break;
  365. }
  366. }
  367. if (i != p)
  368. return i;
  369. return -1;
  370. }
  371. int WordBackward (int p)
  372. {
  373. if (p == 0)
  374. return -1;
  375. int i = p - 1;
  376. if (i == 0)
  377. return 0;
  378. var ti = text [i];
  379. if (Rune.IsPunctuation (ti) || Rune.IsSymbol(ti) || Rune.IsWhiteSpace(ti)) {
  380. for (; i >= 0; i--) {
  381. if (Rune.IsLetterOrDigit (text [i]))
  382. break;
  383. }
  384. for (; i >= 0; i--) {
  385. if (!Rune.IsLetterOrDigit (text [i]))
  386. break;
  387. }
  388. } else {
  389. for (; i >= 0; i--) {
  390. if (!Rune.IsLetterOrDigit (text [i]))
  391. break;
  392. }
  393. }
  394. i++;
  395. if (i != p)
  396. return i;
  397. return -1;
  398. }
  399. /// <summary>
  400. /// Start position of the selected text.
  401. /// </summary>
  402. public int SelectedStart { get; set; } = -1;
  403. /// <summary>
  404. /// Length of the selected text.
  405. /// </summary>
  406. public int SelectedLength { get; set; } = 0;
  407. /// <summary>
  408. /// The selected text.
  409. /// </summary>
  410. public ustring SelectedText { get; set; }
  411. int start, length;
  412. bool isButtonReleased = true;
  413. public override bool MouseEvent (MouseEvent ev)
  414. {
  415. if (!ev.Flags.HasFlag (MouseFlags.Button1Pressed) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) &&
  416. !ev.Flags.HasFlag (MouseFlags.Button1Released))
  417. return false;
  418. if (ev.Flags == MouseFlags.Button1Pressed) {
  419. if (!HasFocus)
  420. SuperView.SetFocus (this);
  421. PositionCursor (ev);
  422. if (isButtonReleased)
  423. ClearAllSelection ();
  424. isButtonReleased = true;
  425. } else if (ev.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
  426. int x = PositionCursor (ev);
  427. isButtonReleased = false;
  428. PrepareSelection (x);
  429. if (Application.mouseGrabView == null) {
  430. Application.GrabMouse (this);
  431. }
  432. } else if (ev.Flags == MouseFlags.Button1Pressed) {
  433. int x = PositionCursor (ev);
  434. if (SelectedLength != 0)
  435. ClearAllSelection ();
  436. } else if (ev.Flags == MouseFlags.Button1Released) {
  437. isButtonReleased = true;
  438. Application.UngrabMouse ();
  439. }
  440. SetNeedsDisplay ();
  441. return true;
  442. }
  443. int PositionCursor (MouseEvent ev)
  444. {
  445. // We could also set the cursor position.
  446. int x;
  447. if (Application.mouseGrabView == null) {
  448. x = ev.X;
  449. } else {
  450. x = ev.X;// - (text.Count > Frame.Width ? text.Count - Frame.Width : 0);
  451. }
  452. point = first + x;
  453. if (point > text.Count)
  454. point = text.Count;
  455. if (point < first)
  456. point = 0;
  457. return x;
  458. }
  459. void PrepareSelection (int x)
  460. {
  461. x = x + first < 0 ? 0 : x + first;
  462. SelectedStart = SelectedStart == -1 && text.Count > 0 && x >= 0 && x <= text.Count ? x : SelectedStart;
  463. if (SelectedStart > -1) {
  464. SelectedLength = x <= text.Count ? x - SelectedStart : text.Count - SelectedStart;
  465. SetSelectedStartSelectedLength ();
  466. SelectedText = length > 0 ? ustring.Make (text).ToString ().Substring (
  467. start < 0 ? 0 : start, length > text.Count ? text.Count : length) : "";
  468. }
  469. Adjust ();
  470. }
  471. /// <summary>
  472. /// Clear the selected text.
  473. /// </summary>
  474. public void ClearAllSelection ()
  475. {
  476. if (SelectedLength == 0)
  477. return;
  478. SelectedStart = -1;
  479. SelectedLength = 0;
  480. SelectedText = "";
  481. start = 0;
  482. }
  483. void SetSelectedStartSelectedLength ()
  484. {
  485. if (SelectedLength < 0) {
  486. start = SelectedLength + SelectedStart;
  487. length = Math.Abs (SelectedLength);
  488. } else {
  489. start = SelectedStart;
  490. length = SelectedLength;
  491. }
  492. }
  493. /// <summary>
  494. /// Copy the selected text to the clipboard.
  495. /// </summary>
  496. public void Copy ()
  497. {
  498. if (SelectedLength != 0) {
  499. Clipboard.Contents = SelectedText;
  500. }
  501. }
  502. /// <summary>
  503. /// Cut the selected text to the clipboard.
  504. /// </summary>
  505. public void Cut ()
  506. {
  507. if (SelectedLength != 0) {
  508. Clipboard.Contents = SelectedText;
  509. DeleteSelectedText ();
  510. }
  511. }
  512. void DeleteSelectedText ()
  513. {
  514. string actualText = Text.ToString ();
  515. int selStart = SelectedLength < 0 ? SelectedLength + SelectedStart : SelectedStart;
  516. int selLength = Math.Abs (SelectedLength);
  517. Text = actualText.Substring (0, selStart) +
  518. actualText.Substring (selStart + selLength, actualText.Length - selStart - selLength);
  519. ClearAllSelection ();
  520. CursorPosition = selStart >= Text.Length ? Text.Length : selStart;
  521. SetNeedsDisplay ();
  522. }
  523. /// <summary>
  524. /// Paste the selected text from the clipboard.
  525. /// </summary>
  526. public void Paste ()
  527. {
  528. string actualText = Text.ToString ();
  529. int start = SelectedStart == -1 ? CursorPosition : SelectedStart;
  530. Text = actualText.Substring (0, start) +
  531. Clipboard.Contents?.ToString () +
  532. actualText.Substring (start + SelectedLength, actualText.Length - start - SelectedLength);
  533. SelectedLength = 0;
  534. SetNeedsDisplay ();
  535. }
  536. }
  537. }