CharacterMap.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Text;
  7. using Terminal.Gui;
  8. namespace UICatalog.Scenarios;
  9. /// <summary>
  10. /// This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
  11. /// "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
  12. /// Illustrates how to do infinite scrolling
  13. /// </summary>
  14. /// <remarks>
  15. /// See <see href="CharacterMap/README.md"/>.
  16. /// </remarks>
  17. [ScenarioMetadata ("Character Map", "Unicode viewer. Demos infinite content drawing and scrolling.")]
  18. [ScenarioCategory ("Text and Formatting")]
  19. [ScenarioCategory ("Drawing")]
  20. [ScenarioCategory ("Controls")]
  21. [ScenarioCategory ("Layout")]
  22. [ScenarioCategory ("Scrolling")]
  23. public class CharacterMap : Scenario
  24. {
  25. private Label? _errorLabel;
  26. private TableView? _categoryList;
  27. private CharMap? _charMap;
  28. // Don't create a Window, just return the top-level view
  29. public override void Main ()
  30. {
  31. Application.Init ();
  32. var top = new Window
  33. {
  34. BorderStyle = LineStyle.None
  35. };
  36. _charMap = new ()
  37. {
  38. X = 0,
  39. Y = 1,
  40. Width = Dim.Fill (Dim.Func (() => _categoryList!.Frame.Width)),
  41. Height = Dim.Fill ()
  42. };
  43. top.Add (_charMap);
  44. var jumpLabel = new Label
  45. {
  46. X = Pos.Right (_charMap) + 1,
  47. Y = Pos.Y (_charMap),
  48. HotKeySpecifier = (Rune)'_',
  49. Text = "_Jump To:"
  50. };
  51. top.Add (jumpLabel);
  52. var jumpEdit = new TextField
  53. {
  54. X = Pos.Right (jumpLabel) + 1,
  55. Y = Pos.Y (_charMap),
  56. Width = 17,
  57. Caption = "e.g. 01BE3 or ✈",
  58. };
  59. top.Add (jumpEdit);
  60. _charMap.SelectedCodePointChanged += (sender, args) =>
  61. {
  62. if (Rune.IsValid (args.CurrentValue))
  63. {
  64. jumpEdit.Text = ((Rune)args.CurrentValue).ToString ();
  65. }
  66. else
  67. {
  68. jumpEdit.Text = $"U+{args.CurrentValue:x5}";
  69. }
  70. };
  71. _errorLabel = new ()
  72. {
  73. X = Pos.Right (jumpEdit) + 1,
  74. Y = Pos.Y (_charMap),
  75. ColorScheme = Colors.ColorSchemes ["error"],
  76. Text = "err",
  77. Visible = false
  78. };
  79. top.Add (_errorLabel);
  80. jumpEdit.Accepting += JumpEditOnAccept;
  81. _categoryList = new () {
  82. X = Pos.Right (_charMap),
  83. Y = Pos.Bottom (jumpLabel),
  84. Height = Dim.Fill (),
  85. };
  86. _categoryList.FullRowSelect = true;
  87. _categoryList.MultiSelect = false;
  88. _categoryList.Style.ShowVerticalCellLines = false;
  89. _categoryList.Style.AlwaysShowHeaders = true;
  90. var isDescending = false;
  91. _categoryList.Table = CreateCategoryTable (0, isDescending);
  92. // if user clicks the mouse in TableView
  93. _categoryList.MouseClick += (s, e) =>
  94. {
  95. _categoryList.ScreenToCell (e.Position, out int? clickedCol);
  96. if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked))
  97. {
  98. EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  99. string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
  100. isDescending = !isDescending;
  101. _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
  102. table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  103. _categoryList.SelectedRow = table.Data
  104. .Select ((item, index) => new { item, index })
  105. .FirstOrDefault (x => x.item.Category == prevSelection)
  106. ?.index
  107. ?? -1;
  108. }
  109. };
  110. int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
  111. _categoryList.Style.ColumnStyles.Add (
  112. 0,
  113. new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
  114. );
  115. _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
  116. _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
  117. _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
  118. _categoryList.SelectedCellChanged += (s, args) =>
  119. {
  120. EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
  121. _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
  122. jumpEdit.Text = $"U+{_charMap.SelectedCodePoint:x5}";
  123. };
  124. top.Add (_categoryList);
  125. var menu = new MenuBar
  126. {
  127. Menus =
  128. [
  129. new (
  130. "_File",
  131. new MenuItem []
  132. {
  133. new (
  134. "_Quit",
  135. $"{Application.QuitKey}",
  136. () => Application.RequestStop ()
  137. )
  138. }
  139. ),
  140. new (
  141. "_Options",
  142. new [] { CreateMenuShowWidth () }
  143. )
  144. ]
  145. };
  146. top.Add (menu);
  147. _charMap.SelectedCodePoint = 0;
  148. _charMap.SetFocus ();
  149. Application.Run (top);
  150. top.Dispose ();
  151. Application.Shutdown ();
  152. return;
  153. void JumpEditOnAccept (object? sender, CommandEventArgs e)
  154. {
  155. if (jumpEdit.Text.Length == 0)
  156. {
  157. return;
  158. }
  159. _errorLabel.Visible = true;
  160. uint result = 0;
  161. if (jumpEdit.Text.Length == 1)
  162. {
  163. result = (uint)jumpEdit.Text.ToRunes () [0].Value;
  164. }
  165. else if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
  166. {
  167. try
  168. {
  169. result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
  170. }
  171. catch (FormatException)
  172. {
  173. _errorLabel.Text = "Invalid hex value";
  174. return;
  175. }
  176. }
  177. else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
  178. {
  179. try
  180. {
  181. result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
  182. }
  183. catch (FormatException)
  184. {
  185. _errorLabel.Text = "Invalid hex value";
  186. return;
  187. }
  188. }
  189. else
  190. {
  191. try
  192. {
  193. result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
  194. }
  195. catch (FormatException)
  196. {
  197. _errorLabel.Text = "Invalid value";
  198. return;
  199. }
  200. }
  201. if (result > RuneExtensions.MaxUnicodeCodePoint)
  202. {
  203. _errorLabel.Text = "Beyond maximum codepoint";
  204. return;
  205. }
  206. _errorLabel.Visible = false;
  207. EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList!.Table;
  208. _categoryList.SelectedRow = table.Data
  209. .Select ((item, index) => new { item, index })
  210. .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
  211. ?.index
  212. ?? -1;
  213. _categoryList.EnsureSelectedCellIsVisible ();
  214. // Ensure the typed glyph is selected
  215. _charMap.SelectedCodePoint = (int)result;
  216. _charMap.SetFocus ();
  217. // Cancel the event to prevent ENTER from being handled elsewhere
  218. e.Cancel = true;
  219. }
  220. }
  221. private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
  222. {
  223. Func<UnicodeRange, object> orderBy;
  224. var categorySort = string.Empty;
  225. var startSort = string.Empty;
  226. var endSort = string.Empty;
  227. string sortIndicator = descending ? Glyphs.DownArrow.ToString () : Glyphs.UpArrow.ToString ();
  228. switch (sortByColumn)
  229. {
  230. case 0:
  231. orderBy = r => r.Category;
  232. categorySort = sortIndicator;
  233. break;
  234. case 1:
  235. orderBy = r => r.Start;
  236. startSort = sortIndicator;
  237. break;
  238. case 2:
  239. orderBy = r => r.End;
  240. endSort = sortIndicator;
  241. break;
  242. default:
  243. throw new ArgumentException ("Invalid column number.");
  244. }
  245. IOrderedEnumerable<UnicodeRange> sortedRanges = descending
  246. ? UnicodeRange.Ranges.OrderByDescending (orderBy)
  247. : UnicodeRange.Ranges.OrderBy (orderBy);
  248. return new (
  249. sortedRanges,
  250. new ()
  251. {
  252. { $"Category{categorySort}", s => s.Category },
  253. { $"Start{startSort}", s => $"{s.Start:x5}" },
  254. { $"End{endSort}", s => $"{s.End:x5}" }
  255. }
  256. );
  257. }
  258. private MenuItem CreateMenuShowWidth ()
  259. {
  260. var item = new MenuItem { Title = "_Show Glyph Width" };
  261. item.CheckType |= MenuItemCheckStyle.Checked;
  262. item.Checked = _charMap?.ShowGlyphWidths;
  263. item.Action += () =>
  264. {
  265. if (_charMap is { })
  266. {
  267. _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked)!;
  268. }
  269. };
  270. return item;
  271. }
  272. public override List<Key> GetDemoKeyStrokes ()
  273. {
  274. List<Key> keys = new ();
  275. for (var i = 0; i < 200; i++)
  276. {
  277. keys.Add (Key.CursorDown);
  278. }
  279. // Category table
  280. keys.Add (Key.Tab.WithShift);
  281. // Block elements
  282. keys.Add (Key.B);
  283. keys.Add (Key.L);
  284. keys.Add (Key.Tab);
  285. for (var i = 0; i < 200; i++)
  286. {
  287. keys.Add (Key.CursorLeft);
  288. }
  289. return keys;
  290. }
  291. }