From 484e98998a2ea9327c7cd1141da7ae4acc64a0ae Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 10 Mar 2022 00:38:38 +0000 Subject: [PATCH] Feature to display the sub-menus on a single frame instead multiple frames. --- Example/demo.cs | 30 ++- Terminal.Gui/Core/Toplevel.cs | 6 +- Terminal.Gui/Views/Menu.cs | 182 ++++++++----- UnitTests/MenuTests.cs | 465 ++++++++++++++++++++++++++++++++++ 4 files changed, 609 insertions(+), 74 deletions(-) diff --git a/Example/demo.cs b/Example/demo.cs index 3f69b42e8..c963aff9e 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -119,10 +119,10 @@ static class Demo { int i = 0; string txt = "Hello world, how are you doing today?"; container.Add ( - new Label ($"{i+1}-{txt}") { TextAlignment = TextAlignment.Left, Y = 3, Width = Dim.Fill () }, - new Label ($"{i+2}-{txt}") { TextAlignment = TextAlignment.Right, Y = 5, Width = Dim.Fill () }, - new Label ($"{i+3}-{txt}") { TextAlignment = TextAlignment.Centered, Y = 7, Width = Dim.Fill () }, - new Label ($"{i+4}-{txt}") { TextAlignment = TextAlignment.Justified, Y = 9, Width = Dim.Fill () } + new Label ($"{i + 1}-{txt}") { TextAlignment = TextAlignment.Left, Y = 3, Width = Dim.Fill () }, + new Label ($"{i + 2}-{txt}") { TextAlignment = TextAlignment.Right, Y = 5, Width = Dim.Fill () }, + new Label ($"{i + 3}-{txt}") { TextAlignment = TextAlignment.Centered, Y = 7, Width = Dim.Fill () }, + new Label ($"{i + 4}-{txt}") { TextAlignment = TextAlignment.Justified, Y = 9, Width = Dim.Fill () } ); Application.Run (container); @@ -487,15 +487,15 @@ static class Demo { .OrderBy (x => x).Select (x => ustring.Make (x)).ToList (); } } - var list = new ComboBox () { Width = Dim.Fill(), Height = Dim.Fill() }; - list.SetSource(items); + var list = new ComboBox () { Width = Dim.Fill (), Height = Dim.Fill () }; + list.SetSource (items); list.OpenSelectedItem += (ListViewItemEventArgs text) => { Application.RequestStop (); }; var d = new Dialog () { Title = "Select source file", Width = Dim.Percent (50), Height = Dim.Percent (50) }; d.Add (list); Application.Run (d); - MessageBox.Query (60, 10, "Selected file", list.Text.ToString() == "" ? "Nothing selected" : list.Text.ToString(), "Ok"); + MessageBox.Query (60, 10, "Selected file", list.Text.ToString () == "" ? "Nothing selected" : list.Text.ToString (), "Ok"); } #endregion @@ -564,10 +564,9 @@ static class Demo { #endregion public static Action running = MainApp; - static void Main(string[] args) + static void Main (string [] args) { - if (args.Length > 0 && args.Contains("-usc")) - { + if (args.Length > 0 && args.Contains ("-usc")) { Application.UseSystemConsole = true; } @@ -588,7 +587,7 @@ static class Demo { if (Debugger.IsAttached) CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); - Application.Init(); + Application.Init (); Application.HeightAsBuffer = true; //ConsoleDriver.Diagnostics = ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler; @@ -621,6 +620,9 @@ static class Demo { menuItems [2].Action = () => ShowMenuItem (menuItems [2]); menuItems [3].Action = () => ShowMenuItem (menuItems [3]); + MenuItem miUseSubMenusSingleFrame = null; + var useSubMenusSingleFrame = false; + menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("Text _Editor Demo", "", () => { running = Editor; Application.RequestStop (); }, null, null, Key.AltMask | Key.CtrlMask | Key.D), @@ -638,7 +640,11 @@ static class Demo { new MenuItem ("_Paste", "", Paste, null, null, Key.AltMask | Key.CtrlMask| Key.V), new MenuBarItem ("_Find and Replace", new MenuItem [] { menuItems [0], menuItems [1] }), - menuItems[3] + menuItems[3], + miUseSubMenusSingleFrame = new MenuItem ("Use_SubMenusSingleFrame", "", + () => menu.UseSubMenusSingleFrame = miUseSubMenusSingleFrame.Checked = useSubMenusSingleFrame = !useSubMenusSingleFrame) { + CheckType = MenuItemCheckStyle.Checked, Checked = useSubMenusSingleFrame + } }), new MenuBarItem ("_List Demos", new MenuItem [] { new MenuItem ("Select _Multiple Items", "", () => ListSelectionDemo (true), null, null, Key.AltMask + 0.ToString () [0]), diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index 6c602903d..3e071f0d5 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -347,10 +347,8 @@ namespace Terminal.Gui { case Key.AltMask: case Key.AltMask | Key.Space: case Key.CtrlMask | Key.Space: - if (MenuBar != null && MenuBar.OnKeyDown (keyEvent)) { - return true; - } - break; + case Key _ when (keyEvent.Key & Key.AltMask) == Key.AltMask: + return MenuBar != null && MenuBar.OnKeyDown (keyEvent); } return false; diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index b8105dd4e..6faeded35 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -180,7 +180,7 @@ namespace Terminal.Gui { { bool nextIsHot = false; foreach (var x in title) { - if (x == '_') { + if (x == MenuBar.HotKeySpecifier) { nextIsHot = true; } else { if (nextIsHot) { @@ -349,7 +349,7 @@ namespace Terminal.Gui { { int len = 0; foreach (var ch in title) { - if (ch == '_') + if (ch == MenuBar.HotKeySpecifier) continue; len++; } @@ -419,8 +419,9 @@ namespace Terminal.Gui { AddCommand (Command.Left, () => { this.host.PreviousMenu (true); return true; }); AddCommand (Command.Right, () => { this.host.NextMenu (this.barItems.IsTopLevel || (this.barItems.Children != null - && current > -1 && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu) - ? true : false); return true; + && current > -1 && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu), + current > -1 && host.UseSubMenusSingleFrame && this.barItems.SubMenu (this.barItems.Children [current]) != null); + return true; }); AddCommand (Command.Cancel, () => { CloseAllMenus (); return true; }); AddCommand (Command.Accept, () => { RunSelected (); return true; }); @@ -466,6 +467,8 @@ namespace Terminal.Gui { continue; if (item == null) Driver.AddRune (Driver.HLine); + else if (i == 0 && p == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) + Driver.AddRune (Driver.LeftArrow); else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null) Driver.AddRune (Driver.RightArrow); else @@ -501,12 +504,22 @@ namespace Terminal.Gui { ViewToScreen (2, i + 1, out int vtsCol, out _, false); if (vtsCol < Driver.Cols) { Move (2, i + 1); - if (!item.IsEnabled ()) + if (!item.IsEnabled ()) { DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled); - else + } else if (i == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) { + var tf = new TextFormatter () { + Alignment = TextAlignment.Centered, + HotKeySpecifier = MenuBar.HotKeySpecifier, + Text = textToDraw + }; + tf.Draw (ViewToScreen (new Rect (2, i + 1, Frame.Width - 3, 1)), + i == current ? ColorScheme.Focus : GetNormalColor (), + i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal); + } else { DrawHotString (textToDraw, - i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, - i == current ? ColorScheme.Focus : GetNormalColor ()); + i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, + i == current ? ColorScheme.Focus : GetNormalColor ()); + } // The help string var l = item.ShortcutTag.RuneCount == 0 ? item.Help.RuneCount : item.Help.RuneCount + item.ShortcutTag.RuneCount + 2; @@ -590,12 +603,13 @@ namespace Terminal.Gui { // TODO: rune-ify if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) { var x = Char.ToUpper ((char)kb.KeyValue); + var idx = -1; foreach (var item in barItems.Children) { + idx++; if (item == null) continue; if (item.IsEnabled () && item.HotKey == x) { - if (host.CloseMenu ()) { - Run (item.Action); - } + current = idx; + RunSelected (); return true; } } @@ -607,8 +621,15 @@ namespace Terminal.Gui { { if (barItems.IsTopLevel) { Run (barItems.Action); - } else if (current > -1) { + } else if (current > -1 && barItems.Children [current].Action != null) { Run (barItems.Children [current].Action); + } else if (current == 0 && host.UseSubMenusSingleFrame + && barItems.Children [current].Parent.Parent != null) { + + host.PreviousMenu (barItems.Children [current].Parent.IsFromSubMenu, true); + } else if (current > -1 && barItems.SubMenu (barItems.Children [current]) != null) { + + CheckSubMenu (); } } @@ -640,7 +661,7 @@ namespace Terminal.Gui { } else { disabled = false; } - if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null && + if (!host.UseSubMenusSingleFrame && host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null && !disabled && host.IsMenuOpen) { if (!CheckSubMenu ()) return false; @@ -651,7 +672,8 @@ namespace Terminal.Gui { } } while (barItems.Children [current] == null || disabled); SetNeedsDisplay (); - host.OnMenuOpened (); + if (!host.UseSubMenusSingleFrame) + host.OnMenuOpened (); return true; } @@ -663,7 +685,7 @@ namespace Terminal.Gui { bool disabled; do { current--; - if (host.UseKeysUpDownAsKeysLeftRight) { + if (host.UseKeysUpDownAsKeysLeftRight && !host.UseSubMenusSingleFrame) { if ((current == -1 || this != host.openCurrentMenu) && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) { current++; host.PreviousMenu (true); @@ -678,7 +700,7 @@ namespace Terminal.Gui { current = barItems.Children.Length - 1; if (!host.SelectEnabledItem (barItems.Children, current, out current, false)) { current = 0; - if (!host.SelectEnabledItem (barItems.Children, current, out current) && !host.CloseMenu ()) { + if (!host.SelectEnabledItem (barItems.Children, current, out current) && !host.CloseMenu (false)) { return false; } break; @@ -689,7 +711,7 @@ namespace Terminal.Gui { } else { disabled = false; } - if (host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null && + if (!host.UseSubMenusSingleFrame && host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null && !disabled && host.IsMenuOpen) { if (!CheckSubMenu ()) return false; @@ -697,7 +719,8 @@ namespace Terminal.Gui { } } while (barItems.Children [current] == null || disabled); SetNeedsDisplay (); - host.OnMenuOpened (); + if (!host.UseSubMenusSingleFrame) + host.OnMenuOpened (); return true; } @@ -717,12 +740,14 @@ namespace Terminal.Gui { return true; var item = barItems.Children [meY]; if (item == null || !item.IsEnabled ()) disabled = true; + current = meY; if (item != null && !disabled) - Run (barItems.Children [meY].Action); + RunSelected (); return true; } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + disabled = false; if (me.Y < 1 || me.Y - 1 >= barItems.Children.Length) { return true; @@ -732,7 +757,7 @@ namespace Terminal.Gui { if (item == null || !item.IsEnabled ()) disabled = true; if (item != null && !disabled) current = me.Y - 1; - if (!CheckSubMenu ()) + if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) return true; host.OnMenuOpened (); return true; @@ -803,15 +828,14 @@ namespace Terminal.Gui { /// /// public class MenuBar : View { - /// - /// Gets or sets the array of s for the menu. Only set this when the is vislble. - /// - /// The menu array. - public MenuBarItem [] Menus { get; set; } internal int selected; internal int selectedSub; - Action action; + /// + /// Gets or sets the array of s for the menu. Only set this when the is visible. + /// + /// The menu array. + public MenuBarItem [] Menus { get; set; } /// /// Used for change the navigation key style. @@ -831,6 +855,16 @@ namespace Terminal.Gui { } } + /// + /// The specifier character for the hotkey to all menus. + /// + new public static Rune HotKeySpecifier => '_'; + + /// + /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. + /// + public bool UseSubMenusSingleFrame { get; set; } + /// /// Initializes a new instance of the . /// @@ -897,7 +931,7 @@ namespace Terminal.Gui { /// public override bool OnKeyUp (KeyEvent keyEvent) { - if (keyEvent.IsAlt || (keyEvent.IsCtrl && keyEvent.Key == (Key.CtrlMask | Key.Space))) { + if (keyEvent.IsAlt || keyEvent.Key == Key.AltMask || (keyEvent.IsCtrl && keyEvent.Key == (Key.CtrlMask | Key.Space))) { // User pressed Alt - this may be a precursor to a menu accelerator (e.g. Alt-F) if (openedByAltKey && !IsMenuOpen && openMenu == null && (((uint)keyEvent.Key & (uint)Key.CharMask) == 0 || ((uint)keyEvent.Key & (uint)Key.CharMask) == (uint)Key.Space)) { @@ -1004,13 +1038,23 @@ namespace Terminal.Gui { pos += 2 + Menus [i].TitleLength + (Menus [i].Help.Length > 0 ? Menus [i].Help.Length + 2 : 0) + 1; } } - //Move (0, 0); } void Selected (MenuItem item) { - // TODO: Running = false; - action = item.Action; + var action = item.Action; + + if (action == null) + return; + + Application.UngrabMouse (); + CloseAllMenus (); + Application.Refresh (); + + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); } /// @@ -1154,7 +1198,21 @@ namespace Terminal.Gui { RemoveSubMenu (sIndex); } else { var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu; - openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu); + if (!UseSubMenusSingleFrame) { + openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width, last.Frame.Top + 1 + last.current, subMenu); + } else { + var first = openSubMenu.Count > 0 ? openSubMenu.First () : openMenu; + var mbi = new MenuItem [2 + subMenu.Children.Length]; + mbi [0] = new MenuItem () { Title = subMenu.Title, Parent = subMenu }; + mbi [1] = null; + for (int j = 0; j < subMenu.Children.Length; j++) { + mbi [j + 2] = subMenu.Children [j]; + } + var newSubMenu = new MenuBarItem (mbi); + openCurrentMenu = new Menu (this, first.Frame.Left, first.Frame.Top, newSubMenu); + last.Visible = false; + Application.GrabMouse (openCurrentMenu); + } openCurrentMenu.previousSubFocused = last.previousSubFocused; openSubMenu.Add (openCurrentMenu); if (SuperView == null) { @@ -1190,7 +1248,7 @@ namespace Terminal.Gui { previousFocused = SuperView == null ? Application.Current.Focused : SuperView.Focused; OpenMenu (selected); - if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu ()) { + if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu (false)) { return; } if (!openCurrentMenu.CheckSubMenu ()) @@ -1209,7 +1267,7 @@ namespace Terminal.Gui { OpenMenu (idx, sIdx, subMenu); if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) - && subMenu == null && !CloseMenu ()) { + && subMenu == null && !CloseMenu (false)) { return; } @@ -1263,19 +1321,23 @@ namespace Terminal.Gui { /// /// Closes the current Menu programatically, if open and not canceled. /// - public bool CloseMenu () + public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false) { - return CloseMenu (false, false); + return CloseMenu (false, false, ignoreUseSubMenusSingleFrame); } bool reopen; - internal bool CloseMenu (bool reopen = false, bool isSubMenu = false) + internal bool CloseMenu (bool reopen = false, bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) { + var mbi = isSubMenu ? openCurrentMenu.barItems : openMenu?.barItems; + if (UseSubMenusSingleFrame && mbi != null && + !ignoreUseSubMenusSingleFrame && mbi.Parent != null) { + return false; + } isMenuClosing = true; this.reopen = reopen; - var args = OnMenuClosing ( - isSubMenu ? openCurrentMenu.barItems : openMenu?.barItems, reopen, isSubMenu); + var args = OnMenuClosing (mbi, reopen, isSubMenu); if (args.Cancel) { isMenuClosing = false; if (args.CurrentMenu.Parent != null) @@ -1327,9 +1389,9 @@ namespace Terminal.Gui { return true; } - void RemoveSubMenu (int index) + void RemoveSubMenu (int index, bool ignoreUseSubMenusSingleFrame = false) { - if (openSubMenu == null) + if (openSubMenu == null || (UseSubMenusSingleFrame && !ignoreUseSubMenusSingleFrame && openSubMenu.Count > 0)) return; for (int i = openSubMenu.Count - 1; i > index; i--) { isMenuClosing = true; @@ -1338,6 +1400,8 @@ namespace Terminal.Gui { menu = openSubMenu [i - 1]; else menu = openMenu; + if (!menu.Visible) + menu.Visible = true; openCurrentMenu = menu; openCurrentMenu.SetFocus (); if (openSubMenu != null) { @@ -1350,7 +1414,7 @@ namespace Terminal.Gui { openSubMenu.Remove (menu); menu.Dispose (); } - RemoveSubMenu (i); + RemoveSubMenu (i, ignoreUseSubMenusSingleFrame); } if (openSubMenu.Count > 0) openCurrentMenu = openSubMenu.Last (); @@ -1397,7 +1461,7 @@ namespace Terminal.Gui { if (!isMenuOpening && !isMenuClosing) { if (openSubMenu != null && !CloseMenu (false, true)) return; - if (!CloseMenu ()) + if (!CloseMenu (false)) return; if (LastFocused != null && LastFocused != this) selected = -1; @@ -1420,7 +1484,7 @@ namespace Terminal.Gui { return view; } - internal void PreviousMenu (bool isSubMenu = false) + internal void PreviousMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) { switch (isSubMenu) { case false: @@ -1429,20 +1493,20 @@ namespace Terminal.Gui { else selected--; - if (selected > -1 && !CloseMenu (true, false)) + if (selected > -1 && !CloseMenu (true, false, ignoreUseSubMenusSingleFrame)) return; OpenMenu (selected); if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current, false)) { openCurrentMenu.current = 0; if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) { - CloseMenu (); + CloseMenu (ignoreUseSubMenusSingleFrame); } } break; case true: if (selectedSub > -1) { selectedSub--; - RemoveSubMenu (selectedSub); + RemoveSubMenu (selectedSub, ignoreUseSubMenusSingleFrame); SetNeedsDisplay (); } else PreviousMenu (); @@ -1451,7 +1515,7 @@ namespace Terminal.Gui { } } - internal void NextMenu (bool isSubMenu = false) + internal void NextMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) { switch (isSubMenu) { case false: @@ -1462,22 +1526,22 @@ namespace Terminal.Gui { else selected++; - if (selected > -1 && !CloseMenu (true)) + if (selected > -1 && !CloseMenu (true, ignoreUseSubMenusSingleFrame)) return; OpenMenu (selected); SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current); break; case true: if (UseKeysUpDownAsKeysLeftRight) { - if (CloseMenu (false, true)) { - NextMenu (); + if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) { + NextMenu (false, ignoreUseSubMenusSingleFrame); } } else { var subMenu = openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current]); if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) { if (openSubMenu != null && !CloseMenu (false, true)) return; - NextMenu (); + NextMenu (false, ignoreUseSubMenusSingleFrame); } else if (subMenu != null || !openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu) selectedSub++; @@ -1498,7 +1562,7 @@ namespace Terminal.Gui { for (int i = 0; i < Menus.Length; i++) { // TODO: this code is duplicated, hotkey should be part of the MenuBarItem var mi = Menus [i]; - int p = mi.Title.IndexOf ('_'); + int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier); if (p != -1 && p + 1 < mi.Title.RuneCount) { if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) { ProcessMenu (i, mi); @@ -1543,7 +1607,7 @@ namespace Terminal.Gui { private void ProcessMenu (int i, MenuBarItem mi) { - if (selected < 0) { + if (selected < 0 && IsMenuOpen) { return; } @@ -1556,7 +1620,7 @@ namespace Terminal.Gui { Application.GrabMouse (this); selected = i; OpenMenu (i); - if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu ()) { + if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu (false)) { return; } if (!openCurrentMenu.CheckSubMenu ()) @@ -1577,7 +1641,7 @@ namespace Terminal.Gui { } // To ncurses simulate a AltMask key pressing Alt+Space because - // it can�t detect an alone special key down was pressed. + // it can't detect an alone special key down was pressed. if (kb.IsAlt && kb.Key == Key.AltMask && openMenu == null) { OnKeyDown (kb); OnKeyUp (kb); @@ -1606,7 +1670,7 @@ namespace Terminal.Gui { foreach (var mi in Menus [selected].Children) { if (mi == null) continue; - int p = mi.Title.IndexOf ('_'); + int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier); if (p != -1 && p + 1 < mi.Title.RuneCount) { if (mi.Title [p + 1] == c) { Selected (mi); @@ -1621,7 +1685,7 @@ namespace Terminal.Gui { void CloseMenuBar () { - if (!CloseMenu ()) + if (!CloseMenu (false)) return; if (openedByAltKey) { openedByAltKey = false; @@ -1758,7 +1822,9 @@ namespace Terminal.Gui { isContextMenuLoading = false; return false; } - } else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { + } else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked + || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { + Application.GrabMouse (current); } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) { Application.GrabMouse (me.View); diff --git a/UnitTests/MenuTests.cs b/UnitTests/MenuTests.cs index 13c1f9f2e..7e687210d 100644 --- a/UnitTests/MenuTests.cs +++ b/UnitTests/MenuTests.cs @@ -589,5 +589,470 @@ Edit pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); Assert.Equal (new Point (0, 1), pos); } + + [Fact, AutoInitShutdown] + public void UseSubMenusSingleFrame_False_By_Keyboard () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("Numbers", new MenuItem [] { + new MenuItem ("One", "", null), + new MenuBarItem ("Two", new MenuItem [] { + new MenuItem ("Sub-Menu 1", "", null), + new MenuItem ("Sub-Menu 2", "", null) + }), + new MenuItem ("Three", "", null), + }) + }); + + Application.Top.Add (menu); + + Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + + Application.Top.Redraw (Application.Top.Bounds); + var expected = @" + Numbers +"; + + var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│┌─────────────┐ +│ Three ││ Sub-Menu 1 │ +└────────┘│ Sub-Menu 2 │ + └─────────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.CursorLeft, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + } + + [Fact, AutoInitShutdown] + public void UseSubMenusSingleFrame_False_By_Mouse () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("Numbers", new MenuItem [] { + new MenuItem ("One", "", null), + new MenuBarItem ("Two", new MenuItem [] { + new MenuItem ("Sub-Menu 1", "", null), + new MenuItem ("Sub-Menu 2", "", null) + }), + new MenuItem ("Three", "", null), + }) + }); + + Application.Top.Add (menu); + + Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + + Application.Top.Redraw (Application.Top.Bounds); + var expected = @" + Numbers +"; + + var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (menu.MouseEvent (new MouseEvent () { + X = 1, + Y = 0, + Flags = MouseFlags.Button1Pressed, + View = menu + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.False (menu.MouseEvent (new MouseEvent () { + X = 1, + Y = 3, + Flags = MouseFlags.ReportMousePosition, + View = Application.Top.Subviews [1] + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│┌─────────────┐ +│ Three ││ Sub-Menu 1 │ +└────────┘│ Sub-Menu 2 │ + └─────────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.False (menu.MouseEvent (new MouseEvent () { + X = 1, + Y = 2, + Flags = MouseFlags.ReportMousePosition, + View = Application.Top.Subviews [1] + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.False (menu.MouseEvent (new MouseEvent () { + X = 70, + Y = 2, + Flags = MouseFlags.Button1Clicked, + View = Application.Top + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + } + + [Fact, AutoInitShutdown] + public void UseSubMenusSingleFrame_True_By_Keyboard () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("Numbers", new MenuItem [] { + new MenuItem ("One", "", null), + new MenuBarItem ("Two", new MenuItem [] { + new MenuItem ("Sub-Menu 1", "", null), + new MenuItem ("Sub-Menu 2", "", null) + }), + new MenuItem ("Three", "", null), + }) + }); + + Application.Top.Add (menu); + + Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + menu.UseSubMenusSingleFrame = true; + Assert.True (menu.UseSubMenusSingleFrame); + + Application.Top.Redraw (Application.Top.Bounds); + var expected = @" + Numbers +"; + + var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (menu.ProcessHotKey (new KeyEvent (Key.F9, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.CursorDown, null))); + Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Enter, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌─────────────┐ +│◄ Two │ +├─────────────┤ +│ Sub-Menu 1 │ +│ Sub-Menu 2 │ +└─────────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [2].ProcessKey (new KeyEvent (Key.Enter, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [1].ProcessKey (new KeyEvent (Key.Esc, null))); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + } + + [Fact, AutoInitShutdown] + public void UseSubMenusSingleFrame_True_By_Mouse () + { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("Numbers", new MenuItem [] { + new MenuItem ("One", "", null), + new MenuBarItem ("Two", new MenuItem [] { + new MenuItem ("Sub-Menu 1", "", null), + new MenuItem ("Sub-Menu 2", "", null) + }), + new MenuItem ("Three", "", null), + }) + }); + + Application.Top.Add (menu); + + Assert.Equal (Point.Empty, new Point (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + menu.UseSubMenusSingleFrame = true; + Assert.True (menu.UseSubMenusSingleFrame); + + Application.Top.Redraw (Application.Top.Bounds); + var expected = @" + Numbers +"; + + var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (menu.MouseEvent (new MouseEvent () { + X = 1, + Y = 0, + Flags = MouseFlags.Button1Pressed, + View = menu + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.False (menu.MouseEvent (new MouseEvent () { + X = 1, + Y = 3, + Flags = MouseFlags.Button1Clicked, + View = Application.Top.Subviews [1] + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌─────────────┐ +│◄ Two │ +├─────────────┤ +│ Sub-Menu 1 │ +│ Sub-Menu 2 │ +└─────────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.False (menu.MouseEvent (new MouseEvent () { + X = 1, + Y = 2, + Flags = MouseFlags.Button1Clicked, + View = Application.Top.Subviews [2] + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.False (menu.MouseEvent (new MouseEvent () { + X = 70, + Y = 2, + Flags = MouseFlags.Button1Clicked, + View = Application.Top + })); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + Numbers +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + } + + [Fact, AutoInitShutdown] + public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKey () + { + var newAction = false; + var copyAction = false; + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_New", "", () => newAction = true) + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", () => copyAction = true) + }) + }); + + Application.Top.Add (menu); + + Assert.False (newAction); + Assert.False (copyAction); + + Assert.False (menu.OnKeyDown (new (Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (menu.OnKeyUp (new (Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + var expected = @" + File Edit +"; + + var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (menu.ProcessKey (new (Key.N, null))); + Application.MainLoop.MainIteration (); + Assert.True (newAction); + + Assert.False (menu.OnKeyDown (new (Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (menu.OnKeyUp (new (Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (menu.IsMenuOpen); + + Assert.True (menu.ProcessKey (new (Key.CursorRight, null))); + Assert.True (menu.ProcessKey (new (Key.C, null))); + Application.MainLoop.MainIteration (); + Assert.True (copyAction); + } + + [Fact, AutoInitShutdown] + public void HotKey_MenuBar_ProcessHotKey_Menu_ProcessKey () + { + var newAction = false; + var copyAction = false; + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_New", "", () => newAction = true) + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", () => copyAction = true) + }) + }); + + Application.Top.Add (menu); + + Assert.False (newAction); + Assert.False (copyAction); + + Assert.True (menu.ProcessHotKey (new (Key.AltMask | Key.F, new KeyModifiers () { Alt = true }))); + Assert.True (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + var expected = @" + File Edit +┌───────┐ +│ New │ +└───────┘ +"; + + var pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.N, null))); + Application.MainLoop.MainIteration (); + Assert.True (newAction); + + Assert.True (menu.ProcessHotKey (new (Key.AltMask | Key.E, new KeyModifiers () { Alt = true }))); + Assert.True (menu.IsMenuOpen); + Application.Top.Redraw (Application.Top.Bounds); + expected = @" + File Edit + ┌────────┐ + │ Copy │ + └────────┘ +"; + + pos = GraphViewTests.AssertDriverContentsWithPosAre (expected, output); + Assert.Equal (new Point (2, 0), pos); + + Assert.True (Application.Top.Subviews [1].ProcessKey (new (Key.C, null))); + Application.MainLoop.MainIteration (); + Assert.True (copyAction); + } } }