CharacterMap.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Text;
  7. namespace UICatalog.Scenarios;
  8. /// <summary>
  9. /// This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
  10. /// "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
  11. /// Illustrates how to do infinite scrolling
  12. /// </summary>
  13. [ScenarioMetadata ("Character Map", "Unicode viewer. Demos infinite content drawing and scrolling.")]
  14. [ScenarioCategory ("Text and Formatting")]
  15. [ScenarioCategory ("Drawing")]
  16. [ScenarioCategory ("Controls")]
  17. [ScenarioCategory ("Layout")]
  18. [ScenarioCategory ("Scrolling")]
  19. public class CharacterMap : Scenario
  20. {
  21. private Label? _errorLabel;
  22. private TableView? _categoryList;
  23. private CharMap? _charMap;
  24. // Don't create a Window, just return the top-level view
  25. public override void Main ()
  26. {
  27. Application.Init ();
  28. var top = new Window
  29. {
  30. BorderStyle = LineStyle.None
  31. };
  32. _charMap = new ()
  33. {
  34. X = 0,
  35. Y = 1,
  36. Width = Dim.Fill (Dim.Func (() => _categoryList!.Frame.Width)),
  37. Height = Dim.Fill (),
  38. // SchemeName = "Base"
  39. };
  40. top.Add (_charMap);
  41. var jumpLabel = new Label
  42. {
  43. X = Pos.Right (_charMap) + 1,
  44. Y = Pos.Y (_charMap),
  45. HotKeySpecifier = (Rune)'_',
  46. Text = "_Jump To:",
  47. //SchemeName = "Dialog"
  48. };
  49. top.Add (jumpLabel);
  50. var jumpEdit = new TextField
  51. {
  52. X = Pos.Right (jumpLabel) + 1,
  53. Y = Pos.Y (_charMap),
  54. Width = 17,
  55. Caption = "e.g. 01BE3 or ✈",
  56. //SchemeName = "Dialog"
  57. };
  58. top.Add (jumpEdit);
  59. _charMap.SelectedCodePointChanged += (sender, args) =>
  60. {
  61. if (Rune.IsValid (args.Value))
  62. {
  63. jumpEdit.Text = ((Rune)args.Value).ToString ();
  64. }
  65. else
  66. {
  67. jumpEdit.Text = $"U+{args.Value:x5}";
  68. }
  69. };
  70. _errorLabel = new ()
  71. {
  72. X = Pos.Right (jumpEdit) + 1,
  73. Y = Pos.Y (_charMap),
  74. SchemeName = "error",
  75. Text = "err",
  76. Visible = false
  77. };
  78. top.Add (_errorLabel);
  79. jumpEdit.Accepting += JumpEditOnAccept;
  80. _categoryList = new () {
  81. X = Pos.Right (_charMap),
  82. Y = Pos.Bottom (jumpLabel),
  83. Height = Dim.Fill (),
  84. //SchemeName = "Dialog"
  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 MenuBarv2
  126. {
  127. Menus =
  128. [
  129. new (
  130. "_File",
  131. new MenuItemv2 []
  132. {
  133. new (
  134. "_Quit",
  135. $"{Application.QuitKey}",
  136. () => Application.RequestStop ()
  137. )
  138. }
  139. ),
  140. new (
  141. "_Options",
  142. new MenuItemv2 [] { 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.Handled = 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 MenuItemv2 CreateMenuShowWidth ()
  259. {
  260. CheckBox cb = new ()
  261. {
  262. Title = "_Show Glyph Width",
  263. CheckedState = _charMap!.ShowGlyphWidths ? CheckState.Checked : CheckState.None
  264. };
  265. var item = new MenuItemv2 { CommandView = cb };
  266. item.Action += () =>
  267. {
  268. if (_charMap is { })
  269. {
  270. _charMap.ShowGlyphWidths = cb.CheckedState == CheckState.Checked;
  271. }
  272. };
  273. return item;
  274. }
  275. public override List<Key> GetDemoKeyStrokes ()
  276. {
  277. List<Key> keys = new ();
  278. for (var i = 0; i < 200; i++)
  279. {
  280. keys.Add (Key.CursorDown);
  281. }
  282. // Category table
  283. keys.Add (Key.Tab.WithShift);
  284. // Block elements
  285. keys.Add (Key.B);
  286. keys.Add (Key.L);
  287. keys.Add (Key.Tab);
  288. for (var i = 0; i < 200; i++)
  289. {
  290. keys.Add (Key.CursorLeft);
  291. }
  292. return keys;
  293. }
  294. }