| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- #nullable enable
- using System.Diagnostics;
- namespace Terminal.Gui;
- /// <summary>
- /// Provides a cascading popover menu.
- /// </summary>
- public class PopoverMenu : PopoverBaseImpl
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="PopoverMenu"/> class.
- /// </summary>
- public PopoverMenu () : this (null) { }
- /// <summary>
- /// Initializes a new instance of the <see cref="PopoverMenu"/> class with the specified root <see cref="Menuv2"/>.
- /// </summary>
- 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);
- }
- }
- /// <summary>
- /// The mouse flags that will cause the popover menu to be visible. The default is
- /// <see cref="MouseFlags.Button3Clicked"/> which is typically the right mouse button.
- /// </summary>
- public static MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked;
- /// <summary>The default key for activating popover menus.</summary>
- [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
- public static Key DefaultKey { get; set; } = Key.F10.WithShift;
- /// <summary>
- /// Makes the popover menu visible and locates it at <paramref name="idealScreenPosition"/>. 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.
- /// </summary>
- /// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
- public void MakeVisible (Point? idealScreenPosition = null)
- {
- UpdateKeyBindings ();
- SetPosition (idealScreenPosition);
- Application.Popover?.ShowPopover (this);
- }
- /// <summary>
- /// Locates the popover menu at <paramref name="idealScreenPosition"/>. 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).
- /// </summary>
- /// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
- 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;
- }
- }
- /// <inheritdoc/>
- protected override void OnVisibleChanged ()
- {
- base.OnVisibleChanged ();
- if (Visible)
- {
- AddAndShowSubMenu (_root);
- }
- else
- {
- HideAndRemoveSubMenu (_root);
- Application.Popover?.HidePopover (this);
- }
- }
- private Menuv2? _root;
- /// <summary>
- /// Gets or sets the <see cref="Menuv2"/> that is the root of the Popover Menu.
- /// </summary>
- 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<Menuv2> 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<MenuItemv2> 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 }))
- {
- }
- }
- /// <inheritdoc/>
- protected override bool OnKeyDownNotHandled (Key key)
- {
- // See if any of our MenuItems have this key as Key
- IEnumerable<MenuItemv2> all = GetMenuItemsOfAllSubMenus ();
- foreach (MenuItemv2 menuItem in all)
- {
- if (menuItem.Key == key)
- {
- return menuItem.NewKeyDownEvent (key);
- }
- }
- return base.OnKeyDownNotHandled (key);
- }
- /// <summary>
- /// Gets all the submenus in the PopoverMenu.
- /// </summary>
- /// <returns></returns>
- internal IEnumerable<Menuv2> GetAllSubMenus ()
- {
- List<Menuv2> result = [];
- if (Root == null)
- {
- return result;
- }
- Stack<Menuv2> 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;
- }
- /// <summary>
- /// Gets all the MenuItems in the PopoverMenu.
- /// </summary>
- /// <returns></returns>
- internal IEnumerable<MenuItemv2> GetMenuItemsOfAllSubMenus ()
- {
- List<MenuItemv2> result = [];
- foreach (Menuv2 menu in GetAllSubMenus ())
- {
- foreach (View subView in menu.SubViews)
- {
- if (subView is MenuItemv2 menuItem)
- {
- result.Add (menuItem);
- }
- }
- }
- return result;
- }
- /// <summary>
- /// Pops up the submenu of the specified MenuItem, if there is one.
- /// </summary>
- /// <param name="menuItem"></param>
- 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;
- }
- }
- /// <summary>
- /// Gets the most visible screen-relative location for <paramref name="menu"/>.
- /// </summary>
- /// <param name="menu">The menu to locate.</param>
- /// <param name="idealLocation">Ideal screen-relative location.</param>
- /// <returns></returns>
- 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);
- }
- }
- /// <summary>
- /// Riases the <see cref="OnAccepted"/>/<see cref="Accepted"/> 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.
- /// </summary>
- /// <param name="ctx"></param>
- /// <returns></returns>
- protected bool? RaiseAccepted (ICommandContext? ctx)
- {
- Logging.Trace ($"RaiseAccepted: {ctx}");
- CommandEventArgs args = new () { Context = ctx };
- OnAccepted (args);
- Accepted?.Invoke (this, args);
- return true;
- }
- /// <summary>
- /// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the
- /// menu.
- /// </summary>
- /// <remarks>
- /// </remarks>
- /// <param name="args"></param>
- protected virtual void OnAccepted (CommandEventArgs args) { }
- /// <summary>
- /// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the
- /// menu.
- /// </summary>
- /// <remarks>
- /// <para>
- /// See <see cref="RaiseAccepted"/> for more information.
- /// </para>
- /// </remarks>
- public event EventHandler<CommandEventArgs>? Accepted;
- private void MenuOnSelectedMenuItemChanged (object? sender, MenuItemv2? e)
- {
- //Logging.Trace ($"{e}");
- ShowSubMenu (e);
- }
- /// <inheritdoc/>
- protected override void Dispose (bool disposing)
- {
- if (disposing)
- {
- IEnumerable<Menuv2> allMenus = GetAllSubMenus ();
- foreach (Menuv2 menu in allMenus)
- {
- menu.Accepting -= MenuOnAccepting;
- menu.Accepted -= MenuAccepted;
- menu.SelectedMenuItemChanged -= MenuOnSelectedMenuItemChanged;
- }
- _root?.Dispose ();
- _root = null;
- }
- base.Dispose (disposing);
- }
- }
|