UICatalog.cs 45 KB

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