diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 7ed394cee..c3651ffe0 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -1,292 +1,5 @@ namespace Terminal.Gui; -/// Specifies how a shows selection state. -[Flags] -public enum MenuItemCheckStyle -{ - /// The menu item will be shown normally, with no check indicator. The default. - NoCheck = 0b_0000_0000, - - /// The menu item will indicate checked/un-checked state (see ). - Checked = 0b_0000_0001, - - /// The menu item is part of a menu radio group (see ) and will indicate selected state. - Radio = 0b_0000_0010 -} - -/// -/// A has title, an associated help text, and an action to execute on activation. MenuItems -/// can also have a checked indicator (see ). -/// -public class MenuItem -{ - private readonly ShortcutHelper _shortcutHelper; - private bool _allowNullChecked; - private MenuItemCheckStyle _checkType; - - private string _title; - - // TODO: Update to use Key instead of KeyCode - /// Initializes a new instance of - public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { } - - // TODO: Update to use Key instead of KeyCode - /// Initializes a new instance of . - /// Title for the menu item. - /// Help text to display. - /// Action to invoke when the menu item is activated. - /// Function to determine if the action can currently be executed. - /// The of this menu item. - /// The keystroke combination. - public MenuItem ( - string title, - string help, - Action action, - Func 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; - } - } - - /// Gets or sets the action to be invoked when the menu item is triggered. - /// Method to invoke. - public Action Action { get; set; } - - /// - /// Used only if is of type. If - /// allows to be null, true or false. If only - /// allows to be true or false. - /// - public bool AllowNullChecked - { - get => _allowNullChecked; - set - { - _allowNullChecked = value; - Checked ??= false; - } - } - - /// - /// Gets or sets the action to be invoked to determine if the menu can be triggered. If - /// returns the menu item will be enabled. Otherwise, it will be disabled. - /// - /// Function to determine if the action is can be executed or not. - public Func CanExecute { get; set; } - - /// - /// Sets or gets whether the shows a check indicator or not. See - /// . - /// - public bool? Checked { set; get; } - - /// - /// Sets or gets the of a menu item where is set to - /// . - /// - public MenuItemCheckStyle CheckType - { - get => _checkType; - set - { - _checkType = value; - - if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null) - { - Checked = false; - } - } - } - - /// Gets or sets arbitrary data for the menu item. - /// This property is not used internally. - public object Data { get; set; } - - /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . - /// The help text. - public string Help { get; set; } - - /// Gets the parent for this . - /// The parent. - public MenuItem Parent { get; set; } - - /// Gets or sets the title of the menu item . - /// The title. - public string Title - { - get => _title; - set - { - if (_title == value) - { - return; - } - - _title = value; - GetHotKey (); - } - } - - /// Gets if this is from a sub-menu. - 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) - - /// Merely a debugging aid to see the interaction with main. - public bool GetMenuBarItem () { return IsFromSubMenu; } - - /// Merely a debugging aid to see the interaction with main. - public MenuItem GetMenuItem () { return this; } - - /// - /// Returns if the menu item is enabled. This method is a wrapper around - /// . - /// - public bool IsEnabled () { return CanExecute?.Invoke () ?? true; } - - /// - /// Toggle the between three states if is - /// or between two states if is . - /// - 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 - /// - /// The HotKey is used to activate a with the keyboard. HotKeys are defined by prefixing the - /// of a MenuItem with an underscore ('_'). - /// - /// Pressing Alt-Hotkey for a (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. - /// - /// - /// 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. - /// - /// See also which enable global key-bindings to menu items. - /// - 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 - /// - /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the - /// that is the parent of the or this - /// . - /// - /// The will be drawn on the MenuItem to the right of the and - /// text. See . - /// - /// - public KeyCode Shortcut - { - get => _shortcutHelper.Shortcut; - set - { - if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null)) - { - _shortcutHelper.Shortcut = value; - } - } - } - - /// Gets the text describing the keystroke combination defined by . - public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null - ? string.Empty - : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter); - - #endregion Keyboard Handling -} - /// /// An internal class used to represent a menu pop-up menu. Created and managed by and /// . diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index c7b371d2d..08b045f81 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -1,183 +1,5 @@ namespace Terminal.Gui; -/// -/// is a menu item on . MenuBarItems do not support -/// . -/// -public class MenuBarItem : MenuItem -{ - /// Initializes a new as a . - /// Title for the menu item. - /// Help text to display. Will be displayed next to the Title surrounded by parentheses. - /// Action to invoke when the menu item is activated. - /// Function to determine if the action can currently be executed. - /// The parent of this if exist, otherwise is null. - public MenuBarItem ( - string title, - string help, - Action action, - Func canExecute = null, - MenuItem parent = null - ) : base (title, help, action, canExecute, parent) - { - SetInitialProperties (title, null, null, true); - } - - /// Initializes a new . - /// Title for the menu item. - /// The items in the current menu. - /// The parent of this if exist, otherwise is null. - public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); } - - /// Initializes a new with separate list of items. - /// Title for the menu item. - /// The list of items in the current menu. - /// The parent of this if exist, otherwise is null. - public MenuBarItem (string title, List children, MenuItem parent = null) { SetInitialProperties (title, children, parent); } - - /// Initializes a new . - /// The items in the current menu. - public MenuBarItem (MenuItem [] children) : this ("", children) { } - - /// Initializes a new . - public MenuBarItem () : this (new MenuItem [] { }) { } - - /// - /// Gets or sets an array of objects that are the children of this - /// - /// - /// The children. - public MenuItem [] Children { get; set; } - - internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null; - - /// Get the index of a child . - /// - /// Returns a greater than -1 if the is a child. - public int GetChildrenIndex (MenuItem children) - { - var i = 0; - - if (Children is { }) - { - foreach (MenuItem child in Children) - { - if (child == children) - { - return i; - } - - i++; - } - } - - return -1; - } - - /// Check if a is a submenu of this MenuBar. - /// - /// Returns true if it is a submenu. false otherwise. - public bool IsSubMenuOf (MenuItem menuItem) - { - foreach (MenuItem child in Children) - { - if (child == menuItem && child.Parent == menuItem.Parent) - { - return true; - } - } - - return false; - } - - /// Check if a is a . - /// - /// Returns a or null otherwise. - 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 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; - } -} - /// /// Provides a menu bar that spans the top of a View with drop-down and cascading menus. /// diff --git a/Terminal.Gui/Views/Menu/MenuBarItem.cs b/Terminal.Gui/Views/Menu/MenuBarItem.cs new file mode 100644 index 000000000..801b0573b --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuBarItem.cs @@ -0,0 +1,179 @@ +namespace Terminal.Gui; + +/// +/// is a menu item on . MenuBarItems do not support +/// . +/// +public class MenuBarItem : MenuItem +{ + /// Initializes a new as a . + /// Title for the menu item. + /// Help text to display. Will be displayed next to the Title surrounded by parentheses. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executed. + /// The parent of this if exist, otherwise is null. + public MenuBarItem ( + string title, + string help, + Action action, + Func canExecute = null, + MenuItem parent = null + ) : base (title, help, action, canExecute, parent) + { + SetInitialProperties (title, null, null, true); + } + + /// Initializes a new . + /// Title for the menu item. + /// The items in the current menu. + /// The parent of this if exist, otherwise is null. + public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); } + + /// Initializes a new with separate list of items. + /// Title for the menu item. + /// The list of items in the current menu. + /// The parent of this if exist, otherwise is null. + public MenuBarItem (string title, List children, MenuItem parent = null) { SetInitialProperties (title, children, parent); } + + /// Initializes a new . + /// The items in the current menu. + public MenuBarItem (MenuItem [] children) : this ("", children) { } + + /// Initializes a new . + public MenuBarItem () : this (new MenuItem [] { }) { } + + /// + /// Gets or sets an array of objects that are the children of this + /// + /// + /// The children. + public MenuItem [] Children { get; set; } + + internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null; + + /// Get the index of a child . + /// + /// Returns a greater than -1 if the is a child. + public int GetChildrenIndex (MenuItem children) + { + var i = 0; + + if (Children is { }) + { + foreach (MenuItem child in Children) + { + if (child == children) + { + return i; + } + + i++; + } + } + + return -1; + } + + /// Check if a is a submenu of this MenuBar. + /// + /// Returns true if it is a submenu. false otherwise. + public bool IsSubMenuOf (MenuItem menuItem) + { + foreach (MenuItem child in Children) + { + if (child == menuItem && child.Parent == menuItem.Parent) + { + return true; + } + } + + return false; + } + + /// Check if a is a . + /// + /// Returns a or null otherwise. + 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 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; + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/MenuItem.cs b/Terminal.Gui/Views/Menu/MenuItem.cs new file mode 100644 index 000000000..7cd5d2ac5 --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuItem.cs @@ -0,0 +1,274 @@ +namespace Terminal.Gui; + +/// +/// A has title, an associated help text, and an action to execute on activation. MenuItems +/// can also have a checked indicator (see ). +/// +public class MenuItem +{ + private readonly ShortcutHelper _shortcutHelper; + private bool _allowNullChecked; + private MenuItemCheckStyle _checkType; + + private string _title; + + // TODO: Update to use Key instead of KeyCode + /// Initializes a new instance of + public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { } + + // TODO: Update to use Key instead of KeyCode + /// Initializes a new instance of . + /// Title for the menu item. + /// Help text to display. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executed. + /// The of this menu item. + /// The keystroke combination. + public MenuItem ( + string title, + string help, + Action action, + Func 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; + } + } + + /// Gets or sets the action to be invoked when the menu item is triggered. + /// Method to invoke. + public Action Action { get; set; } + + /// + /// Used only if is of type. If + /// allows to be null, true or false. If only + /// allows to be true or false. + /// + public bool AllowNullChecked + { + get => _allowNullChecked; + set + { + _allowNullChecked = value; + Checked ??= false; + } + } + + /// + /// Gets or sets the action to be invoked to determine if the menu can be triggered. If + /// returns the menu item will be enabled. Otherwise, it will be disabled. + /// + /// Function to determine if the action is can be executed or not. + public Func CanExecute { get; set; } + + /// + /// Sets or gets whether the shows a check indicator or not. See + /// . + /// + public bool? Checked { set; get; } + + /// + /// Sets or gets the of a menu item where is set to + /// . + /// + public MenuItemCheckStyle CheckType + { + get => _checkType; + set + { + _checkType = value; + + if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null) + { + Checked = false; + } + } + } + + /// Gets or sets arbitrary data for the menu item. + /// This property is not used internally. + public object Data { get; set; } + + /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . + /// The help text. + public string Help { get; set; } + + /// Gets the parent for this . + /// The parent. + public MenuItem Parent { get; set; } + + /// Gets or sets the title of the menu item . + /// The title. + public string Title + { + get => _title; + set + { + if (_title == value) + { + return; + } + + _title = value; + GetHotKey (); + } + } + + /// Gets if this is from a sub-menu. + 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) + + /// Merely a debugging aid to see the interaction with main. + public bool GetMenuBarItem () { return IsFromSubMenu; } + + /// Merely a debugging aid to see the interaction with main. + public MenuItem GetMenuItem () { return this; } + + /// + /// Returns if the menu item is enabled. This method is a wrapper around + /// . + /// + public bool IsEnabled () { return CanExecute?.Invoke () ?? true; } + + /// + /// Toggle the between three states if is + /// or between two states if is . + /// + 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 + /// + /// The HotKey is used to activate a with the keyboard. HotKeys are defined by prefixing the + /// of a MenuItem with an underscore ('_'). + /// + /// Pressing Alt-Hotkey for a (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. + /// + /// + /// 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. + /// + /// See also which enable global key-bindings to menu items. + /// + 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 + /// + /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the + /// that is the parent of the or this + /// . + /// + /// The will be drawn on the MenuItem to the right of the and + /// text. See . + /// + /// + public KeyCode Shortcut + { + get => _shortcutHelper.Shortcut; + set + { + if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null)) + { + _shortcutHelper.Shortcut = value; + } + } + } + + /// Gets the text describing the keystroke combination defined by . + public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null + ? string.Empty + : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter); + + #endregion Keyboard Handling +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs b/Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs new file mode 100644 index 000000000..f7b7b5210 --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui; + +/// Specifies how a shows selection state. +[Flags] +public enum MenuItemCheckStyle +{ + /// The menu item will be shown normally, with no check indicator. The default. + NoCheck = 0b_0000_0000, + + /// The menu item will indicate checked/un-checked state (see ). + Checked = 0b_0000_0001, + + /// The menu item is part of a menu radio group (see ) and will indicate selected state. + Radio = 0b_0000_0010 +} \ No newline at end of file