Menu.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. using System;
  2. using System.Diagnostics;
  3. using System.Text;
  4. using System.Linq;
  5. using System.Reflection.Metadata;
  6. namespace Terminal.Gui;
  7. /// <summary>
  8. /// Specifies how a <see cref="MenuItem"/> shows selection state.
  9. /// </summary>
  10. [Flags]
  11. public enum MenuItemCheckStyle {
  12. /// <summary>
  13. /// The menu item will be shown normally, with no check indicator. The default.
  14. /// </summary>
  15. NoCheck = 0b_0000_0000,
  16. /// <summary>
  17. /// The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).
  18. /// </summary>
  19. Checked = 0b_0000_0001,
  20. /// <summary>
  21. /// The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.
  22. /// </summary>
  23. Radio = 0b_0000_0010
  24. };
  25. /// <summary>
  26. /// A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation.
  27. /// MenuItems can also have a checked indicator (see <see cref="Checked"/>).
  28. /// </summary>
  29. public class MenuItem {
  30. string _title;
  31. ShortcutHelper _shortcutHelper;
  32. bool _allowNullChecked;
  33. MenuItemCheckStyle _checkType;
  34. internal int TitleLength => GetMenuBarItemLength (Title);
  35. /// <summary>
  36. /// Gets or sets arbitrary data for the menu item.
  37. /// </summary>
  38. /// <remarks>This property is not used internally.</remarks>
  39. public object Data { get; set; }
  40. // TODO: Update to use Key instead of KeyCode
  41. /// <summary>
  42. /// Initializes a new instance of <see cref="MenuItem"/>
  43. /// </summary>
  44. public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
  45. // TODO: Update to use Key instead of KeyCode
  46. /// <summary>
  47. /// Initializes a new instance of <see cref="MenuItem"/>.
  48. /// </summary>
  49. /// <param name="title">Title for the menu item.</param>
  50. /// <param name="help">Help text to display.</param>
  51. /// <param name="action">Action to invoke when the menu item is activated.</param>
  52. /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
  53. /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
  54. /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
  55. public MenuItem (string title, string help, Action action, Func<bool> canExecute = null, MenuItem parent = null, KeyCode shortcut = KeyCode.Null)
  56. {
  57. Title = title ?? "";
  58. Help = help ?? "";
  59. Action = action;
  60. CanExecute = canExecute;
  61. Parent = parent;
  62. _shortcutHelper = new ShortcutHelper ();
  63. if (shortcut != KeyCode.Null) {
  64. Shortcut = shortcut;
  65. }
  66. }
  67. #region Keyboard Handling
  68. // TODO: Update to use Key instead of Rune
  69. /// <summary>
  70. /// The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the <see cref="Title"/>
  71. /// of a MenuItem with an underscore ('_').
  72. /// <para>
  73. /// Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is not active).
  74. /// Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
  75. /// </para>
  76. /// <para>
  77. /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the File menu.
  78. /// Pressing the N key will then activate the New MenuItem.
  79. /// </para>
  80. /// <para>
  81. /// See also <see cref="Shortcut"/> which enable global key-bindings to menu items.
  82. /// </para>
  83. /// </summary>
  84. public Rune HotKey { get; set; }
  85. void GetHotKey ()
  86. {
  87. bool nextIsHot = false;
  88. foreach (char x in _title) {
  89. if (x == MenuBar.HotKeySpecifier.Value) {
  90. nextIsHot = true;
  91. } else {
  92. if (nextIsHot) {
  93. HotKey = (Rune)char.ToUpper (x);
  94. break;
  95. }
  96. nextIsHot = false;
  97. HotKey = default;
  98. }
  99. }
  100. }
  101. // TODO: Update to use Key instead of KeyCode
  102. /// <summary>
  103. /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the <see cref="View"/> that is
  104. /// the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this <see cref="MenuItem"/>.
  105. /// <para>
  106. /// The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
  107. /// </para>
  108. /// </summary>
  109. public KeyCode Shortcut {
  110. get => _shortcutHelper.Shortcut;
  111. set {
  112. if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null)) {
  113. _shortcutHelper.Shortcut = value;
  114. }
  115. }
  116. }
  117. /// <summary>
  118. /// Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.
  119. /// </summary>
  120. public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null ? string.Empty : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
  121. #endregion Keyboard Handling
  122. /// <summary>
  123. /// Gets or sets the title of the menu item .
  124. /// </summary>
  125. /// <value>The title.</value>
  126. public string Title {
  127. get => _title;
  128. set {
  129. if (_title != value) {
  130. _title = value;
  131. GetHotKey ();
  132. }
  133. }
  134. }
  135. /// <summary>
  136. /// Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.
  137. /// </summary>
  138. /// <value>The help text.</value>
  139. public string Help { get; set; }
  140. /// <summary>
  141. /// Gets or sets the action to be invoked when the menu item is triggered.
  142. /// </summary>
  143. /// <value>Method to invoke.</value>
  144. public Action Action { get; set; }
  145. /// <summary>
  146. /// Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/> returns <see langword="true"/>
  147. /// the menu item will be enabled. Otherwise, it will be disabled.
  148. /// </summary>
  149. /// <value>Function to determine if the action is can be executed or not.</value>
  150. public Func<bool> CanExecute { get; set; }
  151. /// <summary>
  152. /// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around <see cref="CanExecute"/>.
  153. /// </summary>
  154. public bool IsEnabled ()
  155. {
  156. return CanExecute == null ? true : CanExecute ();
  157. }
  158. //
  159. // ┌─────────────────────────────┐
  160. // │ Quit Quit UI Catalog Ctrl+Q │
  161. // └─────────────────────────────┘
  162. // ┌─────────────────┐
  163. // │ ◌ TopLevel Alt+T │
  164. // └─────────────────┘
  165. // TODO: Replace the `2` literals with named constants
  166. internal int Width => 1 + // space before Title
  167. TitleLength +
  168. 2 + // space after Title - BUGBUG: This should be 1
  169. (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + // check glyph + space
  170. (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0) + // Two spaces before Help
  171. (ShortcutTag.GetColumns () > 0 ? 2 + ShortcutTag.GetColumns () : 0); // Pad two spaces before shortcut tag (which are also aligned right)
  172. /// <summary>
  173. /// Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See <see cref="MenuItemCheckStyle"/>.
  174. /// </summary>
  175. public bool? Checked { set; get; }
  176. /// <summary>
  177. /// Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type.
  178. /// If <see langword="true"/> allows <see cref="Checked"/> to be null, true or false.
  179. /// If <see langword="false"/> only allows <see cref="Checked"/> to be true or false.
  180. /// </summary>
  181. public bool AllowNullChecked {
  182. get => _allowNullChecked;
  183. set {
  184. _allowNullChecked = value;
  185. if (Checked == null) {
  186. Checked = false;
  187. }
  188. }
  189. }
  190. /// <summary>
  191. /// Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to <see langword="true"/>.
  192. /// </summary>
  193. public MenuItemCheckStyle CheckType {
  194. get => _checkType;
  195. set {
  196. _checkType = value;
  197. if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked == null) {
  198. Checked = false;
  199. }
  200. }
  201. }
  202. /// <summary>
  203. /// Gets the parent for this <see cref="MenuItem"/>.
  204. /// </summary>
  205. /// <value>The parent.</value>
  206. public MenuItem Parent { get; set; }
  207. /// <summary>
  208. /// Gets if this <see cref="MenuItem"/> is from a sub-menu.
  209. /// </summary>
  210. internal bool IsFromSubMenu => Parent != null;
  211. /// <summary>
  212. /// Merely a debugging aid to see the interaction with main.
  213. /// </summary>
  214. public MenuItem GetMenuItem ()
  215. {
  216. return this;
  217. }
  218. /// <summary>
  219. /// Merely a debugging aid to see the interaction with main.
  220. /// </summary>
  221. public bool GetMenuBarItem ()
  222. {
  223. return IsFromSubMenu;
  224. }
  225. /// <summary>
  226. /// Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is <see langword="true"/>
  227. /// or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
  228. /// </summary>
  229. public void ToggleChecked ()
  230. {
  231. if (_checkType != MenuItemCheckStyle.Checked) {
  232. throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
  233. }
  234. bool? previousChecked = Checked;
  235. if (AllowNullChecked) {
  236. switch (previousChecked) {
  237. case null:
  238. Checked = true;
  239. break;
  240. case true:
  241. Checked = false;
  242. break;
  243. case false:
  244. Checked = null;
  245. break;
  246. }
  247. } else {
  248. Checked = !Checked;
  249. }
  250. }
  251. int GetMenuBarItemLength (string title)
  252. {
  253. int len = 0;
  254. foreach (var ch in title.EnumerateRunes ()) {
  255. if (ch == MenuBar.HotKeySpecifier) {
  256. continue;
  257. }
  258. len += Math.Max (ch.GetColumns (), 1);
  259. }
  260. return len;
  261. }
  262. }
  263. /// <summary>
  264. /// An internal class used to represent a menu pop-up menu. Created and managed by <see cref="MenuBar"/> and <see cref="ContextMenu"/>.
  265. /// </summary>
  266. class Menu : View {
  267. internal MenuBarItem _barItems;
  268. internal MenuBar _host;
  269. internal int _currentChild;
  270. internal View _previousSubFocused;
  271. internal static Rect MakeFrame (int x, int y, MenuItem [] items, Menu parent = null, LineStyle border = LineStyle.Single)
  272. {
  273. if (items == null || items.Length == 0) {
  274. return new Rect ();
  275. }
  276. int minX = x;
  277. int minY = y;
  278. int borderOffset = 2; // This 2 is for the space around
  279. int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset;
  280. int maxH = items.Length + borderOffset;
  281. if (parent != null && x + maxW > Driver.Cols) {
  282. minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0);
  283. }
  284. if (y + maxH > Driver.Rows) {
  285. minY = Math.Max (Driver.Rows - maxH, 0);
  286. }
  287. return new Rect (minX, minY, maxW, maxH);
  288. }
  289. public Menu (MenuBar host, int x, int y, MenuBarItem barItems, Menu parent = null, LineStyle border = LineStyle.Single)
  290. : base (MakeFrame (x, y, barItems?.Children, parent, border))
  291. {
  292. if (host == null) {
  293. throw new ArgumentNullException (nameof (host));
  294. }
  295. if (barItems == null) {
  296. throw new ArgumentNullException (nameof (barItems));
  297. }
  298. _host = host;
  299. _barItems = barItems;
  300. if (barItems is { IsTopLevel: true }) {
  301. // This is a standalone MenuItem on a MenuBar
  302. ColorScheme = host.ColorScheme;
  303. CanFocus = true;
  304. } else {
  305. _currentChild = -1;
  306. for (int i = 0; i < barItems.Children?.Length; i++) {
  307. if (barItems.Children [i]?.IsEnabled () == true) {
  308. _currentChild = i;
  309. break;
  310. }
  311. }
  312. ColorScheme = host.ColorScheme;
  313. CanFocus = true;
  314. WantMousePositionReports = host.WantMousePositionReports;
  315. }
  316. BorderStyle = host.MenusBorderStyle;
  317. if (Application.Current != null) {
  318. Application.Current.DrawContentComplete += Current_DrawContentComplete;
  319. Application.Current.SizeChanging += Current_TerminalResized;
  320. }
  321. Application.MouseEvent += Application_RootMouseEvent;
  322. // Things this view knows how to do
  323. AddCommand (Command.LineUp, () => MoveUp ());
  324. AddCommand (Command.LineDown, () => MoveDown ());
  325. AddCommand (Command.Left, () => {
  326. _host.PreviousMenu (true);
  327. return true;
  328. });
  329. AddCommand (Command.Right, () => {
  330. _host.NextMenu (!_barItems.IsTopLevel || _barItems.Children != null
  331. && _barItems.Children.Length > 0 && _currentChild > -1
  332. && _currentChild < _barItems.Children.Length && _barItems.Children [_currentChild].IsFromSubMenu,
  333. _barItems.Children != null && _barItems.Children.Length > 0 && _currentChild > -1
  334. && host.UseSubMenusSingleFrame && _barItems.SubMenu (_barItems.Children [_currentChild]) != null);
  335. return true;
  336. });
  337. AddCommand (Command.Cancel, () => {
  338. CloseAllMenus ();
  339. return true;
  340. });
  341. AddCommand (Command.Accept, () => {
  342. RunSelected ();
  343. return true;
  344. });
  345. AddCommand (Command.Select, () => _host?.SelectItem (_menuItemToSelect));
  346. AddCommand (Command.ToggleExpandCollapse, () => SelectOrRun ());
  347. AddCommand (Command.Default, () => _host?.SelectItem (_menuItemToSelect));
  348. // Default key bindings for this view
  349. KeyBindings.Add (KeyCode.CursorUp, Command.LineUp);
  350. KeyBindings.Add (KeyCode.CursorDown, Command.LineDown);
  351. KeyBindings.Add (KeyCode.CursorLeft, Command.Left);
  352. KeyBindings.Add (KeyCode.CursorRight, Command.Right);
  353. KeyBindings.Add (KeyCode.Esc, Command.Cancel);
  354. KeyBindings.Add (KeyCode.Enter, Command.Accept);
  355. KeyBindings.Add (KeyCode.F9, KeyBindingScope.HotKey, Command.ToggleExpandCollapse);
  356. KeyBindings.Add (KeyCode.CtrlMask | KeyCode.Space, KeyBindingScope.HotKey, Command.ToggleExpandCollapse);
  357. AddKeyBindings (barItems);
  358. #if SUPPORT_ALT_TO_ACTIVATE_MENU
  359. Initialized += (s, e) => {
  360. if (SuperView != null) {
  361. SuperView.KeyUp += SuperView_KeyUp;
  362. }
  363. };
  364. #endif
  365. // Debugging aid so ToString() is helpful
  366. Text = _barItems.Title;
  367. }
  368. #if SUPPORT_ALT_TO_ACTIVATE_MENU
  369. void SuperView_KeyUp (object sender, KeyEventArgs e)
  370. {
  371. if (SuperView == null || SuperView.CanFocus == false || SuperView.Visible == false) {
  372. return;
  373. }
  374. _host.AltKeyUpHandler (e);
  375. }
  376. #endif
  377. void AddKeyBindings (MenuBarItem menuBarItem)
  378. {
  379. if (menuBarItem == null || menuBarItem.Children == null) {
  380. return;
  381. }
  382. foreach (var menuItem in menuBarItem.Children.Where (m => m != null)) {
  383. KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, Command.ToggleExpandCollapse);
  384. KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, Command.ToggleExpandCollapse);
  385. if (menuItem.Shortcut != KeyCode.Null) {
  386. KeyBindings.Add (menuItem.Shortcut, KeyBindingScope.HotKey, Command.Select);
  387. }
  388. var subMenu = menuBarItem.SubMenu (menuItem);
  389. AddKeyBindings (subMenu);
  390. }
  391. }
  392. int _menuBarItemToActivate = -1;
  393. MenuItem _menuItemToSelect;
  394. /// <summary>
  395. /// Called when a key bound to Command.Select is pressed. This means a hot key was pressed.
  396. /// </summary>
  397. /// <returns></returns>
  398. bool SelectOrRun ()
  399. {
  400. if (!IsInitialized || !Visible) {
  401. return true;
  402. }
  403. if (_menuBarItemToActivate != -1) {
  404. _host.Activate (1, _menuBarItemToActivate);
  405. } else if (_menuItemToSelect != null) {
  406. var m = _menuItemToSelect as MenuBarItem;
  407. if (m?.Children?.Length > 0) {
  408. var item = _barItems.Children [_currentChild];
  409. if (item == null) {
  410. return true;
  411. }
  412. bool disabled = item == null || !item.IsEnabled ();
  413. if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) {
  414. SetNeedsDisplay ();
  415. SetParentSetNeedsDisplay ();
  416. return true;
  417. }
  418. if (!disabled) {
  419. _host.OnMenuOpened ();
  420. }
  421. } else {
  422. _host.SelectItem (_menuItemToSelect);
  423. }
  424. } else if (_host.IsMenuOpen) {
  425. _host.CloseAllMenus ();
  426. } else {
  427. _host.OpenMenu ();
  428. }
  429. //_openedByHotKey = true;
  430. return true;
  431. }
  432. /// <inheritdoc/>
  433. public override bool? OnInvokingKeyBindings (Key keyEvent)
  434. {
  435. // This is a bit of a hack. We want to handle the key bindings for menu bar but
  436. // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
  437. // So before we call the base class we set SelectedItem appropriately.
  438. var key = keyEvent.KeyCode;
  439. if (KeyBindings.TryGet (key, out _)) {
  440. _menuBarItemToActivate = -1;
  441. _menuItemToSelect = null;
  442. var children = _barItems.Children;
  443. if (children == null) {
  444. return base.OnInvokingKeyBindings (keyEvent);
  445. }
  446. // Search for shortcuts first. If there's a shortcut, we don't want to activate the menu item.
  447. foreach (var c in children) {
  448. if (key == c?.Shortcut) {
  449. _menuBarItemToActivate = -1;
  450. _menuItemToSelect = c;
  451. keyEvent.Scope = KeyBindingScope.HotKey;
  452. return base.OnInvokingKeyBindings (keyEvent);
  453. }
  454. var subMenu = _barItems.SubMenu (c);
  455. if (FindShortcutInChildMenu (key, subMenu)) {
  456. keyEvent.Scope = KeyBindingScope.HotKey;
  457. return base.OnInvokingKeyBindings (keyEvent);
  458. }
  459. }
  460. // Search for hot keys next.
  461. for (int c = 0; c < children.Length; c++) {
  462. int hotKeyValue = children [c]?.HotKey.Value ?? default;
  463. var hotKey = (KeyCode)hotKeyValue;
  464. if (hotKey == KeyCode.Null) {
  465. continue;
  466. }
  467. bool matches = key == hotKey || key == (hotKey | KeyCode.AltMask);
  468. if (!_host.IsMenuOpen) {
  469. // If the menu is open, only match if Alt is not pressed.
  470. matches = key == hotKey;
  471. }
  472. if (matches) {
  473. _menuItemToSelect = children [c];
  474. _currentChild = c;
  475. return base.OnInvokingKeyBindings (keyEvent);
  476. }
  477. }
  478. }
  479. var handled = base.OnInvokingKeyBindings (keyEvent);
  480. if (handled != null && (bool)handled) {
  481. return true;
  482. }
  483. // This supports the case where the menu bar is a context menu
  484. return _host.OnInvokingKeyBindings (keyEvent);
  485. }
  486. bool FindShortcutInChildMenu (KeyCode key, MenuBarItem menuBarItem)
  487. {
  488. if (menuBarItem == null || menuBarItem.Children == null) {
  489. return false;
  490. }
  491. foreach (var menuItem in menuBarItem.Children) {
  492. if (key == menuItem?.Shortcut) {
  493. _menuBarItemToActivate = -1;
  494. _menuItemToSelect = menuItem;
  495. return true;
  496. }
  497. var subMenu = menuBarItem.SubMenu (menuItem);
  498. FindShortcutInChildMenu (key, subMenu);
  499. }
  500. return false;
  501. }
  502. void Current_TerminalResized (object sender, SizeChangedEventArgs e)
  503. {
  504. if (_host.IsMenuOpen) {
  505. _host.CloseAllMenus ();
  506. }
  507. }
  508. /// <inheritdoc/>
  509. public override void OnVisibleChanged ()
  510. {
  511. base.OnVisibleChanged ();
  512. if (Visible) {
  513. Application.MouseEvent += Application_RootMouseEvent;
  514. } else {
  515. Application.MouseEvent -= Application_RootMouseEvent;
  516. }
  517. }
  518. void Application_RootMouseEvent (object sender, MouseEventEventArgs a)
  519. {
  520. if (a.MouseEvent.View is MenuBar) {
  521. return;
  522. }
  523. var locationOffset = _host.GetScreenOffsetFromCurrent ();
  524. if (SuperView != null && SuperView != Application.Current) {
  525. locationOffset.X += SuperView.Border.Thickness.Left;
  526. locationOffset.Y += SuperView.Border.Thickness.Top;
  527. }
  528. var view = FindDeepestView (this, a.MouseEvent.X + locationOffset.X, a.MouseEvent.Y + locationOffset.Y, out int rx, out int ry);
  529. if (view == this) {
  530. if (!Visible) {
  531. throw new InvalidOperationException ("This shouldn't running on a invisible menu!");
  532. }
  533. var nme = new MouseEvent () {
  534. X = rx,
  535. Y = ry,
  536. Flags = a.MouseEvent.Flags,
  537. View = view
  538. };
  539. if (MouseEvent (nme) || a.MouseEvent.Flags == MouseFlags.Button1Pressed || a.MouseEvent.Flags == MouseFlags.Button1Released) {
  540. a.MouseEvent.Handled = true;
  541. }
  542. }
  543. }
  544. internal Attribute DetermineColorSchemeFor (MenuItem item, int index)
  545. {
  546. if (item != null) {
  547. if (index == _currentChild) {
  548. return ColorScheme.Focus;
  549. }
  550. if (!item.IsEnabled ()) {
  551. return ColorScheme.Disabled;
  552. }
  553. }
  554. return GetNormalColor ();
  555. }
  556. public override void OnDrawContent (Rect contentArea)
  557. {
  558. if (_barItems.Children == null) {
  559. return;
  560. }
  561. var savedClip = Driver.Clip;
  562. Driver.Clip = new Rect (0, 0, Driver.Cols, Driver.Rows);
  563. Driver.SetAttribute (GetNormalColor ());
  564. OnDrawAdornments ();
  565. OnRenderLineCanvas ();
  566. for (int i = Bounds.Y; i < _barItems.Children.Length; i++) {
  567. if (i < 0) {
  568. continue;
  569. }
  570. if (BoundsToScreen (Bounds).Y + i >= Driver.Rows) {
  571. break;
  572. }
  573. var item = _barItems.Children [i];
  574. Driver.SetAttribute (item == null ? GetNormalColor ()
  575. : i == _currentChild ? ColorScheme.Focus : GetNormalColor ());
  576. if (item == null && BorderStyle != LineStyle.None) {
  577. Move (-1, i);
  578. Driver.AddRune (Glyphs.LeftTee);
  579. } else if (Frame.X < Driver.Cols) {
  580. Move (0, i);
  581. }
  582. Driver.SetAttribute (DetermineColorSchemeFor (item, i));
  583. for (int p = Bounds.X; p < Frame.Width - 2; p++) {
  584. // This - 2 is for the border
  585. if (p < 0) {
  586. continue;
  587. }
  588. if (BoundsToScreen (Bounds).X + p >= Driver.Cols) {
  589. break;
  590. }
  591. if (item == null) {
  592. Driver.AddRune (Glyphs.HLine);
  593. } else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent.Parent != null) {
  594. Driver.AddRune (Glyphs.LeftArrow);
  595. }
  596. // This `- 3` is left border + right border + one row in from right
  597. else if (p == Frame.Width - 3 && _barItems.SubMenu (_barItems.Children [i]) != null) {
  598. Driver.AddRune (Glyphs.RightArrow);
  599. } else {
  600. Driver.AddRune ((Rune)' ');
  601. }
  602. }
  603. if (item == null) {
  604. if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) {
  605. Move (Frame.Width - 2, i);
  606. Driver.AddRune (Glyphs.RightTee);
  607. }
  608. continue;
  609. }
  610. string textToDraw = null;
  611. var nullCheckedChar = Glyphs.NullChecked;
  612. var checkChar = Glyphs.Selected;
  613. var uncheckedChar = Glyphs.UnSelected;
  614. if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) {
  615. checkChar = Glyphs.Checked;
  616. uncheckedChar = Glyphs.UnChecked;
  617. }
  618. // Support Checked even though CheckType wasn't set
  619. if (item.CheckType == MenuItemCheckStyle.Checked && item.Checked == null) {
  620. textToDraw = $"{nullCheckedChar} {item.Title}";
  621. } else if (item.Checked == true) {
  622. textToDraw = $"{checkChar} {item.Title}";
  623. } else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) {
  624. textToDraw = $"{uncheckedChar} {item.Title}";
  625. } else {
  626. textToDraw = item.Title;
  627. }
  628. BoundsToScreen (0, i, out int vtsCol, out int vtsRow, false);
  629. if (vtsCol < Driver.Cols) {
  630. Driver.Move (vtsCol + 1, vtsRow);
  631. if (!item.IsEnabled ()) {
  632. DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled);
  633. } else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent.Parent != null) {
  634. var tf = new TextFormatter () {
  635. Alignment = TextAlignment.Centered,
  636. HotKeySpecifier = MenuBar.HotKeySpecifier,
  637. Text = textToDraw
  638. };
  639. // The -3 is left/right border + one space (not sure what for)
  640. tf.Draw (BoundsToScreen (new Rect (1, i, Frame.Width - 3, 1)),
  641. i == _currentChild ? ColorScheme.Focus : GetNormalColor (),
  642. i == _currentChild ? ColorScheme.HotFocus : ColorScheme.HotNormal,
  643. SuperView == null ? default : SuperView.BoundsToScreen (SuperView.Bounds));
  644. } else {
  645. DrawHotString (textToDraw,
  646. i == _currentChild ? ColorScheme.HotFocus : ColorScheme.HotNormal,
  647. i == _currentChild ? ColorScheme.Focus : GetNormalColor ());
  648. }
  649. // The help string
  650. int l = item.ShortcutTag.GetColumns () == 0 ? item.Help.GetColumns () : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2;
  651. int col = Frame.Width - l - 3;
  652. BoundsToScreen (col, i, out vtsCol, out vtsRow, false);
  653. if (vtsCol < Driver.Cols) {
  654. Driver.Move (vtsCol, vtsRow);
  655. Driver.AddStr (item.Help);
  656. // The shortcut tag string
  657. if (!string.IsNullOrEmpty (item.ShortcutTag)) {
  658. Driver.Move (vtsCol + l - item.ShortcutTag.GetColumns (), vtsRow);
  659. Driver.AddStr (item.ShortcutTag);
  660. }
  661. }
  662. }
  663. }
  664. Driver.Clip = savedClip;
  665. PositionCursor ();
  666. }
  667. void Current_DrawContentComplete (object sender, DrawEventArgs e)
  668. {
  669. if (Visible) {
  670. OnDrawContent (Bounds);
  671. }
  672. }
  673. public override void PositionCursor ()
  674. {
  675. if (_host == null || _host.IsMenuOpen) {
  676. if (_barItems.IsTopLevel) {
  677. _host.PositionCursor ();
  678. } else {
  679. Move (2, 1 + _currentChild);
  680. }
  681. } else {
  682. _host.PositionCursor ();
  683. }
  684. }
  685. public void Run (Action action)
  686. {
  687. if (action == null || _host == null) {
  688. return;
  689. }
  690. Application.UngrabMouse ();
  691. _host.CloseAllMenus ();
  692. Application.Refresh ();
  693. _host.Run (action);
  694. }
  695. public override bool OnLeave (View view)
  696. {
  697. return _host.OnLeave (view);
  698. }
  699. void RunSelected ()
  700. {
  701. if (_barItems.IsTopLevel) {
  702. Run (_barItems.Action);
  703. } else if (_currentChild > -1 && _barItems.Children [_currentChild].Action != null) {
  704. Run (_barItems.Children [_currentChild].Action);
  705. } else if (_currentChild == 0 && _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild].Parent.Parent != null) {
  706. _host.PreviousMenu (_barItems.Children [_currentChild].Parent.IsFromSubMenu, true);
  707. } else if (_currentChild > -1 && _barItems.SubMenu (_barItems.Children [_currentChild]) != null) {
  708. CheckSubMenu ();
  709. }
  710. }
  711. void CloseAllMenus ()
  712. {
  713. Application.UngrabMouse ();
  714. _host.CloseAllMenus ();
  715. }
  716. bool MoveDown ()
  717. {
  718. if (_barItems.IsTopLevel) {
  719. return true;
  720. }
  721. bool disabled;
  722. do {
  723. _currentChild++;
  724. if (_currentChild >= _barItems.Children.Length) {
  725. _currentChild = 0;
  726. }
  727. if (this != _host.openCurrentMenu && _barItems.Children [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1) {
  728. _host.PreviousMenu (true);
  729. _host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild);
  730. _host.openCurrentMenu = this;
  731. }
  732. var item = _barItems.Children [_currentChild];
  733. if (item?.IsEnabled () != true) {
  734. disabled = true;
  735. } else {
  736. disabled = false;
  737. }
  738. if (!_host.UseSubMenusSingleFrame && _host.UseKeysUpDownAsKeysLeftRight && _barItems.SubMenu (_barItems.Children [_currentChild]) != null &&
  739. !disabled && _host.IsMenuOpen) {
  740. if (!CheckSubMenu ()) {
  741. return false;
  742. }
  743. break;
  744. }
  745. if (!_host.IsMenuOpen) {
  746. _host.OpenMenu (_host._selected);
  747. }
  748. } while (_barItems.Children [_currentChild] == null || disabled);
  749. SetNeedsDisplay ();
  750. SetParentSetNeedsDisplay ();
  751. if (!_host.UseSubMenusSingleFrame) {
  752. _host.OnMenuOpened ();
  753. }
  754. return true;
  755. }
  756. bool MoveUp ()
  757. {
  758. if (_barItems.IsTopLevel || _currentChild == -1) {
  759. return true;
  760. }
  761. bool disabled;
  762. do {
  763. _currentChild--;
  764. if (_host.UseKeysUpDownAsKeysLeftRight && !_host.UseSubMenusSingleFrame) {
  765. if ((_currentChild == -1 || this != _host.openCurrentMenu) && _barItems.Children [_currentChild + 1].IsFromSubMenu && _host._selectedSub > -1) {
  766. _currentChild++;
  767. _host.PreviousMenu (true);
  768. if (_currentChild > 0) {
  769. _currentChild--;
  770. _host.openCurrentMenu = this;
  771. }
  772. break;
  773. }
  774. }
  775. if (_currentChild < 0) {
  776. _currentChild = _barItems.Children.Length - 1;
  777. }
  778. if (!_host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild, false)) {
  779. _currentChild = 0;
  780. if (!_host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild) && !_host.CloseMenu (false)) {
  781. return false;
  782. }
  783. break;
  784. }
  785. var item = _barItems.Children [_currentChild];
  786. if (item?.IsEnabled () != true) {
  787. disabled = true;
  788. } else {
  789. disabled = false;
  790. }
  791. if (!_host.UseSubMenusSingleFrame && _host.UseKeysUpDownAsKeysLeftRight &&
  792. _barItems.SubMenu (_barItems.Children [_currentChild]) != null &&
  793. !disabled && _host.IsMenuOpen) {
  794. if (!CheckSubMenu ()) {
  795. return false;
  796. }
  797. break;
  798. }
  799. } while (_barItems.Children [_currentChild] == null || disabled);
  800. SetNeedsDisplay ();
  801. SetParentSetNeedsDisplay ();
  802. if (!_host.UseSubMenusSingleFrame) {
  803. _host.OnMenuOpened ();
  804. }
  805. return true;
  806. }
  807. void SetParentSetNeedsDisplay ()
  808. {
  809. if (_host._openSubMenu != null) {
  810. foreach (var menu in _host._openSubMenu) {
  811. menu.SetNeedsDisplay ();
  812. }
  813. }
  814. _host?._openMenu?.SetNeedsDisplay ();
  815. _host.SetNeedsDisplay ();
  816. }
  817. public override bool MouseEvent (MouseEvent me)
  818. {
  819. if (!_host._handled && !_host.HandleGrabView (me, this)) {
  820. return false;
  821. }
  822. _host._handled = false;
  823. bool disabled;
  824. int meY = me.Y - (Border == null ? 0 : Border.Thickness.Top);
  825. if (me.Flags == MouseFlags.Button1Clicked) {
  826. disabled = false;
  827. if (meY < 0) {
  828. return true;
  829. }
  830. if (meY >= _barItems.Children.Length) {
  831. return true;
  832. }
  833. var item = _barItems.Children [meY];
  834. if (item == null || !item.IsEnabled ()) {
  835. disabled = true;
  836. }
  837. if (disabled) {
  838. return true;
  839. }
  840. _currentChild = meY;
  841. if (item != null && !disabled) {
  842. RunSelected ();
  843. }
  844. return true;
  845. } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked ||
  846. me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition ||
  847. me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
  848. disabled = false;
  849. if (meY < 0 || meY >= _barItems.Children.Length) {
  850. return true;
  851. }
  852. var item = _barItems.Children [meY];
  853. if (item == null) {
  854. return true;
  855. }
  856. if (item == null || !item.IsEnabled ()) {
  857. disabled = true;
  858. }
  859. if (item != null && !disabled) {
  860. _currentChild = meY;
  861. }
  862. if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
  863. SetNeedsDisplay ();
  864. SetParentSetNeedsDisplay ();
  865. return true;
  866. }
  867. _host.OnMenuOpened ();
  868. return true;
  869. }
  870. return false;
  871. }
  872. internal bool CheckSubMenu ()
  873. {
  874. if (_currentChild == -1 || _barItems.Children [_currentChild] == null) {
  875. return true;
  876. }
  877. var subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]);
  878. if (subMenu != null) {
  879. int pos = -1;
  880. if (_host._openSubMenu != null) {
  881. pos = _host._openSubMenu.FindIndex (o => o?._barItems == subMenu);
  882. }
  883. if (pos == -1 && this != _host.openCurrentMenu && subMenu.Children != _host.openCurrentMenu._barItems.Children
  884. && !_host.CloseMenu (false, true)) {
  885. return false;
  886. }
  887. _host.Activate (_host._selected, pos, subMenu);
  888. } else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems.IsSubMenuOf (_barItems.Children [_currentChild]) == false) {
  889. return _host.CloseMenu (false, true);
  890. } else {
  891. SetNeedsDisplay ();
  892. SetParentSetNeedsDisplay ();
  893. }
  894. return true;
  895. }
  896. int GetSubMenuIndex (MenuBarItem subMenu)
  897. {
  898. int pos = -1;
  899. if (this != null && Subviews.Count > 0) {
  900. Menu v = null;
  901. foreach (var menu in Subviews) {
  902. if (((Menu)menu)._barItems == subMenu) {
  903. v = (Menu)menu;
  904. }
  905. }
  906. if (v != null) {
  907. pos = Subviews.IndexOf (v);
  908. }
  909. }
  910. return pos;
  911. }
  912. ///<inheritdoc/>
  913. public override bool OnEnter (View view)
  914. {
  915. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  916. return base.OnEnter (view);
  917. }
  918. protected override void Dispose (bool disposing)
  919. {
  920. if (Application.Current != null) {
  921. Application.Current.DrawContentComplete -= Current_DrawContentComplete;
  922. Application.Current.SizeChanging -= Current_TerminalResized;
  923. }
  924. Application.MouseEvent -= Application_RootMouseEvent;
  925. base.Dispose (disposing);
  926. }
  927. }