#nullable enable
namespace Terminal.Gui;
///
/// ContextMenu provides a pop-up menu that can be positioned anywhere within a . ContextMenu is
/// analogous to and, once activated, works like a sub-menu of a (but
/// can be positioned anywhere).
///
/// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of
/// the ContextMenu frame (either to the right or left, depending on where the ContextMenu is relative to the edge
/// of the screen). By setting to , this behavior can be
/// changed such that all sub-menus are drawn within the ContextMenu frame.
///
///
/// ContextMenus can be activated using the Shift-F10 key (by default; use the to change to
/// another key).
///
///
/// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling
/// .
///
/// ContextMenus are located using screen coordinates and appear above all other Views.
///
public sealed class ContextMenu : IDisposable
{
private static MenuBar? _menuBar;
private Toplevel? _container;
private Key _key = DefaultKey;
private MouseFlags _mouseFlags = MouseFlags.Button3Clicked;
/// Initializes a context menu with no menu items.
public ContextMenu ()
{
if (IsShow)
{
Hide ();
IsShow = false;
}
}
/// The default shortcut key for activating the context menu.
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key DefaultKey { get; set; } = Key.F10.WithShift;
///
/// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position is
/// less than zero. The default is which means the context menu will be forced to the right. If
/// set to , the context menu will be clipped on the left if x is less than zero.
///
public bool ForceMinimumPosToZero { get; set; } = true;
/// The host which position will be used, otherwise if it's null the container will be used.
public View? Host { get; set; }
/// Gets whether the ContextMenu is showing or not.
public static bool IsShow { get; private set; }
/// Specifies the key that will activate the context menu.
public Key Key
{
get => _key;
set
{
Key oldKey = _key;
_key = value;
KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, _key));
}
}
/// Gets the that is hosting this context menu.
public MenuBar? MenuBar => _menuBar;
/// Gets or sets the menu items for this context menu.
public MenuBarItem? MenuItems { get; private set; }
/// specifies the mouse action used to activate the context menu by mouse.
public MouseFlags MouseFlags
{
get => _mouseFlags;
set
{
MouseFlags oldFlags = _mouseFlags;
_mouseFlags = value;
MouseFlagsChanged?.Invoke (this, new MouseFlagsChangedEventArgs (oldFlags, value));
}
}
/// Gets or sets the menu position.
public Point Position { get; set; }
///
/// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If , the
/// ContextMenu and any sub-menus that would normally cascade will be displayed within a single frame. If
/// (the default), sub-menus will cascade using separate frames for each level of the menu
/// hierarchy.
///
public bool UseSubMenusSingleFrame { get; set; }
/// Disposes the context menu object.
public void Dispose ()
{
if (_menuBar is { })
{
_menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
}
Application.UngrabMouse ();
_menuBar?.Dispose ();
_menuBar = null;
IsShow = false;
if (_container is { })
{
_container.Closing -= Container_Closing;
_container.Deactivate -= Container_Deactivate;
_container.Disposing -= Container_Disposing;
}
}
/// Hides (closes) the ContextMenu.
public void Hide ()
{
RemoveKeyBindings (MenuItems);
_menuBar?.CleanUp ();
IsShow = false;
}
private void RemoveKeyBindings (MenuBarItem? menuBarItem)
{
if (menuBarItem is null)
{
return;
}
foreach (MenuItem? menuItem in menuBarItem.Children!)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (menuItem is null)
{
continue;
}
if (menuItem is MenuBarItem barItem)
{
RemoveKeyBindings (barItem);
}
else
{
if (menuItem.ShortcutKey != Key.Empty)
{
// Remove an existent ShortcutKey
_menuBar?.KeyBindings.Remove (menuItem.ShortcutKey!);
}
}
}
}
/// Event invoked when the is changed.
public event EventHandler? KeyChanged;
/// Event invoked when the is changed.
public event EventHandler? MouseFlagsChanged;
/// Shows (opens) the ContextMenu, displaying the s it contains.
public void Show (MenuBarItem? menuItems)
{
if (_menuBar is { })
{
Hide ();
Dispose ();
}
if (menuItems is null || menuItems.Children!.Length == 0)
{
return;
}
MenuItems = menuItems;
_container = Application.Top;
_container!.Closing += Container_Closing;
_container.Deactivate += Container_Deactivate;
_container.Disposing += Container_Disposing;
Rectangle frame = Application.Screen;
Point position = Position;
if (Host is { })
{
Point pos = Host.ViewportToScreen (frame).Location;
pos.Y += Host.Frame.Height > 0 ? Host.Frame.Height - 1 : 0;
if (position != pos)
{
Position = position = pos;
}
}
Rectangle rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children);
if (rect.Right >= frame.Right)
{
if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero)
{
position.X = frame.Right - rect.Width;
}
else if (ForceMinimumPosToZero)
{
position.X = 0;
}
}
else if (ForceMinimumPosToZero && position.X < 0)
{
position.X = 0;
}
if (rect.Bottom >= frame.Bottom)
{
if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero)
{
if (Host is null)
{
position.Y = frame.Bottom - rect.Height - 1;
}
else
{
Point pos = Host.ViewportToScreen (frame).Location;
position.Y = pos.Y - rect.Height - 1;
}
}
else if (ForceMinimumPosToZero)
{
position.Y = 0;
}
}
else if (ForceMinimumPosToZero && position.Y < 0)
{
position.Y = 0;
}
_menuBar = new MenuBar
{
X = position.X,
Y = position.Y,
Width = 0,
Height = 0,
UseSubMenusSingleFrame = UseSubMenusSingleFrame,
Key = Key,
Menus = [MenuItems]
};
_menuBar._isContextMenuLoading = true;
_menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
_menuBar.BeginInit ();
_menuBar.EndInit ();
IsShow = true;
_menuBar.OpenMenu ();
}
private void Container_Closing (object? sender, ToplevelClosingEventArgs obj) { Hide (); }
private void Container_Deactivate (object? sender, ToplevelEventArgs e) { Hide (); }
private void Container_Disposing (object? sender, EventArgs e) { Dispose (); }
private void MenuBar_MenuAllClosed (object? sender, EventArgs e) { Hide (); }
}