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.
internal bool GetMenuBarItem () { return IsFromSubMenu; }
/// Merely a debugging aid to see the interaction with main.
internal 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
}