MenuItem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
  2. #nullable enable
  3. namespace Terminal.Gui.Views;
  4. /// <summary>
  5. /// A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
  6. /// can also have a checked indicator (see <see cref="Checked"/>).
  7. /// </summary>
  8. [Obsolete ("Use MenuItemv2 instead.", false)]
  9. public class MenuItem
  10. {
  11. internal MenuBar _menuBar;
  12. /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
  13. public MenuItem (Key? shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { }
  14. /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
  15. /// <param name="title">Title for the menu item.</param>
  16. /// <param name="help">Help text to display.</param>
  17. /// <param name="action">Action to invoke when the menu item is activated.</param>
  18. /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
  19. /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
  20. /// <param name="shortcutKey">The <see cref="ShortcutKey"/> keystroke combination.</param>
  21. public MenuItem (
  22. string? title,
  23. string? help,
  24. Action? action,
  25. Func<bool>? canExecute = null,
  26. MenuItem? parent = null,
  27. Key? shortcutKey = null
  28. )
  29. {
  30. Title = title ?? "";
  31. Help = help ?? "";
  32. Action = action!;
  33. CanExecute = canExecute!;
  34. Parent = parent!;
  35. if (Parent is { } && Parent.ShortcutKey != Key.Empty)
  36. {
  37. Parent.ShortcutKey = Key.Empty;
  38. }
  39. // Setter will ensure Key.Empty if it's null
  40. ShortcutKey = shortcutKey!;
  41. }
  42. private bool _allowNullChecked;
  43. private MenuItemCheckStyle _checkType;
  44. private string _title;
  45. /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
  46. /// <value>Method to invoke.</value>
  47. public Action? Action { get; set; }
  48. /// <summary>
  49. /// Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
  50. /// <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
  51. /// allows <see cref="Checked"/> to be true or false.
  52. /// </summary>
  53. public bool AllowNullChecked
  54. {
  55. get => _allowNullChecked;
  56. set
  57. {
  58. _allowNullChecked = value;
  59. Checked ??= false;
  60. }
  61. }
  62. /// <summary>
  63. /// Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
  64. /// returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
  65. /// </summary>
  66. /// <value>Function to determine if the action is can be executed or not.</value>
  67. public Func<bool>? CanExecute { get; set; }
  68. /// <summary>
  69. /// Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
  70. /// <see cref="MenuItemCheckStyle"/>.
  71. /// </summary>
  72. public bool? Checked { set; get; }
  73. /// <summary>
  74. /// Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
  75. /// <see langword="true"/>.
  76. /// </summary>
  77. public MenuItemCheckStyle CheckType
  78. {
  79. get => _checkType;
  80. set
  81. {
  82. _checkType = value;
  83. if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
  84. {
  85. Checked = false;
  86. }
  87. }
  88. }
  89. /// <summary>Gets or sets arbitrary data for the menu item.</summary>
  90. /// <remarks>This property is not used internally.</remarks>
  91. public object Data { get; set; }
  92. /// <summary>Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.</summary>
  93. /// <value>The help text.</value>
  94. public string Help { get; set; }
  95. /// <summary>
  96. /// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
  97. /// <see cref="CanExecute"/>.
  98. /// </summary>
  99. public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
  100. /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
  101. /// <value>The parent.</value>
  102. public MenuItem? Parent { get; set; }
  103. /// <summary>Gets or sets the title of the menu item .</summary>
  104. /// <value>The title.</value>
  105. public string Title
  106. {
  107. get => _title;
  108. set
  109. {
  110. if (_title == value)
  111. {
  112. return;
  113. }
  114. _title = value;
  115. GetHotKey ();
  116. }
  117. }
  118. /// <summary>
  119. /// Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
  120. /// <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
  121. /// </summary>
  122. public void ToggleChecked ()
  123. {
  124. if (_checkType != MenuItemCheckStyle.Checked)
  125. {
  126. throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
  127. }
  128. bool? previousChecked = Checked;
  129. if (AllowNullChecked)
  130. {
  131. Checked = previousChecked switch
  132. {
  133. null => true,
  134. true => false,
  135. false => null
  136. };
  137. }
  138. else
  139. {
  140. Checked = !Checked;
  141. }
  142. }
  143. /// <summary>Merely a debugging aid to see the interaction with main.</summary>
  144. internal bool GetMenuBarItem () { return IsFromSubMenu; }
  145. /// <summary>Merely a debugging aid to see the interaction with main.</summary>
  146. internal MenuItem GetMenuItem () { return this; }
  147. /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
  148. internal bool IsFromSubMenu => Parent != null;
  149. internal int TitleLength => GetMenuBarItemLength (Title);
  150. //
  151. // ┌─────────────────────────────┐
  152. // │ Quit Quit UI Catalog Ctrl+Q │
  153. // └─────────────────────────────┘
  154. // ┌─────────────────┐
  155. // │ ◌ TopLevel Alt+T │
  156. // └─────────────────┘
  157. // TODO: Replace the `2` literals with named constants
  158. internal int Width => 1
  159. + // space before Title
  160. TitleLength
  161. + 2
  162. + // space after Title - BUGBUG: This should be 1
  163. (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
  164. ? 2
  165. : 0)
  166. + // check glyph + space
  167. (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
  168. + // Two spaces before Help
  169. (ShortcutTag.GetColumns () > 0
  170. ? 2 + ShortcutTag.GetColumns ()
  171. : 0); // Pad two spaces before shortcut tag (which are also aligned right)
  172. private static int GetMenuBarItemLength (string title)
  173. {
  174. return title.EnumerateRunes ()
  175. .Where (ch => ch != MenuBar.HotKeySpecifier)
  176. .Sum (ch => Math.Max (ch.GetColumns (), 1));
  177. }
  178. #region Keyboard Handling
  179. private Key _hotKey = Key.Empty;
  180. /// <summary>
  181. /// The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
  182. /// <see cref="Title"/> of a MenuItem with an underscore ('_').
  183. /// <para>
  184. /// Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
  185. /// not active. Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
  186. /// </para>
  187. /// <para>
  188. /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
  189. /// File menu. Pressing the N key will then activate the New MenuItem.
  190. /// </para>
  191. /// <para>See also <see cref="ShortcutKey"/> which enable global key-bindings to menu items.</para>
  192. /// </summary>
  193. public Key? HotKey
  194. {
  195. get => _hotKey;
  196. private set
  197. {
  198. var oldKey = _hotKey;
  199. _hotKey = value ?? Key.Empty;
  200. UpdateHotKeyBinding (oldKey);
  201. }
  202. }
  203. private void GetHotKey ()
  204. {
  205. var nextIsHot = false;
  206. foreach (char x in _title)
  207. {
  208. if (x == MenuBar.HotKeySpecifier.Value)
  209. {
  210. nextIsHot = true;
  211. }
  212. else if (nextIsHot)
  213. {
  214. HotKey = char.ToLower (x);
  215. return;
  216. }
  217. }
  218. HotKey = Key.Empty;
  219. }
  220. private Key _shortcutKey = Key.Empty;
  221. /// <summary>
  222. /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
  223. /// <see cref="View"/> that is the parent of the <see cref="MenuBar"/> this
  224. /// <see cref="MenuItem"/>.
  225. /// <para>
  226. /// The <see cref="Key"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
  227. /// <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
  228. /// </para>
  229. /// </summary>
  230. public Key? ShortcutKey
  231. {
  232. get => _shortcutKey;
  233. set
  234. {
  235. var oldKey = _shortcutKey;
  236. _shortcutKey = value ?? Key.Empty;
  237. UpdateShortcutKeyBinding (oldKey);
  238. }
  239. }
  240. /// <summary>Gets the text describing the keystroke combination defined by <see cref="ShortcutKey"/>.</summary>
  241. public string ShortcutTag => ShortcutKey != Key.Empty ? ShortcutKey!.ToString () : string.Empty;
  242. internal void AddShortcutKeyBinding (MenuBar menuBar, Key key)
  243. {
  244. ArgumentNullException.ThrowIfNull (menuBar);
  245. _menuBar = menuBar;
  246. AddOrUpdateShortcutKeyBinding (key);
  247. }
  248. private void AddOrUpdateShortcutKeyBinding (Key key)
  249. {
  250. if (key != Key.Empty)
  251. {
  252. _menuBar.HotKeyBindings.Remove (key);
  253. }
  254. if (ShortcutKey != Key.Empty)
  255. {
  256. KeyBinding keyBinding = new ([Command.Select], null, data: this);
  257. // Remove an existent ShortcutKey
  258. _menuBar.HotKeyBindings.Remove (ShortcutKey!);
  259. _menuBar.HotKeyBindings.Add (ShortcutKey!, keyBinding);
  260. }
  261. }
  262. private void UpdateHotKeyBinding (Key oldKey)
  263. {
  264. if (_menuBar is null or { IsInitialized: false })
  265. {
  266. return;
  267. }
  268. if (oldKey != Key.Empty)
  269. {
  270. var index = _menuBar.Menus.IndexOf (this);
  271. if (index > -1)
  272. {
  273. _menuBar.HotKeyBindings.Remove (oldKey.WithAlt);
  274. }
  275. }
  276. if (HotKey != Key.Empty)
  277. {
  278. var index = _menuBar.Menus.IndexOf (this);
  279. if (index > -1)
  280. {
  281. _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt);
  282. KeyBinding keyBinding = new ([Command.Toggle], null, data: this);
  283. _menuBar.HotKeyBindings.Add (HotKey.WithAlt, keyBinding);
  284. }
  285. }
  286. }
  287. private void UpdateShortcutKeyBinding (Key oldKey)
  288. {
  289. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
  290. if (_menuBar is null)
  291. {
  292. return;
  293. }
  294. AddOrUpdateShortcutKeyBinding (oldKey);
  295. }
  296. #endregion Keyboard Handling
  297. /// <summary>
  298. /// Removes a <see cref="MenuItem"/> dynamically from the <see cref="Parent"/>.
  299. /// </summary>
  300. public virtual void RemoveMenuItem ()
  301. {
  302. if (Parent is { })
  303. {
  304. MenuItem? []? childrens = ((MenuBarItem)Parent).Children;
  305. var i = 0;
  306. foreach (MenuItem? c in childrens!)
  307. {
  308. if (c != this)
  309. {
  310. childrens [i] = c;
  311. i++;
  312. }
  313. }
  314. Array.Resize (ref childrens, childrens.Length - 1);
  315. if (childrens.Length == 0)
  316. {
  317. ((MenuBarItem)Parent).Children = null;
  318. }
  319. else
  320. {
  321. ((MenuBarItem)Parent).Children = childrens;
  322. }
  323. }
  324. if (ShortcutKey != Key.Empty)
  325. {
  326. // Remove an existent ShortcutKey
  327. _menuBar.HotKeyBindings.Remove (ShortcutKey!);
  328. }
  329. }
  330. }