123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- #nullable enable
- 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
- {
- internal MenuBar _menuBar;
- /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
- public MenuItem (Key? shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { }
- /// <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="shortcutKey">The <see cref="ShortcutKey"/> keystroke combination.</param>
- public MenuItem (
- string? title,
- string? help,
- Action? action,
- Func<bool>? 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;
- /// <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>
- /// 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>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>
- /// 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;
- }
- }
- /// <summary>Merely a debugging aid to see the interaction with main.</summary>
- internal bool GetMenuBarItem () { return IsFromSubMenu; }
- /// <summary>Merely a debugging aid to see the interaction with main.</summary>
- internal MenuItem GetMenuItem () { return this; }
- /// <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)
- 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;
- /// <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="ShortcutKey"/> which enable global key-bindings to menu items.</para>
- /// </summary>
- 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;
- /// <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="Key"/> 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 Key? ShortcutKey
- {
- get => _shortcutKey;
- set
- {
- var oldKey = _shortcutKey;
- _shortcutKey = value ?? Key.Empty;
- UpdateShortcutKeyBinding (oldKey);
- }
- }
- /// <summary>Gets the text describing the keystroke combination defined by <see cref="ShortcutKey"/>.</summary>
- 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.HotKeyBindings.Remove (key);
- }
- if (ShortcutKey != Key.Empty)
- {
- KeyBinding keyBinding = new ([Command.Select], null, data: this);
- // Remove an existent ShortcutKey
- _menuBar.HotKeyBindings.Remove (ShortcutKey!);
- _menuBar.HotKeyBindings.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.HotKeyBindings.Remove (oldKey.WithAlt);
- }
- }
- if (HotKey != Key.Empty)
- {
- var index = _menuBar.Menus.IndexOf (this);
- if (index > -1)
- {
- _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt);
- KeyBinding keyBinding = new ([Command.Toggle], null, data: this);
- _menuBar.HotKeyBindings.Add (HotKey.WithAlt, keyBinding);
- }
- }
- }
- private void UpdateShortcutKeyBinding (Key oldKey)
- {
- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
- if (_menuBar is null)
- {
- return;
- }
- AddOrUpdateShortcutKeyBinding (oldKey);
- }
- #endregion Keyboard Handling
- /// <summary>
- /// Removes a <see cref="MenuItem"/> dynamically from the <see cref="Parent"/>.
- /// </summary>
- 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.HotKeyBindings.Remove (ShortcutKey!);
- }
- }
- }
|