UICatalog.cs 55 KB

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