UICatalog.cs 46 KB

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