#nullable enable
namespace Terminal.Gui;
///
/// Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down
/// all other content. Can be used as a context menu or a drop-down
/// menu as part of as part of .
///
///
///
/// To use as a context menu, register the popover menu with and call
/// .
///
///
public class PopoverMenu : PopoverBaseImpl, IDesignable
{
///
/// Initializes a new instance of the class.
///
public PopoverMenu () : this ((Menuv2?)null) { }
///
public PopoverMenu (IEnumerable? menuItems) : this (new Menuv2 (menuItems)) { }
///
public PopoverMenu (IEnumerable? menuItems) : this (new Menuv2 (menuItems)) { }
///
/// Initializes a new instance of the class with the specified root .
///
public PopoverMenu (Menuv2? root)
{
Key = DefaultKey;
base.Visible = false;
Root = root;
AddCommand (Command.Right, MoveRight);
KeyBindings.Add (Key.CursorRight, Command.Right);
AddCommand (Command.Left, MoveLeft);
KeyBindings.Add (Key.CursorLeft, Command.Left);
// TODO: Remove; for debugging for now
AddCommand (
Command.NotBound,
ctx =>
{
Logging.Trace ($"popoverMenu NotBound: {ctx}");
return false;
});
KeyBindings.Add (Key, Command.Quit);
KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit);
AddCommand (
Command.Quit,
ctx =>
{
if (!Visible)
{
return false;
}
Visible = false;
return false;
});
return;
bool? MoveLeft (ICommandContext? ctx)
{
if (Focused == Root)
{
return false;
}
if (MostFocused is MenuItemv2 { SuperView: Menuv2 focusedMenu })
{
focusedMenu.SuperMenuItem?.SetFocus ();
return true;
}
return AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
}
bool? MoveRight (ICommandContext? ctx)
{
if (MostFocused is MenuItemv2 { SubMenu.Visible: true } focused)
{
focused.SubMenu.SetFocus ();
return true;
}
return false; //AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
}
}
private Key _key = DefaultKey;
/// Specifies the key that will activate the context menu.
public Key Key
{
get => _key;
set
{
Key oldKey = _key;
_key = value;
KeyChanged?.Invoke (this, new (oldKey, _key));
}
}
/// Raised when is changed.
public event EventHandler? KeyChanged;
/// The default key for activating popover menus.
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key DefaultKey { get; set; } = Key.F10.WithShift;
///
/// The mouse flags that will cause the popover menu to be visible. The default is
/// which is typically the right mouse button.
///
public MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked;
///
/// Makes the popover menu visible and locates it at . The actual position of the
/// menu
/// will be adjusted to
/// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the
/// first MenuItem.
///
/// If , the current mouse position will be used.
public void MakeVisible (Point? idealScreenPosition = null)
{
UpdateKeyBindings ();
SetPosition (idealScreenPosition);
Application.Popover?.Show (this);
}
///
/// Locates the popover menu at . The actual position of the menu will be
/// adjusted to
/// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the
/// first MenuItem (if possible).
///
/// If , the current mouse position will be used.
public void SetPosition (Point? idealScreenPosition = null)
{
idealScreenPosition ??= Application.GetLastMousePosition ();
if (idealScreenPosition is null || Root is null)
{
return;
}
Point pos = idealScreenPosition.Value;
if (!Root.IsInitialized)
{
Root.BeginInit ();
Root.EndInit ();
Root.Layout ();
}
pos = GetMostVisibleLocationForSubMenu (Root, pos);
Root.X = pos.X;
Root.Y = pos.Y;
}
///
protected override void OnVisibleChanged ()
{
base.OnVisibleChanged ();
if (Visible)
{
AddAndShowSubMenu (_root);
}
else
{
HideAndRemoveSubMenu (_root);
Application.Popover?.Hide (this);
}
}
private Menuv2? _root;
///
/// Gets or sets the that is the root of the Popover Menu.
///
public Menuv2? Root
{
get => _root;
set
{
if (_root == value)
{
return;
}
if (_root is { })
{
_root.Accepting -= MenuOnAccepting;
}
HideAndRemoveSubMenu (_root);
_root = value;
if (_root is { })
{
_root.Accepting += MenuOnAccepting;
}
// TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus
// TODO: And it needs to clear the old bindings first
UpdateKeyBindings ();
// TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus
IEnumerable allMenus = GetAllSubMenus ();
foreach (Menuv2 menu in allMenus)
{
menu.Accepting += MenuOnAccepting;
menu.Accepted += MenuAccepted;
menu.SelectedMenuItemChanged += MenuOnSelectedMenuItemChanged;
}
}
}
private void UpdateKeyBindings ()
{
IEnumerable all = GetMenuItemsOfAllSubMenus ();
foreach (MenuItemv2 menuItem in all.Where (mi => mi.Command != Command.NotBound))
{
Key? key;
if (menuItem.TargetView is { })
{
// A TargetView implies HotKey
key = menuItem.TargetView.HotKeyBindings.GetFirstFromCommands (menuItem.Command);
}
else
{
// No TargetView implies Application HotKey
key = Application.KeyBindings.GetFirstFromCommands (menuItem.Command);
}
if (key is not { IsValid: true })
{
continue;
}
if (menuItem.Key.IsValid)
{
//Logging.Warning ("Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically.");
}
menuItem.Key = key;
//Logging.Trace ($"HotKey: {menuItem.Key}->{menuItem.Command}");
}
}
///
protected override bool OnKeyDownNotHandled (Key key)
{
// See if any of our MenuItems have this key as Key
IEnumerable all = GetMenuItemsOfAllSubMenus ();
foreach (MenuItemv2 menuItem in all)
{
if (menuItem.Key == key)
{
return menuItem.NewKeyDownEvent (key);
}
}
return base.OnKeyDownNotHandled (key);
}
///
/// Gets all the submenus in the PopoverMenu.
///
///
internal IEnumerable GetAllSubMenus ()
{
List result = [];
if (Root == null)
{
return result;
}
Stack stack = new ();
stack.Push (Root);
while (stack.Count > 0)
{
Menuv2 currentMenu = stack.Pop ();
result.Add (currentMenu);
foreach (View subView in currentMenu.SubViews)
{
if (subView is MenuItemv2 menuItem && menuItem.SubMenu != null)
{
stack.Push (menuItem.SubMenu);
}
}
}
return result;
}
///
/// Gets all the MenuItems in the PopoverMenu.
///
///
internal IEnumerable GetMenuItemsOfAllSubMenus ()
{
List result = [];
foreach (Menuv2 menu in GetAllSubMenus ())
{
foreach (View subView in menu.SubViews)
{
if (subView is MenuItemv2 menuItem)
{
result.Add (menuItem);
}
}
}
return result;
}
///
/// Pops up the submenu of the specified MenuItem, if there is one.
///
///
internal void ShowSubMenu (MenuItemv2? menuItem)
{
var menu = menuItem?.SuperView as Menuv2;
menu?.Layout ();
// If there's a visible peer, remove / hide it
if (menu?.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer)
{
HideAndRemoveSubMenu (visiblePeer.SubMenu);
visiblePeer.ForceFocusColors = false;
}
if (menuItem is { SubMenu: { Visible: false } })
{
AddAndShowSubMenu (menuItem.SubMenu);
Point idealLocation = ScreenToViewport (
new (
menuItem.FrameToScreen ().Right - menuItem.SubMenu.GetAdornmentsThickness ().Left,
menuItem.FrameToScreen ().Top - menuItem.SubMenu.GetAdornmentsThickness ().Top));
Point pos = GetMostVisibleLocationForSubMenu (menuItem.SubMenu, idealLocation);
menuItem.SubMenu.X = pos.X;
menuItem.SubMenu.Y = pos.Y;
menuItem.ForceFocusColors = true;
}
}
///
/// Gets the most visible screen-relative location for .
///
/// The menu to locate.
/// Ideal screen-relative location.
///
internal Point GetMostVisibleLocationForSubMenu (Menuv2 menu, Point idealLocation)
{
var pos = Point.Empty;
// Calculate the initial position to the right of the menu item
GetLocationEnsuringFullVisibility (
menu,
idealLocation.X,
idealLocation.Y,
out int nx,
out int ny);
return new (nx, ny);
}
private void AddAndShowSubMenu (Menuv2? menu)
{
if (menu is { SuperView: null })
{
// TODO: Find the menu item below the mouse, if any, and select it
// TODO: Enable No Border menu style
menu.Border!.LineStyle = LineStyle.Single;
menu.Border.Thickness = new (1);
if (!menu.IsInitialized)
{
menu.BeginInit ();
menu.EndInit ();
}
menu.ClearFocus ();
base.Add (menu);
// IMPORTANT: This must be done after adding the menu to the super view or Add will try
// to set focus to it.
menu.Visible = true;
menu.Layout ();
}
}
private void HideAndRemoveSubMenu (Menuv2? menu)
{
if (menu is { Visible: true })
{
// If there's a visible submenu, remove / hide it
if (menu.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer)
{
HideAndRemoveSubMenu (visiblePeer.SubMenu);
visiblePeer.ForceFocusColors = false;
}
menu.Visible = false;
menu.ClearFocus ();
base.Remove (menu);
if (menu == Root)
{
Visible = false;
}
}
}
private void MenuOnAccepting (object? sender, CommandEventArgs e)
{
var senderView = sender as View;
Logging.Trace ($"Sender: {senderView?.GetType ().Name}, {e.Context?.Source?.Title}");
if (e.Context?.Command != Command.HotKey)
{
Visible = false;
}
// This supports the case when a hotkey of a menuitem with a submenu is pressed
//e.Cancel = true;
}
private void MenuAccepted (object? sender, CommandEventArgs e)
{
//Logging.Trace ($"{e.Context?.Source?.Title}");
if (e.Context?.Source is MenuItemv2 { SubMenu: null })
{
HideAndRemoveSubMenu (_root);
RaiseAccepted (e.Context);
}
else if (e.Context?.Source is MenuItemv2 { SubMenu: { } } menuItemWithSubMenu)
{
ShowSubMenu (menuItemWithSubMenu);
}
}
///
/// Raises the / event indicating a menu (or submenu)
/// was accepted and the Menus in the PopoverMenu were hidden. Use this to determine when to hide the PopoverMenu.
///
///
///
protected bool? RaiseAccepted (ICommandContext? ctx)
{
//Logging.Trace ($"RaiseAccepted: {ctx}");
CommandEventArgs args = new () { Context = ctx };
OnAccepted (args);
Accepted?.Invoke (this, args);
return true;
}
///
/// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the
/// menu.
///
///
///
///
protected virtual void OnAccepted (CommandEventArgs args) { }
///
/// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the
/// menu.
///
///
///
/// See for more information.
///
///
public event EventHandler? Accepted;
private void MenuOnSelectedMenuItemChanged (object? sender, MenuItemv2? e)
{
Logging.Trace ($"e: {e?.Title}");
ShowSubMenu (e);
}
///
protected override void OnSubViewAdded (View view)
{
if (Root is null && (view is Menuv2 || view is MenuItemv2))
{
throw new InvalidOperationException ("Do not add MenuItems or Menus directly to a PopoverMenu. Use the Root property.");
}
base.OnSubViewAdded (view);
}
///
protected override void Dispose (bool disposing)
{
if (disposing)
{
IEnumerable allMenus = GetAllSubMenus ();
foreach (Menuv2 menu in allMenus)
{
menu.Accepting -= MenuOnAccepting;
menu.Accepted -= MenuAccepted;
menu.SelectedMenuItemChanged -= MenuOnSelectedMenuItemChanged;
}
_root?.Dispose ();
_root = null;
}
base.Dispose (disposing);
}
///
public bool EnableForDesign (ref readonly TContext context) where TContext : notnull
{
Root = new (
[
new MenuItemv2 (this, Command.Cut),
new MenuItemv2 (this, Command.Copy),
new MenuItemv2 (this, Command.Paste),
new Line (),
new MenuItemv2 (this, Command.SelectAll)
]);
Visible = true;
return true;
}
}