MenuItem.cs 12 KB

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