diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs index f8d3b2fdc6..e10649f7e2 100644 --- a/Examples/UICatalog/Scenarios/Menus.cs +++ b/Examples/UICatalog/Scenarios/Menus.cs @@ -63,6 +63,18 @@ public override void Main () eventLog.MoveDown (); }; + menuHostView.Selecting += (o, args) => + { + if (o is not View sender || args.Handled) + { + return; + } + + Logging.Debug ($"{sender.Id} Selecting: {args?.Context?.Source?.Title}"); + eventSource.Add ($"{sender.Id} Selecting: {args?.Context?.Source?.Title}: "); + eventLog.MoveDown (); + }; + menuHostView.Accepting += (o, args) => { if (o is not View sender || args.Handled) @@ -75,6 +87,18 @@ public override void Main () eventLog.MoveDown (); }; + menuHostView.ContextMenu!.Selecting += (o, args) => + { + if (o is not View sender || args.Handled) + { + return; + } + + Logging.Debug ($"{sender.Id} Selecting: {args?.Context?.Source?.Title}"); + eventSource.Add ($"{sender.Id} Selecting: {args?.Context?.Source?.Title}: "); + eventLog.MoveDown (); + }; + menuHostView.ContextMenu!.Accepted += (o, args) => { if (o is not View sender || args.Handled) @@ -105,7 +129,6 @@ public MenuHost () { CanFocus = true; BorderStyle = LineStyle.Dashed; - AddCommand ( Command.Context, ctx => diff --git a/Terminal.Gui/Application/PopoverBaseImpl.cs b/Terminal.Gui/Application/PopoverBaseImpl.cs index dfa05c8970..5ec2478887 100644 --- a/Terminal.Gui/Application/PopoverBaseImpl.cs +++ b/Terminal.Gui/Application/PopoverBaseImpl.cs @@ -28,9 +28,9 @@ protected PopoverBaseImpl () ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse; // TODO: Add a diagnostic setting for this? - //TextFormatter.VerticalAlignment = Alignment.End; - //TextFormatter.Alignment = Alignment.End; - //base.Text = "popover"; + TextFormatter.VerticalAlignment = Alignment.End; + TextFormatter.Alignment = Alignment.End; + base.Text = "PopoverBaseImpl"; AddCommand (Command.Quit, Quit); KeyBindings.Add (Application.QuitKey, Command.Quit); diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index df672d65c0..c54cfd305b 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -319,7 +319,7 @@ "Glyphs.VLineHvDa3": "┇", "Glyphs.VLineHvDa4": "┋" } - }, + }, { "Dark": { "Dialog.DefaultButtonAlignment": "End", diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index a11fa80630..f111de44f3 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -221,17 +221,24 @@ private void SetupCommands () Logging.Debug ($"{Title} ({ctx?.Source?.Title})"); CommandEventArgs args = new () { Context = ctx }; - // Best practice is to invoke the virtual method first. - // This allows derived classes to handle the event and potentially cancel it. - if (OnSelecting (args) || args.Handled) + if (!args.Handled && Selecting is { }) { - return true; + // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. + Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Raising Selecting..."); + Selecting?.Invoke (this, args); } - // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. - Selecting?.Invoke (this, args); + // - bubbled up the SuperView hierarchy. + if (!args.Handled) + { + if (SuperView is { }) + { + Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Invoking Select on SuperView ({SuperView.Title}/{SuperView.Id})..."); + return SuperView?.InvokeCommand (Command.Select, ctx); + } + } - return Selecting is null ? null : args.Handled; + return args.Handled; } /// diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 0c78fd873b..307496b81f 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -433,6 +433,13 @@ public string Title { get { +// TODO: Re-enable this after Debug logging in RaiseSelecting is removed +//#if DEBUG_IDISPOSABLE +// if (EnableDebugIDisposableAsserts && WasDisposed) +// { +// throw new ObjectDisposedException (GetType ().FullName); +// } +//#endif return _title; } set diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index 7dcc40c012..7dc816b36f 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -498,7 +498,7 @@ public bool EnableForDesign (ref TContext context) where TContext : no // Note: This menu is used by unit tests. If you modify it, you'll likely have to update // unit tests. - Id = "DemonuBar"; + Id = "DemoBar"; var bordersCb = new CheckBox { @@ -695,7 +695,7 @@ MenuItemv2 [] ConfigureDetailsSubMenu () var editMode = new MenuItemv2 { - Text = "App Binding to Command.Edit", + Text = "Command = Edit; TargetView = null", Id = "EditMode", Command = Command.Edit, CommandView = new CheckBox diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItemv2.cs index ca7cbf578c..f6b0ba4c3b 100644 --- a/Terminal.Gui/Views/Menu/MenuItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuItemv2.cs @@ -111,26 +111,40 @@ public Command Command if (commandContext is CommandContext keyCommandContext) { + if (SubMenu is { Visible: true } && (keyCommandContext.Command == Command.Select || keyCommandContext.Command == Command.Accept)) + { + // Our submenu is open; and user pressed enter or clicked, set focus to it + Logging.Debug ($"{Title} - SubMenu is Visible; Accept/Select - {keyCommandContext.Command}"); + SubMenu.SetFocus (); + return true; + } + + if (SubMenu is { Visible: true } && keyCommandContext.Command == Command.HotKey) + { + // Our submenu is open; let command processing flow to it + Logging.Debug ($"{Title} - SubMenu Visible; HotKey - {keyCommandContext.Command}"); + return false; + } + if (keyCommandContext.Binding.Key is { } && keyCommandContext.Binding.Key == Application.QuitKey && SuperView is { Visible: true }) { // This supports a MenuItem with Key = Application.QuitKey/Command = Command.Quit Logging.Debug ($"{Title} - Ignoring Key = Application.QuitKey/Command = Command.Quit"); quit = true; - //ret = true; } } - // Translate the incoming command to Command - if (Command != Command.NotBound && commandContext is { }) - { - commandContext.Command = Command; - } - if (!quit) { if (TargetView is { }) { - Logging.Debug ($"{Title} - InvokeCommand on TargetView ({TargetView.Title})..."); + // Translate the incoming command to Command + if (Command != Command.NotBound && commandContext is { }) + { + commandContext.Command = Command; + } + + Logging.Debug ($"{Title} - InvokeCommand on TargetView ({TargetView.Title}) - Command: {commandContext?.Command}..."); ret = TargetView.InvokeCommand (Command, commandContext); } else @@ -143,35 +157,33 @@ public Command Command if (ret is not true) { - Logging.Debug ($"{Title} - calling base.DispatchCommand..."); + Logging.Debug ($"{Title} - calling base.DispatchCommand. - Command: {commandContext?.Command}.."); // Base will Raise Selected, then Accepting, then invoke the Action, if any ret = base.DispatchCommand (commandContext); } if (ret is true) { - Logging.Debug ($"{Title} - Calling RaiseAccepted"); + Logging.Debug ($"{Title} - Calling RaiseAccepted - Command: {commandContext?.Command}"); RaiseAccepted (commandContext); } return ret; } - ///// - //protected override bool OnAccepting (CommandEventArgs e) - //{ - // Logging.Debug ($"{Title} - calling base.OnAccepting: {e.Context?.Command}"); - // bool? ret = base.OnAccepting (e); - - // if (ret is true || e.Cancel) - // { - // return true; - // } + /// + protected override bool OnAccepting (CommandEventArgs e) + { + Logging.Debug ($"{Title} - calling base.OnAccepting: {e.Context?.Command}"); + bool? ret = base.OnAccepting (e); - // //RaiseAccepted (e.Context); + if (ret is true || e.Handled) + { + return true; + } - // return ret is true; - //} + return ret is true; + } private Menuv2? _subMenu; @@ -195,6 +207,7 @@ public Menuv2? SubMenu } } + /// protected override bool OnMouseEnter (CancelEventArgs eventArgs) { @@ -230,7 +243,10 @@ protected void RaiseAccepted (ICommandContext? ctx) /// /// /// - protected virtual void OnAccepted (CommandEventArgs args) { } + protected virtual void OnAccepted (CommandEventArgs args) + { + Logging.Debug ($"{Title} ({args.Context?.Command})"); + } /// /// Raised when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menuv2.cs index 9bebb7603b..3315b4ba58 100644 --- a/Terminal.Gui/Views/Menu/Menuv2.cs +++ b/Terminal.Gui/Views/Menu/Menuv2.cs @@ -1,10 +1,14 @@ #nullable enable +using System.ComponentModel; +using System.Diagnostics; + namespace Terminal.Gui; /// -/// A -derived object to be used as a vertically-oriented menu. Each subview is a . +/// A -derived object to be used as a vertically-oriented menu. Each subview is a +/// . /// -public class Menuv2 : Bar +public class Menuv2 : Bar, IDesignable { /// public Menuv2 () : this ([]) { } @@ -30,7 +34,29 @@ public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) BorderStyle = DefaultBorderStyle; + Arrangement = ViewArrangement.Overlapped; + Applied += OnConfigurationManagerApplied; + + KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit); + AddCommand (Command.Quit, Quit); + + return; + + bool? Quit (ICommandContext? ctx) + { + Logging.Debug ($"{Title} Command.Quit - {ctx?.Source?.Title}"); + + if (!Visible) + { + // If we're not visible, the command is not for us + return false; + } + + Visible = false; + + return true; + } } private void OnConfigurationManagerApplied (object? sender, ConfigurationManagerEventArgs e) @@ -42,26 +68,125 @@ private void OnConfigurationManagerApplied (object? sender, ConfigurationManager } /// - /// Gets or sets the default Border Style for Menus. The default is . + /// Gets or sets the default Border Style for Menus. /// [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Rounded; + private MenuItemv2? _superMenuItem; + /// - /// Gets or sets the menu item that opened this menu as a sub-menu. + /// Gets or sets the that is the parent of this menu. + /// If this menu is at the root of the menu hierarchy, this property will be and the parent will be . + /// If this menu is not at the root of the menu hierarchy, this property will be the that has it as a sub-menu. /// - public MenuItemv2? SuperMenuItem { get; set; } + public MenuItemv2? SuperMenuItem + { + get => _superMenuItem; + set + { + if (value is { SuperView: { } }) + { + //throw new ArgumentException ($"A Menu with a SuperView can not also have a SuperMenuItem."); + } + _superMenuItem = value; + } + } - /// + /// + protected override bool OnVisibleChanging () + { + Logging.Debug ($"{Title} - Visible: {Visible}"); + // If we have a SuperView, we are either the root of the menu hierarchy or activated. Act just like a normal View. + if (SuperView is { }) + { + Logging.Debug ($"{Title} - SuperView: {SuperView?.Title} - Calling base.OnVisibleChanged"); + return base.OnVisibleChanging (); + } + + // If we don't have a SuperView, we need to be added to one in order to be visible. + // Our SuperMenuItem will be the one that adds us to a SuperView, or it will pass our request up + // the menu hierarchy. + bool ret = RaiseShowingSubMenu (); + + if (ret) + { + // If handled... + } + else + { + // If not handled, bubble up + + + } + + return ret; + } + + protected bool RaiseShowingSubMenu () + { + Logging.Debug ($"{Title} - SuperView: {SuperView?.Title}; SuperMenuItem: {SuperMenuItem?.Title}"); + HandledEventArgs eventArgs = new HandledEventArgs (); + + if (OnShowingSubMenu (eventArgs) || eventArgs.Handled) + { + return true; + } + + ShowingSubMenu?.Invoke (this, eventArgs); + + return eventArgs.Handled; + + } + + protected virtual bool OnShowingSubMenu (HandledEventArgs cancelEventArgs) { return false; } + + public event EventHandler? ShowingSubMenu; + + /// protected override void OnVisibleChanged () { if (Visible) { - SelectedMenuItem = SubViews.Where (mi => mi is MenuItemv2).ElementAtOrDefault (0) as MenuItemv2; + // Whenever we're made visible, make the first menuitem be selected + SelectedMenuItem = SubViews.OfType ().ElementAtOrDefault (0); } } - /// + + ///// + ///// If a menu does not have a SuperView, it needs to be given one to be made visible. This is done by + ///// raising an event on the Menu's SuperMenuItem, which is a proxy for the SuperView. The SuperMenuItem + ///// will then do what's needed to add the Menu to a SuperView (which may be a Popover if the Menu is a + ///// being used as a context menu or part of a MenuBar, or may just be a normal View). + ///// + ///// + //public bool Activate () + //{ + // if (SuperView is { Visible: false }) + // { + // Visible = true; + + // return true; + // } + + // if (SuperMenuItem is { }) + // { + // // If we have a SuperMenuItem, we need let our SuperMenuItem know we are being activated. + // // This is used to add us to a SuperView, which may be a Popover. + // RaiseActivating (); + + // return true; + // } + // return false; + //} + + //public bool Deactivate () + //{ + // return true; + //} + + /// protected override void OnSubViewAdded (View view) { base.OnSubViewAdded (view); @@ -72,13 +197,14 @@ protected override void OnSubViewAdded (View view) { menuItem.CanFocus = true; - AddCommand (menuItem.Command, (ctx) => - { - RaiseAccepted (ctx); + AddCommand ( + menuItem.Command, + ctx => + { + RaiseAccepted (ctx); - return true; - - }); + return true; + }); menuItem.Accepted += MenuItemOnAccepted; @@ -99,7 +225,6 @@ void MenuItemOnAccepted (object? sender, CommandEventArgs e) } } - /// protected override bool OnAccepting (CommandEventArgs args) { @@ -109,30 +234,37 @@ protected override bool OnAccepting (CommandEventArgs args) // TODO: Consider having PopoverMenu subscribe to Accepting instead of us overriding OnAccepting here // TODO: Doing so would be better encapsulation and might allow us to remove the SuperMenuItem property. - if (SuperView is { }) - { - Logging.Debug ($"{Title} - SuperView is null"); - //return false; - } - - Logging.Debug ($"{Title} - {args.Context}"); + //if (SuperView is null) + //{ + // if (keyCommandContext is { Command: Command.HotKey, Source.HotKey: { } hotkey } && hotkey == keyCommandContext.Binding.Key) + // { + // Logging.Debug ($"{Title} - Returning true - Accepting came from HotKey of menuitem."); + + // //MenuItemv2? source = keyCommandContext.Source as MenuItemv2; + + // //if (source is { SubMenu.Visible: true }) + // //{ + // // return false; + // //} + // return true; + // } + + // // Special case QuitKey if we are Visible - This supports a MenuItem with Key = Application.QuitKey/Command = Command.Quit + // // And causes just the menu to quit. + // //Logging.Debug ($"{Title} - Returning true - Application.QuitKey/Command = Command.Quit"); + // //return true; + //} + + // We need to propagate Command.Accept to the SuperMenuItem if it exists. + var ret = false; if (args.Context is CommandContext { Binding.Key: { } } keyCommandContext && keyCommandContext.Binding.Key == Application.QuitKey) - { - // Special case QuitKey if we are Visible - This supports a MenuItem with Key = Application.QuitKey/Command = Command.Quit - // And causes just the menu to quit. - Logging.Debug ($"{Title} - Returning true - Application.QuitKey/Command = Command.Quit"); - return true; - } - - // Because we may not have a SuperView (if we are in a PopoverMenu), we need to propagate - // Command.Accept to the SuperMenuItem if it exists. - if (SuperView is null && SuperMenuItem is { }) { Logging.Debug ($"{Title} - Invoking Accept on SuperMenuItem: {SuperMenuItem?.Title}..."); - return SuperMenuItem?.InvokeCommand (Command.Accept, args.Context) is true; + ret = SuperMenuItem?.InvokeCommand (Command.Accept, args.Context) is true; } - return false; + + return ret; } // TODO: Consider moving Accepted to Bar? @@ -153,7 +285,8 @@ protected void RaiseAccepted (ICommandContext? ctx) } /// - /// Called when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu. + /// Called when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the + /// menu. /// /// /// @@ -161,16 +294,17 @@ protected void RaiseAccepted (ICommandContext? ctx) 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. + /// 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. - /// + /// + /// See for more information. + /// /// public event EventHandler? Accepted; - /// + /// protected override void OnFocusedChanged (View? previousFocused, View? focused) { base.OnFocusedChanged (previousFocused, focused); @@ -189,9 +323,7 @@ public MenuItemv2? SelectedMenuItem set { if (value == Focused) - { - return; - } + { } // Note we DO NOT set focus here; This property tracks Focused } @@ -201,16 +333,80 @@ internal void RaiseSelectedMenuItemChanged (MenuItemv2? selected) { Logging.Debug ($"{Title} ({selected?.Title})"); + if (RaiseSelecting (new CommandContext () + { + Source = selected + }) is true) + { + if (selected is { SubMenu: { Visible: false } subMenu }) + { + Debug.Assert (subMenu?.SuperView is { }); + + Point idealLocation = ScreenToViewport ( + new ( + selected.FrameToScreen ().Right - selected.SubMenu.GetAdornmentsThickness ().Left, + selected.FrameToScreen ().Top - selected.SubMenu.GetAdornmentsThickness ().Top)); + + Point pos = GetMostVisibleLocationForSubMenu (selected.SubMenu, idealLocation); + selected.SubMenu.X = pos.X; + selected.SubMenu.Y = pos.Y; + + selected.SubMenu.Visible = true; + selected.SubMenu.Layout (); + } + + return; + } + OnSelectedMenuItemChanged (selected); SelectedMenuItemChanged?.Invoke (this, selected); } + /// + /// 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); + } + /// /// Called when the selected menu item has changed. /// /// protected virtual void OnSelectedMenuItemChanged (MenuItemv2? selected) { + Logging.Debug ($"{Title} ({selected?.Title})"); + + if (selected?.SubMenu is { }) + { + selected.SubMenu.Visible = true; + + //Point idealLocation = ScreenToViewport ( + // new ( + // selected.FrameToScreen ().Right - selected.SubMenu.GetAdornmentsThickness ().Left, + // selected.FrameToScreen ().Top - selected.SubMenu.GetAdornmentsThickness ().Top)); + + //Point pos = GetMostVisibleLocationForSubMenu (selected.SubMenu, idealLocation); + //selected.SubMenu.X = pos.X; + //selected.SubMenu.Y = pos.Y; + + //selected.SubMenu.Visible = true; + //selected.SubMenu.Layout (); + } } /// @@ -218,6 +414,82 @@ protected virtual void OnSelectedMenuItemChanged (MenuItemv2? selected) /// public event EventHandler? SelectedMenuItemChanged; + /// + public bool EnableForDesign (ref TContext context) where TContext : notnull + { + // Note: This menu is used by unit tests. If you modify it, you'll likely have to update + // unit tests. + + Add ( + new MenuItemv2 (context as View, Command.Cut), + new MenuItemv2 (context as View, Command.Copy), + new MenuItemv2 (context as View, Command.Paste), + new Line (), + new MenuItemv2 (context as View, Command.SelectAll), + new Line (), + new MenuItemv2 + { + Title = "_Details", + SubMenu = new (ConfigureDetailsSubMenu ()) + }, + new Line (), + new MenuItemv2 (context as View, Command.Quit)); + + return true; + + MenuItemv2 [] ConfigureDetailsSubMenu () + { + var detail = new MenuItemv2 + { + Title = "_Detail 1", + Text = "Some detail #1" + }; + + var nestedSubMenu = new MenuItemv2 + { + Title = "_Moar Details", + SubMenu = new (ConfigureMoreDetailsSubMenu ()) + { + Title = "MoreDetailsSubMenu" + } + }; + + var editMode = new MenuItemv2 + { + Text = "Command = Edit; TargetView = null", + Id = "EditMode", + Command = Command.Edit, + CommandView = new CheckBox + { + Title = "_Edit Mode" + } + }; + + return [detail, nestedSubMenu, null!, editMode]; + + View [] ConfigureMoreDetailsSubMenu () + { + var deeperDetail = new MenuItemv2 + { + Title = "_Deeper Detail", + Text = "Deeper Detail", + Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } + }; + + var belowLineDetail = new MenuItemv2 + { + Title = "_Even more detail", + Text = "Below the line" + }; + + // This ensures the checkbox state toggles when the hotkey of Title is pressed. + //shortcut4.Accepting += (sender, args) => args.Cancel = true; + + return [deeperDetail, new Line (), belowLineDetail]; + } + } + } + /// protected override void Dispose (bool disposing) { @@ -228,4 +500,4 @@ protected override void Dispose (bool disposing) Applied -= OnConfigurationManagerApplied; } } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index 80629b56eb..575df3b731 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -87,9 +87,13 @@ public PopoverMenu (Menuv2? root) if (Visible && ret is not true) { - Visible = false; + if (SuperView is null) + { + Visible = false; + return true; + } - return true; + return false; } // If we are Visible, returning true will stop the QuitKey from propagating @@ -252,7 +256,11 @@ public Menuv2? Root foreach (Menuv2 menu in allMenus) { - menu.Visible = false; + if (SuperView is null) + { + menu.Visible = false; + } + menu.Accepting += MenuOnAccepting; menu.Accepted += MenuAccepted; menu.SelectedMenuItemChanged += MenuOnSelectedMenuItemChanged; @@ -377,12 +385,12 @@ internal void ShowSubMenu (MenuItemv2? menuItem) { var menu = menuItem?.SuperView as Menuv2; - Logging.Debug ($"{Title} - menuItem: {menuItem?.Title}, menu: {menu?.Title}"); + Logging.Debug ($"{Title} - menuItem: {menuItem?.Title}, SuperView: {menu?.Title}"); 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) + if (menu?.SubViews.OfType ().FirstOrDefault (v => v is { SubMenu.Visible: true }) is { } visiblePeer) { HideAndRemoveSubMenu (visiblePeer.SubMenu); visiblePeer.ForceFocusColors = false; @@ -465,10 +473,11 @@ private void HideAndRemoveSubMenu (Menuv2? menu) } menu.Visible = false; + menu.ClearFocus (); base.Remove (menu); - if (menu == Root) + if (SuperView is null && menu == Root) { Visible = false; } @@ -482,11 +491,14 @@ private void MenuOnAccepting (object? sender, CommandEventArgs e) if (e.Context?.Command != Command.HotKey) { - Logging.Debug ($"{Title} - Setting Visible = false"); - Visible = false; + if (SuperView is null) + { + Logging.Debug ($"{Title} - Setting Visible = false"); + Visible = false; + } } - if (e.Context is CommandContext keyCommandContext) + if (SuperView is null && e.Context is CommandContext keyCommandContext) { if (keyCommandContext.Binding.Key is { } && keyCommandContext.Binding.Key == Application.QuitKey && SuperView is { Visible: true }) { @@ -502,7 +514,10 @@ private void MenuAccepted (object? sender, CommandEventArgs e) if (e.Context?.Source is MenuItemv2 { SubMenu: null }) { - HideAndRemoveSubMenu (_root); + if (SuperView is null) + { + HideAndRemoveSubMenu (_root); + } } else if (e.Context?.Source is MenuItemv2 { SubMenu: { } } menuItemWithSubMenu) { @@ -546,6 +561,7 @@ protected override bool OnAccepting (CommandEventArgs args) } // Always return false to enable accepting to continue propagating + Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command} - Returning false."); return false; } @@ -649,5 +665,58 @@ public bool EnableForDesign (ref TContext context) where TContext : no //Visible = true; return true; + + + MenuItemv2 [] ConfigureDetailsSubMenu () + { + var detail = new MenuItemv2 + { + Title = "_Detail 1", + Text = "Some detail #1" + }; + + var nestedSubMenu = new MenuItemv2 + { + Title = "_Moar Details", + SubMenu = new (ConfigureMoreDetailsSubMenu ()) + { + Title = "MoreDetailsSubMenu" + }, + }; + + var editMode = new MenuItemv2 + { + Text = "Command = Edit; TargetView = null", + Id = "EditMode", + Command = Command.Edit, + CommandView = new CheckBox + { + Title = "_Edit Mode", + } + }; + + return [detail, nestedSubMenu, null!, editMode]; + + View [] ConfigureMoreDetailsSubMenu () + { + var deeperDetail = new MenuItemv2 + { + Title = "_Deeper Detail", + Text = "Deeper Detail", + Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } + }; + + var belowLineDetail = new MenuItemv2 + { + Title = "_Even more detail", + Text = "Below the line" + }; + + // This ensures the checkbox state toggles when the hotkey of Title is pressed. + //shortcut4.Accepting += (sender, args) => args.Cancel = true; + + return [deeperDetail, new Line (), belowLineDetail]; + } + } } } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 13a59bcbfc..aaadc285ba 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -270,7 +270,7 @@ private void AddCommands () CommandView.InvokeCommand (Command.Select, keyCommandContext); } - Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - RaiseSelecting ..."); + Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - RaiseSelecting - Command: {commandContext?.Command}..."); if (RaiseSelecting (commandContext) is true) { @@ -290,7 +290,7 @@ private void AddCommands () { commandContext.Source = this; } - Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Calling RaiseAccepting..."); + Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Calling RaiseAccepting - Command: {commandContext?.Command}..."); cancel = RaiseAccepting (commandContext) is true; if (cancel) diff --git a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs index 63a817f59f..dcfc441b3a 100644 --- a/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs +++ b/Tests/IntegrationTests/FluentTests/PopverMenuTests.cs @@ -344,4 +344,50 @@ public void Not_Active_DoesNotEat_QuitKey (V2TestDriver d) .WriteOutLogs (_out) .Stop (); } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void RootMenu_MenuItem_WithSubMenu_HotKey_Activates_SubMenu (V2TestDriver d) + { + PopoverMenu? popoverMenu = null; + + using GuiTestContext c = With.A (50, 20, d) + .Then ( + () => + { + popoverMenu = new PopoverMenu (); + Toplevel top = Application.Top!; + + View view = new View () + { + CanFocus = true, + Id = "focusableView", + + }; + top.Add (view); + popoverMenu.EnableForDesign (ref top); + // EnableForDesign sets to true; undo that + popoverMenu.Visible = false; + + Application.Popover!.Register (popoverMenu); + + view.SetFocus (); + }) + .WaitIteration () + .ScreenShot ("PopoverMenu initial state", _out) + .Then (() => Assert.False (Application.Popover?.GetActivePopover () is PopoverMenu)) + .Then (() => Assert.IsNotType (Application.Navigation!.GetFocused ())) + .Then (() => Application.Popover!.Show (Application.Popover.Popovers.First ())) + .WaitIteration () + .ScreenShot ($"After Show", _out) + .Then (() => Assert.True (Application.Popover?.GetActivePopover () is PopoverMenu)) + .Then (() => Assert.IsType (Application.Navigation!.GetFocused ())) + .RaiseKeyDownEvent (Key.D) // _Details submenu + .WaitIteration () + .ScreenShot ($"After {Key.D}", _out) + .Then (() => Assert.True (Application.Popover?.GetActivePopover () is PopoverMenu)) + .Then (() => Assert.Equal ("_Details", Application.Navigation!.GetFocused ()?.Title)) + .WriteOutLogs (_out) + .Stop (); + } }