UICatalog.cs 46 KB

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