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