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 { internal static MenuBar _menuBar; /// Initializes a new instance of public MenuItem (Key shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { } /// 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, Key shortcutKey = null ) { Title = title ?? ""; Help = help ?? ""; Action = action; CanExecute = canExecute; Parent = parent; if (Parent is { } && Parent.ShortcutKey != Key.Empty) { Parent.ShortcutKey = Key.Empty; } // Setter will ensure Key.Empty if it's null ShortcutKey = shortcutKey; } private bool _allowNullChecked; private MenuItemCheckStyle _checkType; private string _title; /// 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; } /// /// Returns if the menu item is enabled. This method is a wrapper around /// . /// public bool IsEnabled () { return CanExecute?.Invoke () ?? true; } /// 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 (); } } /// /// 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; } } /// Merely a debugging aid to see the interaction with main. internal bool GetMenuBarItem () { return IsFromSubMenu; } /// Merely a debugging aid to see the interaction with main. internal MenuItem GetMenuItem () { return this; } /// 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) private static int GetMenuBarItemLength (string title) { return title.EnumerateRunes () .Where (ch => ch != MenuBar.HotKeySpecifier) .Sum (ch => Math.Max (ch.GetColumns (), 1)); } #region Keyboard Handling private Key _hotKey = Key.Empty; /// /// 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 Key HotKey { get => _hotKey; private set { var oldKey = _hotKey ?? Key.Empty; _hotKey = value ?? Key.Empty; UpdateHotKeyBinding (oldKey); } } private void GetHotKey () { var nextIsHot = false; foreach (char x in _title) { if (x == MenuBar.HotKeySpecifier.Value) { nextIsHot = true; } else if (nextIsHot) { HotKey = char.ToLower (x); return; } } HotKey = Key.Empty; } private Key _shortcutKey = Key.Empty; /// /// 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 Key ShortcutKey { get => _shortcutKey; set { var oldKey = _shortcutKey ?? Key.Empty; _shortcutKey = value ?? Key.Empty; UpdateShortcutKeyBinding (oldKey); } } /// Gets the text describing the keystroke combination defined by . public string ShortcutTag => ShortcutKey != Key.Empty ? ShortcutKey.ToString () : string.Empty; private void UpdateHotKeyBinding (Key oldKey) { if (_menuBar is null || _menuBar?.IsInitialized == false) { return; } if (oldKey != Key.Empty) { var index = _menuBar.Menus?.IndexOf (this); if (index > -1) { _menuBar.KeyBindings.Remove (oldKey.WithAlt); } } if (HotKey != Key.Empty) { var index = _menuBar.Menus?.IndexOf (this); if (index > -1) { _menuBar.KeyBindings.Remove (HotKey.WithAlt); KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, this); _menuBar.KeyBindings.Add (HotKey.WithAlt, keyBinding); } } } internal void UpdateShortcutKeyBinding (Key oldKey) { if (_menuBar is null) { return; } if (oldKey != Key.Empty) { _menuBar.KeyBindings.Remove (oldKey); } if (ShortcutKey != Key.Empty) { KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, this); // Remove an existent ShortcutKey _menuBar?.KeyBindings.Remove (ShortcutKey); _menuBar?.KeyBindings.Add (ShortcutKey, keyBinding); } } #endregion Keyboard Handling /// /// Removes a dynamically from the . /// public virtual void RemoveMenuItem () { if (Parent is { }) { MenuItem [] childrens = ((MenuBarItem)Parent).Children; var i = 0; foreach (MenuItem c in childrens) { if (c != this) { childrens [i] = c; i++; } } Array.Resize (ref childrens, childrens.Length - 1); if (childrens.Length == 0) { ((MenuBarItem)Parent).Children = null; } else { ((MenuBarItem)Parent).Children = childrens; } } if (ShortcutKey != Key.Empty) { // Remove an existent ShortcutKey _menuBar?.KeyBindings.Remove (ShortcutKey); } } }