diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index 7850d673c..4c62e42e2 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -327,7 +327,7 @@ public class MenuBarv2 : Menuv2, IDesignable } /// - public bool EnableForDesign (ref readonly TContext context) where TContext : notnull + public override bool EnableForDesign () { Add ( new MenuBarItemv2 ( diff --git a/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs b/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs new file mode 100644 index 000000000..81247bb94 --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/MenuBarv2Tests.cs @@ -0,0 +1,297 @@ +using System.Reflection; +using Terminal.Gui; +using TerminalGuiFluentTesting; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; + +/// +/// Tests for the MenuBarv2 class +/// +public class MenuBarv2Tests +{ + private readonly TextWriter _out; + + public MenuBarv2Tests (ITestOutputHelper outputHelper) { _out = new BasicFluentAssertionTests.TestOutputWriter (outputHelper); } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void Initializes_WithNoItems (V2TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + // Create a menu bar with no items + var menuBar = new MenuBarv2 (); + Assert.Equal (0, menuBar.SubViews.Count); + Assert.False (menuBar.CanFocus); + Assert.Equal (Orientation.Horizontal, menuBar.Orientation); + Assert.Equal (Key.F9, MenuBarv2.DefaultKey); + }) + .WriteOutLogs (_out) + .Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void Initializes_WithItems (V2TestDriver d) + { + MenuBarItemv2 [] menuItems = []; + + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + // Create items for the menu bar + menuItems = + [ + new ( + "_File", + [ + new MenuItemv2 ("_Open", "Opens a file", () => { }) + ]), + new ( + "_Edit", + [ + new MenuItemv2 ("_Copy", "Copies selection", () => { }) + ]) + ]; + + var menuBar = new MenuBarv2 (menuItems); + Assert.Equal (2, menuBar.SubViews.Count); + + // First item should be the File menu + var fileMenu = menuBar.SubViews.ElementAt (0) as MenuBarItemv2; + Assert.NotNull (fileMenu); + Assert.Equal ("_File", fileMenu.Title); + + // Second item should be the Edit menu + var editMenu = menuBar.SubViews.ElementAt (1) as MenuBarItemv2; + Assert.NotNull (editMenu); + Assert.Equal ("_Edit", editMenu.Title); + }) + .WriteOutLogs (_out) + .Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void AddsItems_WithMenusProperty (V2TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + var menuBar = new MenuBarv2 (); + + // Set items through Menus property + menuBar.Menus = + [ + new ("_File"), + new ("_Edit"), + new ("_View") + ]; + + Assert.Equal (3, menuBar.SubViews.Count); + }) + .WriteOutLogs (_out) + .Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void ChangesKey_RaisesEvent (V2TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + var menuBar = new MenuBarv2 (); + + var oldKeyValue = Key.Empty; + var newKeyValue = Key.Empty; + var eventRaised = false; + + menuBar.KeyChanged += (_, args) => + { + eventRaised = true; + oldKeyValue = args.OldKey; + newKeyValue = args.NewKey; + }; + + // Default key should be F9 + Assert.Equal (Key.F9, menuBar.Key); + + // Change key to F1 + menuBar.Key = Key.F1; + + // Verify event was raised + Assert.True (eventRaised); + Assert.Equal (Key.F9, oldKeyValue); + Assert.Equal (Key.F1, newKeyValue); + + // Verify key was changed + Assert.Equal (Key.F1, menuBar.Key); + }) + .WriteOutLogs (_out) + .Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void ShowHidePopovers (V2TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + // Create a menu bar with items that have submenus + var fileMenuItem = new MenuBarItemv2 ( + "_File", + [ + new MenuItemv2 ("_Open", string.Empty, null), + new MenuItemv2 ("_Save", string.Empty, null) + ]); + + var menuBar = new MenuBarv2 ([fileMenuItem]); + + // Initially, no menu should be open + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.IsActive ()); + + // Initialize the menu bar + menuBar.BeginInit (); + menuBar.EndInit (); + + // Simulate showing a popover menu by manipulating the first menu item + MethodInfo? showPopoverMethod = typeof (MenuBarv2).GetMethod ( + "ShowPopover", + BindingFlags.NonPublic | BindingFlags.Instance); + + // Set menu bar to active state using reflection + FieldInfo? activeField = typeof (MenuBarv2).GetField ( + "_active", + BindingFlags.NonPublic | BindingFlags.Instance); + activeField?.SetValue (menuBar, true); + menuBar.CanFocus = true; + + // Show the popover menu + showPopoverMethod?.Invoke (menuBar, new object? [] { fileMenuItem }); + + // Should be active now + Assert.True (menuBar.IsActive ()); + + // Test if we can hide the popover menu + fileMenuItem.PopoverMenu.Visible = true; + + Assert.True (menuBar.HideActiveItem ()); + + // Menu should no longer be open or active + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.CanFocus); + }) + .WriteOutLogs (_out) + .Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void EnableForDesign_CreatesMenuItems (V2TestDriver d) + { + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + var menuBar = new MenuBarv2 (); + Application.Top.Add (menuBar); + + // Call EnableForDesign + bool result = menuBar.EnableForDesign (); + + // Should return true + Assert.True (result); + + // Should have created menu items + Assert.True (menuBar.SubViews.Count > 0); + + // Should have File, Edit and Help menus + View? fileMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItemv2)?.Title == "_File"); + View? editMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItemv2)?.Title == "_Edit"); + View? helpMenu = menuBar.SubViews.FirstOrDefault (v => (v as MenuBarItemv2)?.Title == "_Help"); + + Assert.NotNull (fileMenu); + Assert.NotNull (editMenu); + Assert.NotNull (helpMenu); + }) + .ScreenShot ("MenuBarv2 EnableForDesign", _out) + .WriteOutLogs (_out) + .Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void Navigation_BetweenItems (V2TestDriver d) + { + var menuBarActivated = false; + + using GuiTestContext c = With.A (80, 25, d) + .Then ( + () => + { + // Create menu items + var fileMenu = new MenuBarItemv2 ( + "_File", + [ + new MenuItemv2 ("_Open", string.Empty, null), + new MenuItemv2 ("_Save", string.Empty, null) + ]); + + var editMenu = new MenuBarItemv2 ( + "_Edit", + [ + new MenuItemv2 ("_Cut", string.Empty, null), + new MenuItemv2 ("_Copy", string.Empty, null) + ]); + + // Create menu bar and add to window + var menuBar = new MenuBarv2 ([fileMenu, editMenu]); + Application.Top.Add (menuBar); + + // Set menu bar to active state using reflection + FieldInfo? activeField = typeof (MenuBarv2).GetField ( + "_active", + BindingFlags.NonPublic | BindingFlags.Instance); + activeField?.SetValue (menuBar, true); + menuBar.CanFocus = true; + menuBarActivated = true; + + // Give focus to the first menu item + fileMenu.SetFocus (); + Assert.True (fileMenu.HasFocus); + + Application.LayoutAndDraw (); + }) + .ScreenShot ("MenuBar initial state", _out) + .Then ( + () => + { + if (!menuBarActivated) + { + // Skip further tests if activation failed + } + + // Move right to select the edit menu + // This simulates navigation between menu items + }) + .Right () + .ScreenShot ("After right arrow", _out) + .Right () + .ScreenShot ("After second right arrow (should wrap)", _out) + .Left () + .ScreenShot ("After left arrow", _out) + .WriteOutLogs (_out) + .Stop (); + } +} diff --git a/Tests/UnitTests/Dialogs/MessageBoxTests.cs b/Tests/UnitTests/Dialogs/MessageBoxTests.cs index 9216b863e..7777806a8 100644 --- a/Tests/UnitTests/Dialogs/MessageBoxTests.cs +++ b/Tests/UnitTests/Dialogs/MessageBoxTests.cs @@ -462,7 +462,7 @@ public class MessageBoxTests { MessageBox.Query ( "", - UICatalog.UICatalogTopLevel.GetAboutBoxMessage (), + UICatalog.UICatalogTop.GetAboutBoxMessage (), wrapMessage: false, buttons: "_Ok" ); diff --git a/Tests/UnitTests/Text/TextFormatterTests.cs b/Tests/UnitTests/Text/TextFormatterTests.cs index fe6dc292a..d4367bdd3 100644 --- a/Tests/UnitTests/Text/TextFormatterTests.cs +++ b/Tests/UnitTests/Text/TextFormatterTests.cs @@ -4146,7 +4146,7 @@ Nice Work")] { TextFormatter tf = new () { - Text = UICatalog.UICatalogTopLevel.GetAboutBoxMessage (), + Text = UICatalog.UICatalogTop.GetAboutBoxMessage (), Alignment = Alignment.Center, VerticalAlignment = Alignment.Start, WordWrap = false, diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index f41f5e354..4006bd980 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -61,8 +61,8 @@ public class UICatalog CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); } - UICatalogTopLevel.CachedScenarios = Scenario.GetScenarios (); - UICatalogTopLevel.CachedCategories = Scenario.GetAllCategories (); + UICatalogTop.CachedScenarios = Scenario.GetScenarios (); + UICatalogTop.CachedCategories = Scenario.GetAllCategories (); // Process command line args @@ -108,7 +108,7 @@ public class UICatalog "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.", getDefaultValue: () => "none" ).FromAmong ( - UICatalogTopLevel.CachedScenarios.Select (s => s.GetName ()) + UICatalogTop.CachedScenarios.Select (s => s.GetName ()) .Append ("none") .ToArray () ); @@ -217,20 +217,20 @@ public class UICatalog Application.Init (driverName: _forceDriver); - if (string.IsNullOrWhiteSpace (UICatalogTopLevel.CachedTheme)) + if (string.IsNullOrWhiteSpace (UICatalogTop.CachedTheme)) { - UICatalogTopLevel.CachedTheme = Themes?.Theme; + UICatalogTop.CachedTheme = Themes?.Theme; } else { - Themes!.Theme = UICatalogTopLevel.CachedTheme; + Themes!.Theme = UICatalogTop.CachedTheme; Apply (); } - Application.Run ().Dispose (); + Application.Run ().Dispose (); Application.Shutdown (); - return UICatalogTopLevel.CachedSelectedScenario!; + return UICatalogTop.CachedSelectedScenario!; } [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] @@ -323,15 +323,15 @@ public class UICatalog // run it and exit when done. if (options.Scenario != "none") { - int item = UICatalogTopLevel.CachedScenarios!.IndexOf ( - UICatalogTopLevel.CachedScenarios!.FirstOrDefault ( + int item = UICatalogTop.CachedScenarios!.IndexOf ( + UICatalogTop.CachedScenarios!.FirstOrDefault ( s => s.GetName () .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase) )!); - UICatalogTopLevel.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogTopLevel.CachedScenarios [item].GetType ())!; + UICatalogTop.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogTop.CachedScenarios [item].GetType ())!; - BenchmarkResults? results = RunScenario (UICatalogTopLevel.CachedSelectedScenario, options.Benchmark); + BenchmarkResults? results = RunScenario (UICatalogTop.CachedSelectedScenario, options.Benchmark); if (results is { }) { @@ -360,9 +360,9 @@ public class UICatalog while (RunUICatalogTopLevel () is { } scenario) { VerifyObjectsWereDisposed (); - Themes!.Theme = UICatalogTopLevel.CachedTheme!; + Themes!.Theme = UICatalogTop.CachedTheme!; Apply (); - scenario.TopLevelColorScheme = UICatalogTopLevel.CachedTopLevelColorScheme!; + scenario.TopLevelColorScheme = UICatalogTop.CachedTopLevelColorScheme!; #if DEBUG_IDISPOSABLE View.DebugIDisposable = true; @@ -412,7 +412,7 @@ public class UICatalog } Application.Init (driverName: _forceDriver); - scenario.TopLevelColorScheme = UICatalogTopLevel.CachedTopLevelColorScheme!; + scenario.TopLevelColorScheme = UICatalogTop.CachedTopLevelColorScheme!; if (benchmark) { @@ -442,7 +442,7 @@ public class UICatalog var maxScenarios = 5; - foreach (Scenario s in UICatalogTopLevel.CachedScenarios!) + foreach (Scenario s in UICatalogTop.CachedScenarios!) { resultsList.Add (RunScenario (s, true)!); maxScenarios--; diff --git a/UICatalog/UICatalogTopLevel.cs b/UICatalog/UICatalogTop.cs similarity index 98% rename from UICatalog/UICatalogTopLevel.cs rename to UICatalog/UICatalogTop.cs index d6f65cc6f..0f29720c4 100644 --- a/UICatalog/UICatalogTopLevel.cs +++ b/UICatalog/UICatalogTop.cs @@ -18,7 +18,7 @@ namespace UICatalog; /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on /// the command line) and each time a Scenario ends. /// -public class UICatalogTopLevel : Toplevel +public class UICatalogTop : Toplevel { // When a scenario is run, the main app is killed. The static // members are cached so that when the scenario exits the @@ -32,7 +32,7 @@ public class UICatalogTopLevel : Toplevel // Diagnostics private static ViewDiagnosticFlags _diagnosticFlags; - public UICatalogTopLevel () + public UICatalogTop () { _diagnosticFlags = Diagnostics; @@ -563,6 +563,7 @@ public class UICatalogTopLevel : Toplevel [JsonPropertyName ("UICatalog.StatusBar")] public static bool ShowStatusBar { get; set; } = true; + private Shortcut? _shQuit; private Shortcut? _shVersion; private CheckBox? _force16ColorsShortcutCb; @@ -582,6 +583,13 @@ public class UICatalogTopLevel : Toplevel maximumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0)); // ReSharper restore All + _shQuit = new () + { + CanFocus = false, + Title = "Quit", + Key = Application.QuitKey + }; + _shVersion = new () { Title = "Version Info", @@ -616,12 +624,7 @@ public class UICatalogTopLevel : Toplevel }; statusBar.Add ( - new Shortcut - { - CanFocus = false, - Title = "Quit", - Key = Application.QuitKey - }, + _shQuit, statusBarShortcut, new Shortcut { @@ -648,13 +651,20 @@ public class UICatalogTopLevel : Toplevel ColorScheme = Colors.ColorSchemes [CachedTopLevelColorScheme!]; - ((Shortcut)_statusBar!.SubViews.ElementAt (0)).Key = Application.QuitKey; - _statusBar.Visible = ShowStatusBar; + if (_shQuit is { }) + { + _shQuit.Key = Application.QuitKey; + } + + if (_statusBar is { }) + { + _statusBar.Visible = ShowStatusBar; + } _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - Application.Top!.SetNeedsDraw (); + Application.Top?.SetNeedsDraw (); } private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); }