UICatalog.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Runtime.InteropServices;
  8. using System.Text;
  9. using Terminal.Gui;
  10. using System.IO;
  11. using System.Reflection;
  12. using System.Threading;
  13. using static Terminal.Gui.ConfigurationManager;
  14. using System.Text.Json.Serialization;
  15. using static Terminal.Gui.TableView;
  16. #nullable enable
  17. /// <summary>
  18. /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios.
  19. /// </summary>
  20. /// <remarks>
  21. /// <para>
  22. /// UI Catalog attempts to satisfy the following goals:
  23. /// </para>
  24. /// <para>
  25. /// <list type="number">
  26. /// <item>
  27. /// <description>
  28. /// Be an easy to use showcase for Terminal.Gui concepts and features.
  29. /// </description>
  30. /// </item>
  31. /// <item>
  32. /// <description>
  33. /// Provide sample code that illustrates how to properly implement said concepts & features.
  34. /// </description>
  35. /// </item>
  36. /// <item>
  37. /// <description>
  38. /// Make it easy for contributors to add additional samples in a structured way.
  39. /// </description>
  40. /// </item>
  41. /// </list>
  42. /// </para>
  43. /// <para>
  44. /// See the project README for more details (https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog/README.md).
  45. /// </para>
  46. /// </remarks>
  47. namespace UICatalog {
  48. /// <summary>
  49. /// UI Catalog is a comprehensive sample app and scenario library for <see cref="Terminal.Gui"/>
  50. /// </summary>
  51. class UICatalogApp {
  52. //[SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true), JsonPropertyName ("UICatalog.StatusBar")]
  53. //public static bool ShowStatusBar { get; set; } = true;
  54. [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true), JsonPropertyName ("UICatalog.StatusBar")]
  55. public static bool ShowStatusBar { get; set; } = true;
  56. static readonly FileSystemWatcher _currentDirWatcher = new FileSystemWatcher ();
  57. static readonly FileSystemWatcher _homeDirWatcher = new FileSystemWatcher ();
  58. static void Main (string [] args)
  59. {
  60. Console.OutputEncoding = Encoding.Default;
  61. if (Debugger.IsAttached) {
  62. CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
  63. }
  64. _scenarios = Scenario.GetScenarios ();
  65. _categories = Scenario.GetAllCategories ();
  66. if (args.Length > 0 && args.Contains ("-usc")) {
  67. _useSystemConsole = true;
  68. args = args.Where (val => val != "-usc").ToArray ();
  69. }
  70. StartConfigFileWatcher ();
  71. // If a Scenario name has been provided on the commandline
  72. // run it and exit when done.
  73. if (args.Length > 0) {
  74. _topLevelColorScheme = "Base";
  75. var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase));
  76. _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ())!;
  77. Application.UseSystemConsole = _useSystemConsole;
  78. Application.Init ();
  79. _selectedScenario.Theme = _cachedTheme;
  80. _selectedScenario.TopLevelColorScheme = _topLevelColorScheme;
  81. _selectedScenario.Init ();
  82. _selectedScenario.Setup ();
  83. _selectedScenario.Run ();
  84. _selectedScenario.Dispose ();
  85. _selectedScenario = null;
  86. Application.Shutdown ();
  87. VerifyObjectsWereDisposed ();
  88. return;
  89. }
  90. _aboutMessage = new StringBuilder ();
  91. _aboutMessage.AppendLine (@"A comprehensive sample library for");
  92. _aboutMessage.AppendLine (@"");
  93. _aboutMessage.AppendLine (@" _______ _ _ _____ _ ");
  94. _aboutMessage.AppendLine (@" |__ __| (_) | | / ____| (_) ");
  95. _aboutMessage.AppendLine (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ ");
  96. _aboutMessage.AppendLine (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | ");
  97. _aboutMessage.AppendLine (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | ");
  98. _aboutMessage.AppendLine (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| ");
  99. _aboutMessage.AppendLine (@"");
  100. _aboutMessage.AppendLine (@"v2 - Work in Progress");
  101. _aboutMessage.AppendLine (@"");
  102. _aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui");
  103. Scenario scenario;
  104. while ((scenario = RunUICatalogTopLevel ()) != null) {
  105. VerifyObjectsWereDisposed ();
  106. ConfigurationManager.Themes!.Theme = _cachedTheme!;
  107. ConfigurationManager.Apply ();
  108. scenario.Theme = _cachedTheme;
  109. scenario.TopLevelColorScheme = _topLevelColorScheme;
  110. scenario.Init ();
  111. scenario.Setup ();
  112. scenario.Run ();
  113. scenario.Dispose ();
  114. // This call to Application.Shutdown brackets the Application.Init call
  115. // made by Scenario.Init() above
  116. Application.Shutdown ();
  117. VerifyObjectsWereDisposed ();
  118. }
  119. StopConfigFileWatcher ();
  120. VerifyObjectsWereDisposed ();
  121. }
  122. private static void StopConfigFileWatcher ()
  123. {
  124. _currentDirWatcher.EnableRaisingEvents = false;
  125. _currentDirWatcher.Changed -= ConfigFileChanged;
  126. _currentDirWatcher.Created -= ConfigFileChanged;
  127. _homeDirWatcher.EnableRaisingEvents = false;
  128. _homeDirWatcher.Changed -= ConfigFileChanged;
  129. _homeDirWatcher.Created -= ConfigFileChanged;
  130. }
  131. private static void StartConfigFileWatcher ()
  132. {
  133. // Setup a file system watcher for `./.tui/`
  134. _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
  135. var f = new FileInfo (Assembly.GetExecutingAssembly ().Location);
  136. var tuiDir = Path.Combine (f.Directory!.FullName, ".tui");
  137. if (!Directory.Exists (tuiDir)) {
  138. Directory.CreateDirectory (tuiDir);
  139. }
  140. _currentDirWatcher.Path = tuiDir;
  141. _currentDirWatcher.Filter = "*config.json";
  142. // Setup a file system watcher for `~/.tui/`
  143. _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
  144. f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
  145. tuiDir = Path.Combine (f.FullName, ".tui");
  146. if (!Directory.Exists (tuiDir)) {
  147. Directory.CreateDirectory (tuiDir);
  148. }
  149. _homeDirWatcher.Path = tuiDir;
  150. _homeDirWatcher.Filter = "*config.json";
  151. _currentDirWatcher.Changed += ConfigFileChanged;
  152. //_currentDirWatcher.Created += ConfigFileChanged;
  153. _currentDirWatcher.EnableRaisingEvents = true;
  154. _homeDirWatcher.Changed += ConfigFileChanged;
  155. //_homeDirWatcher.Created += ConfigFileChanged;
  156. _homeDirWatcher.EnableRaisingEvents = true;
  157. }
  158. private static void ConfigFileChanged (object sender, FileSystemEventArgs e)
  159. {
  160. if (Application.Top == null) {
  161. return;
  162. }
  163. // TOOD: THis is a hack. Figure out how to ensure that the file is fully written before reading it.
  164. Thread.Sleep (500);
  165. ConfigurationManager.Load ();
  166. ConfigurationManager.Apply ();
  167. }
  168. /// <summary>
  169. /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the
  170. /// UI Catalog main app UI is killed and the Scenario is run as though it were Application.Top.
  171. /// When the Scenario exits, this function exits.
  172. /// </summary>
  173. /// <returns></returns>
  174. static Scenario RunUICatalogTopLevel ()
  175. {
  176. Application.UseSystemConsole = _useSystemConsole;
  177. // Run UI Catalog UI. When it exits, if _selectedScenario is != null then
  178. // a Scenario was selected. Otherwise, the user wants to quit UI Catalog.
  179. Application.Init ();
  180. if (_cachedTheme is null) {
  181. _cachedTheme = ConfigurationManager.Themes?.Theme;
  182. } else {
  183. ConfigurationManager.Themes!.Theme = _cachedTheme;
  184. ConfigurationManager.Apply ();
  185. }
  186. //Application.EnableConsoleScrolling = _enableConsoleScrolling;
  187. Application.Run<UICatalogTopLevel> ();
  188. Application.Shutdown ();
  189. return _selectedScenario!;
  190. }
  191. static List<Scenario>? _scenarios;
  192. static List<string>? _categories;
  193. // When a scenario is run, the main app is killed. These items
  194. // are therefore cached so that when the scenario exits the
  195. // main app UI can be restored to previous state
  196. static int _cachedScenarioIndex = 0;
  197. static int _cachedCategoryIndex = 0;
  198. static string? _cachedTheme = string.Empty;
  199. static StringBuilder? _aboutMessage = null;
  200. // If set, holds the scenario the user selected
  201. static Scenario? _selectedScenario = null;
  202. static bool _useSystemConsole = false;
  203. static ConsoleDriver.DiagnosticFlags _diagnosticFlags;
  204. //static bool _enableConsoleScrolling = false;
  205. static bool _isFirstRunning = true;
  206. static string _topLevelColorScheme = string.Empty;
  207. static MenuItem []? _themeMenuItems;
  208. static MenuBarItem? _themeMenuBarItem;
  209. /// <summary>
  210. /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on
  211. /// the command line) and each time a Scenario ends.
  212. /// </summary>
  213. public class UICatalogTopLevel : Toplevel {
  214. public MenuItem? miIsMenuBorderDisabled;
  215. public MenuItem? miIsMouseDisabled;
  216. public MenuItem? miEnableConsoleScrolling;
  217. public ListView CategoryList;
  218. // UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how
  219. // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView
  220. // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app.
  221. public TableView ScenarioList;
  222. private CollectionNavigator _scenarioCollectionNav = new CollectionNavigator ();
  223. public StatusItem Capslock;
  224. public StatusItem Numlock;
  225. public StatusItem Scrolllock;
  226. public StatusItem DriverName;
  227. public StatusItem OS;
  228. public UICatalogTopLevel ()
  229. {
  230. _themeMenuItems = CreateThemeMenuItems ();
  231. _themeMenuBarItem = new MenuBarItem ("_Themes", _themeMenuItems);
  232. MenuBar = new MenuBar (new MenuBarItem [] {
  233. new MenuBarItem ("_File", new MenuItem [] {
  234. new MenuItem ("_Quit", "Quit UI Catalog", () => RequestStop(), null, null)
  235. }),
  236. _themeMenuBarItem,
  237. new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()),
  238. new MenuBarItem ("_Help", new MenuItem [] {
  239. new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/articles/overview.html"), null, null, Key.F1),
  240. new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2),
  241. new MenuItem ("_About...",
  242. "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage!.ToString(), 0, false, "_Ok"), null, null, Key.CtrlMask | Key.A),
  243. }),
  244. });
  245. Capslock = new StatusItem (Key.CharMask, "Caps", null);
  246. Numlock = new StatusItem (Key.CharMask, "Num", null);
  247. Scrolllock = new StatusItem (Key.CharMask, "Scroll", null);
  248. DriverName = new StatusItem (Key.CharMask, "Driver:", null);
  249. OS = new StatusItem (Key.CharMask, "OS:", null);
  250. StatusBar = new StatusBar () {
  251. Visible = UICatalogApp.ShowStatusBar
  252. };
  253. StatusBar.Items = new StatusItem [] {
  254. new StatusItem(Application.QuitKey, $"~{Application.QuitKey} to quit", () => {
  255. if (_selectedScenario is null){
  256. // This causes GetScenarioToRun to return null
  257. _selectedScenario = null;
  258. RequestStop();
  259. } else {
  260. _selectedScenario.RequestStop();
  261. }
  262. }),
  263. new StatusItem(Key.F10, "~F10~ Status Bar", () => {
  264. StatusBar.Visible = !StatusBar.Visible;
  265. //ContentPane!.Height = Dim.Fill(StatusBar.Visible ? 1 : 0);
  266. LayoutSubviews();
  267. SetSubViewNeedsDisplay();
  268. }),
  269. DriverName,
  270. OS
  271. };
  272. // Create the Category list view. This list never changes.
  273. CategoryList = new ListView (_categories) {
  274. X = 0,
  275. Y = 1,
  276. Width = Dim.Percent (30),
  277. Height = Dim.Fill (1),
  278. AllowsMarking = false,
  279. CanFocus = true,
  280. Title = "Categories",
  281. BorderStyle = LineStyle.Single,
  282. SuperViewRendersLineCanvas = true
  283. };
  284. CategoryList.OpenSelectedItem += (s, a) => {
  285. ScenarioList!.SetFocus ();
  286. };
  287. CategoryList.SelectedItemChanged += CategoryView_SelectedChanged;
  288. // Create the scenario list. The contents of the scenario list changes whenever the
  289. // Category list selection changes (to show just the scenarios that belong to the selected
  290. // category).
  291. ScenarioList = new TableView () {
  292. X = Pos.Right (CategoryList) - 1,
  293. Y = 1,
  294. Width = Dim.Fill (0),
  295. Height = Dim.Fill (1),
  296. //AllowsMarking = false,
  297. CanFocus = true,
  298. Title = "Scenarios",
  299. BorderStyle = LineStyle.Single,
  300. SuperViewRendersLineCanvas = true
  301. };
  302. // TableView provides many options for table headers. For simplicity we turn all
  303. // of these off. By enabling FullRowSelect and turning off headers, TableView looks just
  304. // like a ListView
  305. ScenarioList.FullRowSelect = true;
  306. ScenarioList.Style.ShowHeaders = false;
  307. ScenarioList.Style.ShowHorizontalHeaderOverline = false;
  308. ScenarioList.Style.ShowHorizontalHeaderUnderline = false;
  309. ScenarioList.Style.ShowHorizontalBottomline = false;
  310. ScenarioList.Style.ShowVerticalCellLines = false;
  311. ScenarioList.Style.ShowVerticalHeaderLines = false;
  312. /* By default TableView lays out columns at render time and only
  313. * measures y rows of data at a time. Where y is the height of the
  314. * console. This is for the following reasons:
  315. *
  316. * - Performance, when tables have a large amount of data
  317. * - Defensive, prevents a single wide cell value pushing other
  318. * columns off screen (requiring horizontal scrolling
  319. *
  320. * In the case of UICatalog here, such an approach is overkill so
  321. * we just measure all the data ourselves and set the appropriate
  322. * max widths as ColumnStyles
  323. */
  324. var longestName = _scenarios!.Max (s => s.GetName ().Length);
  325. ScenarioList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName });
  326. ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () { MaxWidth = 1 });
  327. // Enable user to find & select a scenario by typing text
  328. // TableView does not (currently) have built-in CollectionNavigator support (the ability for the
  329. // user to type and the items that match get selected). We implement it in the app instead.
  330. ScenarioList.KeyDown += (s, a) => {
  331. if (CollectionNavigator.IsCompatibleKey (a.KeyEvent)) {
  332. var newItem = _scenarioCollectionNav?.GetNextMatchingItem (ScenarioList.SelectedRow, (char)a.KeyEvent.KeyValue);
  333. if (newItem is int && newItem != -1) {
  334. ScenarioList.SelectedRow = (int)newItem;
  335. ScenarioList.EnsureSelectedCellIsVisible ();
  336. ScenarioList.SetNeedsDisplay ();
  337. a.Handled = true;
  338. }
  339. }
  340. };
  341. ScenarioList.CellActivated += ScenarioView_OpenSelectedItem;
  342. // TableView typically is a grid where nav keys are biased for moving left/right.
  343. ScenarioList.AddKeyBinding (Key.Home, Command.TopHome);
  344. ScenarioList.AddKeyBinding (Key.End, Command.BottomEnd);
  345. // Ideally, TableView.MultiSelect = false would turn off any keybindings for
  346. // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
  347. // a shortcut to About.
  348. ScenarioList.MultiSelect = false;
  349. ScenarioList.ClearKeyBinding (Key.CtrlMask | Key.A);
  350. KeyDown += KeyDownHandler;
  351. Add (CategoryList);
  352. Add (ScenarioList);
  353. Add (MenuBar);
  354. Add (StatusBar);
  355. Loaded += LoadedHandler;
  356. Unloaded += UnloadedHandler;
  357. // Restore previous selections
  358. CategoryList.SelectedItem = _cachedCategoryIndex;
  359. CategoryList.EnsureSelectedItemVisible ();
  360. ScenarioList.SelectedRow = _cachedScenarioIndex;
  361. ScenarioList.EnsureSelectedCellIsVisible ();
  362. ConfigurationManager.Applied += ConfigAppliedHandler;
  363. }
  364. void LoadedHandler (object? sender, EventArgs? args)
  365. {
  366. ConfigChanged ();
  367. miIsMouseDisabled!.Checked = Application.IsMouseDisabled;
  368. miEnableConsoleScrolling!.Checked = Application.EnableConsoleScrolling;
  369. DriverName.Title = $"Driver: {Driver.GetType ().Name}";
  370. OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}";
  371. if (_selectedScenario != null) {
  372. _selectedScenario = null;
  373. _isFirstRunning = false;
  374. }
  375. if (!_isFirstRunning) {
  376. ScenarioList.SetFocus ();
  377. }
  378. StatusBar.VisibleChanged += (s, e) => {
  379. UICatalogApp.ShowStatusBar = StatusBar.Visible;
  380. var height = (StatusBar.Visible ? 1 : 0);
  381. CategoryList.Height = Dim.Fill (height);
  382. ScenarioList.Height = Dim.Fill (height);
  383. // ContentPane.Height = Dim.Fill (height);
  384. LayoutSubviews ();
  385. SetSubViewNeedsDisplay ();
  386. };
  387. Loaded -= LoadedHandler;
  388. }
  389. private void UnloadedHandler (object? sender, EventArgs? args)
  390. {
  391. ConfigurationManager.Applied -= ConfigAppliedHandler;
  392. Unloaded -= UnloadedHandler;
  393. }
  394. void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a)
  395. {
  396. ConfigChanged ();
  397. }
  398. /// <summary>
  399. /// Launches the selected scenario, setting the global _selectedScenario
  400. /// </summary>
  401. /// <param name="e"></param>
  402. void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e)
  403. {
  404. if (_selectedScenario is null) {
  405. // Save selected item state
  406. _cachedCategoryIndex = CategoryList.SelectedItem;
  407. _cachedScenarioIndex = ScenarioList.SelectedRow;
  408. // Create new instance of scenario (even though Scenarios contains instances)
  409. string selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0];
  410. _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios!.FirstOrDefault (s => s.GetName () == selectedScenarioName)!.GetType ())!;
  411. // Tell the main app to stop
  412. Application.RequestStop ();
  413. }
  414. }
  415. List<MenuItem []> CreateDiagnosticMenuItems ()
  416. {
  417. List<MenuItem []> menuItems = new List<MenuItem []> {
  418. CreateDiagnosticFlagsMenuItems (),
  419. new MenuItem [] { },
  420. CreateEnableConsoleScrollingMenuItems (),
  421. CreateDisabledEnabledMouseItems (),
  422. CreateDisabledEnabledMenuBorder (),
  423. CreateKeybindingsMenuItems ()
  424. };
  425. return menuItems;
  426. }
  427. MenuItem [] CreateDisabledEnabledMenuBorder ()
  428. {
  429. List<MenuItem> menuItems = new List<MenuItem> ();
  430. miIsMenuBorderDisabled = new MenuItem {
  431. Title = "Disable _Menu Border"
  432. };
  433. miIsMenuBorderDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMenuBorderDisabled!.Title!.ToString ()!.Substring (1, 1) [0];
  434. miIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked;
  435. miIsMenuBorderDisabled.Action += () => {
  436. miIsMenuBorderDisabled.Checked = (bool)!miIsMenuBorderDisabled.Checked!;
  437. MenuBar.MenusBorderStyle = !(bool)miIsMenuBorderDisabled.Checked ? LineStyle.Single : LineStyle.None;
  438. };
  439. menuItems.Add (miIsMenuBorderDisabled);
  440. return menuItems.ToArray ();
  441. }
  442. MenuItem [] CreateDisabledEnabledMouseItems ()
  443. {
  444. List<MenuItem> menuItems = new List<MenuItem> ();
  445. miIsMouseDisabled = new MenuItem {
  446. Title = "_Disable Mouse"
  447. };
  448. miIsMouseDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMouseDisabled!.Title!.ToString ()!.Substring (1, 1) [0];
  449. miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked;
  450. miIsMouseDisabled.Action += () => {
  451. miIsMouseDisabled.Checked = Application.IsMouseDisabled = (bool)!miIsMouseDisabled.Checked!;
  452. };
  453. menuItems.Add (miIsMouseDisabled);
  454. return menuItems.ToArray ();
  455. }
  456. MenuItem [] CreateKeybindingsMenuItems ()
  457. {
  458. List<MenuItem> menuItems = new List<MenuItem> ();
  459. var item = new MenuItem {
  460. Title = "_Key Bindings",
  461. Help = "Change which keys do what"
  462. };
  463. item.Action += () => {
  464. var dlg = new KeyBindingsDialog ();
  465. Application.Run (dlg);
  466. };
  467. menuItems.Add (null!);
  468. menuItems.Add (item);
  469. return menuItems.ToArray ();
  470. }
  471. MenuItem [] CreateEnableConsoleScrollingMenuItems ()
  472. {
  473. List<MenuItem> menuItems = new List<MenuItem> ();
  474. miEnableConsoleScrolling = new MenuItem ();
  475. miEnableConsoleScrolling.Title = "_Enable Console Scrolling";
  476. miEnableConsoleScrolling.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miEnableConsoleScrolling.Title.ToString ()!.Substring (1, 1) [0];
  477. miEnableConsoleScrolling.CheckType |= MenuItemCheckStyle.Checked;
  478. miEnableConsoleScrolling.Action += () => {
  479. miEnableConsoleScrolling.Checked = !miEnableConsoleScrolling.Checked;
  480. Application.EnableConsoleScrolling = (bool)miEnableConsoleScrolling.Checked!;
  481. };
  482. menuItems.Add (miEnableConsoleScrolling);
  483. return menuItems.ToArray ();
  484. }
  485. MenuItem [] CreateDiagnosticFlagsMenuItems ()
  486. {
  487. const string OFF = "Diagnostics: _Off";
  488. const string FRAME_RULER = "Diagnostics: Frame _Ruler";
  489. const string FRAME_PADDING = "Diagnostics: _Frame Padding";
  490. var index = 0;
  491. List<MenuItem> menuItems = new List<MenuItem> ();
  492. foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) {
  493. var item = new MenuItem {
  494. Title = GetDiagnosticsTitle (diag),
  495. Shortcut = Key.AltMask + index.ToString () [0]
  496. };
  497. index++;
  498. item.CheckType |= MenuItemCheckStyle.Checked;
  499. if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) {
  500. item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding
  501. | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0;
  502. } else {
  503. item.Checked = _diagnosticFlags.HasFlag (diag);
  504. }
  505. item.Action += () => {
  506. var t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off);
  507. if (item.Title == t && item.Checked == false) {
  508. _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler);
  509. item.Checked = true;
  510. } else if (item.Title == t && item.Checked == true) {
  511. _diagnosticFlags |= (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler);
  512. item.Checked = false;
  513. } else {
  514. var f = GetDiagnosticsEnumValue (item.Title);
  515. if (_diagnosticFlags.HasFlag (f)) {
  516. SetDiagnosticsFlag (f, false);
  517. } else {
  518. SetDiagnosticsFlag (f, true);
  519. }
  520. }
  521. foreach (var menuItem in menuItems) {
  522. if (menuItem.Title == t) {
  523. menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler)
  524. && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding);
  525. } else if (menuItem.Title != t) {
  526. menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title));
  527. }
  528. }
  529. ConsoleDriver.Diagnostics = _diagnosticFlags;
  530. Application.Top.SetNeedsDisplay ();
  531. };
  532. menuItems.Add (item);
  533. }
  534. return menuItems.ToArray ();
  535. string GetDiagnosticsTitle (Enum diag)
  536. {
  537. return Enum.GetName (_diagnosticFlags.GetType (), diag) switch {
  538. "Off" => OFF,
  539. "FrameRuler" => FRAME_RULER,
  540. "FramePadding" => FRAME_PADDING,
  541. _ => "",
  542. };
  543. }
  544. Enum GetDiagnosticsEnumValue (ustring title)
  545. {
  546. return title!.ToString () switch {
  547. FRAME_RULER => ConsoleDriver.DiagnosticFlags.FrameRuler,
  548. FRAME_PADDING => ConsoleDriver.DiagnosticFlags.FramePadding,
  549. _ => null!,
  550. };
  551. }
  552. void SetDiagnosticsFlag (Enum diag, bool add)
  553. {
  554. switch (diag) {
  555. case ConsoleDriver.DiagnosticFlags.FrameRuler:
  556. if (add) {
  557. _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler;
  558. } else {
  559. _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler;
  560. }
  561. break;
  562. case ConsoleDriver.DiagnosticFlags.FramePadding:
  563. if (add) {
  564. _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding;
  565. } else {
  566. _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding;
  567. }
  568. break;
  569. default:
  570. _diagnosticFlags = default;
  571. break;
  572. }
  573. }
  574. }
  575. public MenuItem []? CreateThemeMenuItems ()
  576. {
  577. List<MenuItem> menuItems = new List<MenuItem> ();
  578. foreach (var theme in ConfigurationManager.Themes!) {
  579. var item = new MenuItem {
  580. Title = theme.Key,
  581. Shortcut = Key.AltMask + theme.Key [0]
  582. };
  583. item.CheckType |= MenuItemCheckStyle.Checked;
  584. item.Checked = theme.Key == _cachedTheme; // ConfigurationManager.Themes.Theme;
  585. item.Action += () => {
  586. ConfigurationManager.Themes.Theme = _cachedTheme = theme.Key;
  587. ConfigurationManager.Apply ();
  588. };
  589. menuItems.Add (item);
  590. }
  591. var schemeMenuItems = new List<MenuItem> ();
  592. foreach (var sc in Colors.ColorSchemes) {
  593. var item = new MenuItem {
  594. Title = $"_{sc.Key}",
  595. Data = sc.Key,
  596. Shortcut = Key.AltMask | (Key)sc.Key [..1] [0]
  597. };
  598. item.CheckType |= MenuItemCheckStyle.Radio;
  599. item.Checked = sc.Key == _topLevelColorScheme;
  600. item.Action += () => {
  601. _topLevelColorScheme = (string)item.Data;
  602. foreach (var schemeMenuItem in schemeMenuItems) {
  603. schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
  604. }
  605. ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
  606. Application.Top.SetNeedsDisplay ();
  607. };
  608. schemeMenuItems.Add (item);
  609. }
  610. menuItems.Add (null!);
  611. var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ());
  612. menuItems.Add (mbi);
  613. return menuItems.ToArray ();
  614. }
  615. public void ConfigChanged ()
  616. {
  617. if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) {
  618. _topLevelColorScheme = "Base";
  619. }
  620. _themeMenuItems = ((UICatalogTopLevel)Application.Top).CreateThemeMenuItems ();
  621. _themeMenuBarItem!.Children = _themeMenuItems;
  622. var checkedThemeMenu = _themeMenuItems?.Where (m => m?.Checked ?? false).FirstOrDefault ();
  623. if (checkedThemeMenu != null) {
  624. checkedThemeMenu.Checked = false;
  625. }
  626. checkedThemeMenu = _themeMenuItems?.Where (m => m != null && m.Title == ConfigurationManager.Themes?.Theme).FirstOrDefault ();
  627. if (checkedThemeMenu != null) {
  628. ConfigurationManager.Themes!.Theme = checkedThemeMenu.Title.ToString ()!;
  629. checkedThemeMenu.Checked = true;
  630. }
  631. var schemeMenuItems = ((MenuBarItem)_themeMenuItems?.Where (i => i is MenuBarItem)!.FirstOrDefault ()!)!.Children;
  632. foreach (var schemeMenuItem in schemeMenuItems) {
  633. schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
  634. }
  635. ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
  636. //ContentPane.LineStyle = FrameView.DefaultBorderStyle;
  637. MenuBar.Menus [0].Children [0].Shortcut = Application.QuitKey;
  638. StatusBar.Items [0].Shortcut = Application.QuitKey;
  639. StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit";
  640. miIsMouseDisabled!.Checked = Application.IsMouseDisabled;
  641. miEnableConsoleScrolling!.Checked = Application.EnableConsoleScrolling;
  642. var height = (UICatalogApp.ShowStatusBar ? 1 : 0);// + (MenuBar.Visible ? 1 : 0);
  643. //ContentPane.Height = Dim.Fill (height);
  644. StatusBar.Visible = UICatalogApp.ShowStatusBar;
  645. Application.Top.SetNeedsDisplay ();
  646. }
  647. void KeyDownHandler (object? sender, KeyEventEventArgs? a)
  648. {
  649. if (a!.KeyEvent.IsCapslock) {
  650. Capslock.Title = "Caps: On";
  651. StatusBar.SetNeedsDisplay ();
  652. } else {
  653. Capslock.Title = "Caps: Off";
  654. StatusBar.SetNeedsDisplay ();
  655. }
  656. if (a!.KeyEvent.IsNumlock) {
  657. Numlock.Title = "Num: On";
  658. StatusBar.SetNeedsDisplay ();
  659. } else {
  660. Numlock.Title = "Num: Off";
  661. StatusBar.SetNeedsDisplay ();
  662. }
  663. if (a!.KeyEvent.IsScrolllock) {
  664. Scrolllock.Title = "Scroll: On";
  665. StatusBar.SetNeedsDisplay ();
  666. } else {
  667. Scrolllock.Title = "Scroll: Off";
  668. StatusBar.SetNeedsDisplay ();
  669. }
  670. }
  671. void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
  672. {
  673. var item = _categories! [e!.Item];
  674. List<Scenario> newlist;
  675. if (e.Item == 0) {
  676. // First category is "All"
  677. newlist = _scenarios!;
  678. newlist = _scenarios!;
  679. } else {
  680. newlist = _scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ();
  681. }
  682. ScenarioList.Table = new EnumerableTableSource<Scenario> (newlist, new Dictionary<string, Func<Scenario, object>> () {
  683. { "Name", (s) => s.GetName() },
  684. { "Description", (s) => s.GetDescription() },
  685. });
  686. // Create a collection of just the scenario names (the 1st column in our TableView)
  687. // for CollectionNavigator.
  688. var firstColumnList = new List<object> ();
  689. for (var i = 0; i < ScenarioList.Table.Rows; i++) {
  690. firstColumnList.Add (ScenarioList.Table [i, 0]);
  691. }
  692. _scenarioCollectionNav.Collection = firstColumnList;
  693. }
  694. }
  695. static void VerifyObjectsWereDisposed ()
  696. {
  697. #if DEBUG_IDISPOSABLE
  698. // Validate there are no outstanding Responder-based instances
  699. // after a scenario was selected to run. This proves the main UI Catalog
  700. // 'app' closed cleanly.
  701. foreach (var inst in Responder.Instances) {
  702. Debug.Assert (inst.WasDisposed);
  703. }
  704. Responder.Instances.Clear ();
  705. // Validate there are no outstanding Application.RunState-based instances
  706. // after a scenario was selected to run. This proves the main UI Catalog
  707. // 'app' closed cleanly.
  708. foreach (var inst in Application.RunState.Instances) {
  709. Debug.Assert (inst.WasDisposed);
  710. }
  711. Application.RunState.Instances.Clear ();
  712. #endif
  713. }
  714. static void OpenUrl (string url)
  715. {
  716. try {
  717. if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) {
  718. url = url.Replace ("&", "^&");
  719. Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true });
  720. } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) {
  721. using var process = new Process {
  722. StartInfo = new ProcessStartInfo {
  723. FileName = "xdg-open",
  724. Arguments = url,
  725. RedirectStandardError = true,
  726. RedirectStandardOutput = true,
  727. CreateNoWindow = true,
  728. UseShellExecute = false
  729. }
  730. };
  731. process.Start ();
  732. } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) {
  733. Process.Start ("open", url);
  734. }
  735. } catch {
  736. throw;
  737. }
  738. }
  739. }
  740. }