UICatalog.cs 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  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.Diagnostics.CodeAnalysis;
  9. using System.Globalization;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Reflection;
  13. using System.Runtime.InteropServices;
  14. using System.Text;
  15. using System.Text.Json.Serialization;
  16. using Terminal.Gui;
  17. using UICatalog.Scenarios;
  18. using static Terminal.Gui.ConfigurationManager;
  19. using Command = Terminal.Gui.Command;
  20. using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
  21. #nullable enable
  22. namespace UICatalog;
  23. /// <summary>
  24. /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the
  25. /// catalog of scenarios.
  26. /// </summary>
  27. /// <remarks>
  28. /// <para>UI Catalog attempts to satisfy the following goals:</para>
  29. /// <para>
  30. /// <list type="number">
  31. /// <item>
  32. /// <description>Be an easy-to-use showcase for Terminal.Gui concepts and features.</description>
  33. /// </item>
  34. /// <item>
  35. /// <description>Provide sample code that illustrates how to properly implement said concepts & features.</description>
  36. /// </item>
  37. /// <item>
  38. /// <description>Make it easy for contributors to add additional samples in a structured way.</description>
  39. /// </item>
  40. /// </list>
  41. /// </para>
  42. /// </remarks>
  43. public class UICatalogApp
  44. {
  45. private static int _cachedCategoryIndex;
  46. // When a scenario is run, the main app is killed. These items
  47. // are therefore cached so that when the scenario exits the
  48. // main app UI can be restored to previous state
  49. private static int _cachedScenarioIndex;
  50. private static string? _cachedTheme = string.Empty;
  51. private static ObservableCollection<string>? _categories;
  52. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  53. private static readonly FileSystemWatcher _currentDirWatcher = new ();
  54. private static ViewDiagnosticFlags _diagnosticFlags;
  55. private static string _forceDriver = string.Empty;
  56. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  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. #if DEBUG_IDISPOSABLE
  289. // Measure how long it takes for the app to shut down
  290. var sw = new Stopwatch ();
  291. string scenarioName = scenario.GetName ();
  292. Application.InitializedChanged += ApplicationOnInitializedChanged;
  293. #endif
  294. scenario.Main ();
  295. scenario.Dispose ();
  296. // This call to Application.Shutdown brackets the Application.Init call
  297. // made by Scenario.Init() above
  298. // TODO: Throw if shutdown was not called already
  299. Application.Shutdown ();
  300. VerifyObjectsWereDisposed ();
  301. #if DEBUG_IDISPOSABLE
  302. Application.InitializedChanged -= ApplicationOnInitializedChanged;
  303. void ApplicationOnInitializedChanged (object? sender, EventArgs<bool> e)
  304. {
  305. if (e.CurrentValue)
  306. {
  307. sw.Start ();
  308. }
  309. else
  310. {
  311. sw.Stop ();
  312. Debug.WriteLine ($"Shutdown of {scenarioName} Scenario took {sw.ElapsedMilliseconds}ms");
  313. }
  314. }
  315. #endif
  316. }
  317. StopConfigFileWatcher ();
  318. VerifyObjectsWereDisposed ();
  319. return;
  320. }
  321. private static void VerifyObjectsWereDisposed ()
  322. {
  323. #if DEBUG_IDISPOSABLE
  324. // Validate there are no outstanding Responder-based instances
  325. // after a scenario was selected to run. This proves the main UI Catalog
  326. // 'app' closed cleanly.
  327. foreach (Responder? inst in Responder.Instances)
  328. {
  329. Debug.Assert (inst.WasDisposed);
  330. }
  331. Responder.Instances.Clear ();
  332. // Validate there are no outstanding Application.RunState-based instances
  333. // after a scenario was selected to run. This proves the main UI Catalog
  334. // 'app' closed cleanly.
  335. foreach (RunState? inst in RunState.Instances)
  336. {
  337. Debug.Assert (inst.WasDisposed);
  338. }
  339. RunState.Instances.Clear ();
  340. #endif
  341. }
  342. /// <summary>
  343. /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on
  344. /// the command line) and each time a Scenario ends.
  345. /// </summary>
  346. public class UICatalogTopLevel : Toplevel
  347. {
  348. public ListView? CategoryList;
  349. public MenuItem? MiForce16Colors;
  350. public MenuItem? MiIsMenuBorderDisabled;
  351. public MenuItem? MiIsMouseDisabled;
  352. public MenuItem? MiUseSubMenusSingleFrame;
  353. public Shortcut? ShForce16Colors;
  354. //public Shortcut? ShDiagnostics;
  355. public Shortcut? ShVersion;
  356. // UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how
  357. // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView
  358. // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app.
  359. public TableView ScenarioList;
  360. private readonly CollectionNavigator _scenarioCollectionNav = new ();
  361. public UICatalogTopLevel ()
  362. {
  363. _diagnosticFlags = Diagnostics;
  364. _themeMenuItems = CreateThemeMenuItems ();
  365. _themeMenuBarItem = new ("_Themes", _themeMenuItems!);
  366. MenuBar menuBar = new ()
  367. {
  368. Menus =
  369. [
  370. new (
  371. "_File",
  372. new MenuItem []
  373. {
  374. new (
  375. "_Quit",
  376. "Quit UI Catalog",
  377. RequestStop
  378. )
  379. }
  380. ),
  381. _themeMenuBarItem,
  382. new ("Diag_nostics", CreateDiagnosticMenuItems ()),
  383. new (
  384. "_Help",
  385. new MenuItem []
  386. {
  387. new (
  388. "_Documentation",
  389. "",
  390. () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"),
  391. null,
  392. null,
  393. (KeyCode)Key.F1
  394. ),
  395. new (
  396. "_README",
  397. "",
  398. () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"),
  399. null,
  400. null,
  401. (KeyCode)Key.F2
  402. ),
  403. new (
  404. "_About...",
  405. "About UI Catalog",
  406. () => MessageBox.Query (
  407. title: "",
  408. message: GetAboutBoxMessage (),
  409. wrapMessage: false,
  410. buttons: "_Ok"
  411. ),
  412. null,
  413. null,
  414. (KeyCode)Key.A.WithCtrl
  415. )
  416. }
  417. )
  418. ]
  419. };
  420. Add (menuBar);
  421. StatusBar statusBar = new ()
  422. {
  423. Visible = ShowStatusBar,
  424. AlignmentModes = AlignmentModes.IgnoreFirstOrLast,
  425. CanFocus = false
  426. };
  427. Add (statusBar);
  428. if (StatusBar is { })
  429. {
  430. ShVersion = new ()
  431. {
  432. Title = "Version Info",
  433. CanFocus = false,
  434. };
  435. var statusBarShortcut = new Shortcut
  436. {
  437. Key = Key.F10,
  438. Title = "Show/Hide Status Bar",
  439. CanFocus = false,
  440. };
  441. statusBarShortcut.Accepted += (sender, args) =>
  442. {
  443. StatusBar.Visible = !StatusBar.Visible;
  444. args.Cancel = true;
  445. };
  446. ShForce16Colors = new ()
  447. {
  448. CanFocus = false,
  449. CommandView = new CheckBox
  450. {
  451. Title = "16 color mode",
  452. CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
  453. CanFocus = false
  454. },
  455. HelpText = "",
  456. KeyBindingScope = KeyBindingScope.Application,
  457. Key = Key.F7
  458. };
  459. ((CheckBox)ShForce16Colors.CommandView).CheckedStateChanging += (sender, args) =>
  460. {
  461. Application.Force16Colors = args.NewValue == CheckState.Checked;
  462. MiForce16Colors!.Checked = Application.Force16Colors;
  463. Application.Refresh ();
  464. };
  465. StatusBar.Add (
  466. new Shortcut
  467. {
  468. CanFocus = false,
  469. Title = "Quit",
  470. Key = Application.QuitKey
  471. },
  472. statusBarShortcut,
  473. ShForce16Colors,
  474. //ShDiagnostics,
  475. ShVersion
  476. );
  477. }
  478. // Create the Category list view. This list never changes.
  479. CategoryList = new ()
  480. {
  481. X = 0,
  482. Y = 1,
  483. Width = Dim.Auto (),
  484. Height = Dim.Fill (Dim.Func (() => IsInitialized ? Subviews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height : 1)),
  485. AllowsMarking = false,
  486. CanFocus = true,
  487. Title = "_Categories",
  488. BorderStyle = LineStyle.Rounded,
  489. SuperViewRendersLineCanvas = true,
  490. Source = new ListWrapper<string> (_categories)
  491. };
  492. CategoryList.OpenSelectedItem += (s, a) => { ScenarioList!.SetFocus (); };
  493. CategoryList.SelectedItemChanged += CategoryView_SelectedChanged;
  494. // Create the scenario list. The contents of the scenario list changes whenever the
  495. // Category list selection changes (to show just the scenarios that belong to the selected
  496. // category).
  497. ScenarioList = new ()
  498. {
  499. X = Pos.Right (CategoryList) - 1,
  500. Y = 1,
  501. Width = Dim.Fill (),
  502. Height = Dim.Height (CategoryList),
  503. //AllowsMarking = false,
  504. CanFocus = true,
  505. Title = "_Scenarios",
  506. BorderStyle = CategoryList.BorderStyle,
  507. SuperViewRendersLineCanvas = true
  508. };
  509. // TableView provides many options for table headers. For simplicity we turn all
  510. // of these off. By enabling FullRowSelect and turning off headers, TableView looks just
  511. // like a ListView
  512. ScenarioList.FullRowSelect = true;
  513. ScenarioList.Style.ShowHeaders = false;
  514. ScenarioList.Style.ShowHorizontalHeaderOverline = false;
  515. ScenarioList.Style.ShowHorizontalHeaderUnderline = false;
  516. ScenarioList.Style.ShowHorizontalBottomline = false;
  517. ScenarioList.Style.ShowVerticalCellLines = false;
  518. ScenarioList.Style.ShowVerticalHeaderLines = false;
  519. /* By default TableView lays out columns at render time and only
  520. * measures y rows of data at a time. Where y is the height of the
  521. * console. This is for the following reasons:
  522. *
  523. * - Performance, when tables have a large amount of data
  524. * - Defensive, prevents a single wide cell value pushing other
  525. * columns off screen (requiring horizontal scrolling
  526. *
  527. * In the case of UICatalog here, such an approach is overkill so
  528. * we just measure all the data ourselves and set the appropriate
  529. * max widths as ColumnStyles
  530. */
  531. int longestName = _scenarios!.Max (s => s.GetName ().Length);
  532. ScenarioList.Style.ColumnStyles.Add (
  533. 0,
  534. new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
  535. );
  536. ScenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 });
  537. // Enable user to find & select a scenario by typing text
  538. // TableView does not (currently) have built-in CollectionNavigator support (the ability for the
  539. // user to type and the items that match get selected). We implement it in the app instead.
  540. ScenarioList.KeyDown += (s, a) =>
  541. {
  542. if (CollectionNavigatorBase.IsCompatibleKey (a))
  543. {
  544. int? newItem =
  545. _scenarioCollectionNav?.GetNextMatchingItem (
  546. ScenarioList.SelectedRow,
  547. (char)a
  548. );
  549. if (newItem is int v && newItem != -1)
  550. {
  551. ScenarioList.SelectedRow = v;
  552. ScenarioList.EnsureSelectedCellIsVisible ();
  553. ScenarioList.SetNeedsDisplay ();
  554. a.Handled = true;
  555. }
  556. }
  557. };
  558. ScenarioList.CellActivated += ScenarioView_OpenSelectedItem;
  559. // TableView typically is a grid where nav keys are biased for moving left/right.
  560. ScenarioList.KeyBindings.Remove (Key.Home);
  561. ScenarioList.KeyBindings.Add (Key.Home, Command.Start);
  562. ScenarioList.KeyBindings.Remove (Key.End);
  563. ScenarioList.KeyBindings.Add (Key.End, Command.End);
  564. // Ideally, TableView.MultiSelect = false would turn off any keybindings for
  565. // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
  566. // a shortcut to About.
  567. ScenarioList.MultiSelect = false;
  568. ScenarioList.KeyBindings.Remove (Key.A.WithCtrl);
  569. Add (CategoryList);
  570. Add (ScenarioList);
  571. Loaded += LoadedHandler;
  572. Unloaded += UnloadedHandler;
  573. // Restore previous selections
  574. CategoryList.SelectedItem = _cachedCategoryIndex;
  575. ScenarioList.SelectedRow = _cachedScenarioIndex;
  576. Applied += ConfigAppliedHandler;
  577. }
  578. public void ConfigChanged ()
  579. {
  580. if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme))
  581. {
  582. _topLevelColorScheme = "Base";
  583. }
  584. _cachedTheme = Themes?.Theme;
  585. _themeMenuItems = CreateThemeMenuItems ();
  586. _themeMenuBarItem!.Children = _themeMenuItems;
  587. foreach (MenuItem mi in _themeMenuItems!)
  588. {
  589. if (mi is { Parent: null })
  590. {
  591. mi.Parent = _themeMenuBarItem;
  592. }
  593. }
  594. ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
  595. MenuBar!.Menus [0].Children! [0]!.ShortcutKey = Application.QuitKey;
  596. if (StatusBar is { })
  597. {
  598. ((Shortcut)StatusBar.Subviews [0]).Key = Application.QuitKey;
  599. StatusBar.Visible = ShowStatusBar;
  600. }
  601. MiIsMouseDisabled!.Checked = Application.IsMouseDisabled;
  602. ((CheckBox)ShForce16Colors!.CommandView!).CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
  603. Application.Top!.SetNeedsDisplay ();
  604. }
  605. public MenuItem []? CreateThemeMenuItems ()
  606. {
  607. List<MenuItem> menuItems = CreateForce16ColorItems ().ToList ();
  608. menuItems.Add (null!);
  609. var schemeCount = 0;
  610. foreach (KeyValuePair<string, ThemeScope> theme in Themes!)
  611. {
  612. var item = new MenuItem
  613. {
  614. Title = theme.Key == "Dark" ? $"{theme.Key.Substring (0, 3)}_{theme.Key.Substring (3, 1)}" : $"_{theme.Key}",
  615. ShortcutKey = new Key ((KeyCode)((uint)KeyCode.D1 + schemeCount++))
  616. .WithCtrl
  617. };
  618. item.CheckType |= MenuItemCheckStyle.Checked;
  619. item.Checked = theme.Key == _cachedTheme; // CM.Themes.Theme;
  620. item.Action += () =>
  621. {
  622. Themes.Theme = _cachedTheme = theme.Key;
  623. Apply ();
  624. };
  625. menuItems.Add (item);
  626. }
  627. List<MenuItem> schemeMenuItems = new ();
  628. foreach (KeyValuePair<string, ColorScheme?> sc in Colors.ColorSchemes)
  629. {
  630. var item = new MenuItem { Title = $"_{sc.Key}", Data = sc.Key };
  631. item.CheckType |= MenuItemCheckStyle.Radio;
  632. item.Checked = sc.Key == _topLevelColorScheme;
  633. item.Action += () =>
  634. {
  635. _topLevelColorScheme = (string)item.Data;
  636. foreach (MenuItem schemeMenuItem in schemeMenuItems)
  637. {
  638. schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
  639. }
  640. ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
  641. Application.Top!.SetNeedsDisplay ();
  642. };
  643. item.ShortcutKey = ((Key)sc.Key [0].ToString ().ToLower ()).WithCtrl;
  644. schemeMenuItems.Add (item);
  645. }
  646. menuItems.Add (null!);
  647. var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ());
  648. menuItems.Add (mbi);
  649. return menuItems.ToArray ();
  650. }
  651. private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
  652. {
  653. string item = _categories! [e!.Item];
  654. ObservableCollection<Scenario> newlist;
  655. if (e.Item == 0)
  656. {
  657. // First category is "All"
  658. newlist = _scenarios!;
  659. }
  660. else
  661. {
  662. newlist = new (_scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ());
  663. }
  664. ScenarioList.Table = new EnumerableTableSource<Scenario> (
  665. newlist,
  666. new ()
  667. {
  668. { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () }
  669. }
  670. );
  671. // Create a collection of just the scenario names (the 1st column in our TableView)
  672. // for CollectionNavigator.
  673. List<object> firstColumnList = new ();
  674. for (var i = 0; i < ScenarioList.Table.Rows; i++)
  675. {
  676. firstColumnList.Add (ScenarioList.Table [i, 0]);
  677. }
  678. _scenarioCollectionNav.Collection = firstColumnList;
  679. }
  680. private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); }
  681. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  682. private MenuItem [] CreateDiagnosticFlagsMenuItems ()
  683. {
  684. const string OFF = "View Diagnostics: _Off";
  685. const string RULER = "View Diagnostics: _Ruler";
  686. const string PADDING = "View Diagnostics: _Padding";
  687. const string Hover = "View Diagnostics: _Hover";
  688. var index = 0;
  689. List<MenuItem> menuItems = new ();
  690. foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ()))
  691. {
  692. var item = new MenuItem
  693. {
  694. Title = GetDiagnosticsTitle (diag), ShortcutKey = new Key (index.ToString () [0]).WithAlt
  695. };
  696. index++;
  697. item.CheckType |= MenuItemCheckStyle.Checked;
  698. if (GetDiagnosticsTitle (ViewDiagnosticFlags.Off) == item.Title)
  699. {
  700. item.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Padding)
  701. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler)
  702. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover);
  703. }
  704. else
  705. {
  706. item.Checked = _diagnosticFlags.HasFlag (diag);
  707. }
  708. item.Action += () =>
  709. {
  710. string t = GetDiagnosticsTitle (ViewDiagnosticFlags.Off);
  711. if (item.Title == t && item.Checked == false)
  712. {
  713. _diagnosticFlags &= ~(ViewDiagnosticFlags.Padding | ViewDiagnosticFlags.Ruler | ViewDiagnosticFlags.Hover);
  714. item.Checked = true;
  715. }
  716. else if (item.Title == t && item.Checked == true)
  717. {
  718. _diagnosticFlags |= ViewDiagnosticFlags.Padding | ViewDiagnosticFlags.Ruler | ViewDiagnosticFlags.Hover;
  719. item.Checked = false;
  720. }
  721. else
  722. {
  723. Enum f = GetDiagnosticsEnumValue (item.Title);
  724. if (_diagnosticFlags.HasFlag (f))
  725. {
  726. SetDiagnosticsFlag (f, false);
  727. }
  728. else
  729. {
  730. SetDiagnosticsFlag (f, true);
  731. }
  732. }
  733. foreach (MenuItem menuItem in menuItems)
  734. {
  735. if (menuItem.Title == t)
  736. {
  737. menuItem.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler)
  738. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Padding)
  739. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover);
  740. }
  741. else if (menuItem.Title != t)
  742. {
  743. menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title));
  744. }
  745. }
  746. Diagnostics = _diagnosticFlags;
  747. Application.Top!.SetNeedsDisplay ();
  748. };
  749. menuItems.Add (item);
  750. }
  751. return menuItems.ToArray ();
  752. string GetDiagnosticsTitle (Enum diag)
  753. {
  754. return Enum.GetName (_diagnosticFlags.GetType (), diag) switch
  755. {
  756. "Off" => OFF,
  757. "Ruler" => RULER,
  758. "Padding" => PADDING,
  759. "Hover" => Hover,
  760. _ => ""
  761. };
  762. }
  763. Enum GetDiagnosticsEnumValue (string? title)
  764. {
  765. return title switch
  766. {
  767. RULER => ViewDiagnosticFlags.Ruler,
  768. PADDING => ViewDiagnosticFlags.Padding,
  769. Hover => ViewDiagnosticFlags.Hover,
  770. _ => null!
  771. };
  772. }
  773. void SetDiagnosticsFlag (Enum diag, bool add)
  774. {
  775. switch (diag)
  776. {
  777. case ViewDiagnosticFlags.Ruler:
  778. if (add)
  779. {
  780. _diagnosticFlags |= ViewDiagnosticFlags.Ruler;
  781. }
  782. else
  783. {
  784. _diagnosticFlags &= ~ViewDiagnosticFlags.Ruler;
  785. }
  786. break;
  787. case ViewDiagnosticFlags.Padding:
  788. if (add)
  789. {
  790. _diagnosticFlags |= ViewDiagnosticFlags.Padding;
  791. }
  792. else
  793. {
  794. _diagnosticFlags &= ~ViewDiagnosticFlags.Padding;
  795. }
  796. break;
  797. case ViewDiagnosticFlags.Hover:
  798. if (add)
  799. {
  800. _diagnosticFlags |= ViewDiagnosticFlags.Hover;
  801. }
  802. else
  803. {
  804. _diagnosticFlags &= ~ViewDiagnosticFlags.Hover;
  805. }
  806. break;
  807. default:
  808. _diagnosticFlags = default (ViewDiagnosticFlags);
  809. break;
  810. }
  811. }
  812. }
  813. private List<MenuItem []> CreateDiagnosticMenuItems ()
  814. {
  815. List<MenuItem []> menuItems = new ()
  816. {
  817. CreateDiagnosticFlagsMenuItems (),
  818. new MenuItem [] { null! },
  819. CreateDisabledEnabledMouseItems (),
  820. CreateDisabledEnabledMenuBorder (),
  821. CreateDisabledEnableUseSubMenusSingleFrame (),
  822. CreateKeyBindingsMenuItems ()
  823. };
  824. return menuItems;
  825. }
  826. // TODO: This should be an ConfigurationManager setting
  827. private MenuItem [] CreateDisabledEnabledMenuBorder ()
  828. {
  829. List<MenuItem> menuItems = new ();
  830. MiIsMenuBorderDisabled = new () { Title = "Disable Menu _Border" };
  831. MiIsMenuBorderDisabled.ShortcutKey =
  832. new Key (MiIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt.WithCtrl.NoShift;
  833. MiIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked;
  834. MiIsMenuBorderDisabled.Action += () =>
  835. {
  836. MiIsMenuBorderDisabled.Checked = (bool)!MiIsMenuBorderDisabled.Checked!;
  837. MenuBar!.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked
  838. ? LineStyle.Single
  839. : LineStyle.None;
  840. };
  841. menuItems.Add (MiIsMenuBorderDisabled);
  842. return menuItems.ToArray ();
  843. }
  844. private MenuItem [] CreateDisabledEnabledMouseItems ()
  845. {
  846. List<MenuItem> menuItems = new ();
  847. MiIsMouseDisabled = new () { Title = "_Disable Mouse" };
  848. MiIsMouseDisabled.ShortcutKey =
  849. new Key (MiIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl.NoShift;
  850. MiIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked;
  851. MiIsMouseDisabled.Action += () =>
  852. {
  853. MiIsMouseDisabled.Checked =
  854. Application.IsMouseDisabled = (bool)!MiIsMouseDisabled.Checked!;
  855. };
  856. menuItems.Add (MiIsMouseDisabled);
  857. return menuItems.ToArray ();
  858. }
  859. // TODO: This should be an ConfigurationManager setting
  860. private MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame ()
  861. {
  862. List<MenuItem> menuItems = new ();
  863. MiUseSubMenusSingleFrame = new () { Title = "Enable _Sub-Menus Single Frame" };
  864. MiUseSubMenusSingleFrame.ShortcutKey = KeyCode.CtrlMask
  865. | KeyCode.AltMask
  866. | (KeyCode)MiUseSubMenusSingleFrame!.Title!.Substring (8, 1) [
  867. 0];
  868. MiUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked;
  869. MiUseSubMenusSingleFrame.Action += () =>
  870. {
  871. MiUseSubMenusSingleFrame.Checked = (bool)!MiUseSubMenusSingleFrame.Checked!;
  872. MenuBar!.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked;
  873. };
  874. menuItems.Add (MiUseSubMenusSingleFrame);
  875. return menuItems.ToArray ();
  876. }
  877. private MenuItem [] CreateForce16ColorItems ()
  878. {
  879. List<MenuItem> menuItems = new ();
  880. MiForce16Colors = new ()
  881. {
  882. Title = "Force _16 Colors",
  883. ShortcutKey = Key.F6,
  884. Checked = Application.Force16Colors,
  885. CanExecute = () => Application.Driver?.SupportsTrueColor ?? false
  886. };
  887. MiForce16Colors.CheckType |= MenuItemCheckStyle.Checked;
  888. MiForce16Colors.Action += () =>
  889. {
  890. MiForce16Colors.Checked = Application.Force16Colors = (bool)!MiForce16Colors.Checked!;
  891. ((CheckBox)ShForce16Colors!.CommandView!).CheckedState =
  892. Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
  893. Application.Refresh ();
  894. };
  895. menuItems.Add (MiForce16Colors);
  896. return menuItems.ToArray ();
  897. }
  898. private MenuItem [] CreateKeyBindingsMenuItems ()
  899. {
  900. List<MenuItem> menuItems = new ();
  901. var item = new MenuItem { Title = "_Key Bindings", Help = "Change which keys do what" };
  902. item.Action += () =>
  903. {
  904. var dlg = new KeyBindingsDialog ();
  905. Application.Run (dlg);
  906. dlg.Dispose ();
  907. };
  908. menuItems.Add (null!);
  909. menuItems.Add (item);
  910. return menuItems.ToArray ();
  911. }
  912. private void LoadedHandler (object? sender, EventArgs? args)
  913. {
  914. ConfigChanged ();
  915. MiIsMouseDisabled!.Checked = Application.IsMouseDisabled;
  916. if (ShVersion is { })
  917. {
  918. ShVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver.GetVersionInfo ()}";
  919. }
  920. if (_selectedScenario != null)
  921. {
  922. _selectedScenario = null;
  923. _isFirstRunning = false;
  924. }
  925. if (!_isFirstRunning)
  926. {
  927. ScenarioList.SetFocus ();
  928. }
  929. if (StatusBar is { })
  930. {
  931. StatusBar.VisibleChanged += (s, e) =>
  932. {
  933. ShowStatusBar = StatusBar.Visible;
  934. int height = StatusBar.Visible ? 1 : 0;
  935. CategoryList!.Height = Dim.Fill (height);
  936. ScenarioList.Height = Dim.Fill (height);
  937. // ContentPane.Height = Dim.Fill (height);
  938. LayoutSubviews ();
  939. SetSubViewNeedsDisplay ();
  940. };
  941. }
  942. Loaded -= LoadedHandler;
  943. CategoryList!.EnsureSelectedItemVisible ();
  944. ScenarioList.EnsureSelectedCellIsVisible ();
  945. }
  946. /// <summary>Launches the selected scenario, setting the global _selectedScenario</summary>
  947. /// <param name="e"></param>
  948. private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e)
  949. {
  950. if (_selectedScenario is null)
  951. {
  952. // Save selected item state
  953. _cachedCategoryIndex = CategoryList!.SelectedItem;
  954. _cachedScenarioIndex = ScenarioList.SelectedRow;
  955. // Create new instance of scenario (even though Scenarios contains instances)
  956. var selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0];
  957. _selectedScenario = (Scenario)Activator.CreateInstance (
  958. _scenarios!.FirstOrDefault (
  959. s => s.GetName ()
  960. == selectedScenarioName
  961. )!
  962. .GetType ()
  963. )!;
  964. // Tell the main app to stop
  965. Application.RequestStop ();
  966. }
  967. }
  968. private void UnloadedHandler (object? sender, EventArgs? args)
  969. {
  970. Applied -= ConfigAppliedHandler;
  971. Unloaded -= UnloadedHandler;
  972. Dispose ();
  973. }
  974. }
  975. private struct Options
  976. {
  977. public string Driver;
  978. public string Scenario;
  979. /* etc. */
  980. }
  981. }