MenuItem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  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. private readonly ShortcutHelper _shortcutHelper;
  9. private bool _allowNullChecked;
  10. private MenuItemCheckStyle _checkType;
  11. private string _title;
  12. // TODO: Update to use Key instead of KeyCode
  13. /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
  14. public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
  15. // TODO: Update to use Key instead of KeyCode
  16. /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
  17. /// <param name="title">Title for the menu item.</param>
  18. /// <param name="help">Help text to display.</param>
  19. /// <param name="action">Action to invoke when the menu item is activated.</param>
  20. /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
  21. /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
  22. /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
  23. public MenuItem (
  24. string title,
  25. string help,
  26. Action action,
  27. Func<bool> canExecute = null,
  28. MenuItem parent = null,
  29. KeyCode shortcut = KeyCode.Null
  30. )
  31. {
  32. Title = title ?? "";
  33. Help = help ?? "";
  34. Action = action;
  35. CanExecute = canExecute;
  36. Parent = parent;
  37. _shortcutHelper = new ();
  38. if (shortcut != KeyCode.Null)
  39. {
  40. Shortcut = shortcut;
  41. }
  42. }
  43. /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
  44. /// <value>Method to invoke.</value>
  45. public Action Action { get; set; }
  46. /// <summary>
  47. /// Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
  48. /// <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
  49. /// allows <see cref="Checked"/> to be true or false.
  50. /// </summary>
  51. public bool AllowNullChecked
  52. {
  53. get => _allowNullChecked;
  54. set
  55. {
  56. _allowNullChecked = value;
  57. Checked ??= false;
  58. }
  59. }
  60. /// <summary>
  61. /// Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
  62. /// returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
  63. /// </summary>
  64. /// <value>Function to determine if the action is can be executed or not.</value>
  65. public Func<bool> CanExecute { get; set; }
  66. /// <summary>
  67. /// Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
  68. /// <see cref="MenuItemCheckStyle"/>.
  69. /// </summary>
  70. public bool? Checked { set; get; }
  71. /// <summary>
  72. /// Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
  73. /// <see langword="true"/>.
  74. /// </summary>
  75. public MenuItemCheckStyle CheckType
  76. {
  77. get => _checkType;
  78. set
  79. {
  80. _checkType = value;
  81. if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
  82. {
  83. Checked = false;
  84. }
  85. }
  86. }
  87. /// <summary>Gets or sets arbitrary data for the menu item.</summary>
  88. /// <remarks>This property is not used internally.</remarks>
  89. public object Data { get; set; }
  90. /// <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>
  91. /// <value>The help text.</value>
  92. public string Help { get; set; }
  93. /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
  94. /// <value>The parent.</value>
  95. public MenuItem Parent { get; set; }
  96. /// <summary>Gets or sets the title of the menu item .</summary>
  97. /// <value>The title.</value>
  98. public string Title
  99. {
  100. get => _title;
  101. set
  102. {
  103. if (_title == value)
  104. {
  105. return;
  106. }
  107. _title = value;
  108. GetHotKey ();
  109. }
  110. }
  111. /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
  112. internal bool IsFromSubMenu => Parent != null;
  113. internal int TitleLength => GetMenuBarItemLength (Title);
  114. //
  115. // ┌─────────────────────────────┐
  116. // │ Quit Quit UI Catalog Ctrl+Q │
  117. // └─────────────────────────────┘
  118. // ┌─────────────────┐
  119. // │ ◌ TopLevel Alt+T │
  120. // └─────────────────┘
  121. // TODO: Replace the `2` literals with named constants
  122. internal int Width => 1
  123. + // space before Title
  124. TitleLength
  125. + 2
  126. + // space after Title - BUGBUG: This should be 1
  127. (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
  128. ? 2
  129. : 0)
  130. + // check glyph + space
  131. (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
  132. + // Two spaces before Help
  133. (ShortcutTag.GetColumns () > 0
  134. ? 2 + ShortcutTag.GetColumns ()
  135. : 0); // Pad two spaces before shortcut tag (which are also aligned right)
  136. /// <summary>Merely a debugging aid to see the interaction with main.</summary>
  137. internal bool GetMenuBarItem () { return IsFromSubMenu; }
  138. /// <summary>Merely a debugging aid to see the interaction with main.</summary>
  139. internal MenuItem GetMenuItem () { return this; }
  140. /// <summary>
  141. /// Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
  142. /// <see cref="CanExecute"/>.
  143. /// </summary>
  144. public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
  145. /// <summary>
  146. /// Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
  147. /// <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
  148. /// </summary>
  149. public void ToggleChecked ()
  150. {
  151. if (_checkType != MenuItemCheckStyle.Checked)
  152. {
  153. throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
  154. }
  155. bool? previousChecked = Checked;
  156. if (AllowNullChecked)
  157. {
  158. Checked = previousChecked switch
  159. {
  160. null => true,
  161. true => false,
  162. false => null
  163. };
  164. }
  165. else
  166. {
  167. Checked = !Checked;
  168. }
  169. }
  170. private static int GetMenuBarItemLength (string title)
  171. {
  172. return title.EnumerateRunes ()
  173. .Where (ch => ch != MenuBar.HotKeySpecifier)
  174. .Sum (ch => Math.Max (ch.GetColumns (), 1));
  175. }
  176. #region Keyboard Handling
  177. // TODO: Update to use Key instead of Rune
  178. /// <summary>
  179. /// The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
  180. /// <see cref="Title"/> of a MenuItem with an underscore ('_').
  181. /// <para>
  182. /// Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
  183. /// not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
  184. /// </para>
  185. /// <para>
  186. /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
  187. /// File menu. Pressing the N key will then activate the New MenuItem.
  188. /// </para>
  189. /// <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
  190. /// </summary>
  191. public Rune HotKey { get; set; }
  192. private void GetHotKey ()
  193. {
  194. var nextIsHot = false;
  195. foreach (char x in _title)
  196. {
  197. if (x == MenuBar.HotKeySpecifier.Value)
  198. {
  199. nextIsHot = true;
  200. }
  201. else
  202. {
  203. if (nextIsHot)
  204. {
  205. HotKey = (Rune)char.ToUpper (x);
  206. break;
  207. }
  208. nextIsHot = false;
  209. HotKey = default (Rune);
  210. }
  211. }
  212. }
  213. // TODO: Update to use Key instead of KeyCode
  214. /// <summary>
  215. /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
  216. /// <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
  217. /// <see cref="MenuItem"/>.
  218. /// <para>
  219. /// The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
  220. /// <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
  221. /// </para>
  222. /// </summary>
  223. public KeyCode Shortcut
  224. {
  225. get => _shortcutHelper.Shortcut;
  226. set
  227. {
  228. if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
  229. {
  230. _shortcutHelper.Shortcut = value;
  231. }
  232. }
  233. }
  234. /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
  235. public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
  236. ? string.Empty
  237. : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
  238. #endregion Keyboard Handling
  239. }