From 02a98f3fc2e7d304d8961a949a441292272e3f20 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 30 Aug 2024 23:26:00 +0100 Subject: [PATCH] Fixes #3700. ContextMenu pollutes Toplevel.MenuBar's key bindings --- Terminal.Gui/Views/FileDialog.cs | 24 +- Terminal.Gui/Views/Menu/ContextMenu.cs | 84 ++-- Terminal.Gui/Views/Menu/Menu.cs | 55 ++- Terminal.Gui/Views/Menu/MenuBar.cs | 128 +++--- Terminal.Gui/Views/Menu/MenuBarItem.cs | 29 +- Terminal.Gui/Views/Menu/MenuItem.cs | 32 +- Terminal.Gui/Views/TextField.cs | 8 +- Terminal.Gui/Views/TextView.cs | 12 +- UICatalog/Scenarios/CharacterMap.cs | 49 ++- UICatalog/Scenarios/ContextMenus.cs | 236 +++++----- UICatalog/Scenarios/Notepad.cs | 4 +- UICatalog/Scenarios/TableEditor.cs | 40 +- UICatalog/Scenarios/TreeViewFileSystem.cs | 13 +- UnitTests/Views/ContextMenuTests.cs | 510 ++++++++++++++++------ 14 files changed, 766 insertions(+), 458 deletions(-) diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index e738d6d1d..00f07a16c 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -1207,19 +1207,19 @@ public class FileDialog : Dialog var contextMenu = new ContextMenu { - Position = new Point (e.MouseEvent.Position.X + 1, e.MouseEvent.Position.Y + 1), - MenuItems = new MenuBarItem ( + Position = new Point (e.MouseEvent.Position.X + 1, e.MouseEvent.Position.Y + 1) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem (Strings.fdCtxNew, string.Empty, New), new MenuItem (Strings.fdCtxRename, string.Empty, Rename), new MenuItem (Strings.fdCtxDelete, string.Empty, Delete) ] - ) - }; - + ); _tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false); - contextMenu.Show (); + contextMenu.Show (menuItems); } private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e) @@ -1228,8 +1228,10 @@ public class FileDialog : Dialog var contextMenu = new ContextMenu { - Position = new Point (e.MouseEvent.Position.X + 1, e.MouseEvent.Position.Y + 1), - MenuItems = new MenuBarItem ( + Position = new Point (e.MouseEvent.Position.X + 1, e.MouseEvent.Position.Y + 1) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ( string.Format ( @@ -1244,10 +1246,8 @@ public class FileDialog : Dialog string.Empty, () => SortColumn (clickedCol, isAsc)) ] - ) - }; - - contextMenu.Show (); + ); + contextMenu.Show (menuItems); } private void SortColumn (int clickedCol) diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index 6a6131195..0ceec6d74 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui; +#nullable enable + +namespace Terminal.Gui; /// /// ContextMenu provides a pop-up menu that can be positioned anywhere within a . ContextMenu is @@ -16,15 +18,15 @@ /// /// /// 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 static MenuBar? _menuBar; - private Toplevel _container; + private Toplevel? _container; private Key _key = DefaultKey; private MouseFlags _mouseFlags = MouseFlags.Button3Clicked; @@ -33,15 +35,9 @@ public sealed class ContextMenu : IDisposable { if (IsShow) { - if (_menuBar.SuperView is { }) - { - Hide (); - } - + Hide (); IsShow = false; } - - MenuItems = new MenuBarItem (); } /// The default shortcut key for activating the context menu. @@ -56,13 +52,13 @@ public sealed class ContextMenu : IDisposable 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; } + 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 + public new Key Key { get => _key; set @@ -74,10 +70,10 @@ public sealed class ContextMenu : IDisposable } /// Gets the that is hosting this context menu. - public MenuBar MenuBar => _menuBar; + public MenuBar? MenuBar => _menuBar; /// Gets or sets the menu items for this context menu. - public MenuBarItem MenuItems { get; set; } + public MenuBarItem? MenuItems { get; private set; } /// specifies the mouse action used to activate the context menu by mouse. public MouseFlags MouseFlags @@ -105,11 +101,10 @@ public sealed class ContextMenu : IDisposable /// Disposes the context menu object. public void Dispose () { - if (_menuBar is null) + if (_menuBar is { }) { - return; + _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed; } - _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed; Application.UngrabMouse (); _menuBar?.Dispose (); _menuBar = null; @@ -126,18 +121,49 @@ public sealed class ContextMenu : IDisposable /// Hides (closes) the ContextMenu. public void Hide () { + RemoveKeyBindings (MenuItems); _menuBar?.CleanUp (); IsShow = false; } + private void RemoveKeyBindings (MenuBarItem? menuBarItem) + { + if (menuBarItem is null) + { + return; + } + + foreach (var 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; + public event EventHandler? KeyChanged; /// Event invoked when the is changed. - public event EventHandler MouseFlagsChanged; + public event EventHandler? MouseFlagsChanged; /// Shows (opens) the ContextMenu, displaying the s it contains. - public void Show () + public void Show (MenuBarItem? menuItems) { if (_menuBar is { }) { @@ -145,6 +171,12 @@ public sealed class ContextMenu : IDisposable Dispose (); } + if (menuItems is null || menuItems.Children.Length == 0) + { + return; + } + + MenuItems = menuItems; _container = Application.Current; _container!.Closing += Container_Closing; _container.Deactivate += Container_Deactivate; @@ -155,7 +187,7 @@ public sealed class ContextMenu : IDisposable if (Host is { }) { Point pos = Host.ViewportToScreen (frame).Location; - pos.Y += Host.Frame.Height - 1; + pos.Y += Host.Frame.Height > 0 ? Host.Frame.Height - 1 : 0; if (position != pos) { @@ -224,9 +256,9 @@ public sealed class ContextMenu : IDisposable _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 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 (); } + private void MenuBar_MenuAllClosed (object? sender, EventArgs e) { Hide (); } } diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 7119c28aa..091e87aa7 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Terminal.Gui; /// @@ -11,7 +13,7 @@ internal sealed class Menu : View internal int _currentChild; internal View _previousSubFocused; - internal static Rectangle MakeFrame (int x, int y, MenuItem [] items, Menu parent = null) + internal static Rectangle MakeFrame (int x, int y, MenuItem [] items, Menu? parent = null) { if (items is null || items.Length == 0) { @@ -68,6 +70,25 @@ internal sealed class Menu : View Frame = MakeFrame (Frame.X, Frame.Y, _barItems?.Children, Parent); + if (_barItems?.Children is { }) + { + foreach (MenuItem menuItem in _barItems!.Children) + { + if (menuItem is { }) + { + menuItem._menuBar = Host; + + if (menuItem.ShortcutKey != Key.Empty) + { + KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem); + // Remove an existent ShortcutKey + menuItem._menuBar?.KeyBindings.Remove (menuItem.ShortcutKey); + menuItem._menuBar?.KeyBindings.Add (menuItem.ShortcutKey, keyBinding); + } + } + } + } + if (_barItems is { IsTopLevel: true }) { // This is a standalone MenuItem on a MenuBar @@ -166,9 +187,9 @@ internal sealed class Menu : View return true; } ); - AddCommand (Command.Select, ctx => _host?.SelectItem (ctx.KeyBinding?.Context as MenuItem)); - AddCommand (Command.ToggleExpandCollapse, ctx => ExpandCollapse (ctx.KeyBinding?.Context as MenuItem)); - AddCommand (Command.HotKey, ctx => _host?.SelectItem (ctx.KeyBinding?.Context as MenuItem)); + AddCommand (Command.Select, ctx => _host?.SelectItem ((ctx.KeyBinding?.Context as MenuItem)!)); + AddCommand (Command.ToggleExpandCollapse, ctx => ExpandCollapse ((ctx.KeyBinding?.Context as MenuItem)!)); + AddCommand (Command.HotKey, ctx => _host?.SelectItem ((ctx.KeyBinding?.Context as MenuItem)!)); // Default key bindings for this view KeyBindings.Add (Key.CursorUp, Command.LineUp); @@ -179,7 +200,7 @@ internal sealed class Menu : View KeyBindings.Add (Key.Enter, Command.Accept); } - private void AddKeyBindingsHotKey (MenuBarItem menuBarItem) + private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem) { if (menuBarItem is null || menuBarItem.Children is null) { @@ -200,7 +221,7 @@ internal sealed class Menu : View } } - private void RemoveKeyBindingsHotKey (MenuBarItem menuBarItem) + private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem) { if (menuBarItem is null || menuBarItem.Children is null) { @@ -219,7 +240,7 @@ internal sealed class Menu : View /// Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed. /// - private bool ExpandCollapse (MenuItem menuItem) + private bool ExpandCollapse (MenuItem? menuItem) { if (!IsInitialized || !Visible) { @@ -243,7 +264,7 @@ internal sealed class Menu : View if (m?.Children?.Length > 0) { - MenuItem item = _barItems.Children [_currentChild]; + MenuItem? item = _barItems.Children [_currentChild]; if (item is null) { @@ -297,7 +318,7 @@ internal sealed class Menu : View return _host.OnInvokingKeyBindings (keyEvent, scope); } - private void Current_TerminalResized (object sender, SizeChangedEventArgs e) + private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) { if (_host.IsMenuOpen) { @@ -320,7 +341,7 @@ internal sealed class Menu : View } } - private void Application_RootMouseEvent (object sender, MouseEvent a) + private void Application_RootMouseEvent (object? sender, MouseEvent a) { if (a.View is { } and (MenuBar or not Menu)) { @@ -350,7 +371,7 @@ internal sealed class Menu : View } } - internal Attribute DetermineColorSchemeFor (MenuItem item, int index) + internal Attribute DetermineColorSchemeFor (MenuItem? item, int index) { if (item is null) { @@ -456,7 +477,7 @@ internal sealed class Menu : View continue; } - string textToDraw = null; + string? textToDraw = null; Rune nullCheckedChar = Glyphs.CheckStateNone; Rune checkChar = Glyphs.Selected; Rune uncheckedChar = Glyphs.UnSelected; @@ -468,7 +489,7 @@ internal sealed class Menu : View } // Support Checked even though CheckType wasn't set - if (item.CheckType == MenuItemCheckStyle.Checked && item.Checked is null) + if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null }) { textToDraw = $"{nullCheckedChar} {item.Title}"; } @@ -548,7 +569,7 @@ internal sealed class Menu : View // PositionCursor (); } - private void Current_DrawContentComplete (object sender, DrawEventArgs e) + private void Current_DrawContentComplete (object? sender, DrawEventArgs e) { if (Visible) { @@ -573,9 +594,9 @@ internal sealed class Menu : View return _host?.PositionCursor (); } - public void Run (Action action) + public void Run (Action? action) { - if (action is null || _host is null) + if (action is null) { return; } @@ -900,7 +921,7 @@ internal sealed class Menu : View if (pos == -1 && this != _host.OpenCurrentMenu - && subMenu.Children != _host.OpenCurrentMenu._barItems.Children + && subMenu.Children != _host.OpenCurrentMenu!._barItems.Children && !_host.CloseMenu (false, true)) { return false; diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 766e89cdf..35cdfa326 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Terminal.Gui; /// @@ -50,14 +52,14 @@ public class MenuBar : View, IDesignable internal bool _isMenuClosing; internal bool _isMenuOpening; - internal Menu _openMenu; - internal List _openSubMenu; + internal Menu? _openMenu; + internal List? _openSubMenu; internal int _selected; internal int _selectedSub; private bool _initialCanFocus; private bool _isCleaning; - private View _lastFocused; + private View? _lastFocused; private Menu _ocm; private View _previousFocused; private bool _reopen; @@ -66,8 +68,6 @@ public class MenuBar : View, IDesignable /// Initializes a new instance of the . public MenuBar () { - MenuItem._menuBar = this; - TabStop = TabBehavior.NoStop; X = 0; Y = 0; @@ -125,7 +125,13 @@ public class MenuBar : View, IDesignable } ); AddCommand (Command.ToggleExpandCollapse, ctx => Select (Menus.IndexOf (ctx.KeyBinding?.Context))); - AddCommand (Command.Select, ctx => Run ((ctx.KeyBinding?.Context as MenuItem)?.Action)); + AddCommand (Command.Select, ctx => + { + var res = Run ((ctx.KeyBinding?.Context as MenuItem)?.Action!); + CloseAllMenus (); + + return res; + }); // Default key bindings for this view KeyBindings.Add (Key.CursorLeft, Command.Left); @@ -150,7 +156,7 @@ public class MenuBar : View, IDesignable public bool IsMenuOpen { get; protected set; } /// Gets the view that was last focused before opening the menu. - public View LastFocused { get; private set; } + public View? LastFocused { get; private set; } /// /// Gets or sets the array of s for the menu. Only set this after the @@ -164,7 +170,7 @@ public class MenuBar : View, IDesignable { _menus = value; - if (Menus is null) + if (Menus is []) { return; } @@ -244,7 +250,7 @@ public class MenuBar : View, IDesignable } } - internal Menu OpenCurrentMenu + internal Menu? OpenCurrentMenu { get => _ocm; set @@ -376,12 +382,9 @@ public class MenuBar : View, IDesignable /// Opens the Menu programatically, as though the F9 key were pressed. public void OpenMenu () { - MenuBar mbar = GetMouseGrabViewInstance (this); + MenuBar? mbar = GetMouseGrabViewInstance (this); - if (mbar is { }) - { - mbar.CleanUp (); - } + mbar?.CleanUp (); if (!Enabled || _openMenu is { }) { @@ -391,11 +394,11 @@ public class MenuBar : View, IDesignable _selected = 0; SetNeedsDisplay (); - _previousFocused = SuperView is null ? Application.Current?.Focused : SuperView.Focused; + _previousFocused = (SuperView is null ? Application.Current?.Focused : SuperView.Focused)!; OpenMenu (_selected); if (!SelectEnabledItem ( - OpenCurrentMenu.BarItems.Children, + OpenCurrentMenu!.BarItems.Children, OpenCurrentMenu._currentChild, out OpenCurrentMenu._currentChild ) @@ -523,11 +526,18 @@ public class MenuBar : View, IDesignable IsMenuOpen = false; _openedByAltKey = false; OnMenuAllClosed (); + + if (Application.Current is { }) + { + // Close others menu bar opened + View? cm = Application.Current!.Subviews.FirstOrDefault (v => v is Menu cm && cm.Host != this && cm.Host.IsMenuOpen); + ((Menu)cm!)?.Host.CleanUp (); + } } - internal bool CloseMenu (bool reopen = false, bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) + internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSingleFrame = false) { - MenuBarItem mbi = isSubMenu ? OpenCurrentMenu.BarItems : _openMenu?.BarItems; + MenuBarItem? mbi = isSubMenu ? OpenCurrentMenu!.BarItems : _openMenu?.BarItems; if (UseSubMenusSingleFrame && mbi is { } && !ignoreUseSubMenusSingleFrame && mbi.Parent is { }) { @@ -615,7 +625,7 @@ public class MenuBar : View, IDesignable _selectedSub = -1; SetNeedsDisplay (); RemoveAllOpensSubMenus (); - OpenCurrentMenu._previousSubFocused.SetFocus (); + OpenCurrentMenu!._previousSubFocused.SetFocus (); _openSubMenu = null; IsMenuOpen = true; @@ -637,8 +647,8 @@ public class MenuBar : View, IDesignable return Point.Empty; } - Rectangle superViewFrame = SuperView is null ? Application.Screen : SuperView.Frame; - View sv = SuperView is null ? Application.Current : SuperView; + Rectangle superViewFrame = SuperView?.Frame ?? Application.Screen; + View? sv = SuperView ?? Application.Current; if (sv is null) { @@ -680,7 +690,7 @@ public class MenuBar : View, IDesignable OpenMenu (_selected); SelectEnabledItem ( - OpenCurrentMenu.BarItems.Children, + OpenCurrentMenu!.BarItems.Children, OpenCurrentMenu._currentChild, out OpenCurrentMenu._currentChild ); @@ -696,7 +706,7 @@ public class MenuBar : View, IDesignable } else { - MenuBarItem subMenu = OpenCurrentMenu._currentChild > -1 && OpenCurrentMenu.BarItems.Children.Length > 0 + MenuBarItem? subMenu = OpenCurrentMenu!._currentChild > -1 && OpenCurrentMenu.BarItems.Children.Length > 0 ? OpenCurrentMenu.BarItems.SubMenu ( OpenCurrentMenu.BarItems.Children [OpenCurrentMenu._currentChild] ) @@ -837,7 +847,7 @@ public class MenuBar : View, IDesignable } else { - Menu last = _openSubMenu.Count > 0 ? _openSubMenu.Last () : _openMenu; + Menu? last = _openSubMenu.Count > 0 ? _openSubMenu.Last () : _openMenu; if (!UseSubMenusSingleFrame) { @@ -854,10 +864,10 @@ public class MenuBar : View, IDesignable } else { - Menu first = _openSubMenu.Count > 0 ? _openSubMenu.First () : _openMenu; + Menu? first = _openSubMenu.Count > 0 ? _openSubMenu.First () : _openMenu; // 2 is for the parent and the separator - MenuItem [] mbi = new MenuItem [2 + subMenu.Children.Length]; + MenuItem? [] mbi = new MenuItem [2 + subMenu.Children.Length]; mbi [0] = new () { Title = subMenu.Title, Parent = subMenu }; mbi [1] = null; @@ -870,7 +880,7 @@ public class MenuBar : View, IDesignable OpenCurrentMenu = new () { - Host = this, X = first.Frame.Left, Y = first.Frame.Top, BarItems = newSubMenu + Host = this, X = first!.Frame.Left, Y = first.Frame.Top, BarItems = newSubMenu }; last.Visible = false; Application.GrabMouse (OpenCurrentMenu); @@ -892,7 +902,7 @@ public class MenuBar : View, IDesignable if (_selectedSub > -1 && SelectEnabledItem ( - OpenCurrentMenu.BarItems.Children, + OpenCurrentMenu!.BarItems.Children, OpenCurrentMenu._currentChild, out OpenCurrentMenu._currentChild )) @@ -929,7 +939,7 @@ public class MenuBar : View, IDesignable OpenMenu (_selected); if (!SelectEnabledItem ( - OpenCurrentMenu.BarItems.Children, + OpenCurrentMenu!.BarItems.Children, OpenCurrentMenu._currentChild, out OpenCurrentMenu._currentChild, false @@ -961,7 +971,7 @@ public class MenuBar : View, IDesignable { foreach (Menu item in _openSubMenu) { - Application.Current.Remove (item); + Application.Current!.Remove (item); item.Dispose (); } } @@ -974,20 +984,20 @@ public class MenuBar : View, IDesignable return false; } - Application.MainLoop.AddIdle ( - () => - { - action (); + Application.MainLoop!.AddIdle ( + () => + { + action (); - return false; - } - ); + return false; + } + ); return true; } internal bool SelectEnabledItem ( - IEnumerable children, + IEnumerable? children, int current, out int newCurrent, bool forward = true @@ -1099,7 +1109,7 @@ public class MenuBar : View, IDesignable return new (-2, 0); } - private void MenuBar_Added (object sender, SuperViewChangedEventArgs e) + private void MenuBar_Added (object? sender, SuperViewChangedEventArgs e) { _initialCanFocus = CanFocus; Added -= MenuBar_Added; @@ -1146,7 +1156,7 @@ public class MenuBar : View, IDesignable OpenMenu (i); if (!SelectEnabledItem ( - OpenCurrentMenu.BarItems.Children, + OpenCurrentMenu!.BarItems.Children, OpenCurrentMenu._currentChild, out OpenCurrentMenu._currentChild ) @@ -1177,7 +1187,7 @@ public class MenuBar : View, IDesignable for (int i = _openSubMenu.Count - 1; i > index; i--) { _isMenuClosing = true; - Menu menu; + Menu? menu; if (_openSubMenu.Count - 1 > 0) { @@ -1188,7 +1198,7 @@ public class MenuBar : View, IDesignable menu = _openMenu; } - if (!menu.Visible) + if (!menu!.Visible) { menu.Visible = true; } @@ -1199,7 +1209,7 @@ public class MenuBar : View, IDesignable if (_openSubMenu is { }) { menu = _openSubMenu [i]; - Application.Current.Remove (menu); + Application.Current!.Remove (menu); _openSubMenu.Remove (menu); if (Application.MouseGrabView == menu) @@ -1213,7 +1223,7 @@ public class MenuBar : View, IDesignable RemoveSubMenu (i, ignoreUseSubMenusSingleFrame); } - if (_openSubMenu.Count > 0) + if (_openSubMenu!.Count > 0) { OpenCurrentMenu = _openSubMenu.Last (); } @@ -1310,7 +1320,7 @@ public class MenuBar : View, IDesignable continue; } - if (open == OpenCurrentMenu.BarItems && i == index) + if (open == OpenCurrentMenu!.BarItems && i == index) { CloseAllMenus (); return true; @@ -1411,7 +1421,7 @@ public class MenuBar : View, IDesignable else if (_selected != i && _selected > -1 && (me.Flags == MouseFlags.ReportMousePosition - || (me.Flags == MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition))) + || (me.Flags is MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition))) { if (IsMenuOpen) { @@ -1463,9 +1473,9 @@ public class MenuBar : View, IDesignable { if (Application.MouseGrabView is { }) { - if (me.View is MenuBar || me.View is Menu) + if (me.View is MenuBar or Menu) { - MenuBar mbar = GetMouseGrabViewInstance (me.View); + MenuBar? mbar = GetMouseGrabViewInstance (me.View); if (mbar is { }) { @@ -1563,14 +1573,14 @@ public class MenuBar : View, IDesignable return true; } - private MenuBar GetMouseGrabViewInstance (View view) + private MenuBar? GetMouseGrabViewInstance (View? view) { if (view is null || Application.MouseGrabView is null) { return null; } - MenuBar hostView = null; + MenuBar? hostView = null; if (view is MenuBar) { @@ -1582,15 +1592,15 @@ public class MenuBar : View, IDesignable } View grabView = Application.MouseGrabView; - MenuBar hostGrabView = null; + MenuBar? hostGrabView = null; - if (grabView is MenuBar) + if (grabView is MenuBar bar) { - hostGrabView = (MenuBar)grabView; + hostGrabView = bar; } - else if (grabView is Menu) + else if (grabView is Menu menu) { - hostGrabView = ((Menu)grabView).Host; + hostGrabView = menu.Host; } return hostView != hostGrabView ? hostGrabView : null; @@ -1770,12 +1780,4 @@ public class MenuBar : View, IDesignable ]; return true; } - - /// - protected override void Dispose (bool disposing) - { - MenuItem._menuBar = null; - - base.Dispose (disposing); - } } diff --git a/Terminal.Gui/Views/Menu/MenuBarItem.cs b/Terminal.Gui/Views/Menu/MenuBarItem.cs index 698499cd7..3c9b0dab8 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItem.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItem.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Terminal.Gui; /// @@ -16,8 +18,8 @@ public class MenuBarItem : MenuItem string title, string help, Action action, - Func canExecute = null, - MenuItem parent = null + Func? canExecute = null, + MenuItem? parent = null ) : base (title, help, action, canExecute, parent) { SetInitialProperties (title, null, null, true); @@ -27,13 +29,13 @@ public class MenuBarItem : MenuItem /// Title for the menu item. /// The items in the current menu. /// The parent of this if any. - public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); } + public MenuBarItem (string title, MenuItem [] children, MenuItem? parent = null) { SetInitialProperties (title, children, parent); } /// Initializes a new with separate list of items. /// Title for the menu item. /// The list of items in the current menu. /// The parent of this if any. - public MenuBarItem (string title, List children, MenuItem parent = null) { SetInitialProperties (title, children, parent); } + public MenuBarItem (string title, List children, MenuItem? parent = null) { SetInitialProperties (title, children, parent); } /// Initializes a new . /// The items in the current menu. @@ -47,7 +49,7 @@ public class MenuBarItem : MenuItem /// /// /// The children. - public MenuItem [] Children { get; set; } + public MenuItem []? Children { get; set; } internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null; @@ -81,13 +83,13 @@ public class MenuBarItem : MenuItem /// Returns true if it is a submenu. false otherwise. public bool IsSubMenuOf (MenuItem menuItem) { - return Children.Any (child => child == menuItem && child.Parent == menuItem.Parent); + return Children!.Any (child => child == menuItem && child.Parent == menuItem.Parent); } /// Check if a is a . /// /// Returns a or null otherwise. - public MenuBarItem SubMenu (MenuItem menuItem) { return menuItem as MenuBarItem; } + public MenuBarItem SubMenu (MenuItem menuItem) { return (menuItem as MenuBarItem)!; } internal void AddShortcutKeyBindings (MenuBar menuBar) { @@ -96,20 +98,24 @@ public class MenuBarItem : MenuItem return; } + _menuBar = menuBar; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract foreach (MenuItem menuItem in Children.Where (m => m is { })) { // For MenuBar only add shortcuts for submenus if (menuItem.ShortcutKey != Key.Empty) { - menuItem.UpdateShortcutKeyBinding (Key.Empty); + menuItem._menuBar = menuBar; + menuItem.UpdateShortcutKeyBinding (menuItem._menuBar, Key.Empty); } SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar); } } - private void SetInitialProperties (string title, object children, MenuItem parent = null, bool isTopLevel = false) + private void SetInitialProperties (string title, object? children, MenuItem? parent = null, bool isTopLevel = false) { if (!isTopLevel && children is null) { @@ -179,7 +185,7 @@ public class MenuBarItem : MenuItem /// Add a dynamically into the .Menus. /// /// - public void AddMenuBarItem (MenuItem menuItem = null) + public void AddMenuBarItem (MenuItem? menuItem = null) { if (menuItem is null) { @@ -227,13 +233,14 @@ public class MenuBarItem : MenuItem _menuBar?.KeyBindings.Remove (HotKey.WithAlt); } - _menuBar!.Menus [index] = null; + _menuBar!.Menus [index] = null!; } var i = 0; foreach (MenuBarItem m in _menuBar.Menus) { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (m != null) { _menuBar.Menus [i] = m; diff --git a/Terminal.Gui/Views/Menu/MenuItem.cs b/Terminal.Gui/Views/Menu/MenuItem.cs index 8b72cbb42..6ab6da4a2 100644 --- a/Terminal.Gui/Views/Menu/MenuItem.cs +++ b/Terminal.Gui/Views/Menu/MenuItem.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Terminal.Gui; /// @@ -6,10 +8,10 @@ namespace Terminal.Gui; /// public class MenuItem { - internal static MenuBar _menuBar; + internal MenuBar _menuBar; /// Initializes a new instance of - public MenuItem (Key shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { } + public MenuItem (Key? shortcutKey = null) : this ("", "", null, null, null, shortcutKey) { } /// Initializes a new instance of . /// Title for the menu item. @@ -21,24 +23,24 @@ public class MenuItem public MenuItem ( string title, string help, - Action action, - Func canExecute = null, - MenuItem parent = null, - Key shortcutKey = null + Action? action, + Func? canExecute = null, + MenuItem? parent = null, + Key? shortcutKey = null ) { Title = title ?? ""; Help = help ?? ""; - Action = action; - CanExecute = canExecute; - Parent = parent; + Action = action!; + CanExecute = canExecute!; + Parent = parent!; if (Parent is { } && Parent.ShortcutKey != Key.Empty) { Parent.ShortcutKey = Key.Empty; } // Setter will ensure Key.Empty if it's null - ShortcutKey = shortcutKey; + ShortcutKey = shortcutKey!; } private bool _allowNullChecked; @@ -112,7 +114,7 @@ public class MenuItem /// Gets the parent for this . /// The parent. - public MenuItem Parent { get; set; } + public MenuItem? Parent { get; set; } /// Gets or sets the title of the menu item . /// The title. @@ -267,7 +269,7 @@ public class MenuItem { var oldKey = _shortcutKey ?? Key.Empty; _shortcutKey = value ?? Key.Empty; - UpdateShortcutKeyBinding (oldKey); + UpdateShortcutKeyBinding (_menuBar, oldKey); } } @@ -304,8 +306,10 @@ public class MenuItem } } - internal void UpdateShortcutKeyBinding (Key oldKey) + internal void UpdateShortcutKeyBinding (MenuBar menuBar, Key oldKey) { + _menuBar ??= menuBar; + if (_menuBar is null) { return; @@ -334,7 +338,7 @@ public class MenuItem { if (Parent is { }) { - MenuItem [] childrens = ((MenuBarItem)Parent).Children; + MenuItem []? childrens = ((MenuBarItem)Parent).Children; var i = 0; foreach (MenuItem c in childrens) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 908012a3b..da8f42347 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -405,7 +405,7 @@ public class TextField : View _currentCulture = Thread.CurrentThread.CurrentUICulture; - ContextMenu = new ContextMenu { Host = this, MenuItems = BuildContextMenuBarItem () }; + ContextMenu = new ContextMenu { Host = this }; ContextMenu.KeyChanged += ContextMenu_KeyChanged; KeyBindings.Add (ContextMenu.Key, KeyBindingScope.HotKey, Command.ShowContextMenu); @@ -1853,14 +1853,12 @@ public class TextField : View private void ShowContextMenu () { - if (_currentCulture != Thread.CurrentThread.CurrentUICulture) + if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) { _currentCulture = Thread.CurrentThread.CurrentUICulture; - - ContextMenu.MenuItems = BuildContextMenuBarItem (); } - ContextMenu.Show (); + ContextMenu.Show (BuildContextMenuBarItem ()); } private void TextField_Added (object sender, SuperViewChangedEventArgs e) diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 23f27f144..44bfcdf5f 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2498,7 +2498,7 @@ public class TextView : View _currentCulture = Thread.CurrentThread.CurrentUICulture; - ContextMenu = new () { MenuItems = BuildContextMenuBarItem () }; + ContextMenu = new (); ContextMenu.KeyChanged += ContextMenu_KeyChanged!; KeyBindings.Add ((KeyCode)ContextMenu.Key, KeyBindingScope.HotKey, Command.ShowContextMenu); @@ -3494,7 +3494,7 @@ public class TextView : View } else if (ev.Flags == ContextMenu!.MouseFlags) { - ContextMenu.Position = new (ev.Position.X + 2, ev.Position.Y + 2); + ContextMenu.Position = ViewportToScreen ((Viewport with { X = ev.Position.X, Y = ev.Position.Y }).Location); ShowContextMenu (); } @@ -4131,7 +4131,7 @@ public class TextView : View private void AppendClipboard (string text) { Clipboard.Contents += text; } - private MenuBarItem BuildContextMenuBarItem () + private MenuBarItem? BuildContextMenuBarItem () { return new ( new MenuItem [] @@ -6289,14 +6289,12 @@ public class TextView : View private void ShowContextMenu () { - if (_currentCulture != Thread.CurrentThread.CurrentUICulture) + if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) { _currentCulture = Thread.CurrentThread.CurrentUICulture; - - ContextMenu!.MenuItems = BuildContextMenuBarItem (); } - ContextMenu!.Show (); + ContextMenu!.Show (BuildContextMenuBarItem ()); } private void StartSelecting () diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap.cs index 867795a75..fba3354db 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -902,31 +902,32 @@ internal class CharMap : View _contextMenu = new () { - Position = new (me.Position.X + 1, me.Position.Y + 1), - MenuItems = new ( - new MenuItem [] - { - new ( - "_Copy Glyph", - "", - CopyGlyph, - null, - null, - (KeyCode)Key.C.WithCtrl - ), - new ( - "Copy Code _Point", - "", - CopyCodePoint, - null, - null, - (KeyCode)Key.C.WithCtrl - .WithShift - ) - } - ) + Position = new (me.Position.X + 1, me.Position.Y + 1) }; - _contextMenu.Show (); + + MenuBarItem menuItems = new ( + new MenuItem [] + { + new ( + "_Copy Glyph", + "", + CopyGlyph, + null, + null, + (KeyCode)Key.C.WithCtrl + ), + new ( + "Copy Code _Point", + "", + CopyCodePoint, + null, + null, + (KeyCode)Key.C.WithCtrl + .WithShift + ) + } + ); + _contextMenu.Show (menuItems); } } diff --git a/UICatalog/Scenarios/ContextMenus.cs b/UICatalog/Scenarios/ContextMenus.cs index e271ccd69..d9aeac1d8 100644 --- a/UICatalog/Scenarios/ContextMenus.cs +++ b/UICatalog/Scenarios/ContextMenus.cs @@ -163,133 +163,133 @@ public class ContextMenus : Scenario _contextMenu = new() { Position = new (x, y), - MenuItems = new ( - new [] - { - new ( - "_Configuration", - "Show configuration", - () => MessageBox.Query ( - 50, - 5, - "Info", - "This would open settings dialog", - "Ok" - ) - ), - new MenuBarItem ( - "More options", - new MenuItem [] - { - new ( - "_Setup", - "Change settings", - () => MessageBox - .Query ( - 50, - 5, - "Info", - "This would open setup dialog", - "Ok" - ), - shortcutKey: KeyCode.T - | KeyCode - .CtrlMask - ), - new ( - "_Maintenance", - "Maintenance mode", - () => MessageBox - .Query ( - 50, - 5, - "Info", - "This would open maintenance dialog", - "Ok" - ) - ) - } - ), - new MenuBarItem ( - "_Languages", - GetSupportedCultures () - ), - _miForceMinimumPosToZero = - new ( - "ForceMinimumPosToZero", - "", - () => - { - _miForceMinimumPosToZero - .Checked = - _forceMinimumPosToZero = - !_forceMinimumPosToZero; - - _tfTopLeft.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfTopRight.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfMiddle.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfBottomLeft.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfBottomRight - .ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - } - ) - { - CheckType = - MenuItemCheckStyle - .Checked, - Checked = - _forceMinimumPosToZero - }, - _miUseSubMenusSingleFrame = - new ( - "Use_SubMenusSingleFrame", - "", - () => _contextMenu - .UseSubMenusSingleFrame = - (bool) - (_miUseSubMenusSingleFrame - .Checked = - _useSubMenusSingleFrame = - !_useSubMenusSingleFrame) - ) - { - CheckType = MenuItemCheckStyle - .Checked, - Checked = - _useSubMenusSingleFrame - }, - null, - new ( - "_Quit", - "", - () => Application.RequestStop () - ) - } - ), ForceMinimumPosToZero = _forceMinimumPosToZero, UseSubMenusSingleFrame = _useSubMenusSingleFrame }; + MenuBarItem menuItems = new ( + new [] + { + new ( + "_Configuration", + "Show configuration", + () => MessageBox.Query ( + 50, + 5, + "Info", + "This would open settings dialog", + "Ok" + ) + ), + new MenuBarItem ( + "More options", + new MenuItem [] + { + new ( + "_Setup", + "Change settings", + () => MessageBox + .Query ( + 50, + 5, + "Info", + "This would open setup dialog", + "Ok" + ), + shortcutKey: KeyCode.T + | KeyCode + .CtrlMask + ), + new ( + "_Maintenance", + "Maintenance mode", + () => MessageBox + .Query ( + 50, + 5, + "Info", + "This would open maintenance dialog", + "Ok" + ) + ) + } + ), + new MenuBarItem ( + "_Languages", + GetSupportedCultures () + ), + _miForceMinimumPosToZero = + new ( + "ForceMinimumPosToZero", + "", + () => + { + _miForceMinimumPosToZero + .Checked = + _forceMinimumPosToZero = + !_forceMinimumPosToZero; + + _tfTopLeft.ContextMenu + .ForceMinimumPosToZero = + _forceMinimumPosToZero; + + _tfTopRight.ContextMenu + .ForceMinimumPosToZero = + _forceMinimumPosToZero; + + _tfMiddle.ContextMenu + .ForceMinimumPosToZero = + _forceMinimumPosToZero; + + _tfBottomLeft.ContextMenu + .ForceMinimumPosToZero = + _forceMinimumPosToZero; + + _tfBottomRight + .ContextMenu + .ForceMinimumPosToZero = + _forceMinimumPosToZero; + } + ) + { + CheckType = + MenuItemCheckStyle + .Checked, + Checked = + _forceMinimumPosToZero + }, + _miUseSubMenusSingleFrame = + new ( + "Use_SubMenusSingleFrame", + "", + () => _contextMenu + .UseSubMenusSingleFrame = + (bool) + (_miUseSubMenusSingleFrame + .Checked = + _useSubMenusSingleFrame = + !_useSubMenusSingleFrame) + ) + { + CheckType = MenuItemCheckStyle + .Checked, + Checked = + _useSubMenusSingleFrame + }, + null, + new ( + "_Quit", + "", + () => Application.RequestStop () + ) + } + ); _tfTopLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; _tfTopRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; _tfMiddle.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; _tfBottomLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; _tfBottomRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _contextMenu.Show (); + _contextMenu.Show (menuItems); } } diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index 31e6f3513..ac0e78c19 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -362,9 +362,9 @@ public class Notepad : Scenario var screen = ((View)sender).ViewportToScreen (e.MouseEvent.Position); - var contextMenu = new ContextMenu { Position = screen, MenuItems = items }; + var contextMenu = new ContextMenu { Position = screen }; - contextMenu.Show (); + contextMenu.Show (items); e.MouseEvent.Handled = true; } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index f7f3d5f17..677d5fe35 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -1266,28 +1266,28 @@ public class TableEditor : Scenario var contextMenu = new ContextMenu { - Position = new (e.MouseEvent.Position.X + 1, e.MouseEvent.Position.Y + 1), - MenuItems = new ( - [ - new ( - $"Hide {TrimArrows (colName)}", - "", - () => HideColumn (clickedCol) - ), - new ( - $"Sort {StripArrows (sort)}", - "", - () => SortColumn ( - clickedCol, - sort, - isAsc - ) - ) - ] - ) + Position = new (e.MouseEvent.Position.X + 1, e.MouseEvent.Position.Y + 1) }; - contextMenu.Show (); + MenuBarItem menuItems = new ( + [ + new ( + $"Hide {TrimArrows (colName)}", + "", + () => HideColumn (clickedCol) + ), + new ( + $"Sort {StripArrows (sort)}", + "", + () => SortColumn ( + clickedCol, + sort, + isAsc + ) + ) + ] + ); + contextMenu.Show (menuItems); } private void SortColumn (int clickedCol) diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index 7b3f4a66e..d2073bd18 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -415,21 +415,20 @@ public class TreeViewFileSystem : Scenario private void ShowContextMenu (Point screenPoint, IFileSystemInfo forObject) { - var menu = new ContextMenu (); - menu.Position = screenPoint; + var menu = new ContextMenu { Position = screenPoint }; - menu.MenuItems = new MenuBarItem ( - new [] { new MenuItem ("Properties", null, () => ShowPropertiesOf (forObject)) } - ); + var menuItems = new MenuBarItem ( + new [] { new MenuItem ("Properties", null, () => ShowPropertiesOf (forObject)) } + ); - Application.Invoke (menu.Show); + Application.Invoke (() => menu.Show (menuItems)); } private void ShowLines () { _miShowLines.Checked = !_miShowLines.Checked; - _treeViewFiles.Style.ShowBranchLines = (bool)_miShowLines.Checked; + _treeViewFiles.Style.ShowBranchLines = (bool)_miShowLines.Checked!; _treeViewFiles.SetNeedsDisplay (); } diff --git a/UnitTests/Views/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index 143e25c8a..e31170b3c 100644 --- a/UnitTests/Views/ContextMenuTests.cs +++ b/UnitTests/Views/ContextMenuTests.cs @@ -10,42 +10,53 @@ public class ContextMenuTests (ITestOutputHelper output) public void ContextMenu_Constructors () { var cm = new ContextMenu (); + var top = new Toplevel (); + Application.Begin (top); + Assert.Equal (Point.Empty, cm.Position); - Assert.Empty (cm.MenuItems.Children); + Assert.Null (cm.MenuItems); Assert.Null (cm.Host); cm.Position = new Point (20, 10); - cm.MenuItems = new MenuBarItem ( + var menuItems = new MenuBarItem ( [ new MenuItem ("First", "", null) ] ); + cm.Show (menuItems); Assert.Equal (new Point (20, 10), cm.Position); - Assert.Single (cm.MenuItems.Children); + Assert.Single (cm.MenuItems!.Children); cm = new ContextMenu { - Position = new Point (5, 10), - MenuItems = new MenuBarItem ( - new [] { new MenuItem ("One", "", null), new MenuItem ("Two", "", null) } - ) + Position = new Point (5, 10) }; + + menuItems = new MenuBarItem ( + new [] { new MenuItem ("One", "", null), new MenuItem ("Two", "", null) } + ); + cm.Show (menuItems); Assert.Equal (new Point (5, 10), cm.Position); - Assert.Equal (2, cm.MenuItems.Children.Length); + Assert.Equal (2, cm.MenuItems!.Children.Length); Assert.Null (cm.Host); + var view = new View { X = 5, Y = 10 }; + top.Add (view); cm = new ContextMenu { - Host = new View { X = 5, Y = 10 }, - Position = new Point (5, 10), - MenuItems = new MenuBarItem ( - new [] { new MenuItem ("One", "", null), new MenuItem ("Two", "", null) } - ) + Host = view, + Position = new Point (5, 10) }; + menuItems = new MenuBarItem ( + new [] { new MenuItem ("One", "", null), new MenuItem ("Two", "", null) } + ); + cm.Show (menuItems); Assert.Equal (new Point (5, 10), cm.Position); Assert.Equal (2, cm.MenuItems.Children.Length); Assert.NotNull (cm.Host); + + top.Dispose (); } [Fact] @@ -54,15 +65,15 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (10, 5), - MenuItems = new MenuBarItem ( + Position = new Point (10, 5) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); var menu = new MenuBar { Menus = @@ -78,7 +89,7 @@ public class ContextMenuTests (ITestOutputHelper output) Assert.Null (Application.MouseGrabView); - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); Assert.Equal (cm.MenuBar, Application.MouseGrabView); Assert.False (menu.IsMenuOpen); @@ -87,7 +98,7 @@ public class ContextMenuTests (ITestOutputHelper output) Assert.Equal (menu, Application.MouseGrabView); Assert.True (menu.IsMenuOpen); - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); Assert.Equal (cm.MenuBar, Application.MouseGrabView); Assert.False (menu.IsMenuOpen); @@ -98,7 +109,7 @@ public class ContextMenuTests (ITestOutputHelper output) Assert.True (menu.IsMenuOpen); #endif - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); Assert.Equal (cm.MenuBar, Application.MouseGrabView); Assert.False (menu.IsMenuOpen); @@ -305,21 +316,21 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (-1, -2), - MenuItems = new MenuBarItem ( + Position = new Point (-1, -2) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Assert.Equal (new Point (-1, -2), cm.Position); - + Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Assert.Equal (new Point (-1, -2), cm.Position); Application.Refresh (); @@ -334,7 +345,7 @@ public class ContextMenuTests (ITestOutputHelper output) Assert.Equal (new Rectangle (0, 1, 8, 4), pos); cm.ForceMinimumPosToZero = false; - cm.Show (); + cm.Show (menuItems); Assert.Equal (new Point (-1, -2), cm.Position); Application.Refresh (); @@ -355,22 +366,22 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (80, 25), - MenuItems = new MenuBarItem ( + Position = new Point (80, 25) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Toplevel top = new (); Application.Begin (top); top.Running = true; Assert.False (ContextMenu.IsShow); - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); top.RequestStop (); @@ -388,7 +399,7 @@ public class ContextMenuTests (ITestOutputHelper output) Application.Begin (top); Assert.True (Application.OnKeyDown (ContextMenu.DefaultKey)); - Assert.True (tf.ContextMenu.MenuBar.IsMenuOpen); + Assert.True (tf.ContextMenu.MenuBar!.IsMenuOpen); Assert.True (Application.OnKeyDown (ContextMenu.DefaultKey)); // The last context menu bar opened is always preserved Assert.NotNull (tf.ContextMenu.MenuBar); @@ -415,18 +426,18 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (10, 5), - MenuItems = new MenuBarItem ( + Position = new Point (10, 5) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); var expected = @" @@ -438,15 +449,15 @@ public class ContextMenuTests (ITestOutputHelper output) TestHelpers.AssertDriverContentsAre (expected, output); - cm.MenuItems = new MenuBarItem ( - [ - new MenuItem ("First", "", null), - new MenuItem ("Second", "", null), - new MenuItem ("Third", "", null) - ] - ); + menuItems = new MenuBarItem ( + [ + new MenuItem ("First", "", null), + new MenuItem ("Second", "", null), + new MenuItem ("Third", "", null) + ] + ); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); expected = @" @@ -467,8 +478,10 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (-1, -2), - MenuItems = new MenuBarItem ( + Position = new Point (-1, -2) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null), @@ -488,15 +501,13 @@ public class ContextMenuTests (ITestOutputHelper output) new MenuItem ("Five", "", null), new MenuItem ("Six", "", null) ] - ) - }; - + ); Assert.Equal (new Point (-1, -2), cm.Position); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); Assert.Equal (new Point (-1, -2), cm.Position); @@ -545,7 +556,7 @@ public class ContextMenuTests (ITestOutputHelper output) ((FakeDriver)Application.Driver!).SetBufferSize (40, 20); cm.Position = new Point (41, -2); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); Assert.Equal (new Point (41, -2), cm.Position); @@ -592,7 +603,7 @@ public class ContextMenuTests (ITestOutputHelper output) ); cm.Position = new Point (41, 9); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); Assert.Equal (new Point (41, 9), cm.Position); @@ -636,7 +647,7 @@ public class ContextMenuTests (ITestOutputHelper output) ); cm.Position = new Point (41, 22); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); Assert.Equal (new Point (41, 22), cm.Position); @@ -680,7 +691,7 @@ public class ContextMenuTests (ITestOutputHelper output) ((FakeDriver)Application.Driver!).SetBufferSize (18, 8); cm.Position = new Point (19, 10); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); Assert.Equal (new Point (19, 10), cm.Position); @@ -773,18 +784,18 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (10, 5), - MenuItems = new MenuBarItem ( + Position = new Point (10, 5) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); var expected = @" @@ -798,7 +809,7 @@ public class ContextMenuTests (ITestOutputHelper output) cm.Position = new Point (5, 10); - cm.Show (); + cm.Show (menuItems); Application.Refresh (); expected = @" @@ -816,7 +827,14 @@ public class ContextMenuTests (ITestOutputHelper output) [AutoInitShutdown] public void RequestStop_While_ContextMenu_Is_Open_Does_Not_Throws () { - ContextMenu cm = Create_ContextMenu_With_Two_MenuItem (10, 5); + ContextMenu cm = new ContextMenu + { + Position = new Point (10, 5) + }; + + var menuItems = new MenuBarItem ( + new MenuItem [] { new ("One", "", null), new ("Two", "", null) } + ); Toplevel top = new (); var isMenuAllClosed = false; MenuBarItem mi = null; @@ -828,7 +846,7 @@ public class ContextMenuTests (ITestOutputHelper output) if (iterations == 0) { - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); mi = cm.MenuBar.Menus [0]; @@ -853,7 +871,7 @@ public class ContextMenuTests (ITestOutputHelper output) else if (iterations == 3) { isMenuAllClosed = false; - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); cm.MenuBar.MenuAllClosed += (_, _) => isMenuAllClosed = true; } @@ -896,20 +914,20 @@ public class ContextMenuTests (ITestOutputHelper output) var cm = new ContextMenu { - Position = Point.Empty, - MenuItems = new MenuBarItem ( + Position = Point.Empty + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Assert.Equal (Point.Empty, cm.Position); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Assert.Equal (Point.Empty, cm.Position); Application.Refresh (); @@ -934,20 +952,20 @@ public class ContextMenuTests (ITestOutputHelper output) var cm = new ContextMenu { - Position = Point.Empty, - MenuItems = new MenuBarItem ( + Position = Point.Empty + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Assert.Equal (Point.Empty, cm.Position); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Assert.Equal (Point.Empty, cm.Position); Application.Refresh (); @@ -981,22 +999,22 @@ public class ContextMenuTests (ITestOutputHelper output) var cm = new ContextMenu { Host = view, - Position = new Point (10, 5), - MenuItems = new MenuBarItem ( + Position = new Point (10, 5) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); var top = new Toplevel (); top.Add (view); Application.Begin (top); Assert.Equal (new Point (10, 5), cm.Position); - cm.Show (); + cm.Show (menuItems); top.Draw (); Assert.Equal (new Point (10, 5), cm.Position); @@ -1017,7 +1035,7 @@ public class ContextMenuTests (ITestOutputHelper output) cm.Host.Y = 10; cm.Host.Height = 3; - cm.Show (); + cm.Show (menuItems); Application.Top.Draw (); Assert.Equal (new Point (5, 12), cm.Position); @@ -1045,20 +1063,20 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (80, 25), - MenuItems = new MenuBarItem ( + Position = new Point (80, 25) + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); Assert.Equal (new Point (80, 25), cm.Position); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Assert.Equal (new Point (80, 25), cm.Position); Application.Refresh (); @@ -1092,15 +1110,15 @@ public class ContextMenuTests (ITestOutputHelper output) var cm = new ContextMenu { - Host = view, - MenuItems = new MenuBarItem ( + Host = view + }; + + var menuItems = new MenuBarItem ( [ new MenuItem ("One", "", null), new MenuItem ("Two", "", null) ] - ) - }; - + ); var top = new Toplevel (); top.Add (view); Application.Begin (top); @@ -1108,7 +1126,7 @@ public class ContextMenuTests (ITestOutputHelper output) Assert.Equal (new Rectangle (70, 24, 10, 1), view.Frame); Assert.Equal (Point.Empty, cm.Position); - cm.Show (); + cm.Show (menuItems); Assert.Equal (new Point (70, 24), cm.Position); top.Draw (); @@ -1132,11 +1150,18 @@ public class ContextMenuTests (ITestOutputHelper output) [AutoInitShutdown] public void Show_Hide_IsShow () { - ContextMenu cm = Create_ContextMenu_With_Two_MenuItem (10, 5); + ContextMenu cm = new ContextMenu + { + Position = new Point (10, 5) + }; + + var menuItems = new MenuBarItem ( + new MenuItem [] { new ("One", "", null), new ("Two", "", null) } + ); Toplevel top = new (); Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Assert.True (ContextMenu.IsShow); Application.Refresh (); @@ -1167,7 +1192,10 @@ public class ContextMenuTests (ITestOutputHelper output) var cm = new ContextMenu { Position = new Point (5, 10), - MenuItems = new MenuBarItem ( + UseSubMenusSingleFrame = true + }; + + var menuItems = new MenuBarItem ( "Numbers", [ new MenuItem ("One", "", null), @@ -1184,19 +1212,11 @@ public class ContextMenuTests (ITestOutputHelper output) ), new MenuItem ("Three", "", null) ] - ), - UseSubMenusSingleFrame = true - }; + ); Toplevel top = new (); RunState rs = Application.Begin (top); - top.SetFocus (); - Assert.NotNull (Application.Current); - - cm.Show (); - Assert.True(ContextMenu.IsShow); - Assert.True (Application.Top.Subviews [0].HasFocus); - Assert.Equal(Application.Top.Subviews [0], Application.Navigation.GetFocused()); - Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.Subviews [0].Frame); + cm.Show (menuItems); + Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top!.Subviews [0].Frame); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre ( @@ -1254,8 +1274,10 @@ public class ContextMenuTests (ITestOutputHelper output) { var cm = new ContextMenu { - Position = new Point (5, 10), - MenuItems = new MenuBarItem ( + Position = new Point (5, 10) + }; + + var menuItems = new MenuBarItem ( "Numbers", [ new MenuItem ("One", "", null), @@ -1270,7 +1292,8 @@ public class ContextMenuTests (ITestOutputHelper output) new MenuItem ("Two-Menu 2", "", null) ] ), - new MenuBarItem ("Three", + new MenuBarItem ( + "Three", [ new MenuItem ( "Three-Menu 1", @@ -1281,11 +1304,10 @@ public class ContextMenuTests (ITestOutputHelper output) ] ) ] - ) - }; + ); Toplevel top = new (); RunState rs = Application.Begin (top); - cm.Show (); + cm.Show (menuItems); Assert.Equal (new Rectangle (5, 11, 10, 5), Application.Top.Subviews [0].Frame); Application.Refresh (); @@ -1356,17 +1378,6 @@ public class ContextMenuTests (ITestOutputHelper output) top.Dispose (); } - private ContextMenu Create_ContextMenu_With_Two_MenuItem (int x, int y) - { - return new ContextMenu - { - Position = new Point (x, y), - MenuItems = new MenuBarItem ( - new MenuItem [] { new ("One", "", null), new ("Two", "", null) } - ) - }; - } - [Fact] [AutoInitShutdown] public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () @@ -1417,4 +1428,239 @@ public class ContextMenuTests (ITestOutputHelper output) Application.End (rs); win.Dispose (); } + + [Fact] + [AutoInitShutdown] + public void Empty_Menus_Items_Children_Does_Not_Open_The_Menu () + { + var cm = new ContextMenu (); + Assert.Null (cm.MenuItems); + + var top = new Toplevel (); + Application.Begin (top); + + cm.Show (cm.MenuItems); + Assert.Null (cm.MenuBar); + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void KeyBinding_Removed_On_Close_ContextMenu () + { + var newFile = false; + var renameFile = false; + var deleteFile = false; + + var cm = new ContextMenu (); + + var menuItems = new MenuBarItem ( + [ + new MenuItem ("New File", string.Empty, New, null, null, Key.N.WithCtrl), + new MenuItem ("Rename File", string.Empty, Rename, null, null, Key.R.WithCtrl), + new MenuItem ("Delete File", string.Empty, Delete, null, null, Key.D.WithCtrl) + ] + ); + var top = new Toplevel (); + Application.Begin (top); + + Assert.Null (cm.MenuBar); + Assert.False (Application.OnKeyDown (Key.N.WithCtrl)); + Assert.False (Application.OnKeyDown (Key.R.WithCtrl)); + Assert.False (Application.OnKeyDown (Key.D.WithCtrl)); + Assert.False (newFile); + Assert.False (renameFile); + Assert.False (deleteFile); + + cm.Show (menuItems); + Assert.True (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); + + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (newFile); + Assert.False (cm.MenuBar!.IsMenuOpen); + cm.Show (menuItems); + Assert.True (Application.OnKeyDown (Key.R.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (renameFile); + Assert.False (cm.MenuBar.IsMenuOpen); + cm.Show (menuItems); + Assert.True (Application.OnKeyDown (Key.D.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (deleteFile); + Assert.False (cm.MenuBar.IsMenuOpen); + + Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); + + newFile = false; + renameFile = false; + deleteFile = false; + Assert.False (Application.OnKeyDown (Key.N.WithCtrl)); + Assert.False (Application.OnKeyDown (Key.R.WithCtrl)); + Assert.False (Application.OnKeyDown (Key.D.WithCtrl)); + Assert.False (newFile); + Assert.False (renameFile); + Assert.False (deleteFile); + + top.Dispose (); + + void New () { newFile = true; } + + void Rename () { renameFile = true; } + + void Delete () { deleteFile = true; } + } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_With_ContextMenu_And_MenuBar () + { + var newFile = false; + var renameFile = false; + + var menuBar = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ("New", string.Empty, New, null, null, Key.N.WithCtrl) + }) + ] + }; + var cm = new ContextMenu (); + + var menuItems = new MenuBarItem ( + [ + new MenuItem ("Rename File", string.Empty, Rename, null, null, Key.R.WithCtrl), + ] + ); + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.Null (cm.MenuBar); + + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Assert.False (Application.OnKeyDown (Key.R.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (newFile); + Assert.False (renameFile); + + newFile = false; + + cm.Show (menuItems); + Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + + Assert.True (cm.MenuBar.IsMenuOpen); + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (newFile); + Assert.False (cm.MenuBar!.IsMenuOpen); + cm.Show (menuItems); + Assert.True (Application.OnKeyDown (Key.R.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (renameFile); + Assert.False (cm.MenuBar.IsMenuOpen); + + Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + + newFile = false; + renameFile = false; + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Assert.False (Application.OnKeyDown (Key.R.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (newFile); + Assert.False (renameFile); + + top.Dispose (); + + void New () { newFile = true; } + + void Rename () { renameFile = true; } + } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () + { + var newMenuBar = false; + var newContextMenu = false; + + var menuBar = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ("New", string.Empty, NewMenuBar, null, null, Key.N.WithCtrl) + }) + ] + }; + var cm = new ContextMenu (); + + var menuItems = new MenuBarItem ( + [ + new MenuItem ("New File", string.Empty, NewContextMenu, null, null, Key.N.WithCtrl), + ] + ); + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.Null (cm.MenuBar); + + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (newMenuBar); + Assert.False (newContextMenu); + + newMenuBar = false; + + cm.Show (menuItems); + Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + + Assert.True (cm.MenuBar.IsMenuOpen); + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.False (newMenuBar); + // The most focused shortcut is executed + Assert.True (newContextMenu); + Assert.False (cm.MenuBar!.IsMenuOpen); + + Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + + newMenuBar = false; + newContextMenu = false; + Assert.True (Application.OnKeyDown (Key.N.WithCtrl)); + Application.MainLoop!.RunIteration (); + Assert.True (newMenuBar); + Assert.False (newContextMenu); + + top.Dispose (); + + void NewMenuBar () { newMenuBar = true; } + + void NewContextMenu () { newContextMenu = true; } + } }