#nullable enable
using System.Diagnostics;
namespace Terminal.Gui;
///
/// Provides a cascading popover menu.
///
public class PopoverMenu : PopoverBaseImpl
{
///
/// Initializes a new instance of the class.
///
public PopoverMenu () : this (null) { }
///
/// Initializes a new instance of the class with the specified root .
///
public PopoverMenu (Menuv2? root)
{
base.Visible = false;
//base.ColorScheme = Colors.ColorSchemes ["Menu"];
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 (DefaultKey, Command.Quit);
KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit);
AddCommand (
Command.Quit,
ctx =>
{
if (!Visible)
{
return false;
}
Visible = false;
return RaiseAccepted (ctx);
});
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 (Focused == Root)
{
return false;
}
if (MostFocused is MenuItemv2 { SubMenu.Visible: true } focused)
{
focused.SubMenu.SetFocus ();
return true;
}
return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
}
}
///
/// The mouse flags that will cause the popover menu to be visible. The default is
/// which is typically the right mouse button.
///
public static MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked;
/// The default key for activating popover menus.
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key DefaultKey { get; set; } = Key.F10.WithShift;
///
/// 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?.ShowPopover (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 { } && Root is { })
{
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?.HidePopover (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;
}
UpdateKeyBindings ();
IEnumerable allMenus = GetAllSubMenus ();
foreach (Menuv2 menu in allMenus)
{
menu.Accepting += MenuOnAccepting;
menu.Accepted += MenuAccepted;
menu.SelectedMenuItemChanged += MenuOnSelectedMenuItemChanged;
}
}
}
private void UpdateKeyBindings ()
{
// TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus
// TODO: And it needs to clear them first
IEnumerable all = GetMenuItemsOfAllSubMenus ();
foreach (MenuItemv2 menuItem in all.Where(mi => mi.Command != Command.NotBound))
{
if (menuItem.TargetView is { })
{
// A TargetView implies HotKey
// Automatically set MenuItem.Key
Key? key = menuItem.TargetView.HotKeyBindings.GetFirstFromCommands (menuItem.Command);
if (key is { IsValid: true })
{
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}");
}
}
else
{
// No TargetView implies Application HotKey
Key? key = Application.KeyBindings.GetFirstFromCommands (menuItem.Command);
if (key is { IsValid: true })
{
if (menuItem.Key.IsValid)
{
// Logging.Warning ("App HotKey: Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically.");
}
menuItem.Key = key;
Logging.Trace ($"App HotKey: {menuItem.Key}->{menuItem.Command}");
}
}
}
foreach (MenuItemv2 menuItem in all.Where (mi => mi is { Command: Command.NotBound, Key.IsValid: true }))
{
}
}
///
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;
if (menu is { })
{
menu.Layout ();
}
// If there's a visible peer, remove / hide it
// Debug.Assert (menu is null || menu?.SubViews.Count (v => v is MenuItemv2 { SubMenu.Visible: true }) < 2);
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
// Debug.Assert (menu.SubViews.Count (v => v is MenuItemv2 { SubMenu.Visible: true }) <= 1);
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)
{
if (e.Context?.Command != Command.HotKey)
{
Visible = false;
}
else
{
// This supports the case when a hotkey of a menuitem with a submenu is pressed
e.Cancel = true;
}
Logging.Trace ($"{e.Context?.Source?.Title}");
}
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);
}
}
///
/// Riases 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}");
ShowSubMenu (e);
}
///
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);
}
}