UICatalog.cs 43 KB

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