#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!);
}
}
}