Menus.cs 19 KB

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