Browse Source

Menu -> One class per file

Tig 1 year ago
parent
commit
364723eaf6

+ 0 - 287
Terminal.Gui/Views/Menu/Menu.cs

@@ -1,292 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>Specifies how a <see cref="MenuItem"/> shows selection state.</summary>
-[Flags]
-public enum MenuItemCheckStyle
-{
-    /// <summary>The menu item will be shown normally, with no check indicator. The default.</summary>
-    NoCheck = 0b_0000_0000,
-
-    /// <summary>The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).</summary>
-    Checked = 0b_0000_0001,
-
-    /// <summary>The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.</summary>
-    Radio = 0b_0000_0010
-}
-
-/// <summary>
-///     A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
-///     can also have a checked indicator (see <see cref="Checked"/>).
-/// </summary>
-public class MenuItem
-{
-    private readonly ShortcutHelper _shortcutHelper;
-    private bool _allowNullChecked;
-    private MenuItemCheckStyle _checkType;
-
-    private string _title;
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
-    public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="help">Help text to display.</param>
-    /// <param name="action">Action to invoke when the menu item is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
-    /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
-    public MenuItem (
-        string title,
-        string help,
-        Action action,
-        Func<bool> canExecute = null,
-        MenuItem parent = null,
-        KeyCode shortcut = KeyCode.Null
-    )
-    {
-        Title = title ?? "";
-        Help = help ?? "";
-        Action = action;
-        CanExecute = canExecute;
-        Parent = parent;
-        _shortcutHelper = new ();
-
-        if (shortcut != KeyCode.Null)
-        {
-            Shortcut = shortcut;
-        }
-    }
-
-    /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
-    /// <value>Method to invoke.</value>
-    public Action Action { get; set; }
-
-    /// <summary>
-    ///     Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
-    ///     <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
-    ///     allows <see cref="Checked"/> to be true or false.
-    /// </summary>
-    public bool AllowNullChecked
-    {
-        get => _allowNullChecked;
-        set
-        {
-            _allowNullChecked = value;
-            Checked ??= false;
-        }
-    }
-
-    /// <summary>
-    ///     Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
-    ///     returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
-    /// </summary>
-    /// <value>Function to determine if the action is can be executed or not.</value>
-    public Func<bool> CanExecute { get; set; }
-
-    /// <summary>
-    ///     Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
-    ///     <see cref="MenuItemCheckStyle"/>.
-    /// </summary>
-    public bool? Checked { set; get; }
-
-    /// <summary>
-    ///     Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
-    ///     <see langword="true"/>.
-    /// </summary>
-    public MenuItemCheckStyle CheckType
-    {
-        get => _checkType;
-        set
-        {
-            _checkType = value;
-
-            if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
-            {
-                Checked = false;
-            }
-        }
-    }
-
-    /// <summary>Gets or sets arbitrary data for the menu item.</summary>
-    /// <remarks>This property is not used internally.</remarks>
-    public object Data { get; set; }
-
-    /// <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>
-    /// <value>The help text.</value>
-    public string Help { get; set; }
-
-    /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
-    /// <value>The parent.</value>
-    public MenuItem Parent { get; set; }
-
-    /// <summary>Gets or sets the title of the menu item .</summary>
-    /// <value>The title.</value>
-    public string Title
-    {
-        get => _title;
-        set
-        {
-            if (_title == value)
-            {
-                return;
-            }
-
-            _title = value;
-            GetHotKey ();
-        }
-    }
-
-    /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
-    internal bool IsFromSubMenu => Parent != null;
-
-    internal int TitleLength => GetMenuBarItemLength (Title);
-
-    // 
-    // ┌─────────────────────────────┐
-    // │ Quit  Quit UI Catalog  Ctrl+Q │
-    // └─────────────────────────────┘
-    // ┌─────────────────┐
-    // │ ◌ TopLevel Alt+T │
-    // └─────────────────┘
-    // TODO: Replace the `2` literals with named constants 
-    internal int Width => 1
-                          + // space before Title
-                          TitleLength
-                          + 2
-                          + // space after Title - BUGBUG: This should be 1 
-                          (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
-                               ? 2
-                               : 0)
-                          + // check glyph + space 
-                          (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
-                          + // Two spaces before Help
-                          (ShortcutTag.GetColumns () > 0
-                               ? 2 + ShortcutTag.GetColumns ()
-                               : 0); // Pad two spaces before shortcut tag (which are also aligned right)
-
-    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
-    public bool GetMenuBarItem () { return IsFromSubMenu; }
-
-    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
-    public MenuItem GetMenuItem () { return this; }
-
-    /// <summary>
-    ///     Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
-    ///     <see cref="CanExecute"/>.
-    /// </summary>
-    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
-
-    /// <summary>
-    ///     Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
-    ///     <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
-    /// </summary>
-    public void ToggleChecked ()
-    {
-        if (_checkType != MenuItemCheckStyle.Checked)
-        {
-            throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
-        }
-
-        bool? previousChecked = Checked;
-
-        if (AllowNullChecked)
-        {
-            Checked = previousChecked switch
-                      {
-                          null => true,
-                          true => false,
-                          false => null
-                      };
-        }
-        else
-        {
-            Checked = !Checked;
-        }
-    }
-
-    private static int GetMenuBarItemLength (string title)
-    {
-        return title.EnumerateRunes ()
-                    .Where (ch => ch != MenuBar.HotKeySpecifier)
-                    .Sum (ch => Math.Max (ch.GetColumns (), 1));
-    }
-
-    #region Keyboard Handling
-
-    // TODO: Update to use Key instead of Rune
-    /// <summary>
-    ///     The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
-    ///     <see cref="Title"/> of a MenuItem with an underscore ('_').
-    ///     <para>
-    ///         Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
-    ///         not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
-    ///     </para>
-    ///     <para>
-    ///         For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
-    ///         File menu. Pressing the N key will then activate the New MenuItem.
-    ///     </para>
-    ///     <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
-    /// </summary>
-    public Rune HotKey { get; set; }
-
-    private void GetHotKey ()
-    {
-        var nextIsHot = false;
-
-        foreach (char x in _title)
-        {
-            if (x == MenuBar.HotKeySpecifier.Value)
-            {
-                nextIsHot = true;
-            }
-            else
-            {
-                if (nextIsHot)
-                {
-                    HotKey = (Rune)char.ToUpper (x);
-
-                    break;
-                }
-
-                nextIsHot = false;
-                HotKey = default (Rune);
-            }
-        }
-    }
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>
-    ///     Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
-    ///     <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
-    ///     <see cref="MenuItem"/>.
-    ///     <para>
-    ///         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"/>.
-    ///     </para>
-    /// </summary>
-    public KeyCode Shortcut
-    {
-        get => _shortcutHelper.Shortcut;
-        set
-        {
-            if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
-            {
-                _shortcutHelper.Shortcut = value;
-            }
-        }
-    }
-
-    /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
-    public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
-                                     ? string.Empty
-                                     : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
-
-    #endregion Keyboard Handling
-}
-
 /// <summary>
 ///     An internal class used to represent a menu pop-up menu. Created and managed by <see cref="MenuBar"/> and
 ///     <see cref="ContextMenu"/>.

+ 0 - 178
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -1,183 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>
-///     <see cref="MenuBarItem"/> is a menu item on  <see cref="MenuBar"/>. MenuBarItems do not support
-///     <see cref="MenuItem.Shortcut"/>.
-/// </summary>
-public class MenuBarItem : MenuItem
-{
-    /// <summary>Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
-    /// <param name="action">Action to invoke when the menu item is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
-    public MenuBarItem (
-        string title,
-        string help,
-        Action action,
-        Func<bool> canExecute = null,
-        MenuItem parent = null
-    ) : base (title, help, action, canExecute, parent)
-    {
-        SetInitialProperties (title, null, null, true);
-    }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="children">The items in the current menu.</param>
-    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
-    public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/> with separate list of items.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="children">The list of items in the current menu.</param>
-    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
-    public MenuBarItem (string title, List<MenuItem []> children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
-    /// <param name="children">The items in the current menu.</param>
-    public MenuBarItem (MenuItem [] children) : this ("", children) { }
-
-    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
-    public MenuBarItem () : this (new MenuItem [] { }) { }
-
-    /// <summary>
-    ///     Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this
-    ///     <see cref="MenuBarItem"/>
-    /// </summary>
-    /// <value>The children.</value>
-    public MenuItem [] Children { get; set; }
-
-    internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null;
-
-    /// <summary>Get the index of a child <see cref="MenuItem"/>.</summary>
-    /// <param name="children"></param>
-    /// <returns>Returns a greater than -1 if the <see cref="MenuItem"/> is a child.</returns>
-    public int GetChildrenIndex (MenuItem children)
-    {
-        var i = 0;
-
-        if (Children is { })
-        {
-            foreach (MenuItem child in Children)
-            {
-                if (child == children)
-                {
-                    return i;
-                }
-
-                i++;
-            }
-        }
-
-        return -1;
-    }
-
-    /// <summary>Check if a <see cref="MenuItem"/> is a submenu of this MenuBar.</summary>
-    /// <param name="menuItem"></param>
-    /// <returns>Returns <c>true</c> if it is a submenu. <c>false</c> otherwise.</returns>
-    public bool IsSubMenuOf (MenuItem menuItem)
-    {
-        foreach (MenuItem child in Children)
-        {
-            if (child == menuItem && child.Parent == menuItem.Parent)
-            {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /// <summary>Check if a <see cref="MenuItem"/> is a <see cref="MenuBarItem"/>.</summary>
-    /// <param name="menuItem"></param>
-    /// <returns>Returns a <see cref="MenuBarItem"/> or null otherwise.</returns>
-    public MenuBarItem SubMenu (MenuItem menuItem) { return menuItem as MenuBarItem; }
-
-    internal void AddShortcutKeyBindings (MenuBar menuBar)
-    {
-        if (Children is null)
-        {
-            return;
-        }
-
-        foreach (MenuItem menuItem in Children.Where (m => m is { }))
-        {
-            // For MenuBar only add shortcuts for submenus
-
-            if (menuItem.Shortcut != KeyCode.Null)
-            {
-                KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
-                menuBar.KeyBindings.Add (menuItem.Shortcut, keyBinding);
-            }
-
-            SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar);
-        }
-    }
-
-    private void SetInitialProperties (string title, object children, MenuItem parent = null, bool isTopLevel = false)
-    {
-        if (!isTopLevel && children is null)
-        {
-            throw new ArgumentNullException (
-                                             nameof (children),
-                                             @"The parameter cannot be null. Use an empty array instead."
-                                            );
-        }
-
-        SetTitle (title ?? "");
-
-        if (parent is { })
-        {
-            Parent = parent;
-        }
-
-        if (children is List<MenuItem []> childrenList)
-        {
-            MenuItem [] newChildren = [];
-
-            foreach (MenuItem [] grandChild in childrenList)
-            {
-                foreach (MenuItem child in grandChild)
-                {
-                    SetParent (grandChild);
-                    Array.Resize (ref newChildren, newChildren.Length + 1);
-                    newChildren [^1] = child;
-                }
-            }
-
-            Children = newChildren;
-        }
-        else if (children is MenuItem [] items)
-        {
-            SetParent (items);
-            Children = items;
-        }
-        else
-        {
-            Children = null;
-        }
-    }
-
-    private void SetParent (MenuItem [] children)
-    {
-        foreach (MenuItem child in children)
-        {
-            if (child is { Parent: null })
-            {
-                child.Parent = this;
-            }
-        }
-    }
-
-    private void SetTitle (string title)
-    {
-        title ??= string.Empty;
-        Title = title;
-    }
-}
-
 /// <summary>
 ///     <para>Provides a menu bar that spans the top of a <see cref="Toplevel"/> View with drop-down and cascading menus.</para>
 ///     <para>

+ 179 - 0
Terminal.Gui/Views/Menu/MenuBarItem.cs

@@ -0,0 +1,179 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="MenuBarItem"/> is a menu item on  <see cref="MenuBar"/>. MenuBarItems do not support
+///     <see cref="MenuItem.Shortcut"/>.
+/// </summary>
+public class MenuBarItem : MenuItem
+{
+    /// <summary>Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
+    /// <param name="action">Action to invoke when the menu item is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
+    public MenuBarItem (
+        string title,
+        string help,
+        Action action,
+        Func<bool> canExecute = null,
+        MenuItem parent = null
+    ) : base (title, help, action, canExecute, parent)
+    {
+        SetInitialProperties (title, null, null, true);
+    }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="children">The items in the current menu.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
+    public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/> with separate list of items.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="children">The list of items in the current menu.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if exist, otherwise is null.</param>
+    public MenuBarItem (string title, List<MenuItem []> children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    /// <param name="children">The items in the current menu.</param>
+    public MenuBarItem (MenuItem [] children) : this ("", children) { }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    public MenuBarItem () : this (new MenuItem [] { }) { }
+
+    /// <summary>
+    ///     Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this
+    ///     <see cref="MenuBarItem"/>
+    /// </summary>
+    /// <value>The children.</value>
+    public MenuItem [] Children { get; set; }
+
+    internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null;
+
+    /// <summary>Get the index of a child <see cref="MenuItem"/>.</summary>
+    /// <param name="children"></param>
+    /// <returns>Returns a greater than -1 if the <see cref="MenuItem"/> is a child.</returns>
+    public int GetChildrenIndex (MenuItem children)
+    {
+        var i = 0;
+
+        if (Children is { })
+        {
+            foreach (MenuItem child in Children)
+            {
+                if (child == children)
+                {
+                    return i;
+                }
+
+                i++;
+            }
+        }
+
+        return -1;
+    }
+
+    /// <summary>Check if a <see cref="MenuItem"/> is a submenu of this MenuBar.</summary>
+    /// <param name="menuItem"></param>
+    /// <returns>Returns <c>true</c> if it is a submenu. <c>false</c> otherwise.</returns>
+    public bool IsSubMenuOf (MenuItem menuItem)
+    {
+        foreach (MenuItem child in Children)
+        {
+            if (child == menuItem && child.Parent == menuItem.Parent)
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>Check if a <see cref="MenuItem"/> is a <see cref="MenuBarItem"/>.</summary>
+    /// <param name="menuItem"></param>
+    /// <returns>Returns a <see cref="MenuBarItem"/> or null otherwise.</returns>
+    public MenuBarItem SubMenu (MenuItem menuItem) { return menuItem as MenuBarItem; }
+
+    internal void AddShortcutKeyBindings (MenuBar menuBar)
+    {
+        if (Children is null)
+        {
+            return;
+        }
+
+        foreach (MenuItem menuItem in Children.Where (m => m is { }))
+        {
+            // For MenuBar only add shortcuts for submenus
+
+            if (menuItem.Shortcut != KeyCode.Null)
+            {
+                KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
+                menuBar.KeyBindings.Add (menuItem.Shortcut, keyBinding);
+            }
+
+            SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar);
+        }
+    }
+
+    private void SetInitialProperties (string title, object children, MenuItem parent = null, bool isTopLevel = false)
+    {
+        if (!isTopLevel && children is null)
+        {
+            throw new ArgumentNullException (
+                                             nameof (children),
+                                             @"The parameter cannot be null. Use an empty array instead."
+                                            );
+        }
+
+        SetTitle (title ?? "");
+
+        if (parent is { })
+        {
+            Parent = parent;
+        }
+
+        if (children is List<MenuItem []> childrenList)
+        {
+            MenuItem [] newChildren = [];
+
+            foreach (MenuItem [] grandChild in childrenList)
+            {
+                foreach (MenuItem child in grandChild)
+                {
+                    SetParent (grandChild);
+                    Array.Resize (ref newChildren, newChildren.Length + 1);
+                    newChildren [^1] = child;
+                }
+            }
+
+            Children = newChildren;
+        }
+        else if (children is MenuItem [] items)
+        {
+            SetParent (items);
+            Children = items;
+        }
+        else
+        {
+            Children = null;
+        }
+    }
+
+    private void SetParent (MenuItem [] children)
+    {
+        foreach (MenuItem child in children)
+        {
+            if (child is { Parent: null })
+            {
+                child.Parent = this;
+            }
+        }
+    }
+
+    private void SetTitle (string title)
+    {
+        title ??= string.Empty;
+        Title = title;
+    }
+}

+ 274 - 0
Terminal.Gui/Views/Menu/MenuItem.cs

@@ -0,0 +1,274 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
+///     can also have a checked indicator (see <see cref="Checked"/>).
+/// </summary>
+public class MenuItem
+{
+    private readonly ShortcutHelper _shortcutHelper;
+    private bool _allowNullChecked;
+    private MenuItemCheckStyle _checkType;
+
+    private string _title;
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
+    public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="help">Help text to display.</param>
+    /// <param name="action">Action to invoke when the menu item is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
+    /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
+    public MenuItem (
+        string title,
+        string help,
+        Action action,
+        Func<bool> canExecute = null,
+        MenuItem parent = null,
+        KeyCode shortcut = KeyCode.Null
+    )
+    {
+        Title = title ?? "";
+        Help = help ?? "";
+        Action = action;
+        CanExecute = canExecute;
+        Parent = parent;
+        _shortcutHelper = new ();
+
+        if (shortcut != KeyCode.Null)
+        {
+            Shortcut = shortcut;
+        }
+    }
+
+    /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
+    /// <value>Method to invoke.</value>
+    public Action Action { get; set; }
+
+    /// <summary>
+    ///     Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
+    ///     <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
+    ///     allows <see cref="Checked"/> to be true or false.
+    /// </summary>
+    public bool AllowNullChecked
+    {
+        get => _allowNullChecked;
+        set
+        {
+            _allowNullChecked = value;
+            Checked ??= false;
+        }
+    }
+
+    /// <summary>
+    ///     Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
+    ///     returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
+    /// </summary>
+    /// <value>Function to determine if the action is can be executed or not.</value>
+    public Func<bool> CanExecute { get; set; }
+
+    /// <summary>
+    ///     Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
+    ///     <see cref="MenuItemCheckStyle"/>.
+    /// </summary>
+    public bool? Checked { set; get; }
+
+    /// <summary>
+    ///     Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
+    ///     <see langword="true"/>.
+    /// </summary>
+    public MenuItemCheckStyle CheckType
+    {
+        get => _checkType;
+        set
+        {
+            _checkType = value;
+
+            if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
+            {
+                Checked = false;
+            }
+        }
+    }
+
+    /// <summary>Gets or sets arbitrary data for the menu item.</summary>
+    /// <remarks>This property is not used internally.</remarks>
+    public object Data { get; set; }
+
+    /// <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>
+    /// <value>The help text.</value>
+    public string Help { get; set; }
+
+    /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
+    /// <value>The parent.</value>
+    public MenuItem Parent { get; set; }
+
+    /// <summary>Gets or sets the title of the menu item .</summary>
+    /// <value>The title.</value>
+    public string Title
+    {
+        get => _title;
+        set
+        {
+            if (_title == value)
+            {
+                return;
+            }
+
+            _title = value;
+            GetHotKey ();
+        }
+    }
+
+    /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
+    internal bool IsFromSubMenu => Parent != null;
+
+    internal int TitleLength => GetMenuBarItemLength (Title);
+
+    // 
+    // ┌─────────────────────────────┐
+    // │ Quit  Quit UI Catalog  Ctrl+Q │
+    // └─────────────────────────────┘
+    // ┌─────────────────┐
+    // │ ◌ TopLevel Alt+T │
+    // └─────────────────┘
+    // TODO: Replace the `2` literals with named constants 
+    internal int Width => 1
+                          + // space before Title
+                          TitleLength
+                          + 2
+                          + // space after Title - BUGBUG: This should be 1 
+                          (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
+                               ? 2
+                               : 0)
+                          + // check glyph + space 
+                          (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
+                          + // Two spaces before Help
+                          (ShortcutTag.GetColumns () > 0
+                               ? 2 + ShortcutTag.GetColumns ()
+                               : 0); // Pad two spaces before shortcut tag (which are also aligned right)
+
+    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
+    public bool GetMenuBarItem () { return IsFromSubMenu; }
+
+    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
+    public MenuItem GetMenuItem () { return this; }
+
+    /// <summary>
+    ///     Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
+    ///     <see cref="CanExecute"/>.
+    /// </summary>
+    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
+
+    /// <summary>
+    ///     Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
+    ///     <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
+    /// </summary>
+    public void ToggleChecked ()
+    {
+        if (_checkType != MenuItemCheckStyle.Checked)
+        {
+            throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
+        }
+
+        bool? previousChecked = Checked;
+
+        if (AllowNullChecked)
+        {
+            Checked = previousChecked switch
+                      {
+                          null => true,
+                          true => false,
+                          false => null
+                      };
+        }
+        else
+        {
+            Checked = !Checked;
+        }
+    }
+
+    private static int GetMenuBarItemLength (string title)
+    {
+        return title.EnumerateRunes ()
+                    .Where (ch => ch != MenuBar.HotKeySpecifier)
+                    .Sum (ch => Math.Max (ch.GetColumns (), 1));
+    }
+
+    #region Keyboard Handling
+
+    // TODO: Update to use Key instead of Rune
+    /// <summary>
+    ///     The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
+    ///     <see cref="Title"/> of a MenuItem with an underscore ('_').
+    ///     <para>
+    ///         Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
+    ///         not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
+    ///     </para>
+    ///     <para>
+    ///         For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
+    ///         File menu. Pressing the N key will then activate the New MenuItem.
+    ///     </para>
+    ///     <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
+    /// </summary>
+    public Rune HotKey { get; set; }
+
+    private void GetHotKey ()
+    {
+        var nextIsHot = false;
+
+        foreach (char x in _title)
+        {
+            if (x == MenuBar.HotKeySpecifier.Value)
+            {
+                nextIsHot = true;
+            }
+            else
+            {
+                if (nextIsHot)
+                {
+                    HotKey = (Rune)char.ToUpper (x);
+
+                    break;
+                }
+
+                nextIsHot = false;
+                HotKey = default (Rune);
+            }
+        }
+    }
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>
+    ///     Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
+    ///     <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
+    ///     <see cref="MenuItem"/>.
+    ///     <para>
+    ///         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"/>.
+    ///     </para>
+    /// </summary>
+    public KeyCode Shortcut
+    {
+        get => _shortcutHelper.Shortcut;
+        set
+        {
+            if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
+            {
+                _shortcutHelper.Shortcut = value;
+            }
+        }
+    }
+
+    /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
+    public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
+                                     ? string.Empty
+                                     : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
+
+    #endregion Keyboard Handling
+}

+ 15 - 0
Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs

@@ -0,0 +1,15 @@
+namespace Terminal.Gui;
+
+/// <summary>Specifies how a <see cref="MenuItem"/> shows selection state.</summary>
+[Flags]
+public enum MenuItemCheckStyle
+{
+    /// <summary>The menu item will be shown normally, with no check indicator. The default.</summary>
+    NoCheck = 0b_0000_0000,
+
+    /// <summary>The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).</summary>
+    Checked = 0b_0000_0001,
+
+    /// <summary>The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.</summary>
+    Radio = 0b_0000_0010
+}