#nullable enable 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 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; _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; _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; internal void AddShortcutKeyBinding (MenuBar menuBar, Key key) { ArgumentNullException.ThrowIfNull (menuBar); _menuBar = menuBar; AddOrUpdateShortcutKeyBinding (key); } private void AddOrUpdateShortcutKeyBinding (Key key) { if (key != Key.Empty) { _menuBar.KeyBindings.Remove (key); } 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); } } private void UpdateHotKeyBinding (Key oldKey) { if (_menuBar is null or { 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.Toggle], KeyBindingScope.HotKey, this); _menuBar.KeyBindings.Add (HotKey.WithAlt, keyBinding); } } } private void UpdateShortcutKeyBinding (Key oldKey) { // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (_menuBar is null) { return; } AddOrUpdateShortcutKeyBinding (oldKey); } #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!); } } }