MenuBarv2.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. #nullable enable
  2. using System.ComponentModel;
  3. using System.Diagnostics;
  4. namespace Terminal.Gui;
  5. /// <summary>
  6. /// A horizontal list of <see cref="MenuBarItemv2"/>s. Each <see cref="MenuBarItemv2"/> can have a
  7. /// <see cref="PopoverMenu"/> that is shown when the <see cref="MenuBarItemv2"/> is selected.
  8. /// </summary>
  9. /// <remarks>
  10. /// MenuBars may be hosted by any View and will, by default, be positioned the full width across the top of the View's
  11. /// Viewport.
  12. /// </remarks>
  13. public class MenuBarv2 : Menuv2, IDesignable
  14. {
  15. /// <inheritdoc/>
  16. public MenuBarv2 () : this ([]) { }
  17. /// <inheritdoc/>
  18. public MenuBarv2 (IEnumerable<MenuBarItemv2> menuBarItems) : base (menuBarItems)
  19. {
  20. CanFocus = false;
  21. TabStop = TabBehavior.TabGroup;
  22. Y = 0;
  23. Width = Dim.Fill ();
  24. Orientation = Orientation.Horizontal;
  25. Key = DefaultKey;
  26. AddCommand (Command.HotKey,
  27. () =>
  28. {
  29. if (HideActiveItem ())
  30. {
  31. return true;
  32. }
  33. if (SubViews.FirstOrDefault (sv => sv is MenuBarItemv2 { PopoverMenu: { } }) is MenuBarItemv2 { } first)
  34. {
  35. _active = true;
  36. ShowPopover (first);
  37. return true;
  38. }
  39. return false;
  40. });
  41. HotKeyBindings.Add (Key, Command.HotKey);
  42. KeyBindings.Add (Key, Command.Quit);
  43. KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit);
  44. AddCommand (
  45. Command.Quit,
  46. ctx =>
  47. {
  48. if (HideActiveItem ())
  49. {
  50. return true;
  51. }
  52. if (CanFocus)
  53. {
  54. CanFocus = false;
  55. _active = false;
  56. return true;
  57. }
  58. return false;//RaiseAccepted (ctx);
  59. });
  60. AddCommand (Command.Right, MoveRight);
  61. KeyBindings.Add (Key.CursorRight, Command.Right);
  62. AddCommand (Command.Left, MoveLeft);
  63. KeyBindings.Add (Key.CursorLeft, Command.Left);
  64. return;
  65. bool? MoveLeft (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); }
  66. bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); }
  67. }
  68. private Key _key = DefaultKey;
  69. /// <summary>Specifies the key that will activate the context menu.</summary>
  70. public Key Key
  71. {
  72. get => _key;
  73. set
  74. {
  75. Key oldKey = _key;
  76. _key = value;
  77. KeyChanged?.Invoke (this, new (oldKey, _key));
  78. }
  79. }
  80. /// <summary>
  81. /// Sets the Menu Bar Items for this Menu Bar. This will replace any existing Menu Bar Items.
  82. /// </summary>
  83. /// <remarks>
  84. /// <para>
  85. /// This is a convenience property to help porting from the v1 MenuBar.
  86. /// </para>
  87. /// </remarks>
  88. public MenuBarItemv2 []? Menus
  89. {
  90. set
  91. {
  92. RemoveAll ();
  93. if (value is null)
  94. {
  95. return;
  96. }
  97. foreach (MenuBarItemv2 mbi in value)
  98. {
  99. Add (mbi);
  100. }
  101. }
  102. }
  103. /// <summary>Raised when <see cref="Key"/> is changed.</summary>
  104. public event EventHandler<KeyChangedEventArgs>? KeyChanged;
  105. /// <summary>The default key for activating menu bars.</summary>
  106. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
  107. public static Key DefaultKey { get; set; } = Key.F9;
  108. /// <summary>
  109. /// Gets whether any of the menu bar items have a visible <see cref="PopoverMenu"/>.
  110. /// </summary>
  111. /// <exception cref="NotImplementedException"></exception>
  112. public bool IsOpen ()
  113. {
  114. return SubViews.Count (sv => sv is MenuBarItemv2 { PopoverMenu: { Visible: true } }) > 0;
  115. }
  116. private bool _active;
  117. /// <summary>
  118. /// Returns a value indicating whether the menu bar is active or not. When active, moving the mouse
  119. /// over a menu bar item will activate it.
  120. /// </summary>
  121. /// <returns></returns>
  122. public bool IsActive ()
  123. {
  124. return _active;
  125. }
  126. /// <inheritdoc />
  127. protected override bool OnMouseEnter (CancelEventArgs eventArgs)
  128. {
  129. // If the MenuBar does not have focus and the mouse enters: Enable CanFocus
  130. // But do NOT show a Popover unless the user clicks or presses a hotkey
  131. if (!HasFocus)
  132. {
  133. CanFocus = true;
  134. }
  135. return base.OnMouseEnter (eventArgs);
  136. }
  137. /// <inheritdoc />
  138. protected override void OnMouseLeave ()
  139. {
  140. if (!IsOpen ())
  141. {
  142. CanFocus = false;
  143. }
  144. base.OnMouseLeave ();
  145. }
  146. /// <inheritdoc />
  147. protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
  148. {
  149. if (!newHasFocus)
  150. {
  151. _active = false;
  152. CanFocus = false;
  153. }
  154. }
  155. /// <inheritdoc/>
  156. protected override void OnSelectedMenuItemChanged (MenuItemv2? selected)
  157. {
  158. if (selected is MenuBarItemv2 { PopoverMenu.Visible: false } selectedMenuBarItem)
  159. {
  160. ShowPopover (selectedMenuBarItem);
  161. }
  162. }
  163. /// <inheritdoc/>
  164. public override void EndInit ()
  165. {
  166. base.EndInit ();
  167. if (Border is { })
  168. {
  169. Border.Thickness = new (0);
  170. Border.LineStyle = LineStyle.None;
  171. }
  172. // TODO: This needs to be done whenever a menuitem in any MenuBarItem changes
  173. foreach (MenuBarItemv2? mbi in SubViews.Select (s => s as MenuBarItemv2))
  174. {
  175. Application.Popover?.Register (mbi?.PopoverMenu);
  176. }
  177. }
  178. /// <inheritdoc/>
  179. protected override bool OnAccepting (CommandEventArgs args)
  180. {
  181. Logging.Trace ($"{args.Context?.Source?.Title}");
  182. if (Visible && args.Context?.Source is MenuBarItemv2 { PopoverMenu.Visible: false } sourceMenuBarItem)
  183. {
  184. _active = true;
  185. if (!CanFocus)
  186. {
  187. // Enabling CanFocus will cause focus to change, which will cause OnSelectedMenuItem to change
  188. // This will call ShowPopover
  189. CanFocus = true;
  190. sourceMenuBarItem.SetFocus ();
  191. }
  192. else
  193. {
  194. ShowPopover (sourceMenuBarItem);
  195. }
  196. return true;
  197. }
  198. return base.OnAccepting (args);
  199. }
  200. /// <summary>
  201. /// Shows the specified popover, but only if the menu bar is active.
  202. /// </summary>
  203. /// <param name="menuBarItem"></param>
  204. private void ShowPopover (MenuBarItemv2? menuBarItem)
  205. {
  206. Logging.Trace ($"{menuBarItem?.Id}");
  207. if (!_active || !Visible)
  208. {
  209. return;
  210. }
  211. //menuBarItem!.PopoverMenu.Id = menuBarItem.Id;
  212. // TODO: We should init the PopoverMenu in a smarter way
  213. if (menuBarItem?.PopoverMenu is { IsInitialized: false })
  214. {
  215. menuBarItem.PopoverMenu.BeginInit ();
  216. menuBarItem.PopoverMenu.EndInit ();
  217. }
  218. // If the active Application Popover is part of this MenuBar, hide it.
  219. //HideActivePopover ();
  220. if (Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu
  221. && popoverMenu?.Root?.SuperMenuItem?.SuperView == this)
  222. {
  223. Application.Popover?.Hide (popoverMenu);
  224. }
  225. if (menuBarItem is null)
  226. {
  227. return;
  228. }
  229. if (menuBarItem.PopoverMenu is { })
  230. {
  231. menuBarItem.PopoverMenu.Accepted += (sender, args) =>
  232. {
  233. if (HasFocus)
  234. {
  235. CanFocus = false;
  236. }
  237. };
  238. }
  239. _active = true;
  240. CanFocus = true;
  241. menuBarItem.SetFocus ();
  242. if (menuBarItem.PopoverMenu?.Root is { })
  243. {
  244. menuBarItem.PopoverMenu.Root.SuperMenuItem = menuBarItem;
  245. }
  246. menuBarItem.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom));
  247. }
  248. private MenuBarItemv2? GetActiveItem ()
  249. {
  250. return SubViews.FirstOrDefault (sv => sv is MenuBarItemv2 { PopoverMenu: { Visible: true } }) as MenuBarItemv2;
  251. }
  252. /// <summary>
  253. /// Hides the popover menu associated with the active menu bar item and updates the focus state.
  254. /// </summary>
  255. /// <returns><see langword="true"/> if the popover was hidden</returns>
  256. public bool HideActiveItem ()
  257. {
  258. return HideItem (GetActiveItem ());
  259. }
  260. /// <summary>
  261. /// Hides popover menu associated with the specified menu bar item and updates the focus state.
  262. /// </summary>
  263. /// <param name="activeItem"></param>
  264. /// <returns><see langword="true"/> if the popover was hidden</returns>
  265. public bool HideItem (MenuBarItemv2? activeItem)
  266. {
  267. if (activeItem is null || !activeItem.PopoverMenu!.Visible)
  268. {
  269. return false;
  270. }
  271. _active = false;
  272. HasFocus = false;
  273. activeItem.PopoverMenu!.Visible = false;
  274. CanFocus = false;
  275. return true;
  276. }
  277. /// <inheritdoc/>
  278. public bool EnableForDesign<TContext> (ref readonly TContext context) where TContext : notnull
  279. {
  280. Add (
  281. new MenuBarItemv2 (
  282. "_File",
  283. [
  284. new MenuItemv2 (this, Command.New),
  285. new MenuItemv2 (this, Command.Open),
  286. new MenuItemv2 (this, Command.Save),
  287. new MenuItemv2 (this, Command.SaveAs),
  288. new Line (),
  289. new MenuItemv2
  290. {
  291. Title = "_Preferences",
  292. SubMenu = new (
  293. [
  294. new MenuItemv2
  295. {
  296. CommandView = new CheckBox ()
  297. {
  298. Title = "O_ption",
  299. },
  300. HelpText = "Toggle option"
  301. },
  302. new MenuItemv2
  303. {
  304. Title = "_Settings...",
  305. HelpText = "More settings",
  306. Action = () => MessageBox.Query ("Settings", "This is the Settings Dialog\n", ["_Ok", "_Cancel"])
  307. }
  308. ]
  309. )
  310. },
  311. new Line (),
  312. new MenuItemv2 (this, Command.Quit)
  313. ]
  314. )
  315. );
  316. Add (
  317. new MenuBarItemv2 (
  318. "_Edit",
  319. [
  320. new MenuItemv2 (this, Command.Cut),
  321. new MenuItemv2 (this, Command.Copy),
  322. new MenuItemv2 (this, Command.Paste),
  323. new Line (),
  324. new MenuItemv2 (this, Command.SelectAll)
  325. ]
  326. )
  327. );
  328. Add (
  329. new MenuBarItemv2 (
  330. "_Help",
  331. [
  332. new MenuItemv2
  333. {
  334. Title = "_Online Help...",
  335. Action = () => MessageBox.Query ("Online Help", "https://gui-cs.github.io/Terminal.GuiV2Docs", "Ok")
  336. },
  337. new MenuItemv2
  338. {
  339. Title = "About...",
  340. Action = () => MessageBox.Query ("About", "Something About Mary.", "Ok")
  341. }
  342. ]
  343. )
  344. );
  345. return true;
  346. }
  347. }