Menus.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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 += (_, _) => enableOverwriteMenuItemCb!.CheckedState = enableOverwriteStatusCb.CheckedState;
  207. menuBar.Accepted += (o, args) =>
  208. {
  209. if (args.Context?.Source is MenuItem mi && mi.CommandView == enableOverwriteMenuItemCb)
  210. {
  211. Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}");
  212. // Set Cancel to true to stop propagation of Accepting to superview
  213. args.Handled = true;
  214. // Since overwrite uses a MenuItem.Command the menu item CB is the source of truth
  215. enableOverwriteStatusCb.CheckedState = ((CheckBox)mi.CommandView).CheckedState;
  216. lastAcceptedText.Text = args?.Context?.Source?.Title!;
  217. }
  218. };
  219. HotKeyBindings.Add (Key.W.WithCtrl, Command.EnableOverwrite);
  220. AddCommand (
  221. Command.EnableOverwrite,
  222. ctx =>
  223. {
  224. // The command was invoked. Toggle the status Cb.
  225. enableOverwriteStatusCb.AdvanceCheckState ();
  226. return HandleCommand (ctx);
  227. });
  228. base.Add (enableOverwriteStatusCb);
  229. // MenuItem: EditMode - Demos App Level Key Bindings
  230. // In MenuBar.EnableForDesign, the edit mode MenuItem specifies a Command (Command.Edit).
  231. // F5 is bound to Command.EnableOverwrite as an Applicatio-Level Key Binding
  232. // Thus when F5 is pressed the MenuBar never sees it, but the command is invoked on this, via
  233. // a Application.KeyBinding.
  234. // If the user clicks on the MenuItem, Accept will be raised.
  235. CheckBox editModeStatusCb = new ()
  236. {
  237. Title = "EditMode (App Binding to F5)",
  238. X = Pos.Left (enableOverwriteStatusCb),
  239. Y = Pos.Bottom (enableOverwriteStatusCb)
  240. };
  241. // The source of truth is our status CB; any time it changes, update the menu item
  242. var editModeMenuItemCb = menuBar.GetMenuItemsWithTitle ("EditMode").FirstOrDefault ()?.CommandView as CheckBox;
  243. editModeStatusCb.CheckedStateChanged += (_, _) => editModeMenuItemCb!.CheckedState = editModeStatusCb.CheckedState;
  244. menuBar.Accepted += (o, args) =>
  245. {
  246. if (args.Context?.Source is MenuItem mi && mi.CommandView == editModeMenuItemCb)
  247. {
  248. Logging.Debug ($"menuBar.Accepted: {args.Context.Source?.Title}");
  249. // Set Cancel to true to stop propagation of Accepting to superview
  250. args.Handled = true;
  251. // Since overwrite uses a MenuItem.Command the menu item CB is the source of truth
  252. editModeMenuItemCb.CheckedState = ((CheckBox)mi.CommandView).CheckedState;
  253. lastAcceptedText.Text = args?.Context?.Source?.Title!;
  254. }
  255. };
  256. AddCommand (
  257. Command.Edit,
  258. ctx =>
  259. {
  260. // The command was invoked. Toggle the status Cb.
  261. editModeStatusCb.AdvanceCheckState ();
  262. return HandleCommand (ctx);
  263. });
  264. base.Add (editModeStatusCb);
  265. // Set up the Context Menu
  266. ContextMenu = new ()
  267. {
  268. Title = "ContextMenu",
  269. Id = "ContextMenu"
  270. };
  271. ContextMenu.EnableForDesign (ref host);
  272. ContextMenu.Visible = false;
  273. // Demo of PopoverMenu as a context menu
  274. // If we want Commands from the ContextMenu to be handled by the MenuHost
  275. // we need to subscribe to the ContextMenu's Accepted event.
  276. ContextMenu!.Accepted += (o, args) =>
  277. {
  278. Logging.Debug ($"ContextMenu.Accepted: {args.Context?.Source?.Title}");
  279. // Forward the event to the MenuHost
  280. if (args.Context is { })
  281. {
  282. //InvokeCommand (args.Context.Command);
  283. }
  284. };
  285. ContextMenu!.VisibleChanged += (sender, args) =>
  286. {
  287. if (ContextMenu!.Visible)
  288. { }
  289. };
  290. // Add a button to open the contextmenu
  291. var openBtn = new Button { X = Pos.Center (), Y = 4, Text = "_Open Menu", IsDefault = true };
  292. openBtn.Accepting += (s, e) =>
  293. {
  294. e.Handled = true;
  295. Logging.Trace ($"openBtn.Accepting - Sending F9. {e.Context?.Source?.Title}");
  296. NewKeyDownEvent (menuBar.Key);
  297. };
  298. Add (openBtn);
  299. //var hideBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (openBtn), Text = "Toggle Menu._Visible" };
  300. //hideBtn.Accepting += (s, e) => { menuBar.Visible = !menuBar.Visible; };
  301. //appWindow.Add (hideBtn);
  302. //var enableBtn = new Button { X = Pos.Center (), Y = Pos.Bottom (hideBtn), Text = "_Toggle Menu.Enable" };
  303. //enableBtn.Accepting += (s, e) => { menuBar.Enabled = !menuBar.Enabled; };
  304. //appWindow.Add (enableBtn);
  305. autoSaveStatusCb.SetFocus ();
  306. return;
  307. // Add the commands supported by this View
  308. bool? HandleCommand (ICommandContext? ctx)
  309. {
  310. lastCommandText.Text = ctx?.Command!.ToString ()!;
  311. Logging.Debug ($"lastCommand: {lastCommandText.Text}");
  312. return true;
  313. }
  314. }
  315. /// <inheritdoc/>
  316. protected override void Dispose (bool disposing)
  317. {
  318. if (ContextMenu is { })
  319. {
  320. ContextMenu.Dispose ();
  321. ContextMenu = null;
  322. }
  323. base.Dispose (disposing);
  324. }
  325. }
  326. private const string LOGFILE_LOCATION = "./logs";
  327. private static readonly string _logFilePath = string.Empty;
  328. private static readonly LoggingLevelSwitch _logLevelSwitch = new ();
  329. private static ILogger CreateLogger ()
  330. {
  331. // Configure Serilog to write logs to a file
  332. _logLevelSwitch.MinimumLevel = LogEventLevel.Verbose;
  333. Log.Logger = new LoggerConfiguration ()
  334. .MinimumLevel.ControlledBy (_logLevelSwitch)
  335. .Enrich.FromLogContext () // Enables dynamic enrichment
  336. .WriteTo.Debug ()
  337. .WriteTo.File (
  338. _logFilePath,
  339. rollingInterval: RollingInterval.Day,
  340. outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
  341. .CreateLogger ();
  342. // Create a logger factory compatible with Microsoft.Extensions.Logging
  343. using ILoggerFactory loggerFactory = LoggerFactory.Create (
  344. builder =>
  345. {
  346. builder
  347. .AddSerilog (dispose: true) // Integrate Serilog with ILogger
  348. .SetMinimumLevel (LogLevel.Trace); // Set minimum log level
  349. });
  350. // Get an ILogger instance
  351. return loggerFactory.CreateLogger ("Global Logger");
  352. }
  353. }