UICatalog.cs 43 KB

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