Menus.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. #nullable enable
  2. using System.Collections.ObjectModel;
  3. using System.Diagnostics;
  4. using Microsoft.Extensions.Logging;
  5. using Serilog;
  6. using Serilog.Core;
  7. using Serilog.Events;
  8. using Terminal.Gui;
  9. using ILogger = Microsoft.Extensions.Logging.ILogger;
  10. namespace UICatalog.Scenarios;
  11. [ScenarioMetadata ("Menus", "Illustrates MenuBar, Menu, and MenuItem")]
  12. [ScenarioCategory ("Controls")]
  13. [ScenarioCategory ("Menus")]
  14. [ScenarioCategory ("Shortcuts")]
  15. public class Menus : Scenario
  16. {
  17. public override void Main ()
  18. {
  19. Logging.Logger = CreateLogger ();
  20. Application.Init ();
  21. Toplevel app = new ();
  22. app.Title = GetQuitKeyAndName ();
  23. ObservableCollection<string> eventSource = new ();
  24. var eventLog = new ListView
  25. {
  26. Title = "Event Log",
  27. X = Pos.AnchorEnd (),
  28. Width = Dim.Auto (),
  29. Height = Dim.Fill (), // Make room for some wide things
  30. ColorScheme = Colors.ColorSchemes ["Toplevel"],
  31. Source = new ListWrapper<string> (eventSource)
  32. };
  33. eventLog.Border!.Thickness = new (0, 1, 0, 0);
  34. MenuHost menuHostView = new ()
  35. {
  36. Id = "menuHostView",
  37. Title = $"Menu Host - Use {PopoverMenu.DefaultKey} for Popover Menu",
  38. X = 0,
  39. Y = 0,
  40. Width = Dim.Fill ()! - Dim.Width (eventLog),
  41. Height = Dim.Fill (),
  42. BorderStyle = LineStyle.Dotted
  43. };
  44. app.Add (menuHostView);
  45. menuHostView.CommandNotBound += (o, args) =>
  46. {
  47. if (o is not View sender || args.Handled)
  48. {
  49. return;
  50. }
  51. Logging.Debug ($"{sender.Id} CommandNotBound: {args?.Context?.Command}");
  52. eventSource.Add ($"{sender.Id} CommandNotBound: {args?.Context?.Command}");
  53. eventLog.MoveDown ();
  54. };
  55. menuHostView.Accepting += (o, args) =>
  56. {
  57. if (o is not View sender || args.Handled)
  58. {
  59. return;
  60. }
  61. Logging.Debug ($"{sender.Id} Accepting: {args?.Context?.Source?.Title}");
  62. eventSource.Add ($"{sender.Id} Accepting: {args?.Context?.Source?.Title}: ");
  63. eventLog.MoveDown ();
  64. };
  65. menuHostView.ContextMenu!.Accepted += (o, args) =>
  66. {
  67. if (o is not View sender || args.Handled)
  68. {
  69. return;
  70. }
  71. Logging.Debug ($"{sender.Id} Accepted: {args?.Context?.Source?.Text}");
  72. eventSource.Add ($"{sender.Id} Accepted: {args?.Context?.Source?.Text}: ");
  73. eventLog.MoveDown ();
  74. };
  75. app.Add (eventLog);
  76. Application.Run (app);
  77. app.Dispose ();
  78. Application.Shutdown ();
  79. }
  80. /// <summary>
  81. /// A demo view class that contains a menu bar and a popover menu.
  82. /// </summary>
  83. public class MenuHost : View
  84. {
  85. internal PopoverMenu? ContextMenu { get; private set; }
  86. public MenuHost ()
  87. {
  88. CanFocus = true;
  89. BorderStyle = LineStyle.Dashed;
  90. AddCommand (
  91. Command.Context,
  92. ctx =>
  93. {
  94. ContextMenu?.MakeVisible ();
  95. return true;
  96. });
  97. MouseBindings.ReplaceCommands (MouseFlags.Button3Clicked, Command.Context);
  98. KeyBindings.Add (PopoverMenu.DefaultKey, Command.Context);
  99. AddCommand (
  100. Command.Cancel,
  101. ctx =>
  102. {
  103. if (Application.Popover?.GetActivePopover () as PopoverMenu is { Visible: true } visiblePopover)
  104. {
  105. visiblePopover.Visible = false;
  106. }
  107. return true;
  108. });
  109. MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked, Command.Cancel);
  110. Label lastCommandLabel = new ()
  111. {
  112. Title = "_Last Command:",
  113. X = 15,
  114. Y = 10
  115. };
  116. View lastCommandText = new ()
  117. {
  118. X = Pos.Right (lastCommandLabel) + 1,
  119. Y = Pos.Top (lastCommandLabel),
  120. Height = Dim.Auto (),
  121. Width = Dim.Auto ()
  122. };
  123. Add (lastCommandLabel, lastCommandText);
  124. AddCommand (Command.New, HandleCommand);
  125. HotKeyBindings.Add (Key.F2, Command.New);
  126. AddCommand (Command.Open, HandleCommand);
  127. HotKeyBindings.Add (Key.F3, Command.Open);
  128. AddCommand (Command.Save, HandleCommand);
  129. HotKeyBindings.Add (Key.F4, Command.Save);
  130. AddCommand (Command.SaveAs, HandleCommand);
  131. HotKeyBindings.Add (Key.A.WithCtrl, Command.SaveAs);
  132. AddCommand (
  133. Command.Quit,
  134. ctx =>
  135. {
  136. Logging.Debug ("MenuHost Command.Quit - RequestStop");
  137. Application.RequestStop ();
  138. return true;
  139. });
  140. HotKeyBindings.Add (Application.QuitKey, Command.Quit);
  141. AddCommand (Command.Cut, HandleCommand);
  142. HotKeyBindings.Add (Key.X.WithCtrl, Command.Cut);
  143. AddCommand (Command.Copy, HandleCommand);
  144. HotKeyBindings.Add (Key.C.WithCtrl, Command.Copy);
  145. AddCommand (Command.Paste, HandleCommand);
  146. HotKeyBindings.Add (Key.V.WithCtrl, Command.Paste);
  147. AddCommand (Command.SelectAll, HandleCommand);
  148. HotKeyBindings.Add (Key.T.WithCtrl, Command.SelectAll);
  149. // BUGBUG: This must come before we create the MenuBar or it will not work.
  150. // BUGBUG: This is due to TODO's in PopoverMenu where key bindings are not
  151. // BUGBUG: updated after the MenuBar is created.
  152. Application.KeyBindings.Remove (Key.F5);
  153. Application.KeyBindings.Add (Key.F5, this, Command.Edit);
  154. var menuBar = new MenuBarv2
  155. {
  156. Title = "MenuHost MenuBar"
  157. };
  158. MenuHost host = this;
  159. menuBar.EnableForDesign (ref host);
  160. base.Add (menuBar);
  161. Label lastAcceptedLabel = new ()
  162. {
  163. Title = "Last Accepted:",
  164. X = Pos.Left (lastCommandLabel),
  165. Y = Pos.Bottom (lastCommandLabel)
  166. };
  167. View lastAcceptedText = new ()
  168. {
  169. X = Pos.Right (lastAcceptedLabel) + 1,
  170. Y = Pos.Top (lastAcceptedLabel),
  171. Height = Dim.Auto (),
  172. Width = Dim.Auto ()
  173. };
  174. Add (lastAcceptedLabel, lastAcceptedText);
  175. // MenuItem: AutoSave - Demos simple CommandView state tracking
  176. // In MenuBar.EnableForDesign, the auto save MenuItem does not specify a Command. But does
  177. // set a Key (F10). MenuBar adds this key as a hotkey and thus if it's pressed, it toggles the MenuItem
  178. // CB.
  179. // So that is needed is to mirror the two check boxes.
  180. var autoSaveMenuItemCb = menuBar.GetMenuItemsWithTitle ("_Auto Save").FirstOrDefault ()?.CommandView as CheckBox;
  181. Debug.Assert (autoSaveMenuItemCb is { });
  182. CheckBox autoSaveStatusCb = new ()
  183. {
  184. Title = "AutoSave Status (MenuItem Binding to F10)",
  185. X = Pos.Left (lastAcceptedLabel),
  186. Y = Pos.Bottom (lastAcceptedLabel)
  187. };
  188. autoSaveStatusCb.CheckedStateChanged += (_, _) => { autoSaveMenuItemCb!.CheckedState = autoSaveStatusCb.CheckedState; };
  189. if (autoSaveMenuItemCb is { })
  190. {
  191. autoSaveMenuItemCb.CheckedStateChanged += (_, _) => { autoSaveStatusCb!.CheckedState = autoSaveMenuItemCb.CheckedState; };
  192. }
  193. base.Add (autoSaveStatusCb);
  194. // MenuItem: Enable Overwrite - Demos View Key Binding
  195. // In MenuBar.EnableForDesign, the overwrite MenuItem specifies a Command (Command.EnableOverwrite).
  196. // Ctrl+W is bound to Command.EnableOverwrite by this View.
  197. // Thus when Ctrl+W is pressed the MenuBar never sees it, but the command is invoked on this.
  198. // If the user clicks on the MenuItem, Accept will be raised.
  199. CheckBox enableOverwriteStatusCb = new ()
  200. {
  201. Title = "Enable Overwrite (View Binding to Ctrl+W)",
  202. X = Pos.Left (autoSaveStatusCb),
  203. Y = Pos.Bottom (autoSaveStatusCb)
  204. };
  205. // The source of truth is our status CB; any time it changes, update the menu item
  206. var enableOverwriteMenuItemCb = menuBar.GetMenuItemsWithTitle ("Overwrite").FirstOrDefault ()?.CommandView as CheckBox;
  207. enableOverwriteStatusCb.CheckedStateChanged += (_, _) => enableOverwriteMenuItemCb!.CheckedState = enableOverwriteStatusCb.CheckedState;
  208. menuBar.Accepted += (o, args) =>
  209. {
  210. if (args.Context?.Source is MenuItemv2 mi && mi.CommandView == enableOverwriteMenuItemCb)
  211. {
  212. Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}");
  213. // Set Cancel to true to stop propagation of Accepting to superview
  214. args.Handled = true;
  215. // Since overwrite uses a MenuItem.Command the menu item CB is the source of truth
  216. enableOverwriteStatusCb.CheckedState = ((CheckBox)mi.CommandView).CheckedState;
  217. lastAcceptedText.Text = args?.Context?.Source?.Title!;
  218. }
  219. };
  220. HotKeyBindings.Add (Key.W.WithCtrl, Command.EnableOverwrite);
  221. AddCommand (
  222. Command.EnableOverwrite,
  223. ctx =>
  224. {
  225. // The command was invoked. Toggle the status Cb.
  226. enableOverwriteStatusCb.AdvanceCheckState ();
  227. return HandleCommand (ctx);
  228. });
  229. base.Add (enableOverwriteStatusCb);
  230. // MenuItem: EditMode - Demos App Level Key Bindings
  231. // In MenuBar.EnableForDesign, the edit mode MenuItem specifies a Command (Command.Edit).
  232. // F5 is bound to Command.EnableOverwrite as an Applicatio-Level Key Binding
  233. // Thus when F5 is pressed the MenuBar never sees it, but the command is invoked on this, via
  234. // a Application.KeyBinding.
  235. // If the user clicks on the MenuItem, Accept will be raised.
  236. CheckBox editModeStatusCb = new ()
  237. {
  238. Title = "EditMode (App Binding to F5)",
  239. X = Pos.Left (enableOverwriteStatusCb),
  240. Y = Pos.Bottom (enableOverwriteStatusCb)
  241. };
  242. // The source of truth is our status CB; any time it changes, update the menu item
  243. var editModeMenuItemCb = menuBar.GetMenuItemsWithTitle ("EditMode").FirstOrDefault ()?.CommandView as CheckBox;
  244. editModeStatusCb.CheckedStateChanged += (_, _) => editModeMenuItemCb!.CheckedState = editModeStatusCb.CheckedState;
  245. menuBar.Accepted += (o, args) =>
  246. {
  247. if (args.Context?.Source is MenuItemv2 mi && mi.CommandView == editModeMenuItemCb)
  248. {
  249. Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}");
  250. // Set Cancel to true to stop propagation of Accepting to superview
  251. args.Handled = true;
  252. // Since overwrite uses a MenuItem.Command the menu item CB is the source of truth
  253. editModeMenuItemCb.CheckedState = ((CheckBox)mi.CommandView).CheckedState;
  254. lastAcceptedText.Text = args?.Context?.Source?.Title!;
  255. }
  256. };
  257. AddCommand (
  258. Command.Edit,
  259. ctx =>
  260. {
  261. // The command was invoked. Toggle the status Cb.
  262. editModeStatusCb.AdvanceCheckState ();
  263. return HandleCommand (ctx);
  264. });
  265. base.Add (editModeStatusCb);
  266. // Set up the Context Menu
  267. ContextMenu = new ()
  268. {
  269. Title = "ContextMenu",
  270. Id = "ContextMenu"
  271. };
  272. ContextMenu.EnableForDesign (ref host);
  273. ContextMenu.Visible = false;
  274. // Demo of PopoverMenu as a context menu
  275. // If we want Commands from the ContextMenu to be handled by the MenuHost
  276. // we need to subscribe to the ContextMenu's Accepted event.
  277. ContextMenu!.Accepted += (o, args) =>
  278. {
  279. Logging.Debug ($"ContextMenu.Accepted: {args.Context?.Source?.Title}");
  280. // Forward the event to the MenuHost
  281. if (args.Context is { })
  282. {
  283. //InvokeCommand (args.Context.Command);
  284. }
  285. };
  286. ContextMenu!.VisibleChanged += (sender, args) =>
  287. {
  288. if (ContextMenu!.Visible)
  289. { }
  290. };
  291. // Add a button to open the contextmenu
  292. var openBtn = new Button { X = Pos.Center (), Y = 4, Text = "_Open Menu", IsDefault = true };
  293. openBtn.Accepting += (s, e) =>
  294. {
  295. e.Handled = true;
  296. Logging.Trace ($"openBtn.Accepting - Sending F9. {e.Context?.Source?.Title}");
  297. NewKeyDownEvent (menuBar.Key);
  298. };
  299. Add (openBtn);
  300. //var hideBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (openBtn), Text = "Toggle Menu._Visible" };
  301. //hideBtn.Accepting += (s, e) => { menuBar.Visible = !menuBar.Visible; };
  302. //appWindow.Add (hideBtn);
  303. //var enableBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (hideBtn), Text = "_Toggle Menu.Enable" };
  304. //enableBtn.Accepting += (s, e) => { menuBar.Enabled = !menuBar.Enabled; };
  305. //appWindow.Add (enableBtn);
  306. autoSaveStatusCb.SetFocus ();
  307. return;
  308. // Add the commands supported by this View
  309. bool? HandleCommand (ICommandContext? ctx)
  310. {
  311. lastCommandText.Text = ctx?.Command!.ToString ()!;
  312. Logging.Debug ($"lastCommand: {lastCommandText.Text}");
  313. return true;
  314. }
  315. }
  316. /// <inheritdoc/>
  317. protected override void Dispose (bool disposing)
  318. {
  319. if (ContextMenu is { })
  320. {
  321. ContextMenu.Dispose ();
  322. ContextMenu = null;
  323. }
  324. base.Dispose (disposing);
  325. }
  326. }
  327. private const string LOGFILE_LOCATION = "./logs";
  328. private static readonly string _logFilePath = string.Empty;
  329. private static readonly LoggingLevelSwitch _logLevelSwitch = new ();
  330. private static ILogger CreateLogger ()
  331. {
  332. // Configure Serilog to write logs to a file
  333. _logLevelSwitch.MinimumLevel = LogEventLevel.Verbose;
  334. Log.Logger = new LoggerConfiguration ()
  335. .MinimumLevel.ControlledBy (_logLevelSwitch)
  336. .Enrich.FromLogContext () // Enables dynamic enrichment
  337. .WriteTo.Debug ()
  338. .WriteTo.File (
  339. _logFilePath,
  340. rollingInterval: RollingInterval.Day,
  341. outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
  342. .CreateLogger ();
  343. // Create a logger factory compatible with Microsoft.Extensions.Logging
  344. using ILoggerFactory loggerFactory = LoggerFactory.Create (
  345. builder =>
  346. {
  347. builder
  348. .AddSerilog (dispose: true) // Integrate Serilog with ILogger
  349. .SetMinimumLevel (LogLevel.Trace); // Set minimum log level
  350. });
  351. // Get an ILogger instance
  352. return loggerFactory.CreateLogger ("Global Logger");
  353. }
  354. }