UICatalog.cs 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396
  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.CommandLine.Builder;
  8. using System.CommandLine.Help;
  9. using System.CommandLine.Parsing;
  10. using System.Data;
  11. using System.Diagnostics;
  12. using System.Diagnostics.CodeAnalysis;
  13. using System.Globalization;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Reflection;
  17. using System.Runtime.InteropServices;
  18. using System.Text;
  19. using System.Text.Json;
  20. using System.Text.Json.Serialization;
  21. using System.Threading;
  22. using System.Threading.Tasks;
  23. using Terminal.Gui;
  24. using UICatalog.Scenarios;
  25. using static Terminal.Gui.ConfigurationManager;
  26. using Command = Terminal.Gui.Command;
  27. using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
  28. #nullable enable
  29. namespace UICatalog;
  30. /// <summary>
  31. /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the
  32. /// catalog of scenarios.
  33. /// </summary>
  34. /// <remarks>
  35. /// <para>UI Catalog attempts to satisfy the following goals:</para>
  36. /// <para>
  37. /// <list type="number">
  38. /// <item>
  39. /// <description>Be an easy-to-use showcase for Terminal.Gui concepts and features.</description>
  40. /// </item>
  41. /// <item>
  42. /// <description>Provide sample code that illustrates how to properly implement said concepts & features.</description>
  43. /// </item>
  44. /// <item>
  45. /// <description>Make it easy for contributors to add additional samples in a structured way.</description>
  46. /// </item>
  47. /// </list>
  48. /// </para>
  49. /// </remarks>
  50. public class UICatalogApp
  51. {
  52. private static int _cachedCategoryIndex;
  53. // When a scenario is run, the main app is killed. These items
  54. // are therefore cached so that when the scenario exits the
  55. // main app UI can be restored to previous state
  56. private static int _cachedScenarioIndex;
  57. private static string? _cachedTheme = string.Empty;
  58. private static ObservableCollection<string>? _categories;
  59. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  60. private static readonly FileSystemWatcher _currentDirWatcher = new ();
  61. private static ViewDiagnosticFlags _diagnosticFlags;
  62. private static string _forceDriver = string.Empty;
  63. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  64. private static readonly FileSystemWatcher _homeDirWatcher = new ();
  65. private static bool _isFirstRunning = true;
  66. private static Options _options;
  67. private static ObservableCollection<Scenario>? _scenarios;
  68. // If set, holds the scenario the user selected
  69. private static Scenario? _selectedScenario;
  70. private static MenuBarItem? _themeMenuBarItem;
  71. private static MenuItem []? _themeMenuItems;
  72. private static string _topLevelColorScheme = string.Empty;
  73. [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true)]
  74. [JsonPropertyName ("UICatalog.StatusBar")]
  75. public static bool ShowStatusBar { get; set; } = true;
  76. /// <summary>
  77. /// Gets the message displayed in the About Box. `public` so it can be used from Unit tests.
  78. /// </summary>
  79. /// <returns></returns>
  80. public static string GetAboutBoxMessage ()
  81. {
  82. // NOTE: Do not use multiline verbatim strings here.
  83. // WSL gets all confused.
  84. StringBuilder msg = new ();
  85. msg.AppendLine ("UI Catalog: A comprehensive sample library for");
  86. msg.AppendLine ();
  87. msg.AppendLine ("""
  88. _______ _ _ _____ _
  89. |__ __| (_) | | / ____| (_)
  90. | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _
  91. | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | |
  92. | | __/ | | | | | | | | | | | (_| | || |__| | |_| | |
  93. |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_|
  94. """);
  95. msg.AppendLine ();
  96. msg.AppendLine ("v2 - Pre-Alpha");
  97. msg.AppendLine ();
  98. msg.AppendLine ("https://github.com/gui-cs/Terminal.Gui");
  99. return msg.ToString ();
  100. }
  101. private static void ConfigFileChanged (object sender, FileSystemEventArgs e)
  102. {
  103. if (Application.Top == null)
  104. {
  105. return;
  106. }
  107. // TODO: This is a hack. Figure out how to ensure that the file is fully written before reading it.
  108. //Thread.Sleep (500);
  109. Load ();
  110. Apply ();
  111. }
  112. private static int Main (string [] args)
  113. {
  114. Console.OutputEncoding = Encoding.Default;
  115. if (Debugger.IsAttached)
  116. {
  117. CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US");
  118. }
  119. _scenarios = Scenario.GetScenarios ();
  120. _categories = Scenario.GetAllCategories ();
  121. // Process command line args
  122. // "UICatalog [--driver <driver>] [--benchmark] [scenario name]"
  123. // If no driver is provided, the default driver is used.
  124. Option<string> driverOption = new Option<string> ("--driver", "The IConsoleDriver to use.").FromAmong (
  125. Application.GetDriverTypes ()
  126. .Select (d => d!.Name)
  127. .ToArray ()
  128. );
  129. driverOption.AddAlias ("-d");
  130. driverOption.AddAlias ("--d");
  131. Option<bool> benchmarkFlag = new Option<bool> ("--benchmark", "Enables benchmarking. If a Scenario is specified, just that Scenario will be benchmarked.");
  132. benchmarkFlag.AddAlias ("-b");
  133. benchmarkFlag.AddAlias ("--b");
  134. Option<uint> benchmarkTimeout = new Option<uint> ("--timeout", getDefaultValue: () => Scenario.BenchmarkTimeout, $"The maximum time in milliseconds to run a benchmark for. Default is {Scenario.BenchmarkTimeout}ms.");
  135. benchmarkTimeout.AddAlias ("-t");
  136. benchmarkTimeout.AddAlias ("--t");
  137. Option<string> resultsFile = new Option<string> ("--file", "The file to save benchmark results to. If not specified, the results will be displayed in a TableView.");
  138. resultsFile.AddAlias ("-f");
  139. resultsFile.AddAlias ("--f");
  140. Argument<string> scenarioArgument = new Argument<string> (
  141. name: "scenario",
  142. description: "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.",
  143. getDefaultValue: () => "none"
  144. ).FromAmong (
  145. _scenarios.Select (s => s.GetName ())
  146. .Append ("none")
  147. .ToArray ()
  148. );
  149. var rootCommand = new RootCommand ("A comprehensive sample library for Terminal.Gui")
  150. {
  151. scenarioArgument, benchmarkFlag, benchmarkTimeout, resultsFile, driverOption,
  152. };
  153. rootCommand.SetHandler (
  154. context =>
  155. {
  156. var options = new Options
  157. {
  158. Scenario = context.ParseResult.GetValueForArgument (scenarioArgument),
  159. Driver = context.ParseResult.GetValueForOption (driverOption) ?? string.Empty,
  160. Benchmark = context.ParseResult.GetValueForOption (benchmarkFlag),
  161. BenchmarkTimeout = context.ParseResult.GetValueForOption (benchmarkTimeout),
  162. ResultsFile = context.ParseResult.GetValueForOption (resultsFile) ?? string.Empty,
  163. /* etc. */
  164. };
  165. // See https://github.com/dotnet/command-line-api/issues/796 for the rationale behind this hackery
  166. _options = options;
  167. }
  168. );
  169. bool helpShown = false;
  170. var parser = new CommandLineBuilder (rootCommand)
  171. .UseHelp (ctx => helpShown = true)
  172. .Build ();
  173. parser.Invoke (args);
  174. if (helpShown)
  175. {
  176. return 0;
  177. }
  178. Scenario.BenchmarkTimeout = _options.BenchmarkTimeout;
  179. UICatalogMain (_options);
  180. return 0;
  181. }
  182. private static void OpenUrl (string url)
  183. {
  184. if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
  185. {
  186. url = url.Replace ("&", "^&");
  187. Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true });
  188. }
  189. else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux))
  190. {
  191. using var process = new Process
  192. {
  193. StartInfo = new ()
  194. {
  195. FileName = "xdg-open",
  196. Arguments = url,
  197. RedirectStandardError = true,
  198. RedirectStandardOutput = true,
  199. CreateNoWindow = true,
  200. UseShellExecute = false
  201. }
  202. };
  203. process.Start ();
  204. }
  205. else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX))
  206. {
  207. Process.Start ("open", url);
  208. }
  209. }
  210. /// <summary>
  211. /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is
  212. /// killed and the Scenario is run as though it were Application.Top. When the Scenario exits, this function exits.
  213. /// </summary>
  214. /// <returns></returns>
  215. private static Scenario RunUICatalogTopLevel ()
  216. {
  217. // Run UI Catalog UI. When it exits, if _selectedScenario is != null then
  218. // a Scenario was selected. Otherwise, the user wants to quit UI Catalog.
  219. // If the user specified a driver on the command line then use it,
  220. // ignoring Config files.
  221. Application.Init (driverName: _forceDriver);
  222. if (_cachedTheme is null)
  223. {
  224. _cachedTheme = Themes?.Theme;
  225. }
  226. else
  227. {
  228. Themes!.Theme = _cachedTheme;
  229. Apply ();
  230. }
  231. Application.Run<UICatalogTopLevel> ().Dispose ();
  232. Application.Shutdown ();
  233. return _selectedScenario!;
  234. }
  235. private static void StartConfigFileWatcher ()
  236. {
  237. // Setup a file system watcher for `./.tui/`
  238. _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
  239. string assemblyLocation = Assembly.GetExecutingAssembly ().Location;
  240. string tuiDir;
  241. if (!string.IsNullOrEmpty (assemblyLocation))
  242. {
  243. var assemblyFile = new FileInfo (assemblyLocation);
  244. tuiDir = Path.Combine (assemblyFile.Directory!.FullName, ".tui");
  245. }
  246. else
  247. {
  248. tuiDir = Path.Combine (AppContext.BaseDirectory, ".tui");
  249. }
  250. if (!Directory.Exists (tuiDir))
  251. {
  252. Directory.CreateDirectory (tuiDir);
  253. }
  254. _currentDirWatcher.Path = tuiDir;
  255. _currentDirWatcher.Filter = "*config.json";
  256. // Setup a file system watcher for `~/.tui/`
  257. _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite;
  258. var f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
  259. tuiDir = Path.Combine (f.FullName, ".tui");
  260. if (!Directory.Exists (tuiDir))
  261. {
  262. Directory.CreateDirectory (tuiDir);
  263. }
  264. _homeDirWatcher.Path = tuiDir;
  265. _homeDirWatcher.Filter = "*config.json";
  266. _currentDirWatcher.Changed += ConfigFileChanged;
  267. //_currentDirWatcher.Created += ConfigFileChanged;
  268. _currentDirWatcher.EnableRaisingEvents = true;
  269. _homeDirWatcher.Changed += ConfigFileChanged;
  270. //_homeDirWatcher.Created += ConfigFileChanged;
  271. _homeDirWatcher.EnableRaisingEvents = true;
  272. }
  273. private static void StopConfigFileWatcher ()
  274. {
  275. _currentDirWatcher.EnableRaisingEvents = false;
  276. _currentDirWatcher.Changed -= ConfigFileChanged;
  277. _currentDirWatcher.Created -= ConfigFileChanged;
  278. _homeDirWatcher.EnableRaisingEvents = false;
  279. _homeDirWatcher.Changed -= ConfigFileChanged;
  280. _homeDirWatcher.Created -= ConfigFileChanged;
  281. }
  282. private static void UICatalogMain (Options options)
  283. {
  284. StartConfigFileWatcher ();
  285. // By setting _forceDriver we ensure that if the user has specified a driver on the command line, it will be used
  286. // regardless of what's in a config file.
  287. Application.ForceDriver = _forceDriver = options.Driver;
  288. // If a Scenario name has been provided on the commandline
  289. // run it and exit when done.
  290. if (options.Scenario != "none")
  291. {
  292. _topLevelColorScheme = "Base";
  293. int item = _scenarios!.IndexOf (
  294. _scenarios!.FirstOrDefault (
  295. s =>
  296. s.GetName ()
  297. .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase)
  298. )!);
  299. _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ())!;
  300. var results = RunScenario (_selectedScenario, options.Benchmark);
  301. if (results is { })
  302. {
  303. Console.WriteLine (JsonSerializer.Serialize (results, new JsonSerializerOptions ()
  304. {
  305. WriteIndented = true
  306. }));
  307. }
  308. VerifyObjectsWereDisposed ();
  309. return;
  310. }
  311. // Benchmark all Scenarios
  312. if (options.Benchmark)
  313. {
  314. BenchmarkAllScenarios ();
  315. return;
  316. }
  317. while (RunUICatalogTopLevel () is { } scenario)
  318. {
  319. VerifyObjectsWereDisposed ();
  320. Themes!.Theme = _cachedTheme!;
  321. Apply ();
  322. scenario.TopLevelColorScheme = _topLevelColorScheme;
  323. #if DEBUG_IDISPOSABLE
  324. // Measure how long it takes for the app to shut down
  325. var sw = new Stopwatch ();
  326. string scenarioName = scenario.GetName ();
  327. Application.InitializedChanged += ApplicationOnInitializedChanged;
  328. #endif
  329. scenario.Main ();
  330. scenario.Dispose ();
  331. // This call to Application.Shutdown brackets the Application.Init call
  332. // made by Scenario.Init() above
  333. // TODO: Throw if shutdown was not called already
  334. Application.Shutdown ();
  335. VerifyObjectsWereDisposed ();
  336. #if DEBUG_IDISPOSABLE
  337. Application.InitializedChanged -= ApplicationOnInitializedChanged;
  338. void ApplicationOnInitializedChanged (object? sender, EventArgs<bool> e)
  339. {
  340. if (e.CurrentValue)
  341. {
  342. sw.Start ();
  343. }
  344. else
  345. {
  346. sw.Stop ();
  347. Debug.WriteLine ($"Shutdown of {scenarioName} Scenario took {sw.ElapsedMilliseconds}ms");
  348. }
  349. }
  350. #endif
  351. }
  352. StopConfigFileWatcher ();
  353. VerifyObjectsWereDisposed ();
  354. return;
  355. }
  356. private static BenchmarkResults? RunScenario (Scenario scenario, bool benchmark)
  357. {
  358. if (benchmark)
  359. {
  360. scenario.StartBenchmark ();
  361. }
  362. Application.Init (driverName: _forceDriver);
  363. scenario.TopLevelColorScheme = _topLevelColorScheme;
  364. if (benchmark)
  365. {
  366. Application.Screen = new (0, 0, 120, 40);
  367. }
  368. scenario.Main ();
  369. BenchmarkResults? results = null;
  370. if (benchmark)
  371. {
  372. results = scenario.EndBenchmark ();
  373. }
  374. scenario.Dispose ();
  375. // TODO: Throw if shutdown was not called already
  376. Application.Shutdown ();
  377. return results;
  378. }
  379. private static void BenchmarkAllScenarios ()
  380. {
  381. List<BenchmarkResults> resultsList = new List<BenchmarkResults> ();
  382. int maxScenarios = 5;
  383. foreach (var s in _scenarios!)
  384. {
  385. resultsList.Add (RunScenario (s, true)!);
  386. maxScenarios--;
  387. if (maxScenarios == 0)
  388. {
  389. // break;
  390. }
  391. }
  392. if (resultsList.Count > 0)
  393. {
  394. if (!string.IsNullOrEmpty (_options.ResultsFile))
  395. {
  396. var output = JsonSerializer.Serialize (
  397. resultsList,
  398. new JsonSerializerOptions ()
  399. {
  400. WriteIndented = true
  401. });
  402. using var file = File.CreateText (_options.ResultsFile);
  403. file.Write (output);
  404. file.Close ();
  405. return;
  406. }
  407. Application.Init ();
  408. var benchmarkWindow = new Window ()
  409. {
  410. Title = "Benchmark Results",
  411. };
  412. if (benchmarkWindow.Border is { })
  413. {
  414. benchmarkWindow.Border.Thickness = new (0, 0, 0, 0);
  415. }
  416. TableView resultsTableView = new ()
  417. {
  418. Width = Dim.Fill (),
  419. Height = Dim.Fill (),
  420. };
  421. // TableView provides many options for table headers. For simplicity we turn all
  422. // of these off. By enabling FullRowSelect and turning off headers, TableView looks just
  423. // like a ListView
  424. resultsTableView.FullRowSelect = true;
  425. resultsTableView.Style.ShowHeaders = true;
  426. resultsTableView.Style.ShowHorizontalHeaderOverline = false;
  427. resultsTableView.Style.ShowHorizontalHeaderUnderline = true;
  428. resultsTableView.Style.ShowHorizontalBottomline = false;
  429. resultsTableView.Style.ShowVerticalCellLines = true;
  430. resultsTableView.Style.ShowVerticalHeaderLines = true;
  431. /* By default TableView lays out columns at render time and only
  432. * measures y rows of data at a time. Where y is the height of the
  433. * console. This is for the following reasons:
  434. *
  435. * - Performance, when tables have a large amount of data
  436. * - Defensive, prevents a single wide cell value pushing other
  437. * columns off screen (requiring horizontal scrolling
  438. *
  439. * In the case of UICatalog here, such an approach is overkill so
  440. * we just measure all the data ourselves and set the appropriate
  441. * max widths as ColumnStyles
  442. */
  443. //int longestName = _scenarios!.Max (s => s.GetName ().Length);
  444. //resultsTableView.Style.ColumnStyles.Add (
  445. // 0,
  446. // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
  447. // );
  448. //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 });
  449. //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem;
  450. // TableView typically is a grid where nav keys are biased for moving left/right.
  451. resultsTableView.KeyBindings.Remove (Key.Home);
  452. resultsTableView.KeyBindings.Add (Key.Home, Command.Start);
  453. resultsTableView.KeyBindings.Remove (Key.End);
  454. resultsTableView.KeyBindings.Add (Key.End, Command.End);
  455. // Ideally, TableView.MultiSelect = false would turn off any keybindings for
  456. // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
  457. // a shortcut to About.
  458. resultsTableView.MultiSelect = false;
  459. var dt = new DataTable ();
  460. dt.Columns.Add (new DataColumn ("Scenario", typeof (string)));
  461. dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan)));
  462. dt.Columns.Add (new DataColumn ("Refreshed", typeof (int)));
  463. dt.Columns.Add (new DataColumn ("LaidOut", typeof (int)));
  464. dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int)));
  465. dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int)));
  466. dt.Columns.Add (new DataColumn ("Updated", typeof (int)));
  467. dt.Columns.Add (new DataColumn ("Iterations", typeof (int)));
  468. foreach (var r in resultsList)
  469. {
  470. dt.Rows.Add (
  471. r.Scenario,
  472. r.Duration,
  473. r.RefreshedCount,
  474. r.LaidOutCount,
  475. r.ClearedContentCount,
  476. r.DrawCompleteCount,
  477. r.UpdatedCount,
  478. r.IterationCount
  479. );
  480. }
  481. BenchmarkResults totalRow = new ()
  482. {
  483. Scenario = "TOTAL",
  484. Duration = new TimeSpan (resultsList.Sum (r => r.Duration.Ticks)),
  485. RefreshedCount = resultsList.Sum (r => r.RefreshedCount),
  486. LaidOutCount = resultsList.Sum (r => r.LaidOutCount),
  487. ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount),
  488. DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount),
  489. UpdatedCount = resultsList.Sum (r => r.UpdatedCount),
  490. IterationCount = resultsList.Sum (r => r.IterationCount),
  491. };
  492. dt.Rows.Add (
  493. totalRow.Scenario,
  494. totalRow.Duration,
  495. totalRow.RefreshedCount,
  496. totalRow.LaidOutCount,
  497. totalRow.ClearedContentCount,
  498. totalRow.DrawCompleteCount,
  499. totalRow.UpdatedCount,
  500. totalRow.IterationCount
  501. );
  502. dt.DefaultView.Sort = "Duration";
  503. DataTable sortedCopy = dt.DefaultView.ToTable ();
  504. resultsTableView.Table = new DataTableSource (sortedCopy);
  505. benchmarkWindow.Add (resultsTableView);
  506. Application.Run (benchmarkWindow);
  507. benchmarkWindow.Dispose ();
  508. Application.Shutdown ();
  509. }
  510. }
  511. private static void VerifyObjectsWereDisposed ()
  512. {
  513. #if DEBUG_IDISPOSABLE
  514. // Validate there are no outstanding Responder-based instances
  515. // after a scenario was selected to run. This proves the main UI Catalog
  516. // 'app' closed cleanly.
  517. foreach (View? inst in View.Instances)
  518. {
  519. Debug.Assert (inst.WasDisposed);
  520. }
  521. View.Instances.Clear ();
  522. // Validate there are no outstanding Application.RunState-based instances
  523. // after a scenario was selected to run. This proves the main UI Catalog
  524. // 'app' closed cleanly.
  525. foreach (RunState? inst in RunState.Instances)
  526. {
  527. Debug.Assert (inst.WasDisposed);
  528. }
  529. RunState.Instances.Clear ();
  530. #endif
  531. }
  532. /// <summary>
  533. /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on
  534. /// the command line) and each time a Scenario ends.
  535. /// </summary>
  536. public class UICatalogTopLevel : Toplevel
  537. {
  538. public ListView? CategoryList;
  539. public MenuItem? MiForce16Colors;
  540. public MenuItem? MiIsMenuBorderDisabled;
  541. public MenuItem? MiIsMouseDisabled;
  542. public MenuItem? MiUseSubMenusSingleFrame;
  543. public Shortcut? ShForce16Colors;
  544. //public Shortcut? ShDiagnostics;
  545. public Shortcut? ShVersion;
  546. // UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how
  547. // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView
  548. // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app.
  549. public TableView ScenarioList;
  550. private readonly StatusBar? _statusBar;
  551. private readonly CollectionNavigator _scenarioCollectionNav = new ();
  552. public UICatalogTopLevel ()
  553. {
  554. _diagnosticFlags = Diagnostics;
  555. _themeMenuItems = CreateThemeMenuItems ();
  556. _themeMenuBarItem = new ("_Themes", _themeMenuItems!);
  557. MenuBar menuBar = new ()
  558. {
  559. Menus =
  560. [
  561. new (
  562. "_File",
  563. new MenuItem []
  564. {
  565. new (
  566. "_Quit",
  567. "Quit UI Catalog",
  568. RequestStop
  569. )
  570. }
  571. ),
  572. _themeMenuBarItem,
  573. new ("Diag_nostics", CreateDiagnosticMenuItems ()),
  574. new (
  575. "_Help",
  576. new MenuItem []
  577. {
  578. new (
  579. "_Documentation",
  580. "",
  581. () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"),
  582. null,
  583. null,
  584. (KeyCode)Key.F1
  585. ),
  586. new (
  587. "_README",
  588. "",
  589. () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"),
  590. null,
  591. null,
  592. (KeyCode)Key.F2
  593. ),
  594. new (
  595. "_About...",
  596. "About UI Catalog",
  597. () => MessageBox.Query (
  598. title: "",
  599. message: GetAboutBoxMessage (),
  600. wrapMessage: false,
  601. buttons: "_Ok"
  602. ),
  603. null,
  604. null,
  605. (KeyCode)Key.A.WithCtrl
  606. )
  607. }
  608. )
  609. ]
  610. };
  611. _statusBar = new ()
  612. {
  613. Visible = ShowStatusBar,
  614. AlignmentModes = AlignmentModes.IgnoreFirstOrLast,
  615. CanFocus = false
  616. };
  617. _statusBar.Height = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: Dim.Func (() => _statusBar.Visible ? 1 : 0), maximumContentDim: Dim.Func (() => _statusBar.Visible ? 1 : 0));
  618. ShVersion = new ()
  619. {
  620. Title = "Version Info",
  621. CanFocus = false,
  622. };
  623. var statusBarShortcut = new Shortcut
  624. {
  625. Key = Key.F10,
  626. Title = "Show/Hide Status Bar",
  627. CanFocus = false,
  628. };
  629. statusBarShortcut.Accepting += (sender, args) =>
  630. {
  631. _statusBar.Visible = !_statusBar.Visible;
  632. args.Cancel = true;
  633. };
  634. ShForce16Colors = new ()
  635. {
  636. CanFocus = false,
  637. CommandView = new CheckBox
  638. {
  639. Title = "16 color mode",
  640. CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
  641. CanFocus = false
  642. },
  643. HelpText = "",
  644. KeyBindingScope = KeyBindingScope.Application,
  645. Key = Key.F7
  646. };
  647. ((CheckBox)ShForce16Colors.CommandView).CheckedStateChanging += (sender, args) =>
  648. {
  649. Application.Force16Colors = args.NewValue == CheckState.Checked;
  650. MiForce16Colors!.Checked = Application.Force16Colors;
  651. Application.LayoutAndDraw ();
  652. };
  653. _statusBar.Add (
  654. new Shortcut
  655. {
  656. CanFocus = false,
  657. Title = "Quit",
  658. Key = Application.QuitKey
  659. },
  660. statusBarShortcut,
  661. ShForce16Colors,
  662. //ShDiagnostics,
  663. ShVersion
  664. );
  665. // Create the Category list view. This list never changes.
  666. CategoryList = new ()
  667. {
  668. X = 0,
  669. Y = Pos.Bottom (menuBar),
  670. Width = Dim.Auto (),
  671. Height = Dim.Fill (Dim.Func (() =>
  672. {
  673. if (_statusBar.NeedsLayout)
  674. {
  675. // throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout.");
  676. }
  677. return _statusBar.Frame.Height;
  678. })),
  679. AllowsMarking = false,
  680. CanFocus = true,
  681. Title = "_Categories",
  682. BorderStyle = LineStyle.Rounded,
  683. SuperViewRendersLineCanvas = true,
  684. Source = new ListWrapper<string> (_categories)
  685. };
  686. CategoryList.OpenSelectedItem += (s, a) => { ScenarioList!.SetFocus (); };
  687. CategoryList.SelectedItemChanged += CategoryView_SelectedChanged;
  688. // This enables the scrollbar by causing lazy instantiation to happen
  689. CategoryList.VerticalScrollBar.AutoShow = true;
  690. // Create the scenario list. The contents of the scenario list changes whenever the
  691. // Category list selection changes (to show just the scenarios that belong to the selected
  692. // category).
  693. ScenarioList = new ()
  694. {
  695. X = Pos.Right (CategoryList) - 1,
  696. Y = Pos.Bottom (menuBar),
  697. Width = Dim.Fill (),
  698. Height = Dim.Fill (Dim.Func (() =>
  699. {
  700. if (_statusBar.NeedsLayout)
  701. {
  702. // throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout.");
  703. }
  704. return _statusBar.Frame.Height;
  705. })),
  706. //AllowsMarking = false,
  707. CanFocus = true,
  708. Title = "_Scenarios",
  709. BorderStyle = CategoryList.BorderStyle,
  710. SuperViewRendersLineCanvas = true
  711. };
  712. //ScenarioList.VerticalScrollBar.AutoHide = false;
  713. //ScenarioList.HorizontalScrollBar.AutoHide = false;
  714. // TableView provides many options for table headers. For simplicity we turn all
  715. // of these off. By enabling FullRowSelect and turning off headers, TableView looks just
  716. // like a ListView
  717. ScenarioList.FullRowSelect = true;
  718. ScenarioList.Style.ShowHeaders = false;
  719. ScenarioList.Style.ShowHorizontalHeaderOverline = false;
  720. ScenarioList.Style.ShowHorizontalHeaderUnderline = false;
  721. ScenarioList.Style.ShowHorizontalBottomline = false;
  722. ScenarioList.Style.ShowVerticalCellLines = false;
  723. ScenarioList.Style.ShowVerticalHeaderLines = false;
  724. /* By default TableView lays out columns at render time and only
  725. * measures y rows of data at a time. Where y is the height of the
  726. * console. This is for the following reasons:
  727. *
  728. * - Performance, when tables have a large amount of data
  729. * - Defensive, prevents a single wide cell value pushing other
  730. * columns off screen (requiring horizontal scrolling
  731. *
  732. * In the case of UICatalog here, such an approach is overkill so
  733. * we just measure all the data ourselves and set the appropriate
  734. * max widths as ColumnStyles
  735. */
  736. int longestName = _scenarios!.Max (s => s.GetName ().Length);
  737. ScenarioList.Style.ColumnStyles.Add (
  738. 0,
  739. new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
  740. );
  741. ScenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 });
  742. ScenarioList.CellActivated += ScenarioView_OpenSelectedItem;
  743. // TableView typically is a grid where nav keys are biased for moving left/right.
  744. ScenarioList.KeyBindings.Remove (Key.Home);
  745. ScenarioList.KeyBindings.Add (Key.Home, Command.Start);
  746. ScenarioList.KeyBindings.Remove (Key.End);
  747. ScenarioList.KeyBindings.Add (Key.End, Command.End);
  748. // Ideally, TableView.MultiSelect = false would turn off any keybindings for
  749. // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for
  750. // a shortcut to About.
  751. ScenarioList.MultiSelect = false;
  752. ScenarioList.KeyBindings.Remove (Key.A.WithCtrl);
  753. Add (menuBar);
  754. Add (CategoryList);
  755. Add (ScenarioList);
  756. Add (_statusBar);
  757. Loaded += LoadedHandler;
  758. Unloaded += UnloadedHandler;
  759. // Restore previous selections
  760. CategoryList.SelectedItem = _cachedCategoryIndex;
  761. ScenarioList.SelectedRow = _cachedScenarioIndex;
  762. Applied += ConfigAppliedHandler;
  763. }
  764. public void ConfigChanged ()
  765. {
  766. if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme))
  767. {
  768. _topLevelColorScheme = "Base";
  769. }
  770. _cachedTheme = Themes?.Theme;
  771. _themeMenuItems = CreateThemeMenuItems ();
  772. _themeMenuBarItem!.Children = _themeMenuItems;
  773. foreach (MenuItem mi in _themeMenuItems!)
  774. {
  775. if (mi is { Parent: null })
  776. {
  777. mi.Parent = _themeMenuBarItem;
  778. }
  779. }
  780. ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
  781. MenuBar!.Menus [0].Children! [0]!.ShortcutKey = Application.QuitKey;
  782. ((Shortcut)_statusBar!.Subviews [0]).Key = Application.QuitKey;
  783. _statusBar.Visible = ShowStatusBar;
  784. MiIsMouseDisabled!.Checked = Application.IsMouseDisabled;
  785. ((CheckBox)ShForce16Colors!.CommandView!).CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
  786. Application.Top!.SetNeedsDraw ();
  787. }
  788. public MenuItem []? CreateThemeMenuItems ()
  789. {
  790. List<MenuItem> menuItems = CreateForce16ColorItems ().ToList ();
  791. menuItems.Add (null!);
  792. var schemeCount = 0;
  793. foreach (KeyValuePair<string, ThemeScope> theme in Themes!)
  794. {
  795. var item = new MenuItem
  796. {
  797. Title = theme.Key == "Dark" ? $"{theme.Key.Substring (0, 3)}_{theme.Key.Substring (3, 1)}" : $"_{theme.Key}",
  798. ShortcutKey = new Key ((KeyCode)((uint)KeyCode.D1 + schemeCount++))
  799. .WithCtrl
  800. };
  801. item.CheckType |= MenuItemCheckStyle.Checked;
  802. item.Checked = theme.Key == _cachedTheme; // CM.Themes.Theme;
  803. item.Action += () =>
  804. {
  805. Themes.Theme = _cachedTheme = theme.Key;
  806. Apply ();
  807. };
  808. menuItems.Add (item);
  809. }
  810. List<MenuItem> schemeMenuItems = new ();
  811. foreach (KeyValuePair<string, ColorScheme?> sc in Colors.ColorSchemes)
  812. {
  813. var item = new MenuItem { Title = $"_{sc.Key}", Data = sc.Key };
  814. item.CheckType |= MenuItemCheckStyle.Radio;
  815. item.Checked = sc.Key == _topLevelColorScheme;
  816. item.Action += () =>
  817. {
  818. _topLevelColorScheme = (string)item.Data;
  819. foreach (MenuItem schemeMenuItem in schemeMenuItems)
  820. {
  821. schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme;
  822. }
  823. ColorScheme = Colors.ColorSchemes [_topLevelColorScheme];
  824. };
  825. item.ShortcutKey = ((Key)sc.Key [0].ToString ().ToLower ()).WithCtrl;
  826. schemeMenuItems.Add (item);
  827. }
  828. menuItems.Add (null!);
  829. var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ());
  830. menuItems.Add (mbi);
  831. return menuItems.ToArray ();
  832. }
  833. private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e)
  834. {
  835. string item = _categories! [e!.Item];
  836. ObservableCollection<Scenario> newlist;
  837. if (e.Item == 0)
  838. {
  839. // First category is "All"
  840. newlist = _scenarios!;
  841. }
  842. else
  843. {
  844. newlist = new (_scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ());
  845. }
  846. ScenarioList.Table = new EnumerableTableSource<Scenario> (
  847. newlist,
  848. new ()
  849. {
  850. { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () }
  851. }
  852. );
  853. // Create a collection of just the scenario names (the 1st column in our TableView)
  854. // for CollectionNavigator.
  855. List<object> firstColumnList = new ();
  856. for (var i = 0; i < ScenarioList.Table.Rows; i++)
  857. {
  858. firstColumnList.Add (ScenarioList.Table [i, 0]);
  859. }
  860. _scenarioCollectionNav.Collection = firstColumnList;
  861. }
  862. private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); }
  863. [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
  864. private MenuItem [] CreateDiagnosticFlagsMenuItems ()
  865. {
  866. const string OFF = "View Diagnostics: _Off";
  867. const string RULER = "View Diagnostics: _Ruler";
  868. const string THICKNESS = "View Diagnostics: _Thickness";
  869. const string HOVER = "View Diagnostics: _Hover";
  870. const string DRAWINDICATOR = "View Diagnostics: _DrawIndicator";
  871. var index = 0;
  872. List<MenuItem> menuItems = new ();
  873. foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ()))
  874. {
  875. var item = new MenuItem
  876. {
  877. Title = GetDiagnosticsTitle (diag), ShortcutKey = new Key (index.ToString () [0]).WithAlt
  878. };
  879. index++;
  880. item.CheckType |= MenuItemCheckStyle.Checked;
  881. if (GetDiagnosticsTitle (ViewDiagnosticFlags.Off) == item.Title)
  882. {
  883. item.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness)
  884. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler)
  885. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover)
  886. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.DrawIndicator);
  887. }
  888. else
  889. {
  890. item.Checked = _diagnosticFlags.HasFlag (diag);
  891. }
  892. item.Action += () =>
  893. {
  894. string t = GetDiagnosticsTitle (ViewDiagnosticFlags.Off);
  895. if (item.Title == t && item.Checked == false)
  896. {
  897. _diagnosticFlags &= ~(ViewDiagnosticFlags.Thickness | ViewDiagnosticFlags.Ruler | ViewDiagnosticFlags.Hover | ViewDiagnosticFlags.DrawIndicator);
  898. item.Checked = true;
  899. }
  900. else if (item.Title == t && item.Checked == true)
  901. {
  902. _diagnosticFlags |= ViewDiagnosticFlags.Thickness | ViewDiagnosticFlags.Ruler | ViewDiagnosticFlags.Hover | ViewDiagnosticFlags.DrawIndicator;
  903. item.Checked = false;
  904. }
  905. else
  906. {
  907. Enum f = GetDiagnosticsEnumValue (item.Title);
  908. if (_diagnosticFlags.HasFlag (f))
  909. {
  910. SetDiagnosticsFlag (f, false);
  911. }
  912. else
  913. {
  914. SetDiagnosticsFlag (f, true);
  915. }
  916. }
  917. foreach (MenuItem menuItem in menuItems)
  918. {
  919. if (menuItem.Title == t)
  920. {
  921. menuItem.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler)
  922. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness)
  923. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover)
  924. && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.DrawIndicator);
  925. }
  926. else if (menuItem.Title != t)
  927. {
  928. menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title));
  929. }
  930. }
  931. Diagnostics = _diagnosticFlags;
  932. };
  933. menuItems.Add (item);
  934. }
  935. return menuItems.ToArray ();
  936. string GetDiagnosticsTitle (Enum diag)
  937. {
  938. return Enum.GetName (_diagnosticFlags.GetType (), diag) switch
  939. {
  940. "Off" => OFF,
  941. "Ruler" => RULER,
  942. "Thickness" => THICKNESS,
  943. "Hover" => HOVER,
  944. "DrawIndicator" => DRAWINDICATOR,
  945. _ => ""
  946. };
  947. }
  948. Enum GetDiagnosticsEnumValue (string? title)
  949. {
  950. return title switch
  951. {
  952. RULER => ViewDiagnosticFlags.Ruler,
  953. THICKNESS => ViewDiagnosticFlags.Thickness,
  954. HOVER => ViewDiagnosticFlags.Hover,
  955. DRAWINDICATOR => ViewDiagnosticFlags.DrawIndicator,
  956. _ => null!
  957. };
  958. }
  959. void SetDiagnosticsFlag (Enum diag, bool add)
  960. {
  961. switch (diag)
  962. {
  963. case ViewDiagnosticFlags.Ruler:
  964. if (add)
  965. {
  966. _diagnosticFlags |= ViewDiagnosticFlags.Ruler;
  967. }
  968. else
  969. {
  970. _diagnosticFlags &= ~ViewDiagnosticFlags.Ruler;
  971. }
  972. break;
  973. case ViewDiagnosticFlags.Thickness:
  974. if (add)
  975. {
  976. _diagnosticFlags |= ViewDiagnosticFlags.Thickness;
  977. }
  978. else
  979. {
  980. _diagnosticFlags &= ~ViewDiagnosticFlags.Thickness;
  981. }
  982. break;
  983. case ViewDiagnosticFlags.Hover:
  984. if (add)
  985. {
  986. _diagnosticFlags |= ViewDiagnosticFlags.Hover;
  987. }
  988. else
  989. {
  990. _diagnosticFlags &= ~ViewDiagnosticFlags.Hover;
  991. }
  992. break;
  993. case ViewDiagnosticFlags.DrawIndicator:
  994. if (add)
  995. {
  996. _diagnosticFlags |= ViewDiagnosticFlags.DrawIndicator;
  997. }
  998. else
  999. {
  1000. _diagnosticFlags &= ~ViewDiagnosticFlags.DrawIndicator;
  1001. }
  1002. break;
  1003. default:
  1004. _diagnosticFlags = default (ViewDiagnosticFlags);
  1005. break;
  1006. }
  1007. }
  1008. }
  1009. private List<MenuItem []> CreateDiagnosticMenuItems ()
  1010. {
  1011. List<MenuItem []> menuItems = new ()
  1012. {
  1013. CreateDiagnosticFlagsMenuItems (),
  1014. new MenuItem [] { null! },
  1015. CreateDisabledEnabledMouseItems (),
  1016. CreateDisabledEnabledMenuBorder (),
  1017. CreateDisabledEnableUseSubMenusSingleFrame (),
  1018. CreateKeyBindingsMenuItems ()
  1019. };
  1020. return menuItems;
  1021. }
  1022. // TODO: This should be an ConfigurationManager setting
  1023. private MenuItem [] CreateDisabledEnabledMenuBorder ()
  1024. {
  1025. List<MenuItem> menuItems = new ();
  1026. MiIsMenuBorderDisabled = new () { Title = "Disable Menu _Border" };
  1027. MiIsMenuBorderDisabled.ShortcutKey =
  1028. new Key (MiIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt.WithCtrl.NoShift;
  1029. MiIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked;
  1030. MiIsMenuBorderDisabled.Action += () =>
  1031. {
  1032. MiIsMenuBorderDisabled.Checked = (bool)!MiIsMenuBorderDisabled.Checked!;
  1033. MenuBar!.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked
  1034. ? LineStyle.Single
  1035. : LineStyle.None;
  1036. };
  1037. menuItems.Add (MiIsMenuBorderDisabled);
  1038. return menuItems.ToArray ();
  1039. }
  1040. private MenuItem [] CreateDisabledEnabledMouseItems ()
  1041. {
  1042. List<MenuItem> menuItems = new ();
  1043. MiIsMouseDisabled = new () { Title = "_Disable Mouse" };
  1044. MiIsMouseDisabled.ShortcutKey =
  1045. new Key (MiIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl.NoShift;
  1046. MiIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked;
  1047. MiIsMouseDisabled.Action += () =>
  1048. {
  1049. MiIsMouseDisabled.Checked =
  1050. Application.IsMouseDisabled = (bool)!MiIsMouseDisabled.Checked!;
  1051. };
  1052. menuItems.Add (MiIsMouseDisabled);
  1053. return menuItems.ToArray ();
  1054. }
  1055. // TODO: This should be an ConfigurationManager setting
  1056. private MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame ()
  1057. {
  1058. List<MenuItem> menuItems = new ();
  1059. MiUseSubMenusSingleFrame = new () { Title = "Enable _Sub-Menus Single Frame" };
  1060. MiUseSubMenusSingleFrame.ShortcutKey = KeyCode.CtrlMask
  1061. | KeyCode.AltMask
  1062. | (KeyCode)MiUseSubMenusSingleFrame!.Title!.Substring (8, 1) [
  1063. 0];
  1064. MiUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked;
  1065. MiUseSubMenusSingleFrame.Action += () =>
  1066. {
  1067. MiUseSubMenusSingleFrame.Checked = (bool)!MiUseSubMenusSingleFrame.Checked!;
  1068. MenuBar!.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked;
  1069. };
  1070. menuItems.Add (MiUseSubMenusSingleFrame);
  1071. return menuItems.ToArray ();
  1072. }
  1073. private MenuItem [] CreateForce16ColorItems ()
  1074. {
  1075. List<MenuItem> menuItems = new ();
  1076. MiForce16Colors = new ()
  1077. {
  1078. Title = "Force _16 Colors",
  1079. ShortcutKey = Key.F6,
  1080. Checked = Application.Force16Colors,
  1081. CanExecute = () => Application.Driver?.SupportsTrueColor ?? false
  1082. };
  1083. MiForce16Colors.CheckType |= MenuItemCheckStyle.Checked;
  1084. MiForce16Colors.Action += () =>
  1085. {
  1086. MiForce16Colors.Checked = Application.Force16Colors = (bool)!MiForce16Colors.Checked!;
  1087. ((CheckBox)ShForce16Colors!.CommandView!).CheckedState =
  1088. Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
  1089. Application.LayoutAndDraw ();
  1090. };
  1091. menuItems.Add (MiForce16Colors);
  1092. return menuItems.ToArray ();
  1093. }
  1094. private MenuItem [] CreateKeyBindingsMenuItems ()
  1095. {
  1096. List<MenuItem> menuItems = new ();
  1097. var item = new MenuItem { Title = "_Key Bindings", Help = "Change which keys do what" };
  1098. item.Action += () =>
  1099. {
  1100. var dlg = new KeyBindingsDialog ();
  1101. Application.Run (dlg);
  1102. dlg.Dispose ();
  1103. };
  1104. menuItems.Add (null!);
  1105. menuItems.Add (item);
  1106. return menuItems.ToArray ();
  1107. }
  1108. private void LoadedHandler (object? sender, EventArgs? args)
  1109. {
  1110. ConfigChanged ();
  1111. MiIsMouseDisabled!.Checked = Application.IsMouseDisabled;
  1112. if (ShVersion is { })
  1113. {
  1114. ShVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver!.GetVersionInfo ()}";
  1115. }
  1116. if (_selectedScenario != null)
  1117. {
  1118. _selectedScenario = null;
  1119. _isFirstRunning = false;
  1120. }
  1121. if (!_isFirstRunning)
  1122. {
  1123. ScenarioList.SetFocus ();
  1124. }
  1125. if (_statusBar is { })
  1126. {
  1127. _statusBar.VisibleChanged += (s, e) =>
  1128. {
  1129. ShowStatusBar = _statusBar.Visible;
  1130. };
  1131. }
  1132. Loaded -= LoadedHandler;
  1133. CategoryList!.EnsureSelectedItemVisible ();
  1134. ScenarioList.EnsureSelectedCellIsVisible ();
  1135. }
  1136. /// <summary>Launches the selected scenario, setting the global _selectedScenario</summary>
  1137. /// <param name="e"></param>
  1138. private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e)
  1139. {
  1140. if (_selectedScenario is null)
  1141. {
  1142. // Save selected item state
  1143. _cachedCategoryIndex = CategoryList!.SelectedItem;
  1144. _cachedScenarioIndex = ScenarioList.SelectedRow;
  1145. // Create new instance of scenario (even though Scenarios contains instances)
  1146. var selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0];
  1147. _selectedScenario = (Scenario)Activator.CreateInstance (
  1148. _scenarios!.FirstOrDefault (
  1149. s => s.GetName ()
  1150. == selectedScenarioName
  1151. )!
  1152. .GetType ()
  1153. )!;
  1154. // Tell the main app to stop
  1155. Application.RequestStop ();
  1156. }
  1157. }
  1158. private void UnloadedHandler (object? sender, EventArgs? args)
  1159. {
  1160. Applied -= ConfigAppliedHandler;
  1161. Unloaded -= UnloadedHandler;
  1162. Dispose ();
  1163. }
  1164. }
  1165. private struct Options
  1166. {
  1167. public string Driver;
  1168. public string Scenario;
  1169. public uint BenchmarkTimeout;
  1170. public bool Benchmark;
  1171. public string ResultsFile;
  1172. /* etc. */
  1173. }
  1174. }