UICatalog.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. using NStack;
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Globalization;
  7. using System.Linq;
  8. using System.Reflection;
  9. using System.Runtime.InteropServices;
  10. using System.Text;
  11. using Terminal.Gui;
  12. using Rune = System.Rune;
  13. /// <remarks>
  14. /// <para>
  15. /// UI Catalog attempts to satisfy the following goals:
  16. /// </para>
  17. /// <para>
  18. /// <list type="number">
  19. /// <item>
  20. /// <description>
  21. /// Be an easy to use showcase for Terminal.Gui concepts and features.
  22. /// </description>
  23. /// </item>
  24. /// <item>
  25. /// <description>
  26. /// Provide sample code that illustrates how to properly implement said concepts & features.
  27. /// </description>
  28. /// </item>
  29. /// <item>
  30. /// <description>
  31. /// Make it easy for contributors to add additional samples in a structured way.
  32. /// </description>
  33. /// </item>
  34. /// </list>
  35. /// </para>
  36. /// <para>
  37. /// See the project README for more details (https://github.com/migueldeicaza/gui.cs/tree/master/UICatalog/README.md).
  38. /// </para>
  39. /// </remarks>
  40. namespace UICatalog {
  41. /// <summary>
  42. /// UI Catalog is a comprehensive sample app and scenario library for <see cref="Terminal.Gui"/>
  43. /// </summary>
  44. public class UICatalogApp {
  45. private static Toplevel _top;
  46. private static MenuBar _menu;
  47. private static int _nameColumnWidth;
  48. private static FrameView _leftPane;
  49. private static List<string> _categories;
  50. private static ListView _categoryListView;
  51. private static FrameView _rightPane;
  52. private static List<Type> _scenarios;
  53. private static ListView _scenarioListView;
  54. private static StatusBar _statusBar;
  55. private static StatusItem _capslock;
  56. private static StatusItem _numlock;
  57. private static StatusItem _scrolllock;
  58. private static int _categoryListViewItem;
  59. private static int _scenarioListViewItem;
  60. private static Scenario _runningScenario = null;
  61. private static bool _useSystemConsole = false;
  62. static void Main (string [] args)
  63. {
  64. Console.OutputEncoding = Encoding.Default;
  65. if (Debugger.IsAttached)
  66. CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
  67. _scenarios = Scenario.GetDerivedClasses<Scenario> ().OrderBy (t => Scenario.ScenarioMetadata.GetName (t)).ToList ();
  68. if (args.Length > 0) {
  69. var item = _scenarios.FindIndex (t => Scenario.ScenarioMetadata.GetName (t).Equals (args [0], StringComparison.OrdinalIgnoreCase));
  70. _runningScenario = (Scenario)Activator.CreateInstance (_scenarios [item]);
  71. Application.Init ();
  72. _runningScenario.Init (Application.Top, _baseColorScheme);
  73. _runningScenario.Setup ();
  74. _runningScenario.Run ();
  75. _runningScenario = null;
  76. return;
  77. }
  78. Scenario scenario;
  79. while ((scenario = GetScenarioToRun ()) != null) {
  80. #if DEBUG_IDISPOSABLE
  81. // Validate there are no outstanding Responder-based instances
  82. // after a sceanario was selected to run. This proves the main UI Catalog
  83. // 'app' closed cleanly.
  84. foreach (var inst in Responder.Instances) {
  85. Debug.Assert (inst.WasDisposed);
  86. }
  87. Responder.Instances.Clear ();
  88. #endif
  89. Application.UseSystemConsole = _useSystemConsole;
  90. scenario.Init (Application.Top, _baseColorScheme);
  91. scenario.Setup ();
  92. scenario.Run ();
  93. static void LoadedHandler ()
  94. {
  95. _rightPane.SetFocus ();
  96. _top.Loaded -= LoadedHandler;
  97. }
  98. _top.Loaded += LoadedHandler;
  99. #if DEBUG_IDISPOSABLE
  100. // After the scenario runs, validate all Responder-based instances
  101. // were disposed. This proves the scenario 'app' closed cleanly.
  102. foreach (var inst in Responder.Instances) {
  103. Debug.Assert (inst.WasDisposed);
  104. }
  105. Responder.Instances.Clear ();
  106. #endif
  107. }
  108. Application.Shutdown ();
  109. #if DEBUG_IDISPOSABLE
  110. // This proves that when the user exited the UI Catalog app
  111. // it cleaned up properly.
  112. foreach (var inst in Responder.Instances) {
  113. Debug.Assert (inst.WasDisposed);
  114. }
  115. Responder.Instances.Clear ();
  116. #endif
  117. }
  118. /// <summary>
  119. /// This shows the selection UI. Each time it is run, it calls Application.Init to reset everything.
  120. /// </summary>
  121. /// <returns></returns>
  122. private static Scenario GetScenarioToRun ()
  123. {
  124. Application.UseSystemConsole = false;
  125. Application.Init ();
  126. // Set this here because not initialized until driver is loaded
  127. _baseColorScheme = Colors.Base;
  128. StringBuilder aboutMessage = new StringBuilder ();
  129. aboutMessage.AppendLine ("UI Catalog is a comprehensive sample library for Terminal.Gui");
  130. aboutMessage.AppendLine (@" _ ");
  131. aboutMessage.AppendLine (@" __ _ _ _(_) ___ ___ ");
  132. aboutMessage.AppendLine (@" / _` | | | | | / __/ __|");
  133. aboutMessage.AppendLine (@"| (_| | |_| | || (__\__ \");
  134. aboutMessage.AppendLine (@" \__, |\__,_|_(_)___|___/");
  135. aboutMessage.AppendLine (@" |___/ ");
  136. aboutMessage.AppendLine ("");
  137. aboutMessage.AppendLine ($"Version: {typeof (UICatalogApp).Assembly.GetName ().Version}");
  138. aboutMessage.AppendLine ($"Using Terminal.Gui Version: {typeof (Terminal.Gui.Application).Assembly.GetName ().Version}");
  139. aboutMessage.AppendLine ("");
  140. _menu = new MenuBar (new MenuBarItem [] {
  141. new MenuBarItem ("_File", new MenuItem [] {
  142. new MenuItem ("_Quit", "", () => Application.RequestStop(), null, null, Key.Q | Key.CtrlMask)
  143. }),
  144. new MenuBarItem ("_Color Scheme", CreateColorSchemeMenuItems()),
  145. new MenuBarItem ("Diag_ostics", CreateDiagnosticMenuItems()),
  146. new MenuBarItem ("_Help", new MenuItem [] {
  147. new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://migueldeicaza.github.io/gui.cs/articles/overview.html"), null, null, Key.F1),
  148. new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/migueldeicaza/gui.cs"), null, null, Key.F2),
  149. new MenuItem ("_About...", "About this app", () => MessageBox.Query ("About UI Catalog", aboutMessage.ToString(), "_Ok"), null, null, Key.CtrlMask | Key.A),
  150. })
  151. });
  152. _leftPane = new FrameView ("Categories") {
  153. X = 0,
  154. Y = 1, // for menu
  155. Width = 25,
  156. Height = Dim.Fill (1),
  157. CanFocus = false,
  158. Shortcut = Key.CtrlMask | Key.C
  159. };
  160. _leftPane.Title = $"{_leftPane.Title} ({_leftPane.ShortcutTag})";
  161. _leftPane.ShortcutAction = () => _leftPane.SetFocus ();
  162. _categories = Scenario.GetAllCategories ().OrderBy (c => c).ToList ();
  163. _categoryListView = new ListView (_categories) {
  164. X = 0,
  165. Y = 0,
  166. Width = Dim.Fill (0),
  167. Height = Dim.Fill (0),
  168. AllowsMarking = false,
  169. CanFocus = true,
  170. };
  171. _categoryListView.OpenSelectedItem += (a) => {
  172. _rightPane.SetFocus ();
  173. };
  174. _categoryListView.SelectedItemChanged += CategoryListView_SelectedChanged;
  175. _leftPane.Add (_categoryListView);
  176. _rightPane = new FrameView ("Scenarios") {
  177. X = 25,
  178. Y = 1, // for menu
  179. Width = Dim.Fill (),
  180. Height = Dim.Fill (1),
  181. CanFocus = true,
  182. Shortcut = Key.CtrlMask | Key.S
  183. };
  184. _rightPane.Title = $"{_rightPane.Title} ({_rightPane.ShortcutTag})";
  185. _rightPane.ShortcutAction = () => _rightPane.SetFocus ();
  186. _nameColumnWidth = Scenario.ScenarioMetadata.GetName (_scenarios.OrderByDescending (t => Scenario.ScenarioMetadata.GetName (t).Length).FirstOrDefault ()).Length;
  187. _scenarioListView = new ListView () {
  188. X = 0,
  189. Y = 0,
  190. Width = Dim.Fill (0),
  191. Height = Dim.Fill (0),
  192. AllowsMarking = false,
  193. CanFocus = true,
  194. };
  195. _scenarioListView.OpenSelectedItem += _scenarioListView_OpenSelectedItem;
  196. _rightPane.Add (_scenarioListView);
  197. _categoryListView.SelectedItem = _categoryListViewItem;
  198. _categoryListView.OnSelectedChanged ();
  199. _capslock = new StatusItem (Key.CharMask, "Caps", null);
  200. _numlock = new StatusItem (Key.CharMask, "Num", null);
  201. _scrolllock = new StatusItem (Key.CharMask, "Scroll", null);
  202. _statusBar = new StatusBar () {
  203. Visible = true,
  204. };
  205. _statusBar.Items = new StatusItem [] {
  206. _capslock,
  207. _numlock,
  208. _scrolllock,
  209. new StatusItem(Key.Q | Key.CtrlMask, "~CTRL-Q~ Quit", () => {
  210. if (_runningScenario is null){
  211. // This causes GetScenarioToRun to return null
  212. _runningScenario = null;
  213. Application.RequestStop();
  214. } else {
  215. _runningScenario.RequestStop();
  216. }
  217. }),
  218. new StatusItem(Key.F10, "~F10~ Hide/Show Status Bar", () => {
  219. _statusBar.Visible = !_statusBar.Visible;
  220. _leftPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0);
  221. _rightPane.Height = Dim.Fill(_statusBar.Visible ? 1 : 0);
  222. _top.LayoutSubviews();
  223. _top.ChildNeedsDisplay();
  224. }),
  225. };
  226. SetColorScheme ();
  227. _top = Application.Top;
  228. _top.KeyDown += KeyDownHandler;
  229. _top.Add (_menu);
  230. _top.Add (_leftPane);
  231. _top.Add (_rightPane);
  232. _top.Add (_statusBar);
  233. _top.Loaded += () => {
  234. if (_runningScenario != null) {
  235. _runningScenario = null;
  236. }
  237. };
  238. Application.Run (_top);
  239. return _runningScenario;
  240. }
  241. static MenuItem [] CreateDiagnosticMenuItems ()
  242. {
  243. var index = 0;
  244. MenuItem CheckedMenuMenuItem (ustring menuItem, Action action, Func<bool> checkFunction)
  245. {
  246. var mi = new MenuItem ();
  247. mi.Title = menuItem;
  248. mi.Shortcut = Key.AltMask + index.ToString () [0];
  249. index++;
  250. mi.CheckType |= MenuItemCheckStyle.Checked;
  251. mi.Checked = checkFunction ();
  252. mi.Action = () => {
  253. action?.Invoke ();
  254. mi.Title = menuItem;
  255. mi.Checked = checkFunction ();
  256. };
  257. return mi;
  258. }
  259. return new MenuItem [] {
  260. CheckedMenuMenuItem ("Use _System Console",
  261. () => {
  262. _useSystemConsole = !_useSystemConsole;
  263. },
  264. () => _useSystemConsole),
  265. CheckedMenuMenuItem ("Diagnostics: _Frame Padding",
  266. () => {
  267. ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding;
  268. _top.SetNeedsDisplay ();
  269. },
  270. () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding),
  271. CheckedMenuMenuItem ("Diagnostics: Frame _Ruler",
  272. () => {
  273. ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler;
  274. _top.SetNeedsDisplay ();
  275. },
  276. () => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler),
  277. };
  278. }
  279. static void SetColorScheme ()
  280. {
  281. _leftPane.ColorScheme = _baseColorScheme;
  282. _rightPane.ColorScheme = _baseColorScheme;
  283. _top?.SetNeedsDisplay ();
  284. }
  285. static ColorScheme _baseColorScheme;
  286. static MenuItem [] CreateColorSchemeMenuItems ()
  287. {
  288. List<MenuItem> menuItems = new List<MenuItem> ();
  289. foreach (var sc in Colors.ColorSchemes) {
  290. var item = new MenuItem ();
  291. item.Title = $"_{sc.Key}";
  292. item.Shortcut = Key.AltMask | (Key)sc.Key.Substring (0, 1) [0];
  293. item.CheckType |= MenuItemCheckStyle.Radio;
  294. item.Checked = sc.Value == _baseColorScheme;
  295. item.Action += () => {
  296. _baseColorScheme = sc.Value;
  297. SetColorScheme ();
  298. foreach (var menuItem in menuItems) {
  299. menuItem.Checked = menuItem.Title.Equals ($"_{sc.Key}") && sc.Value == _baseColorScheme;
  300. }
  301. };
  302. menuItems.Add (item);
  303. }
  304. return menuItems.ToArray ();
  305. }
  306. private static void _scenarioListView_OpenSelectedItem (EventArgs e)
  307. {
  308. if (_runningScenario is null) {
  309. _scenarioListViewItem = _scenarioListView.SelectedItem;
  310. var source = _scenarioListView.Source as ScenarioListDataSource;
  311. _runningScenario = (Scenario)Activator.CreateInstance (source.Scenarios [_scenarioListView.SelectedItem]);
  312. Application.RequestStop ();
  313. }
  314. }
  315. internal class ScenarioListDataSource : IListDataSource {
  316. public List<Type> Scenarios { get; set; }
  317. public bool IsMarked (int item) => false;
  318. public int Count => Scenarios.Count;
  319. public ScenarioListDataSource (List<Type> itemList) => Scenarios = itemList;
  320. public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width)
  321. {
  322. container.Move (col, line);
  323. // Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible
  324. var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item]));
  325. RenderUstr (driver, $"{s} {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width);
  326. }
  327. public void SetMark (int item, bool value)
  328. {
  329. }
  330. // A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
  331. private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width)
  332. {
  333. int used = 0;
  334. int index = 0;
  335. while (index < ustr.Length) {
  336. (var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length);
  337. var count = Rune.ColumnWidth (rune);
  338. if (used + count >= width) break;
  339. driver.AddRune (rune);
  340. used += count;
  341. index += size;
  342. }
  343. while (used < width) {
  344. driver.AddRune (' ');
  345. used++;
  346. }
  347. }
  348. public IList ToList ()
  349. {
  350. return Scenarios;
  351. }
  352. }
  353. /// <summary>
  354. /// When Scenarios are running we need to override the behavior of the Menu
  355. /// and Statusbar to enable Scenarios that use those (or related key input)
  356. /// to not be impacted. Same as for tabs.
  357. /// </summary>
  358. /// <param name="ke"></param>
  359. private static void KeyDownHandler (View.KeyEventEventArgs a)
  360. {
  361. //if (a.KeyEvent.Key == Key.Tab || a.KeyEvent.Key == Key.BackTab) {
  362. // // BUGBUG: Work around Issue #434 by implementing our own TAB navigation
  363. // if (_top.MostFocused == _categoryListView)
  364. // _top.SetFocus (_rightPane);
  365. // else
  366. // _top.SetFocus (_leftPane);
  367. //}
  368. if (a.KeyEvent.IsCapslock) {
  369. _capslock.Title = "Caps: On";
  370. _statusBar.SetNeedsDisplay ();
  371. } else {
  372. _capslock.Title = "Caps: Off";
  373. _statusBar.SetNeedsDisplay ();
  374. }
  375. if (a.KeyEvent.IsNumlock) {
  376. _numlock.Title = "Num: On";
  377. _statusBar.SetNeedsDisplay ();
  378. } else {
  379. _numlock.Title = "Num: Off";
  380. _statusBar.SetNeedsDisplay ();
  381. }
  382. if (a.KeyEvent.IsScrolllock) {
  383. _scrolllock.Title = "Scroll: On";
  384. _statusBar.SetNeedsDisplay ();
  385. } else {
  386. _scrolllock.Title = "Scroll: Off";
  387. _statusBar.SetNeedsDisplay ();
  388. }
  389. }
  390. private static void CategoryListView_SelectedChanged (ListViewItemEventArgs e)
  391. {
  392. if (_categoryListViewItem != _categoryListView.SelectedItem) {
  393. _scenarioListViewItem = 0;
  394. }
  395. _categoryListViewItem = _categoryListView.SelectedItem;
  396. var item = _categories [_categoryListView.SelectedItem];
  397. List<Type> newlist;
  398. if (item.Equals ("All")) {
  399. newlist = _scenarios;
  400. } else {
  401. newlist = _scenarios.Where (t => Scenario.ScenarioCategory.GetCategories (t).Contains (item)).ToList ();
  402. }
  403. _scenarioListView.Source = new ScenarioListDataSource (newlist);
  404. _scenarioListView.SelectedItem = _scenarioListViewItem;
  405. }
  406. private static void OpenUrl (string url)
  407. {
  408. try {
  409. Process.Start (url);
  410. } catch {
  411. // hack because of this: https://github.com/dotnet/corefx/issues/10361
  412. if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
  413. url = url.Replace ("&", "^&");
  414. Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true });
  415. } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
  416. Process.Start ("xdg-open", url);
  417. } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
  418. Process.Start ("open", url);
  419. } else {
  420. throw;
  421. }
  422. }
  423. }
  424. }
  425. }