diff --git a/Terminal.Gui/Application/ApplicationPopover.cs b/Terminal.Gui/Application/ApplicationPopover.cs index bc4a0e010..1124faefb 100644 --- a/Terminal.Gui/Application/ApplicationPopover.cs +++ b/Terminal.Gui/Application/ApplicationPopover.cs @@ -126,8 +126,8 @@ public sealed class ApplicationPopover : IDisposable // If there's an existing popover, hide it. if (_activePopover is View popoverView && popoverView == popover) { - popoverView.Visible = false; _activePopover = null; + popoverView.Visible = false; Application.Top?.SetNeedsDraw (); } } diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index 760315736..8446c9f97 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -115,15 +115,18 @@ public partial class View // Command APIs /// protected bool? RaiseAccepting (ICommandContext? ctx) { + Logging.Trace($"{ctx?.Source?.Title}"); CommandEventArgs args = new () { Context = ctx }; // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. + Logging.Trace ($"Calling OnAccepting..."); args.Cancel = OnAccepting (args) || args.Cancel; if (!args.Cancel) { // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. + Logging.Trace ($"Raising Accepting..."); Accepting?.Invoke (this, args); } @@ -148,11 +151,12 @@ public partial class View // Command APIs if (SuperView is { }) { - return SuperView?.InvokeCommand (Command.Accept, ctx) is true; + Logging.Trace ($"Invoking Accept on SuperView: {SuperView.Title}..."); + return SuperView?.InvokeCommand (Command.Accept, ctx); } } - return Accepting is null ? null : args.Cancel; + return args.Cancel; } /// diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 095ff6946..fcaf96914 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -555,6 +555,11 @@ public partial class View // Keyboard APIs private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, ref bool? handled) { + if (!adornment.Enabled) + { + return false; + } + bool? adornmentHandled = adornment.InvokeCommands (key); if (adornmentHandled is true) @@ -599,6 +604,11 @@ public partial class View // Keyboard APIs /// internal bool? InvokeCommandsBoundToHotKey (Key hotKey) { + if (!Enabled) + { + return false; + } + bool? handled = null; // Process this View if (HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) diff --git a/Terminal.Gui/Views/FlagSelector.cs b/Terminal.Gui/Views/FlagSelector.cs new file mode 100644 index 000000000..97316f789 --- /dev/null +++ b/Terminal.Gui/Views/FlagSelector.cs @@ -0,0 +1,367 @@ +#nullable enable +namespace Terminal.Gui; + + +/// +/// Provides a user interface for displaying and selecting flags. +/// Flags can be set from a dictionary or directly from an enum type. +/// +public class FlagSelector : View, IDesignable, IOrientation +{ + /// + /// Initializes a new instance of the class. + /// + public FlagSelector () + { + CanFocus = true; + + Width = Dim.Auto (DimAutoStyle.Content); + Height = Dim.Auto (DimAutoStyle.Content); + + // ReSharper disable once UseObjectOrCollectionInitializer + _orientationHelper = new (this); + _orientationHelper.Orientation = Orientation.Vertical; + + // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state + AddCommand (Command.Accept, HandleAcceptCommand); + + CreateSubViews (); + } + + private bool? HandleAcceptCommand (ICommandContext? ctx) { return RaiseAccepting (ctx); } + + private uint _value; + + /// + /// Gets or sets the value of the selected flags. + /// + public uint Value + { + get => _value; + set + { + if (_value == value) + { + return; + } + + _value = value; + + if (_value == 0) + { + UncheckAll (); + } + else + { + UncheckNone (); + UpdateChecked (); + } + + if (ValueEdit is { }) + { + ValueEdit.Text = value.ToString (); + } + + RaiseValueChanged (); + } + } + + private void RaiseValueChanged () + { + OnValueChanged (); + ValueChanged?.Invoke (this, new (Value)); + } + + /// + /// Called when has changed. + /// + protected virtual void OnValueChanged () { } + + /// + /// Raised when has changed. + /// + public event EventHandler>? ValueChanged; + + private FlagSelectorStyles _styles; + + /// + /// Gets or sets the styles for the flag selector. + /// + public FlagSelectorStyles Styles + { + get => _styles; + set + { + if (_styles == value) + { + return; + } + + _styles = value; + + CreateSubViews (); + } + } + + /// + /// Set the flags and flag names. + /// + /// + public void SetFlags (IReadOnlyDictionary flags) + { + Flags = flags; + CreateSubViews (); + } + + /// + /// Set the flags and flag names from an enum type. + /// + /// The enum type to extract flags from + /// + /// This is a convenience method that converts an enum to a dictionary of flag values and names. + /// The enum values are converted to uint values and the enum names become the display text. + /// + public void SetFlags () where TEnum : struct, Enum + { + // Convert enum names and values to a dictionary + Dictionary flagsDictionary = Enum.GetValues () + .ToDictionary ( + f => Convert.ToUInt32 (f), + f => f.ToString () + ); + + SetFlags (flagsDictionary); + } + + /// + /// Set the flags and flag names from an enum type with custom display names. + /// + /// The enum type to extract flags from + /// A function that converts enum values to display names + /// + /// This is a convenience method that converts an enum to a dictionary of flag values and custom names. + /// The enum values are converted to uint values and the display names are determined by the nameSelector function. + /// + /// + /// + /// // Use enum values with custom display names + /// var flagSelector = new FlagSelector (); + /// flagSelector.SetFlags<FlagSelectorStyles> + /// (f => f switch { + /// FlagSelectorStyles.ShowNone => "Show None Value", + /// FlagSelectorStyles.ShowValueEdit => "Show Value Editor", + /// FlagSelectorStyles.All => "Everything", + /// _ => f.ToString() + /// }); + /// + /// + public void SetFlags (Func nameSelector) where TEnum : struct, Enum + { + // Convert enum values and custom names to a dictionary + Dictionary flagsDictionary = Enum.GetValues () + .ToDictionary ( + f => Convert.ToUInt32 (f), + nameSelector + ); + + SetFlags (flagsDictionary); + } + + /// + /// Gets the flags. + /// + public IReadOnlyDictionary? Flags { get; internal set; } + + private TextField? ValueEdit { get; set; } + + private void CreateSubViews () + { + if (Flags is null) + { + return; + } + + View [] subviews = SubViews.ToArray (); + + RemoveAll (); + + foreach (View v in subviews) + { + v.Dispose (); + } + + if (Styles.HasFlag (FlagSelectorStyles.ShowNone) && !Flags.ContainsKey (0)) + { + Add (CreateCheckBox ("None", 0)); + } + + for (var index = 0; index < Flags.Count; index++) + { + if (!Styles.HasFlag (FlagSelectorStyles.ShowNone) && Flags.ElementAt (index).Key == 0) + { + continue; + } + + Add (CreateCheckBox (Flags.ElementAt (index).Value, Flags.ElementAt (index).Key)); + } + + if (Styles.HasFlag (FlagSelectorStyles.ShowValueEdit)) + { + ValueEdit = new () + { + Id = "valueEdit", + CanFocus = false, + Text = Value.ToString (), + Width = 5, + ReadOnly = true + }; + + Add (ValueEdit); + } + + SetLayout (); + + return; + + CheckBox CreateCheckBox (string name, uint flag) + { + var checkbox = new CheckBox + { + CanFocus = false, + Title = name, + Id = name, + Data = flag, + HighlightStyle = HighlightStyle + }; + + checkbox.Selecting += (sender, args) => { RaiseSelecting (args.Context); }; + + checkbox.CheckedStateChanged += (sender, args) => + { + uint newValue = Value; + + if (checkbox.CheckedState == CheckState.Checked) + { + if ((uint)checkbox.Data == 0) + { + newValue = 0; + } + else + { + newValue |= flag; + } + } + else + { + newValue &= ~flag; + } + + Value = newValue; + + //UpdateChecked(); + }; + + return checkbox; + } + } + + private void SetLayout () + { + foreach (View sv in SubViews) + { + if (Orientation == Orientation.Vertical) + { + sv.X = 0; + sv.Y = Pos.Align (Alignment.Start); + } + else + { + sv.X = Pos.Align (Alignment.Start); + sv.Y = 0; + sv.Margin!.Thickness = new (0, 0, 1, 0); + } + } + } + + private void UncheckAll () + { + foreach (CheckBox cb in SubViews.Where (sv => sv is CheckBox cb && cb.Title != "None").Cast ()) + { + cb.CheckedState = CheckState.UnChecked; + } + } + + private void UncheckNone () + { + foreach (CheckBox cb in SubViews.Where (sv => sv is CheckBox { Title: "None" }).Cast ()) + { + cb.CheckedState = CheckState.UnChecked; + } + } + + private void UpdateChecked () + { + foreach (CheckBox cb in SubViews.Where (sv => sv is CheckBox { }).Cast ()) + { + var flag = (uint)(cb.Data ?? throw new InvalidOperationException ("ComboBox.Data must be set")); + + // If this flag is set in Value, check the checkbox. Otherwise, uncheck it. + if (flag == 0 && Value != 0) + { + cb.CheckedState = CheckState.UnChecked; + } + else + { + cb.CheckedState = (Value & flag) == flag ? CheckState.Checked : CheckState.UnChecked; + } + } + } + + /// + protected override void OnSubViewAdded (View view) { } + + #region IOrientation + + /// + /// Gets or sets the for this . The default is + /// . + /// + public Orientation Orientation + { + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; + } + + private readonly OrientationHelper _orientationHelper; + +#pragma warning disable CS0067 // The event is never used + /// + public event EventHandler>? OrientationChanging; + + /// + public event EventHandler>? OrientationChanged; +#pragma warning restore CS0067 // The event is never used + +#pragma warning restore CS0067 + + /// Called when has changed. + /// + public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); } + + #endregion IOrientation + + /// + public bool EnableForDesign () + { + SetFlags ( + f => f switch + { + FlagSelectorStyles.ShowNone => "Show _None Value", + FlagSelectorStyles.ShowValueEdit => "Show _Value Editor", + FlagSelectorStyles.All => "Show _All Flags Selector", + _ => f.ToString () + }); + + return true; + } +} diff --git a/Terminal.Gui/Views/FlagSelectorStyles.cs b/Terminal.Gui/Views/FlagSelectorStyles.cs new file mode 100644 index 000000000..95ea6f230 --- /dev/null +++ b/Terminal.Gui/Views/FlagSelectorStyles.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Styles for . +/// +[Flags] +public enum FlagSelectorStyles +{ + /// + /// No styles. + /// + None = 0b_0000_0000, + + /// + /// Show the `None` checkbox. This will add a checkbox with the title "None" and a value of 0 + /// even if the flags do not contain a value of 0. + /// + ShowNone = 0b_0000_0001, + + /// + /// Show the value edit. This will add a read-only to the to allow + /// the user to see the value. + /// + ShowValueEdit = 0b_0000_0010, + + /// + /// All styles. + /// + All = ShowNone | ShowValueEdit +} diff --git a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs index 0ed8cdfcd..6df8e0759 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs @@ -77,13 +77,41 @@ public class MenuBarItemv2 : MenuItemv2 new (menuItems)) { } - // TODO: Hide base.SubMenu? + /// + /// Do not use this property. MenuBarItem does not support SubMenu. Use instead. + /// + /// + public new Menuv2? SubMenu + { + get => null; + set => throw new InvalidOperationException ("MenuBarItem does not support SubMenu. Use PopoverMenu instead."); + } /// /// The Popover Menu that will be displayed when this item is selected. /// public PopoverMenu? PopoverMenu { get; set; } + /// + protected override bool OnKeyDownNotHandled (Key key) + { + Logging.Trace ($"{key}"); + + if (PopoverMenu is { Visible: true } && HotKeyBindings.TryGet (key, out _)) + { + // If the user presses the hotkey for a menu item that is already open, + // it should close the menu item (Test: MenuBarItem_HotKey_DeActivates) + if (SuperView is MenuBarv2 { } menuBar) + { + menuBar.HideActiveItem (); + } + + + return true; + } + return false; + } + /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index 6ad4560eb..7850d673c 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -1,4 +1,7 @@ #nullable enable +using System.ComponentModel; +using System.Diagnostics; + namespace Terminal.Gui; /// @@ -17,11 +20,56 @@ public class MenuBarv2 : Menuv2, IDesignable /// public MenuBarv2 (IEnumerable menuBarItems) : base (menuBarItems) { + CanFocus = false; TabStop = TabBehavior.TabGroup; Y = 0; Width = Dim.Fill (); Orientation = Orientation.Horizontal; + Key = DefaultKey; + AddCommand (Command.HotKey, + () => + { + if (HideActiveItem ()) + { + return true; + } + + if (SubViews.FirstOrDefault (sv => sv is MenuBarItemv2 { PopoverMenu: { } }) is MenuBarItemv2 { } first) + { + _active = true; + ShowPopover (first); + + return true; + } + + return false; + }); + HotKeyBindings.Add (Key, Command.HotKey); + + KeyBindings.Add (Key, Command.Quit); + KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit); + + AddCommand ( + Command.Quit, + ctx => + { + if (HideActiveItem ()) + { + return true; + } + + if (CanFocus) + { + CanFocus = false; + _active = false; + + return true; + } + + return false;//RaiseAccepted (ctx); + }); + AddCommand (Command.Right, MoveRight); KeyBindings.Add (Key.CursorRight, Command.Right); @@ -35,10 +83,108 @@ public class MenuBarv2 : Menuv2, IDesignable bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); } } + private Key _key = DefaultKey; + + /// Specifies the key that will activate the context menu. + public Key Key + { + get => _key; + set + { + Key oldKey = _key; + _key = value; + KeyChanged?.Invoke (this, new (oldKey, _key)); + } + } + + /// + /// Sets the Menu Bar Items for this Menu Bar. This will replace any existing Menu Bar Items. + /// + /// + /// + /// This is a convenience property to help porting from the v1 MenuBar. + /// + /// + public MenuBarItemv2 []? Menus + { + set + { + RemoveAll (); + if (value is null) + { + return; + } + foreach (MenuBarItemv2 mbi in value) + { + Add (mbi); + } + } + } + + /// Raised when is changed. + public event EventHandler? KeyChanged; + + /// The default key for activating menu bars. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key DefaultKey { get; set; } = Key.F9; + + /// + /// Gets whether any of the menu bar items have a visible . + /// + /// + public bool IsOpen () + { + return SubViews.Count (sv => sv is MenuBarItemv2 { PopoverMenu: { Visible: true } }) > 0; + } + + private bool _active; + + /// + /// Returns a value indicating whether the menu bar is active or not. When active, moving the mouse + /// over a menu bar item will activate it. + /// + /// + public bool IsActive () + { + return _active; + } + + /// + protected override bool OnMouseEnter (CancelEventArgs eventArgs) + { + // If the MenuBar does not have focus and the mouse enters: Enable CanFocus + // But do NOT show a Popover unless the user clicks or presses a hotkey + if (!HasFocus) + { + CanFocus = true; + } + return base.OnMouseEnter (eventArgs); + } + + /// + protected override void OnMouseLeave () + { + if (!IsOpen ()) + { + CanFocus = false; + } + base.OnMouseLeave (); + } + + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) + { + if (!newHasFocus) + { + _active = false; + CanFocus = false; + } + } + /// protected override void OnSelectedMenuItemChanged (MenuItemv2? selected) { - if (selected is MenuBarItemv2 { } selectedMenuBarItem) + if (selected is MenuBarItemv2 { PopoverMenu.Visible: false } selectedMenuBarItem) { ShowPopover (selectedMenuBarItem); } @@ -56,7 +202,7 @@ public class MenuBarv2 : Menuv2, IDesignable } // TODO: This needs to be done whenever a menuitem in any MenuBarItem changes - foreach (MenuBarItemv2? mbi in SubViews.Select(s => s as MenuBarItemv2)) + foreach (MenuBarItemv2? mbi in SubViews.Select (s => s as MenuBarItemv2)) { Application.Popover?.Register (mbi?.PopoverMenu); } @@ -65,36 +211,119 @@ public class MenuBarv2 : Menuv2, IDesignable /// protected override bool OnAccepting (CommandEventArgs args) { - if (args.Context?.Source is MenuBarItemv2 { PopoverMenu: { } } menuBarItem) + Logging.Trace ($"{args.Context?.Source?.Title}"); + + if (Visible && args.Context?.Source is MenuBarItemv2 { PopoverMenu.Visible: false } sourceMenuBarItem) { - ShowPopover (menuBarItem); + _active = true; + + if (!CanFocus) + { + // Enabling CanFocus will cause focus to change, which will cause OnSelectedMenuItem to change + // This will call ShowPopover + CanFocus = true; + sourceMenuBarItem.SetFocus (); + } + else + { + ShowPopover (sourceMenuBarItem); + } + + return true; } return base.OnAccepting (args); } + /// + /// Shows the specified popover, but only if the menu bar is active. + /// + /// private void ShowPopover (MenuBarItemv2? menuBarItem) { + Logging.Trace ($"{menuBarItem?.Id}"); + + if (!_active || !Visible) + { + return; + } + + //menuBarItem!.PopoverMenu.Id = menuBarItem.Id; + + // TODO: We should init the PopoverMenu in a smarter way if (menuBarItem?.PopoverMenu is { IsInitialized: false }) { menuBarItem.PopoverMenu.BeginInit (); menuBarItem.PopoverMenu.EndInit (); } - // If the active popover is a PopoverMenu and part of this MenuBar... - if (menuBarItem?.PopoverMenu is null - && Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu + // If the active Application Popover is part of this MenuBar, hide it. + //HideActivePopover (); + if (Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu && popoverMenu?.Root?.SuperMenuItem?.SuperView == this) { Application.Popover?.Hide (popoverMenu); } - menuBarItem?.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + if (menuBarItem is null) + { + return; + } - if (menuBarItem?.PopoverMenu?.Root is { }) + if (menuBarItem.PopoverMenu is { }) + { + menuBarItem.PopoverMenu.Accepted += (sender, args) => + { + if (HasFocus) + { + CanFocus = false; + } + }; + } + + _active = true; + CanFocus = true; + menuBarItem.SetFocus (); + + if (menuBarItem.PopoverMenu?.Root is { }) { menuBarItem.PopoverMenu.Root.SuperMenuItem = menuBarItem; } + + menuBarItem.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + } + + private MenuBarItemv2? GetActiveItem () + { + return SubViews.FirstOrDefault (sv => sv is MenuBarItemv2 { PopoverMenu: { Visible: true } }) as MenuBarItemv2; + } + + /// + /// Hides the popover menu associated with the active menu bar item and updates the focus state. + /// + /// if the popover was hidden + public bool HideActiveItem () + { + return HideItem (GetActiveItem ()); + } + + /// + /// Hides popover menu associated with the specified menu bar item and updates the focus state. + /// + /// + /// if the popover was hidden + public bool HideItem (MenuBarItemv2? activeItem) + { + if (activeItem is null || !activeItem.PopoverMenu!.Visible) + { + return false; + } + _active = false; + HasFocus = false; + activeItem.PopoverMenu!.Visible = false; + CanFocus = false; + + return true; } /// diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItemv2.cs index f655d0b86..e57f96201 100644 --- a/Terminal.Gui/Views/Menu/MenuItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuItemv2.cs @@ -7,6 +7,7 @@ namespace Terminal.Gui; /// /// A -derived object to be used as a menu item in a . Has title, an +/// A -derived object to be used as a menu item in a . Has title, an /// associated help text, and an action to execute on activation. /// public class MenuItemv2 : Shortcut @@ -53,6 +54,11 @@ public class MenuItemv2 : Shortcut : base (key ?? Key.Empty, commandText, action, helpText) { } + /// + public MenuItemv2 (string commandText, Key key, Action ? action = null) + : base (key ?? Key.Empty, commandText, action, null) + { } + /// public MenuItemv2 (string? commandText = null, string? helpText = null, Menuv2? subMenu = null) : base (Key.Empty, commandText, null, helpText) @@ -98,6 +104,7 @@ public class MenuItemv2 : Shortcut internal override bool? DispatchCommand (ICommandContext? commandContext) { + Logging.Trace($"{commandContext?.Source?.Title}"); bool? ret = null; if (commandContext is { Command: not Command.HotKey }) @@ -116,11 +123,11 @@ public class MenuItemv2 : Shortcut if (ret is not true) { + Logging.Trace($"Calling base.DispatchCommand"); ret = base.DispatchCommand (commandContext); } - //Logging.Trace ($"{commandContext?.Source?.Title}"); - + Logging.Trace($"Calling RaiseAccepted"); RaiseAccepted (commandContext); return ret; diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menuv2.cs index 269acf4e4..a8a0b9574 100644 --- a/Terminal.Gui/Views/Menu/Menuv2.cs +++ b/Terminal.Gui/Views/Menu/Menuv2.cs @@ -10,7 +10,7 @@ public class Menuv2 : Bar public Menuv2 () : this ([]) { } /// - public Menuv2 (IEnumerable? shortcuts) : this (shortcuts?.Cast()) { } + public Menuv2 (IEnumerable? menuItems) : this (menuItems?.Cast ()) { } /// public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) @@ -55,21 +55,21 @@ public class Menuv2 : Bar switch (view) { case MenuItemv2 menuItem: - { - menuItem.CanFocus = true; - - AddCommand (menuItem.Command, RaiseAccepted); - - menuItem.Accepted += MenuItemOnAccepted; - - break; - - void MenuItemOnAccepted (object? sender, CommandEventArgs e) { - //Logging.Trace ($"Accepted: {e.Context?.Source?.Title}"); - RaiseAccepted (e.Context); + menuItem.CanFocus = true; + + AddCommand (menuItem.Command, RaiseAccepted); + + menuItem.Accepted += MenuItemOnAccepted; + + break; + + void MenuItemOnAccepted (object? sender, CommandEventArgs e) + { + Logging.Trace ($"MenuItemOnAccepted: {e.Context?.Source?.Title}"); + RaiseAccepted (e.Context); + } } - } case Line line: // Grow line so we get auto-join line line.X = Pos.Func (() => -Border!.Thickness.Left); @@ -79,6 +79,19 @@ public class Menuv2 : Bar } } + /// + protected override bool OnAccepting (CommandEventArgs args) + { + Logging.Trace ($"{args.Context}"); + + if (SuperMenuItem is { }) + { + Logging.Trace ($"Invoking Accept on SuperMenuItem: {SuperMenuItem.Title}..."); + return SuperMenuItem?.SuperView?.InvokeCommand (Command.Accept, args.Context) is true; + } + return false; + } + // TODO: Consider moving Accepted to Bar? /// @@ -120,6 +133,7 @@ public class Menuv2 : Bar protected override void OnFocusedChanged (View? previousFocused, View? focused) { base.OnFocusedChanged (previousFocused, focused); + SelectedMenuItem = focused as MenuItemv2; RaiseSelectedMenuItemChanged (SelectedMenuItem); } diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index 555dd9175..a725750f7 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -3,7 +3,8 @@ namespace Terminal.Gui; /// /// Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down -/// menu as part of . +/// all other content. Can be used as a context menu or a drop-down +/// menu as part of as part of . /// /// /// @@ -33,8 +34,6 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable base.Visible = false; - //base.ColorScheme = Colors.ColorSchemes ["Menu"]; - Root = root; AddCommand (Command.Right, MoveRight); @@ -53,7 +52,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable return false; }); - KeyBindings.Add (DefaultKey, Command.Quit); + KeyBindings.Add (Key, Command.Quit); KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit); AddCommand ( @@ -67,7 +66,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable Visible = false; - return RaiseAccepted (ctx); + return false; }); return; @@ -98,7 +97,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable return true; } - return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + return false; //AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); } } @@ -220,8 +219,11 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable _root.Accepting += MenuOnAccepting; } + // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus + // TODO: And it needs to clear the old bindings first UpdateKeyBindings (); + // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus IEnumerable allMenus = GetAllSubMenus (); foreach (Menuv2 menu in allMenus) @@ -235,13 +237,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable private void UpdateKeyBindings () { - // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus - // TODO: And it needs to clear them first IEnumerable all = GetMenuItemsOfAllSubMenus (); foreach (MenuItemv2 menuItem in all.Where (mi => mi.Command != Command.NotBound)) { Key? key; + if (menuItem.TargetView is { }) { // A TargetView implies HotKey @@ -447,17 +448,16 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable private void MenuOnAccepting (object? sender, CommandEventArgs e) { + var senderView = sender as View; + Logging.Trace ($"Sender: {senderView?.GetType ().Name}, {e.Context?.Source?.Title}"); + if (e.Context?.Command != Command.HotKey) { Visible = false; } - else - { - // This supports the case when a hotkey of a menuitem with a submenu is pressed - e.Cancel = true; - } - //Logging.Trace ($"{e.Context?.Source?.Title}"); + // This supports the case when a hotkey of a menuitem with a submenu is pressed + //e.Cancel = true; } private void MenuAccepted (object? sender, CommandEventArgs e) @@ -514,10 +514,21 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable private void MenuOnSelectedMenuItemChanged (object? sender, MenuItemv2? e) { - //Logging.Trace ($"{e}"); + Logging.Trace ($"e: {e?.Title}"); ShowSubMenu (e); } + /// + protected override void OnSubViewAdded (View view) + { + if (Root is null && (view is Menuv2 || view is MenuItemv2)) + { + throw new InvalidOperationException ("Do not add MenuItems or Menus directly to a PopoverMenu. Use the Root property."); + } + + base.OnSubViewAdded (view); + } + /// protected override void Dispose (bool disposing) { @@ -539,20 +550,20 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable base.Dispose (disposing); } - /// public bool EnableForDesign (ref readonly TContext context) where TContext : notnull { - Root = new Menuv2 ( - [ - new MenuItemv2 (this, Command.Cut), - new MenuItemv2 (this, Command.Copy), - new MenuItemv2 (this, Command.Paste), - new Line (), - new MenuItemv2 (this, Command.SelectAll) - ]); + Root = new ( + [ + new MenuItemv2 (this, Command.Cut), + new MenuItemv2 (this, Command.Copy), + new MenuItemv2 (this, Command.Paste), + new Line (), + new MenuItemv2 (this, Command.SelectAll) + ]); Visible = true; + return true; } } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 5fc2b2c1b..daefd8b44 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -1,9 +1,7 @@ #nullable enable -using System.Diagnostics; - namespace Terminal.Gui; -/// Displays a group of labels with an idicator of which one is selected. +/// Displays a list of mutually-exclusive items. Each items can have its own hotkey. public class RadioGroup : View, IDesignable, IOrientation { /// @@ -19,7 +17,7 @@ public class RadioGroup : View, IDesignable, IOrientation // Select (Space key or mouse click) - The default implementation sets focus. RadioGroup does not. AddCommand (Command.Select, HandleSelectCommand); - // Accept (Enter key or Doubleclick) - Raise Accept event - DO NOT advance state + // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state AddCommand (Command.Accept, HandleAcceptCommand); // Hotkey - ctx may indicate a radio item hotkey was pressed. Behavior depends on HasFocus @@ -59,7 +57,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (HasFocus) { - if ((item is null || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift!)) + if (item is null || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift!) { // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select) return InvokeCommand (Command.Select); @@ -145,14 +143,14 @@ public class RadioGroup : View, IDesignable, IOrientation if (c > -1) { // Just like the user pressing the items' hotkey - return InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], target: this, data: c)) == true; + return InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], this, c)) == true; } } return false; } - bool cursorChanged = false; + var cursorChanged = false; if (SelectedItem == Cursor) { @@ -164,7 +162,7 @@ public class RadioGroup : View, IDesignable, IOrientation } } - bool selectedItemChanged = false; + var selectedItemChanged = false; if (SelectedItem != Cursor) { @@ -209,7 +207,8 @@ public class RadioGroup : View, IDesignable, IOrientation } /// - /// Gets or sets whether double clicking on a Radio Item will cause the event to be raised. + /// Gets or sets whether double-clicking on a Radio Item will cause the event to be + /// raised. /// /// /// @@ -241,7 +240,21 @@ public class RadioGroup : View, IDesignable, IOrientation } } - private List _radioLabels = []; + /// + /// If the will each be automatically assigned a hotkey. + /// will be used to ensure unique keys are assigned. Set + /// before setting with any hotkeys that may conflict with other Views. + /// + public bool AssignHotKeysToRadioLabels { get; set; } + + /// + /// Gets the list of hotkeys already used by or that should not be used if + /// + /// is enabled. + /// + public List UsedHotKeys { get; } = []; + + private readonly List _radioLabels = []; /// /// The radio labels to display. A key binding will be added for each label enabling the @@ -263,16 +276,40 @@ public class RadioGroup : View, IDesignable, IOrientation } } - int prevCount = _radioLabels.Count; - _radioLabels = value.ToList (); + _radioLabels.Clear (); - for (var index = 0; index < _radioLabels.Count; index++) + // Pick a unique hotkey for each radio label + for (var labelIndex = 0; labelIndex < value.Length; labelIndex++) { - string label = _radioLabels [index]; + string label = value [labelIndex]; + string? newLabel = label; - if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey)) + if (AssignHotKeysToRadioLabels) { - AddKeyBindingsForHotKey (Key.Empty, hotKey, index); + // Find the first char in label that is [a-z], [A-Z], or [0-9] + for (var i = 0; i < label.Length; i++) + { + if (UsedHotKeys.Contains (new (label [i])) || !char.IsAsciiLetterOrDigit (label [i])) + { + continue; + } + + if (char.IsAsciiLetterOrDigit (label [i])) + { + char? hotChar = label [i]; + newLabel = label.Insert (i, HotKeySpecifier.ToString ()); + UsedHotKeys.Add (new (hotChar)); + + break; + } + } + } + + _radioLabels.Add (newLabel); + + if (TextFormatter.FindHotKey (newLabel, HotKeySpecifier, out _, out Key hotKey)) + { + AddKeyBindingsForHotKey (Key.Empty, hotKey, labelIndex); } } @@ -351,7 +388,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (j == hotPos && i == Cursor) { - SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); + SetAttribute (HasFocus ? GetHotFocusColor () : GetHotNormalColor ()); } else if (j == hotPos && i != Cursor) { @@ -369,7 +406,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (i == Cursor) { - SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); + SetAttribute (HasFocus ? GetHotFocusColor () : GetHotNormalColor ()); } else if (i != Cursor) { @@ -386,6 +423,7 @@ public class RadioGroup : View, IDesignable, IOrientation DrawHotString (rl, HasFocus && i == Cursor); } } + return true; } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 11f4e5175..7f98a9400 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -103,9 +103,6 @@ public class Shortcut : View, IOrientation, IDesignable ShowHide (); } - /// - protected override bool OnClearingViewport () { return base.OnClearingViewport (); } - // Helper to set Width consistently internal Dim GetWidthDimAuto () { @@ -269,6 +266,7 @@ public class Shortcut : View, IOrientation, IDesignable /// internal virtual bool? DispatchCommand (ICommandContext? commandContext) { + Logging.Trace($"{commandContext?.Source?.Title}"); CommandContext? keyCommandContext = commandContext as CommandContext? ?? default (CommandContext); if (keyCommandContext?.Binding.Data != this) @@ -276,16 +274,21 @@ public class Shortcut : View, IOrientation, IDesignable // Invoke Select on the CommandView to cause it to change state if it wants to // If this causes CommandView to raise Accept, we eat it keyCommandContext = keyCommandContext!.Value with { Binding = keyCommandContext.Value.Binding with { Data = this } }; + + Logging.Trace ($"Invoking Select on CommandView."); + CommandView.InvokeCommand (Command.Select, keyCommandContext); } // BUGBUG: Why does this use keyCommandContext and not commandContext? + Logging.Trace ($"RaiseSelecting ..."); if (RaiseSelecting (keyCommandContext) is true) { return true; } // The default HotKey handler sets Focus + Logging.Trace ($"SetFocus..."); SetFocus (); var cancel = false; @@ -294,6 +297,7 @@ public class Shortcut : View, IOrientation, IDesignable { commandContext.Source = this; } + Logging.Trace ($"RaiseAccepting..."); cancel = RaiseAccepting (commandContext) is true; if (cancel) @@ -308,6 +312,7 @@ public class Shortcut : View, IOrientation, IDesignable if (Action is { }) { + Logging.Trace ($"Invoke Action..."); Action.Invoke (); // Assume if there's a subscriber to Action, it's handled. @@ -496,6 +501,7 @@ public class Shortcut : View, IOrientation, IDesignable // This is a helper to make it easier to set the CommandView text. // CommandView is public and replaceable, but this is a convenience. _commandView.Text = Title; + //_commandView.Title = Title; } #endregion Command diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 510252622..eb3995de1 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -394,6 +394,7 @@ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> diff --git a/Tests/UnitTests/Dialogs/MessageBoxTests.cs b/Tests/UnitTests/Dialogs/MessageBoxTests.cs index 3b93d9819..9216b863e 100644 --- a/Tests/UnitTests/Dialogs/MessageBoxTests.cs +++ b/Tests/UnitTests/Dialogs/MessageBoxTests.cs @@ -462,7 +462,7 @@ public class MessageBoxTests { MessageBox.Query ( "", - UICatalogApp.GetAboutBoxMessage (), + UICatalog.UICatalogTopLevel.GetAboutBoxMessage (), wrapMessage: false, buttons: "_Ok" ); diff --git a/Tests/UnitTests/Text/TextFormatterTests.cs b/Tests/UnitTests/Text/TextFormatterTests.cs index d214dffb8..fe6dc292a 100644 --- a/Tests/UnitTests/Text/TextFormatterTests.cs +++ b/Tests/UnitTests/Text/TextFormatterTests.cs @@ -4146,7 +4146,7 @@ Nice Work")] { TextFormatter tf = new () { - Text = UICatalogApp.GetAboutBoxMessage (), + Text = UICatalog.UICatalogTopLevel.GetAboutBoxMessage (), Alignment = Alignment.Center, VerticalAlignment = Alignment.Start, WordWrap = false, diff --git a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs index 64685a9d6..d90ac3240 100644 --- a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs @@ -129,6 +129,28 @@ public class KeyBindingsTests () top.Dispose (); } + [Fact] + [AutoInitShutdown] + public void HotKey_Enabled_False_Does_Not_Invoke () + { + var view = new ScopedKeyBindingView (); + var keyWasHandled = false; + view.KeyDownNotHandled += (s, e) => keyWasHandled = true; + + var top = new Toplevel (); + top.Add (view); + Application.Begin (top); + + Application.RaiseKeyDownEvent (Key.Z); + Assert.False (keyWasHandled); + Assert.False (view.HotKeyCommand); + + keyWasHandled = false; + view.Enabled = false; + Application.RaiseKeyDownEvent (Key.F); + Assert.False (view.HotKeyCommand); + top.Dispose (); + } // tests that test KeyBindingScope.Focus and KeyBindingScope.HotKey (tests for KeyBindingScope.Application are in Application/KeyboardTests.cs) public class ScopedKeyBindingView : View diff --git a/Tests/UnitTests/Views/MenuBarTests.cs b/Tests/UnitTests/Views/MenuBarTests.cs index b65fdf996..8fb2b55e6 100644 --- a/Tests/UnitTests/Views/MenuBarTests.cs +++ b/Tests/UnitTests/Views/MenuBarTests.cs @@ -3,3884 +3,691 @@ using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; -public class MenuBarTests (ITestOutputHelper output) +public class MenuBarTests () { [Fact] [AutoInitShutdown] - public void AddMenuBarItem_RemoveMenuItem_Dynamically () + public void DefaultKey_Activates () { - var menuBar = new MenuBar (); - var menuBarItem = new MenuBarItem { Title = "_New" }; - var action = ""; - var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; - Assert.Equal ("n", menuBarItem.HotKey); - Assert.Equal ("i", menuItem.HotKey); - Assert.Empty (menuBar.Menus); - menuBarItem.AddMenuBarItem (menuBar, menuItem); - menuBar.Menus = [menuBarItem]; - Assert.Single (menuBar.Menus); - Assert.Single (menuBar.Menus [0].Children!); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); top.Add (menuBar); - Application.Begin (top); - - top.NewKeyDownEvent (Key.N.WithAlt); - Application.MainLoop.RunIteration (); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("", action); - - top.NewKeyDownEvent (Key.I); - Application.MainLoop.RunIteration (); - Assert.False (menuBar.IsMenuOpen); - Assert.Equal ("I", action); - - menuItem.RemoveMenuItem (); - Assert.Single (menuBar.Menus); - Assert.Null (menuBar.Menus [0].Children); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); - - menuBarItem.RemoveMenuItem (); - Assert.Empty (menuBar.Menus); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void AllowNullChecked_Get_Set () - { - var mi = new MenuItem ("Check this out 你", "", null) { CheckType = MenuItemCheckStyle.Checked }; - mi.Action = mi.ToggleChecked; - - var menu = new MenuBar - { - Menus = - [ - new ("Nullable Checked", new [] { mi }) - ] - }; - - //new CheckBox (); - Toplevel top = new (); - top.Add (menu); - Application.Begin (top); - - Assert.False (mi.Checked); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - Application.MainLoop.RunIteration (); - Assert.True (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - Application.MainLoop.RunIteration (); - Assert.False (mi.Checked); - - mi.AllowNullChecked = true; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - Application.MainLoop.RunIteration (); - Assert.Null (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @$" - Nullable Checked -┌──────────────────────┐ -│ {Glyphs.CheckStateNone} Check this out 你 │ -└──────────────────────┘", - output - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - Application.MainLoop.RunIteration (); - Assert.True (mi.Checked); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - Application.MainLoop.RunIteration (); - Assert.False (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - Application.MainLoop.RunIteration (); - Assert.Null (mi.Checked); - - mi.AllowNullChecked = false; - Assert.False (mi.Checked); - - mi.CheckType = MenuItemCheckStyle.NoCheck; - Assert.Throws (mi.ToggleChecked); - - mi.CheckType = MenuItemCheckStyle.Radio; - Assert.Throws (mi.ToggleChecked); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void CanExecute_False_Does_Not_Throws () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] - { - new ("New", "", null, () => false), - null, - new ("Quit", "", null) - }) - ] - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void CanExecute_HotKey () - { - Window win = null; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", "", New, CanExecuteNew), - new ( - "_Close", - "", - Close, - CanExecuteClose - ) - } - ) - ] - }; - Toplevel top = new (); - top.Add (menu); - - bool CanExecuteNew () { return win == null; } - - void New () { win = new (); } - - bool CanExecuteClose () { return win != null; } - - void Close () { win = null; } - - Application.Begin (top); - - Assert.Null (win); - Assert.True (CanExecuteNew ()); - Assert.False (CanExecuteClose ()); - - Assert.True (top.NewKeyDownEvent (Key.F.WithAlt)); - Assert.True (top.NewKeyDownEvent (Key.N.WithAlt)); - Application.MainLoop.RunIteration (); - Assert.NotNull (win); - Assert.False (CanExecuteNew ()); - Assert.True (CanExecuteClose ()); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Click_Another_View_Close_An_Open_Menu () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - - var btnClicked = false; - var btn = new Button { Y = 4, Text = "Test" }; - btn.Accepting += (s, e) => btnClicked = true; - var top = new Toplevel (); - top.Add (menu, btn); - Application.Begin (top); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = MouseFlags.Button1Clicked }); - Assert.True (btnClicked); - top.Dispose (); - } - - // TODO: Lots of tests in here really test Menu and MenuItem - Move them to MenuTests.cs - - [Fact] - public void Constructors_Defaults () - { - var menuBar = new MenuBar (); - Assert.Equal (KeyCode.F9, menuBar.Key); - var menu = new Menu { Host = menuBar, X = 0, Y = 0, BarItems = new () }; - Assert.Null (menu.ColorScheme); - Assert.False (menu.IsInitialized); - menu.BeginInit (); - menu.EndInit (); - Assert.Equal (Colors.ColorSchemes ["Menu"], menu.ColorScheme); - Assert.True (menu.CanFocus); - Assert.False (menu.WantContinuousButtonPressed); - Assert.Equal (LineStyle.Single, menuBar.MenusBorderStyle); - - menuBar = new (); - Assert.Equal (0, menuBar.X); - Assert.Equal (0, menuBar.Y); - Assert.IsType (menuBar.Width); - Assert.Equal (1, menuBar.Height); - Assert.Empty (menuBar.Menus); - Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); - Assert.True (menuBar.WantMousePositionReports); - Assert.False (menuBar.IsMenuOpen); - - menuBar = new () { Menus = [] }; - Assert.Equal (0, menuBar.X); - Assert.Equal (0, menuBar.Y); - Assert.IsType (menuBar.Width); - Assert.Equal (1, menuBar.Height); - Assert.Empty (menuBar.Menus); - Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); - Assert.True (menuBar.WantMousePositionReports); - Assert.False (menuBar.IsMenuOpen); - - var menuBarItem = new MenuBarItem (); - Assert.Equal ("", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new (new MenuBarItem [] { }); - Assert.Equal ("", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", new MenuBarItem [] { }); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", new List ()); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", "Help", null); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Equal ("Help", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.Null (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - } - - [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.Default)] - public void Disabled_MenuBar_Is_Never_Opened () - { - Toplevel top = new (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - top.Add (menu); - Application.Begin (top); - Assert.True (menu.Enabled); - menu.OpenMenu (); - Assert.True (menu.IsMenuOpen); - - menu.Enabled = false; - menu.CloseAllMenus (); - menu.OpenMenu (); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown (configLocation: ConfigLocations.Default)] - public void Disabled_MenuItem_Is_Never_Selected () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Menu", - new MenuItem [] - { - new ("Enabled 1", "", null), - new ("Disabled", "", null, () => false), - null, - new ("Enabled 2", "", null) - } - ) - ] - }; - - Toplevel top = new (); - top.Add (menu); - Application.Begin (top); - - Attribute [] attributes = - { - // 0 - menu.ColorScheme.Normal, - - // 1 - menu.ColorScheme.Focus, - - // 2 - menu.ColorScheme.Disabled - }; - - DriverAssert.AssertDriverAttributesAre ( - @" -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - top.Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - top.SubViews.ElementAt (1) - .NewMouseEvent ( - new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.SubViews.ElementAt (1) } - ) - ); - top.SubViews.ElementAt (1).Layout(); - top.SubViews.ElementAt (1).Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - top.SubViews.ElementAt (1) - .NewMouseEvent ( - new () { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (1) } - ) - ); - top.SubViews.ElementAt (1).Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_Menu_Over_A_Dialog () - { - // Override CM - Window.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultButtonAlignment = Alignment.Center; - Dialog.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultShadow = ShadowStyle.None; - Button.DefaultShadow = ShadowStyle.None; - - Toplevel top = new (); - var win = new Window (); - top.Add (win); - RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); - - Assert.Equal (new (0, 0, 40, 15), win.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - List items = new () - { - "New", - "Open", - "Close", - "Save", - "Save As", - "Delete" - }; - var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; - var menu = new MenuBar { X = Pos.Center (), Width = 10 }; - - menu.Menus = new MenuBarItem [] - { - new ( - "File", - new MenuItem [] - { - new ( - items [0], - "Create a new file", - () => ChangeMenuTitle ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - items [1], - "Open a file", - () => ChangeMenuTitle ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - items [2], - "Close a file", - () => ChangeMenuTitle ("Close"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - items [3], - "Save a file", - () => ChangeMenuTitle ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - items [4], - "Save a file as", - () => ChangeMenuTitle ("Save As"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ), - new ( - items [5], - "Delete a file", - () => ChangeMenuTitle ("Delete"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ) - } - ) - }; - dialog.Add (menu); - - void ChangeMenuTitle (string title) - { - menu.Menus [0].Title = title; - menu.SetNeedsDraw (); - } - - RunState rsDialog = Application.Begin (dialog); - Application.RunIteration (ref rsDialog); - - Assert.Equal (new (2, 2, 15, 4), dialog.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ File │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.Equal ("File", menu.Menus [0].Title); - menu.OpenMenu (); - Application.RunIteration (ref rsDialog); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ File │ │ -│ │ ┌──────────────────────────────────┐ -│ └─│ New Create a new file Ctrl+N │ -│ │ Open Open a file Ctrl+O │ -│ │ Close Close a file Ctrl+C │ -│ │ Save Save a file Ctrl+S │ -│ │ Save As Save a file as Ctrl+A │ -│ │ Delete Delete a file Ctrl+A │ -│ └──────────────────────────────────┘ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); - - // Need to fool MainLoop into thinking it's running - Application.MainLoop.Running = true; - bool firstIteration = true; - Application.RunIteration (ref rsDialog, firstIteration); - Assert.Equal (items [0], menu.Menus [0].Title); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ New │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - for (var i = 0; i < items.Count; i++) - { - menu.OpenMenu (); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); - - Application.RunIteration (ref rsDialog); - Assert.Equal (items [i], menu.Menus [0].Title); - } - - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - menu.OpenMenu (); - Application.RunIteration (ref rsDialog); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ Delete │ │ -│ │ ┌─────────────── -│ └─│ New Create -│ │ Open O -│ │ Close Cl -│ │ Save S -│ │ Save As Save -│ │ Delete Del -│ └─────────────── -│ │ -│ │ -└──────────────────┘", - output - ); - - Application.End (rsDialog); - Application.End (rsTop); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_Menu_Over_A_Top_Dialog () - { - ((FakeDriver)Application.Driver).SetBufferSize (40, 15); - - // Override CM - Window.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultButtonAlignment = Alignment.Center; - Dialog.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultShadow = ShadowStyle.None; - Button.DefaultShadow = ShadowStyle.None; - - Assert.Equal (new (0, 0, 40, 15), View.GetClip ()!.GetBounds()); - DriverAssert.AssertDriverContentsWithFrameAre (@"", output); - - List items = new () - { - "New", - "Open", - "Close", - "Save", - "Save As", - "Delete" - }; - var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; - var menu = new MenuBar { X = Pos.Center (), Width = 10 }; - - menu.Menus = new MenuBarItem [] - { - new ( - "File", - new MenuItem [] - { - new ( - items [0], - "Create a new file", - () => ChangeMenuTitle ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - items [1], - "Open a file", - () => ChangeMenuTitle ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - items [2], - "Close a file", - () => ChangeMenuTitle ("Close"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - items [3], - "Save a file", - () => ChangeMenuTitle ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - items [4], - "Save a file as", - () => ChangeMenuTitle ("Save As"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ), - new ( - items [5], - "Delete a file", - () => ChangeMenuTitle ("Delete"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ) - } - ) - }; - dialog.Add (menu); - - void ChangeMenuTitle (string title) - { - menu.Menus [0].Title = title; - menu.SetNeedsDraw (); - } - - RunState rs = Application.Begin (dialog); - Application.RunIteration (ref rs); - - Assert.Equal (new (2, 2, 15, 4), dialog.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ File │ - │ │ - └─────────────┘", - output - ); - - Assert.Equal ("File", menu.Menus [0].Title); - menu.OpenMenu (); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ File │ - │ ┌──────────────────────────────────┐ - └─│ New Create a new file Ctrl+N │ - │ Open Open a file Ctrl+O │ - │ Close Close a file Ctrl+C │ - │ Save Save a file Ctrl+S │ - │ Save As Save a file as Ctrl+A │ - │ Delete Delete a file Ctrl+A │ - └──────────────────────────────────┘", - output - ); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); - - // Need to fool MainLoop into thinking it's running - Application.MainLoop.Running = true; - Application.RunIteration (ref rs); - Assert.Equal (items [0], menu.Menus [0].Title); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ New │ - │ │ - └─────────────┘", - output - ); - - for (var i = 1; i < items.Count; i++) - { - menu.OpenMenu (); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); - - Application.RunIteration (ref rs); - Assert.Equal (items [i], menu.Menus [0].Title); - } - - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - menu.OpenMenu (); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ Delete │ - │ ┌─────────────── - └─│ New Create - │ Open O - │ Close Cl - │ Save S - │ Save As Save - │ Delete Del - └───────────────", - output - ); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); Application.End (rs); - dialog.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void DrawFrame_With_Negative_Positions () - { - var menu = new MenuBar - { - X = -1, - Y = -1, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - menu.Layout (); - - Assert.Equal (new (-1, -1), new Point (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" -──────┐ - One │ - Two │ -──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 0, 7, 4), pos); - - menu.CloseAllMenus (); - menu.Frame = new (-1, -2, menu.Frame.Width, menu.Frame.Height); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" - One │ - Two │ -──────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 7, 3), pos); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (7, 5); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" -┌────── -│ One -│ Two -└────── -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 1, 7, 4), pos); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (7, 3); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" -┌────── -│ One -│ Two -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 0, 7, 3), pos); top.Dispose (); } [Fact] [AutoInitShutdown] - public void DrawFrame_With_Negative_Positions_Disabled_Border () + public void DefaultKey_Deactivates () { - var menu = new MenuBar - { - X = -2, - Y = -1, - MenusBorderStyle = LineStyle.None, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - menu.Layout (); - - Assert.Equal (new (-2, -1), new Point (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" -ne -wo -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (-2, -2, menu.Frame.Width, menu.Frame.Height); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" -wo -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (3, 2); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" - On - Tw -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (3, 1); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" - On -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void DrawFrame_With_Positive_Positions () - { - var menu = new MenuBar - { - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" -┌──────┐ -│ One │ -│ Two │ -└──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 1, 8, 4), pos); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void DrawFrame_With_Positive_Positions_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" - One - Two -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact] - public void Exceptions () - { - Assert.Throws (() => new MenuBarItem ("Test", (MenuItem [])null)); - Assert.Throws (() => new MenuBarItem ("Test", (List)null)); - } - - [Fact] - [AutoInitShutdown] - public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKeyPressed () - { - var newAction = false; - var copyAction = false; - - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_New", "", () => newAction = true) }), - new ( - "_Edit", - new MenuItem [] { new ("_Copy", "", () => copyAction = true) } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.False (newAction); - Assert.False (copyAction); - -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - - string expected = @" - File Edit -"; - - var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 11, 1), pos); - - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); - Application.MainLoop.RunIteration (); - Assert.False (newAction); // not yet, hot keys don't work if the item is not visible - - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.F))); - Application.MainLoop.RunIteration (); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); - Application.MainLoop.RunIteration (); - Assert.True (newAction); - Application.Top.Draw (); - - expected = @" - File Edit -"; - - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - - expected = @" - File Edit -"; - - pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 11, 1), pos); - - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.C))); - Application.MainLoop.RunIteration (); - Assert.True (copyAction); -#endif - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void HotKey_MenuBar_ProcessKeyPressed_Menu_ProcessKey () - { - var newAction = false; - var copyAction = false; - - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // The real menu - var menu = new MenuBar - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] - { - new ( - "_" + expectedMenu.Menus [0].Children [0].Title, - "", - () => newAction = true - ) - } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" - + expectedMenu.Menus [1] - .Children [0] - .Title, - "", - () => copyAction = true - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.False (newAction); - Assert.False (copyAction); - - Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); - Application.MainLoop.RunIteration (); - Assert.True (newAction); - - Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); - Application.MainLoop.RunIteration (); - Assert.True (copyAction); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Key_Open_And_Close_The_MenuBar () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (top.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.True (top.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - - menu.Key = Key.F10.WithShift; - Assert.False (top.NewKeyDownEvent (Key.F9)); - Assert.False (menu.IsMenuOpen); - - Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); - Assert.True (menu.IsMenuOpen); - Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Theory] - [AutoInitShutdown] - [InlineData ("_File", "_New", "", KeyCode.Space | KeyCode.CtrlMask)] - [InlineData ("Closed", "None", "", KeyCode.Space | KeyCode.CtrlMask, KeyCode.Space | KeyCode.CtrlMask)] - [InlineData ("_File", "_New", "", KeyCode.F9)] - [InlineData ("Closed", "None", "", KeyCode.F9, KeyCode.F9)] - [InlineData ("_File", "_Open", "", KeyCode.F9, KeyCode.CursorDown)] - [InlineData ("_File", "_Save", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown)] - [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown, KeyCode.CursorDown)] - [InlineData ( - "_File", - "_New", - "", - KeyCode.F9, - KeyCode.CursorDown, - KeyCode.CursorDown, - KeyCode.CursorDown, - KeyCode.CursorDown - )] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorUp)] - [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorUp)] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorUp, KeyCode.CursorDown)] - [InlineData ("Closed", "None", "Open", KeyCode.F9, KeyCode.CursorDown, KeyCode.Enter)] - [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorRight)] - [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorLeft)] - [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorLeft, KeyCode.CursorLeft)] - [InlineData ("_Edit", "_Select All", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorUp)] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorDown, KeyCode.CursorLeft)] - [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight)] - [InlineData ("Closed", "None", "New", KeyCode.F9, KeyCode.Enter)] - [InlineData ("Closed", "None", "Quit", KeyCode.F9, KeyCode.CursorUp, KeyCode.Enter)] - [InlineData ("Closed", "None", "Copy", KeyCode.F9, KeyCode.CursorRight, KeyCode.Enter)] - [InlineData ( - "Closed", - "None", - "Find", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.Enter - )] - [InlineData ( - "Closed", - "None", - "Replace", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.CursorDown, - KeyCode.Enter - )] - [InlineData ( - "_Edit", - "F_ind", - "", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.CursorLeft, - KeyCode.Enter - )] - [InlineData ("Closed", "None", "About", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight, KeyCode.Enter)] - - //// Hotkeys - [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.ShiftMask | KeyCode.F)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.Esc)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.O)] - [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F, KeyCode.ShiftMask | KeyCode.O)] - [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.O)] - [InlineData ("_Edit", "_Copy", "", KeyCode.AltMask | KeyCode.E)] - [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.F)] - [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "Replace", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.R)] - [InlineData ("Closed", "None", "Copy", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)] - [InlineData ("_Edit", "_1st", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3)] - [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D1)] - [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.Enter)] - [InlineData ("Closed", "None", "2", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D2)] - [InlineData ("_Edit", "_5th", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)] - [InlineData ("Closed", "None", "5", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D4, KeyCode.D5)] - [InlineData ("Closed", "None", "About", KeyCode.AltMask | KeyCode.A)] - public void KeyBindings_Navigation_Commands ( - string expectedBarTitle, - string expectedItemTitle, - string expectedAction, - params KeyCode [] keys - ) - { - var miAction = ""; - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var menu = new MenuBar (); - - Func fn = s => - { - miAction = s as string; - - return true; - }; - menu.EnableForDesign (ref fn); - - menu.Key = KeyCode.F9; - menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; - menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; - - menu.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - foreach (Key key in keys) - { - top.NewKeyDownEvent (key); - Application.MainLoop.RunIteration (); - } - - Assert.Equal (expectedBarTitle, mbiCurrent != null ? mbiCurrent.Title : "Closed"); - Assert.Equal (expectedItemTitle, miCurrent != null ? miCurrent.Title : "None"); - Assert.Equal (expectedAction, miAction); - top.Dispose (); - } - - [Theory] - [AutoInitShutdown] - [InlineData ("New", KeyCode.CtrlMask | KeyCode.N)] - [InlineData ("Quit", KeyCode.CtrlMask | KeyCode.Q)] - [InlineData ("Copy", KeyCode.CtrlMask | KeyCode.C)] - [InlineData ("Replace", KeyCode.CtrlMask | KeyCode.H)] - [InlineData ("1", KeyCode.F1)] - [InlineData ("5", KeyCode.CtrlMask | KeyCode.D5)] - public void KeyBindings_Shortcut_Commands (string expectedAction, params KeyCode [] keys) - { - var miAction = ""; - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var menu = new MenuBar (); - - bool FnAction (string s) - { - miAction = s; - - return true; - } - - // Declare a variable for the function - Func fnActionVariable = FnAction; - - menu.EnableForDesign (ref fnActionVariable); - - menu.Key = KeyCode.F9; - menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; - menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; - - menu.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - foreach (KeyCode key in keys) - { - Assert.True (top.NewKeyDownEvent (new (key))); - Application.MainLoop!.RunIteration (); - } - - Assert.Equal (expectedAction, miAction); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Menu_With_Separator () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "_Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - null, - new ("_Quit", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - menu.OpenMenu (); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File -┌────────────────────────────┐ -│ Open Open a file Ctrl+O │ -├────────────────────────────┤ -│ Quit │ -└────────────────────────────┘", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Menu_With_Separator_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "_Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - null, - new ("_Quit", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - menu.OpenMenu (); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File - Open Open a file Ctrl+O -──────────────────────────── - Quit ", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("Open", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // Test without HotKeys first - var menu = new MenuBar - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" - + expectedMenu.Menus [1] - .Children [0] - .Title, - "", - null - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.True (menu.IsMenuOpen); - top.Draw (); - - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_With_Top_Init () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Toplevel top = new (); - top.Add (win); - Application.Begin (top); - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - View.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_With_Top_Init_With_Parameterless_Run () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Toplevel top = new (); - top.Add (win); - - Application.Iteration += (s, a) => - { - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - View.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RequestStop (); - }; - - Application.Run (top); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - RunState rs = Application.Begin (win); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - win.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init_With_Run_T () - { - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - - Application.Iteration += (s, a) => - { - Toplevel top = Application.Top; - Application.LayoutAndDraw(); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (top.NewKeyDownEvent (Key.F9)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (top.SubViews.ElementAt (0).NewKeyDownEvent (Key.CursorRight)); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True ( - ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) - ); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True ( - ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) - ); - View.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RequestStop (); - }; - - Application.Run ().Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("12", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // Test without HotKeys first - var menu = new MenuBar - { - Menus = - [ - new ( - expectedMenu.Menus [0].Title, - new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - expectedMenu.Menus [1].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - // Open first - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Open second - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Close menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - top.Remove (menu); - - // Now test WITH HotKeys - menu = new () - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" + expectedMenu.Menus [1].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - top.Add (menu); - - // Open first - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Open second - Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - Application.Top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Close menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_Submenus_Alignment_Correct () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "Really Long Sub Menu", - "", - null - ) - } - ), - new ( - "123", - new MenuItem [] { new ("Copy", "", null) } - ), - new ( - "Format", - new MenuItem [] { new ("Word Wrap", "", null) } - ), - new ( - "Help", - new MenuItem [] { new ("About", "", null) } - ), - new ( - "1", - new MenuItem [] { new ("2", "", null) } - ), - new ( - "3", - new MenuItem [] { new ("2", "", null) } - ), - new ( - "Last one", - new MenuItem [] { new ("Test", "", null) } - ) - ] - }; - - MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - items [i] = new ( - expectedMenu.Menus [i].Title, - new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } - ); - } - - var menu = new MenuBar { Menus = items }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - menu.OpenMenu (i); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (i), output); - } - - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_With_Action_But_Without_MenuItems_Not_Throw () - { - var menu = new MenuBar - { - Menus = - [ - new () { Title = "Test 1", Action = () => { } }, - - new () { Title = "Test 2", Action = () => { } } - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.True ( - Application.OnKeyUp ( - new KeyEventArgs ( - Key.AltMask - ) - ) - ); // changed to true because Alt activates menu bar -#endif - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBarItem_Children_Null_Does_Not_Throw () - { - var menu = new MenuBar - { - Menus = - [ - new ("Test", "", null) - ] - }; - var top = new Toplevel (); - top.Add (menu); - - Exception exception = Record.Exception (() => menu.NewKeyDownEvent (Key.Space)); - Assert.Null (exception); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuOpened_On_Disabled_MenuItem () - { - MenuItem parent = null; - MenuItem miCurrent = null; - Menu mCurrent = null; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new MenuBarItem ( - "_New", - new MenuItem [] - { - new ( - "_New doc", - "Creates new doc.", - null, - () => false - ) - } - ), - null, - new ("_Save", "Saves the file.", null) - } - ) - ] - }; - - menu.MenuOpened += (s, e) => - { - parent = e.Parent; - miCurrent = e.MenuItem; - mCurrent = menu._openMenu; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - // open the menu - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_Save", miCurrent.Title); - - // close the menu - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Assert.False (menu.IsMenuOpen); - - // open the menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - - // The _New doc is enabled but the sub-menu isn't enabled. Is show but can't be selected and executed - Assert.Equal ("_New", parent.Title); - Assert.Equal ("_New", miCurrent.Parent.Title); - Assert.Equal ("_New doc", miCurrent.Title); - - Assert.True (mCurrent.NewKeyDownEvent (Key.CursorDown)); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_Save", miCurrent.Title); - - Assert.True (mCurrent.NewKeyDownEvent (Key.CursorUp)); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Null (miCurrent); - - // close the menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuOpening_MenuOpened_MenuClosing_Events () - { - var miAction = ""; - var isMenuClosed = true; - var cancelClosing = false; - - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_New", "Creates new file.", New) }) - ] - }; - - menu.MenuOpening += (s, e) => - { - Assert.Equal ("_File", e.CurrentMenu.Title); - Assert.Equal ("_New", e.CurrentMenu.Children [0].Title); - Assert.Equal ("Creates new file.", e.CurrentMenu.Children [0].Help); - Assert.Equal (New, e.CurrentMenu.Children [0].Action); - e.CurrentMenu.Children [0].Action (); - Assert.Equal ("New", miAction); - - e.NewMenuBarItem = new ( - "_Edit", - new MenuItem [] { new ("_Copy", "Copies the selection.", Copy) } - ); - }; - - menu.MenuOpened += (s, e) => - { - MenuItem mi = e.MenuItem; - - Assert.Equal ("_Edit", mi.Parent.Title); - Assert.Equal ("_Copy", mi.Title); - Assert.Equal ("Copies the selection.", mi.Help); - Assert.Equal (Copy, mi.Action); - mi.Action (); - Assert.Equal ("Copy", miAction); - }; - - menu.MenuClosing += (s, e) => - { - Assert.False (isMenuClosed); - - if (cancelClosing) - { - e.Cancel = true; - isMenuClosed = false; - } - else - { - isMenuClosed = true; - } - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - isMenuClosed = !menu.IsMenuOpen; - Assert.False (isMenuClosed); - top.Draw (); - - var expected = @" -Edit -┌──────────────────────────────┐ -│ Copy Copies the selection. │ -└──────────────────────────────┘ -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - cancelClosing = true; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.False (isMenuClosed); - View.SetClipToScreen (); - top.Draw (); - - expected = @" -Edit -┌──────────────────────────────┐ -│ Copy Copies the selection. │ -└──────────────────────────────┘ -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - cancelClosing = false; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - Assert.True (isMenuClosed); - View.SetClipToScreen (); - top.Draw (); - - expected = @" -Edit -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - void New () { miAction = "New"; } - - void Copy () { miAction = "Copy"; } - - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MouseEvent_Test () - { - MenuItem miCurrent = null; - Menu mCurrent = null; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] { new ("_New", "", null), new ("_Open", "", null), new ("_Save", "", null) } - ), - new ( - "_Edit", - new MenuItem [] { new ("_Copy", "", null), new ("C_ut", "", null), new ("_Paste", "", null) } - ) - ] - }; - - menuBar.MenuOpened += (s, e) => - { - miCurrent = e.MenuItem; - mCurrent = menuBar.OpenCurrentMenu; - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); top.Add (menuBar); - Application.Begin (top); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - // Click on Edit - Assert.True ( - menuBar.NewMouseEvent ( - new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menuBar } - ) - ); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("_Edit", miCurrent.Parent.Title); - Assert.Equal ("_Copy", miCurrent.Title); + // Act + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - // Click on Paste - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("_Edit", miCurrent.Parent.Title); - Assert.Equal ("_Paste", miCurrent.Title); - - for (var i = 4; i >= -1; i--) - { - Application.RaiseMouseEvent ( - new () { ScreenPosition = new (10, i), Flags = MouseFlags.ReportMousePosition } - ); - - Assert.True (menuBar.IsMenuOpen); - Menu menu = (Menu)top.SubViews.First (v => v is Menu); - - if (i is < 0 or > 0) - { - Assert.Equal (menu, Application.MouseGrabView); - } - else - { - Assert.Equal (menuBar, Application.MouseGrabView); - } - - Assert.Equal ("_Edit", miCurrent.Parent.Title); - - if (i == 4) - { - Assert.Equal ("_Paste", miCurrent.Title); - } - else if (i == 3) - { - Assert.Equal ("C_ut", miCurrent.Title); - } - else if (i == 2) - { - Assert.Equal ("_Copy", miCurrent.Title); - } - else - { - Assert.Equal ("_Copy", miCurrent.Title); - } - } + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard () + public void QuitKey_DeActivates () { - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ("Edit", Array.Empty ()), - new ( - "Format", - new MenuItem [] { new ("Wrap", "", null) } - ) - ] - }; - - MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - items [i] = new ( - expectedMenu.Menus [i].Title, - expectedMenu.Menus [i].Children.Length > 0 - ? new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } - : Array.Empty () - ); - } - - var menu = new MenuBar { Menus = items }; - - var tf = new TextField { Y = 2, Width = 10 }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu, tf); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Application.Begin (top); - Assert.True (tf.HasFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + // Act + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - // Right - Edit has no sub menu; this tests that no sub menu shows - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - Assert.Equal (1, menu._selected); - Assert.Equal (-1, menu._selectedSub); - Assert.Null (menu._openSubMenu); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + Application.RaiseKeyDownEvent (Application.QuitKey); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - // Right - Format - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); - - // Left - Edit - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (Application.RaiseKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - Assert.True (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse () + public void MenuBarItem_HotKey_Activates () { - // File Edit Format - //┌──────┐ ┌───────┐ - //│ New │ │ Wrap │ - //└──────┘ └───────┘ - - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ("Edit", new MenuItem [] { }), - new ( - "Format", - new MenuItem [] { new ("Wrap", "", null) } - ) - ] - }; - - var menu = new MenuBar - { - Menus = - [ - new ( - expectedMenu.Menus [0].Title, - new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new (expectedMenu.Menus [1].Title, new MenuItem [] { }), - new ( - expectedMenu.Menus [2].Title, - new MenuItem [] - { - new ( - expectedMenu.Menus [2].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - var tf = new TextField { Y = 2, Width = 10 }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu, tf); - Application.Begin (top); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.True (tf.HasFocus); - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.False (menu.IsMenuOpen); - Assert.True (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + Application.End (rs); top.Dispose (); } [Fact] - public void RemoveAndThenAddMenuBar_ShouldNotChangeWidth () + [AutoInitShutdown] + public void MenuBarItem_HotKey_Deactivates () { - MenuBar menuBar; - MenuBar menuBar2; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, - // TODO: Change this to Window - var w = new View (); - menuBar2 = new (); - menuBar = new (); - w.Width = Dim.Fill (); - w.Height = Dim.Fill (); - w.X = 0; - w.Y = 0; + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - w.Visible = true; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, - // TODO: uncomment this. - //w.Modal = false; - w.Title = ""; - menuBar.Width = Dim.Fill (); - menuBar.Height = 1; - menuBar.X = 0; - menuBar.Y = 0; - menuBar.Visible = true; - w.Add (menuBar); + Application.End (rs); + top.Dispose (); + } - menuBar2.Width = Dim.Fill (); - menuBar2.Height = 1; - menuBar2.X = 0; - menuBar2.Y = 4; - menuBar2.Visible = true; - w.Add (menuBar2); - MenuBar [] menuBars = w.SubViews.OfType ().ToArray (); - Assert.Equal (2, menuBars.Length); + [Fact] + [AutoInitShutdown] + public void MenuItem_HotKey_Deactivates () + { + // Arrange + int action = 0; + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item", Action = () => action++ }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.Equal (Dim.Fill (), menuBars [0].Width); - Assert.Equal (Dim.Fill (), menuBars [1].Width); + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - // Goes wrong here - w.Remove (menuBar); - w.Remove (menuBar2); + Application.RaiseKeyDownEvent (Key.I); + Assert.Equal (1, action); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - w.Add (menuBar); - w.Add (menuBar2); - - // These assertions fail - Assert.Equal (Dim.Fill (), menuBars [0].Width); - Assert.Equal (Dim.Fill (), menuBars [1].Width); + Application.End (rs); + top.Dispose (); } [Fact] [AutoInitShutdown] - public void Resizing_Close_Menus () + public void HotKey_Activates_Only_Once () { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ) - } - ) - ] - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + int visibleChangeCount = 0; + menuBarItemPopover.VisibleChanged += (sender, args) => + { + if (menuBarItemPopover.Visible) + { + visibleChangeCount++; + } + }; + + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.Equal (1, visibleChangeCount); + + Application.End (rs); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void WhenActive_Other_MenuBarItem_HotKey_Activates () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + + var menuItem2 = new MenuItemv2 { Id = "menuItem2", Title = "_Copy" }; + var menu2 = new Menuv2 ([menuItem2]) { Id = "menu2" }; + var menuBarItem2 = new MenuBarItemv2 () { Id = "menuBarItem2", Title = "_Edit" }; + var menuBarItemPopover2 = new PopoverMenu () { Id = "menuBarItemPopover2" }; + menuBarItem2.PopoverMenu = menuBarItemPopover2; + menuBarItemPopover2.Root = menu2; + + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + menuBar.Add (menuBarItem2); + + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); + + Application.RaiseKeyDownEvent (Key.E.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem2.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.Visible); + + Application.End (rs); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Mouse_Enter_Sets_Can_Focus_But_Does_Not_Activate () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + Application.RaiseMouseEvent (new () + { + Flags = MouseFlags.ReportMousePosition + }); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); + + Application.End (rs); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Mouse_Click_Activates () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + Application.RaiseMouseEvent (new () + { + Flags = MouseFlags.Button1Clicked + }); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + Application.End (rs); + top.Dispose (); + } + + // QUESTION: Windows' menus close the menu when you click on the menu bar item again. + // QUESTION: What does Mac do? + // QUESTION: How bad is it that this test is skipped? + // QUESTION: Fixing this could be challenging. Should we fix it? + [Fact (Skip = "Clicking outside Popover, passes mouse event to MenuBar, which activates the same item again.")] + [AutoInitShutdown] + public void Mouse_Click_Deactivates () + { + // Arrange + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + Application.RaiseMouseEvent (new () + { + Flags = MouseFlags.Button1Clicked + }); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + // Act + Application.RaiseMouseEvent (new () + { + Flags = MouseFlags.Button1Clicked + }); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); + + Application.End (rs); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Dynamic_Change_MenuItem_Title () + { + // Arrange + int action = 0; + var menuItem = new MenuItemv2 { Title = "_Item", Action = () => action++ }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 (); + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); RunState rs = Application.Begin (top); - menu.OpenMenu (); - var firstIteration = false; - Application.RunIteration (ref rs, firstIteration); + Assert.False (menuBar.IsActive()); + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.Equal (0, action); - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File -┌────────────────────────────┐ -│ Open Open a file Ctrl+O │ -└────────────────────────────┘", - output - ); + Assert.Equal(Key.I, menuItem.HotKey); + Application.RaiseKeyDownEvent (Key.I); + Assert.Equal (1, action); + Assert.False (menuBar.IsActive ()); - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - firstIteration = false; - Application.RunIteration (ref rs, firstIteration); + menuItem.Title = "_Foo"; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Application.RaiseKeyDownEvent (Key.I); + Assert.Equal (1, action); + Assert.True (menuBar.IsActive ()); - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File", - output - ); + Application.RaiseKeyDownEvent (Key.F); + Assert.Equal (2, action); Application.End (rs); top.Dispose (); } [Fact] - public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey () + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBar_Is_Not_Activated () { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] { new ("_New", "", null), null, new ("_Quit", "", null) } - ) - ] - }; - Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt)); - } - - [Fact] - public void SetMenus_With_Same_HotKey_Does_Not_Throws () - { - var mb = new MenuBar (); - - var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); - - mb.Menus = new [] { i1 }; - mb.Menus = new [] { i1 }; - - Assert.Equal (Key.H, mb.Menus [0].HotKey); - } - - [Fact] - [AutoInitShutdown] - public void ShortCut_Activates () - { - var saveAction = false; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Save", - "Saves the file.", - () => { saveAction = true; }, - null, - null, - (KeyCode)Key.S.WithCtrl - ) - } - ) - ] - }; - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Application.RaiseKeyDownEvent (Key.S.WithCtrl); - Application.MainLoop.RunIteration (); + // Act + menuBar.Enabled = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBarItem.PopoverMenu.Visible); - Assert.True (saveAction); + Application.End (rs); top.Dispose (); } [Fact] - public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBarItem_Is_Not_Activated () { - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("New", "Create New", null, null, null, Key.A.WithCtrl) - } - ) - ] - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); + // Act + menuBarItem.Enabled = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBarItem.PopoverMenu.Visible); - menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; + Application.End (rs); + top.Dispose (); + } + + + [Fact] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBarItem_Popover_Is_Activated () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); + var top = new Toplevel (); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + menuBarItem.PopoverMenu.Enabled = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); + + Application.End (rs); + top.Dispose (); + } + + [Fact (Skip = "For v2, should the menu close on resize?")] + [AutoInitShutdown] + public void Resizing_Closes_Menus () + { - Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _)); } [Fact] - public void UseKeysUpDownAsKeysLeftRight_And_UseSubMenusSingleFrame_Cannot_Be_Both_True () - { - var menu = new MenuBar (); - Assert.False (menu.UseKeysUpDownAsKeysLeftRight); - Assert.False (menu.UseSubMenusSingleFrame); - - menu.UseKeysUpDownAsKeysLeftRight = true; - Assert.True (menu.UseKeysUpDownAsKeysLeftRight); - Assert.False (menu.UseSubMenusSingleFrame); - - menu.UseSubMenusSingleFrame = true; - Assert.False (menu.UseKeysUpDownAsKeysLeftRight); - Assert.True (menu.UseSubMenusSingleFrame); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Keyboard () + public void Update_MenuBarItem_HotKey_Works () { - var menu = new MenuBar - { - Menus = new MenuBarItem [] - { - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ("Sub-Menu 1", "", null), - new ("Sub-Menu 2", "", null) - } - ), - new ("Three", "", null) - } - ) - } - }; - menu.UseKeysUpDownAsKeysLeftRight = true; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│┌─────────────┐ -│ Three ││ Sub-Menu 1 │ -└────────┘│ Sub-Menu 2 │ - └─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Mouse () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - menu.NewMouseEvent ( - new () - { - Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) - } - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│┌─────────────┐ -│ Three ││ Sub-Menu 1 │ -└────────┘│ Sub-Menu 2 │ - └─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 25, 7), pos); - - Assert.False ( - menu.NewMouseEvent ( - new () - { - Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) - } - ) - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } - ); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - menu.UseKeysUpDownAsKeysLeftRight = true; - menu.BeginInit (); - menu.EndInit (); - - menu.OpenMenu (); - menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); - Assert.True (menu.IsMenuOpen); - - menu.Draw (); - menu._openMenu.Draw (); - - var expected = @" - Numbers - One - Two ► - Three "; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); - menu.Draw (); - menu._openMenu.Draw (); - menu.OpenCurrentMenu.Draw (); - - expected = @" - Numbers - One - Two ► Sub-Menu 1 - Three Sub-Menu 2"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_By_Keyboard () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - menu.UseSubMenusSingleFrame = true; - Assert.True (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Enter)); - top.Draw (); - - expected = @" - Numbers -┌─────────────┐ -│◄ Two │ -├─────────────┤ -│ Sub-Menu 1 │ -│ Sub-Menu 2 │ -└─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 15, 7), pos); - - Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.Enter)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_By_Mouse () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - menu.UseSubMenusSingleFrame = true; - Assert.True (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.False (menu.NewMouseEvent (new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) })); - top.Draw (); - - expected = @" - Numbers -┌─────────────┐ -│◄ Two │ -├─────────────┤ -│ Sub-Menu 1 │ -│ Sub-Menu 2 │ -└─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 15, 7), pos); - - menu.NewMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) }); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.False (menu.NewMouseEvent (new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top })); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - menu.UseSubMenusSingleFrame = true; - menu.BeginInit (); - menu.EndInit (); - - menu.OpenMenu (); - Assert.True (menu.IsMenuOpen); - - menu.Draw (); - menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); - menu._openMenu.Draw (); - - var expected = @" - Numbers - One - Two ► - Three "; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - menu.Draw (); - menu._openMenu.Draw (); - menu.OpenCurrentMenu.Draw (); - - expected = @" - Numbers -◄ Two -───────────── - Sub-Menu 1 - Sub-Menu 2 "; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_Without_Border () - { - var menu = new MenuBar - { - UseSubMenusSingleFrame = true, - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.True (menu.UseSubMenusSingleFrame); - Assert.Equal (LineStyle.None, menu.MenusBorderStyle); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - top.Draw (); - - expected = @" - Numbers - One - Two ► - Three -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 4), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) } - ); - top.Draw (); - - expected = @" - Numbers -◄ Two -───────────── - Sub-Menu 1 - Sub-Menu 2 -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 13, 5), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) } - ); - top.Draw (); - - expected = @" - Numbers - One - Two ► - Three -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 4), pos); - - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } - ); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + // Act + menuBarItem.HotKey = Key.E.WithAlt; + + // old key should do nothing + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + // use new key + Application.RaiseKeyDownEvent (Key.E.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); + + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Visible_False_Key_Does_Not_Open_And_Close_All_Opened_Menus () + public void Visible_False_HotKey_Does_Not_Activate () { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.True (menu.Visible); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); + // Act + menuBar.Visible = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - menu.Visible = false; - Assert.False (menu.IsMenuOpen); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void CanFocus_True_Key_Esc_Exit_Toplevel_If_IsMenuOpen_False () + public void Visible_False_MenuItem_Key_Does_Action () { - var menu = new MenuBar + // Arrange + int action = 0; + var menuItem = new MenuItemv2 () { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ], - CanFocus = true + Id = "menuItem", + Title = "_Item", + Key = Key.F1, + Action = () => action++ }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.True (menu.CanFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); + // Act + menuBar.Visible = false; + Application.RaiseKeyDownEvent (Key.F1); - Assert.True (menu.NewKeyDownEvent (Key.Esc)); - Assert.False (menu.IsMenuOpen); + Assert.Equal (1, action); - Assert.False (menu.NewKeyDownEvent (Key.Esc)); - Assert.False (menu.IsMenuOpen); + Application.End (rs); top.Dispose (); } - - // Defines the expected strings for a Menu. Currently supports - // - MenuBar with any number of MenuItems - // - Each top-level MenuItem can have a SINGLE sub-menu - // - // TODO: Enable multiple sub-menus - // TODO: Enable checked sub-menus - // TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?) - // - // E.g: - // - // File Edit - // New Copy - public class ExpectedMenuBar : MenuBar - { - private FakeDriver _d = (FakeDriver)Application.Driver; - - // The expected strings when the menu is closed - public string ClosedMenuText => MenuBarText + "\n"; - - public string ExpectedBottomRow (int i) - { - return $"{Glyphs.LLCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.LRCorner} \n"; - } - - // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) - public string ExpectedMenuItemRow (int i) { return $"{Glyphs.VLine} {Menus [i].Children [0].Title} {Glyphs.VLine} \n"; } - - // The full expected string for an open sub menu - public string ExpectedSubMenuOpen (int i) - { - return ClosedMenuText - + (Menus [i].Children.Length > 0 - ? ExpectedPadding (i) - + ExpectedTopRow (i) - + ExpectedPadding (i) - + ExpectedMenuItemRow (i) - + ExpectedPadding (i) - + ExpectedBottomRow (i) - : ""); - } - - // Define expected menu frame - // "┌──────┐" - // "│ New │" - // "└──────┘" - // - // The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated - // 1 space before the Title and 2 spaces after the Title/Check/Help - public string ExpectedTopRow (int i) - { - return $"{Glyphs.ULCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.URCorner} \n"; - } - - // Each MenuBar title has a 1 space pad on each side - // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs - public string MenuBarText - { - get - { - var txt = string.Empty; - - foreach (MenuBarItem m in Menus) - { - txt += " " + m.Title + " "; - } - - return txt; - } - } - - // Padding for the X of the sub menu Frame - // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created - private string ExpectedPadding (int i) - { - var n = 0; - - while (i > 0) - { - n += Menus [i - 1].TitleLength + 2; - i--; - } - - return new (' ', n); - } - } - - private class CustomWindow : Window - { - public CustomWindow () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - Add (menu); - } - } } diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs new file mode 100644 index 000000000..e050b816e --- /dev/null +++ b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs @@ -0,0 +1,3886 @@ +using UnitTests; +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewsTests; + +public class MenuBarv1Tests (ITestOutputHelper output) +{ + [Fact] + [AutoInitShutdown] + public void AddMenuBarItem_RemoveMenuItem_Dynamically () + { + var menuBar = new MenuBar (); + var menuBarItem = new MenuBarItem { Title = "_New" }; + var action = ""; + var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; + Assert.Equal ("n", menuBarItem.HotKey); + Assert.Equal ("i", menuItem.HotKey); + Assert.Empty (menuBar.Menus); + menuBarItem.AddMenuBarItem (menuBar, menuItem); + menuBar.Menus = [menuBarItem]; + Assert.Single (menuBar.Menus); + Assert.Single (menuBar.Menus [0].Children!); + + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); + + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + top.NewKeyDownEvent (Key.N.WithAlt); + Application.MainLoop.RunIteration (); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("", action); + + top.NewKeyDownEvent (Key.I); + Application.MainLoop.RunIteration (); + Assert.False (menuBar.IsMenuOpen); + Assert.Equal ("I", action); + + menuItem.RemoveMenuItem (); + Assert.Single (menuBar.Menus); + Assert.Null (menuBar.Menus [0].Children); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); + + menuBarItem.RemoveMenuItem (); + Assert.Empty (menuBar.Menus); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void AllowNullChecked_Get_Set () + { + var mi = new MenuItem ("Check this out 你", "", null) { CheckType = MenuItemCheckStyle.Checked }; + mi.Action = mi.ToggleChecked; + + var menu = new MenuBar + { + Menus = + [ + new ("Nullable Checked", new [] { mi }) + ] + }; + + //new CheckBox (); + Toplevel top = new (); + top.Add (menu); + Application.Begin (top); + + Assert.False (mi.Checked); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + Application.MainLoop.RunIteration (); + Assert.True (mi.Checked); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + + Assert.True ( + menu._openMenu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + ) + ); + Application.MainLoop.RunIteration (); + Assert.False (mi.Checked); + + mi.AllowNullChecked = true; + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + Application.MainLoop.RunIteration (); + Assert.Null (mi.Checked); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @$" + Nullable Checked +┌──────────────────────┐ +│ {Glyphs.CheckStateNone} Check this out 你 │ +└──────────────────────┘", + output + ); + + Assert.True ( + menu._openMenu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + ) + ); + Application.MainLoop.RunIteration (); + Assert.True (mi.Checked); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + Application.MainLoop.RunIteration (); + Assert.False (mi.Checked); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + + Assert.True ( + menu._openMenu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + ) + ); + Application.MainLoop.RunIteration (); + Assert.Null (mi.Checked); + + mi.AllowNullChecked = false; + Assert.False (mi.Checked); + + mi.CheckType = MenuItemCheckStyle.NoCheck; + Assert.Throws (mi.ToggleChecked); + + mi.CheckType = MenuItemCheckStyle.Radio; + Assert.Throws (mi.ToggleChecked); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanExecute_False_Does_Not_Throws () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] + { + new ("New", "", null, () => false), + null, + new ("Quit", "", null) + }) + ] + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanExecute_HotKey () + { + Window win = null; + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ("_New", "", New, CanExecuteNew), + new ( + "_Close", + "", + Close, + CanExecuteClose + ) + } + ) + ] + }; + Toplevel top = new (); + top.Add (menu); + + bool CanExecuteNew () { return win == null; } + + void New () { win = new (); } + + bool CanExecuteClose () { return win != null; } + + void Close () { win = null; } + + Application.Begin (top); + + Assert.Null (win); + Assert.True (CanExecuteNew ()); + Assert.False (CanExecuteClose ()); + + Assert.True (top.NewKeyDownEvent (Key.F.WithAlt)); + Assert.True (top.NewKeyDownEvent (Key.N.WithAlt)); + Application.MainLoop.RunIteration (); + Assert.NotNull (win); + Assert.False (CanExecuteNew ()); + Assert.True (CanExecuteClose ()); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Click_Another_View_Close_An_Open_Menu () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + + var btnClicked = false; + var btn = new Button { Y = 4, Text = "Test" }; + btn.Accepting += (s, e) => btnClicked = true; + var top = new Toplevel (); + top.Add (menu, btn); + Application.Begin (top); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = MouseFlags.Button1Clicked }); + Assert.True (btnClicked); + top.Dispose (); + } + + // TODO: Lots of tests in here really test Menu and MenuItem - Move them to MenuTests.cs + + [Fact] + public void Constructors_Defaults () + { + var menuBar = new MenuBar (); + Assert.Equal (KeyCode.F9, menuBar.Key); + var menu = new Menu { Host = menuBar, X = 0, Y = 0, BarItems = new () }; + Assert.Null (menu.ColorScheme); + Assert.False (menu.IsInitialized); + menu.BeginInit (); + menu.EndInit (); + Assert.Equal (Colors.ColorSchemes ["Menu"], menu.ColorScheme); + Assert.True (menu.CanFocus); + Assert.False (menu.WantContinuousButtonPressed); + Assert.Equal (LineStyle.Single, menuBar.MenusBorderStyle); + + menuBar = new (); + Assert.Equal (0, menuBar.X); + Assert.Equal (0, menuBar.Y); + Assert.IsType (menuBar.Width); + Assert.Equal (1, menuBar.Height); + Assert.Empty (menuBar.Menus); + Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); + Assert.True (menuBar.WantMousePositionReports); + Assert.False (menuBar.IsMenuOpen); + + menuBar = new () { Menus = [] }; + Assert.Equal (0, menuBar.X); + Assert.Equal (0, menuBar.Y); + Assert.IsType (menuBar.Width); + Assert.Equal (1, menuBar.Height); + Assert.Empty (menuBar.Menus); + Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); + Assert.True (menuBar.WantMousePositionReports); + Assert.False (menuBar.IsMenuOpen); + + var menuBarItem = new MenuBarItem (); + Assert.Equal ("", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new (new MenuBarItem [] { }); + Assert.Equal ("", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new ("Test", new MenuBarItem [] { }); + Assert.Equal ("Test", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new ("Test", new List ()); + Assert.Equal ("Test", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new ("Test", "Help", null); + Assert.Equal ("Test", menuBarItem.Title); + Assert.Equal ("Help", menuBarItem.Help); + Assert.Null (menuBarItem.Action); + Assert.Null (menuBarItem.CanExecute); + Assert.Null (menuBarItem.Parent); + Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); + } + + [Fact] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBar_Is_Never_Opened () + { + Toplevel top = new (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + top.Add (menu); + Application.Begin (top); + Assert.True (menu.Enabled); + menu.OpenMenu (); + Assert.True (menu.IsMenuOpen); + + menu.Enabled = false; + menu.CloseAllMenus (); + menu.OpenMenu (); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuItem_Is_Never_Selected () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Menu", + new MenuItem [] + { + new ("Enabled 1", "", null), + new ("Disabled", "", null, () => false), + null, + new ("Enabled 2", "", null) + } + ) + ] + }; + + Toplevel top = new (); + top.Add (menu); + Application.Begin (top); + + Attribute [] attributes = + { + // 0 + menu.ColorScheme.Normal, + + // 1 + menu.ColorScheme.Focus, + + // 2 + menu.ColorScheme.Disabled + }; + + DriverAssert.AssertDriverAttributesAre ( + @" +00000000000000", + output, + Application.Driver, + attributes + ); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + top.Draw (); + + DriverAssert.AssertDriverAttributesAre ( + @" +11111100000000 +00000000000000 +01111111111110 +02222222222220 +00000000000000 +00000000000000 +00000000000000", + output, + Application.Driver, + attributes + ); + + Assert.True ( + top.SubViews.ElementAt (1) + .NewMouseEvent ( + new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.SubViews.ElementAt (1) } + ) + ); + top.SubViews.ElementAt (1).Layout(); + top.SubViews.ElementAt (1).Draw (); + + DriverAssert.AssertDriverAttributesAre ( + @" +11111100000000 +00000000000000 +01111111111110 +02222222222220 +00000000000000 +00000000000000 +00000000000000", + output, + Application.Driver, + attributes + ); + + Assert.True ( + top.SubViews.ElementAt (1) + .NewMouseEvent ( + new () { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (1) } + ) + ); + top.SubViews.ElementAt (1).Draw (); + + DriverAssert.AssertDriverAttributesAre ( + @" +11111100000000 +00000000000000 +01111111111110 +02222222222220 +00000000000000 +00000000000000 +00000000000000", + output, + Application.Driver, + attributes + ); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void Draw_A_Menu_Over_A_Dialog () + { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultShadow = ShadowStyle.None; + Button.DefaultShadow = ShadowStyle.None; + + Toplevel top = new (); + var win = new Window (); + top.Add (win); + RunState rsTop = Application.Begin (top); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); + + Assert.Equal (new (0, 0, 40, 15), win.Frame); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + List items = new () + { + "New", + "Open", + "Close", + "Save", + "Save As", + "Delete" + }; + var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; + var menu = new MenuBar { X = Pos.Center (), Width = 10 }; + + menu.Menus = new MenuBarItem [] + { + new ( + "File", + new MenuItem [] + { + new ( + items [0], + "Create a new file", + () => ChangeMenuTitle ("New"), + null, + null, + KeyCode.CtrlMask | KeyCode.N + ), + new ( + items [1], + "Open a file", + () => ChangeMenuTitle ("Open"), + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + new ( + items [2], + "Close a file", + () => ChangeMenuTitle ("Close"), + null, + null, + KeyCode.CtrlMask | KeyCode.C + ), + new ( + items [3], + "Save a file", + () => ChangeMenuTitle ("Save"), + null, + null, + KeyCode.CtrlMask | KeyCode.S + ), + new ( + items [4], + "Save a file as", + () => ChangeMenuTitle ("Save As"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ), + new ( + items [5], + "Delete a file", + () => ChangeMenuTitle ("Delete"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ) + } + ) + }; + dialog.Add (menu); + + void ChangeMenuTitle (string title) + { + menu.Menus [0].Title = title; + menu.SetNeedsDraw (); + } + + RunState rsDialog = Application.Begin (dialog); + Application.RunIteration (ref rsDialog); + + Assert.Equal (new (2, 2, 15, 4), dialog.Frame); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ File │ │ +│ │ │ │ +│ └─────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.Equal ("File", menu.Menus [0].Title); + menu.OpenMenu (); + Application.RunIteration (ref rsDialog); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ File │ │ +│ │ ┌──────────────────────────────────┐ +│ └─│ New Create a new file Ctrl+N │ +│ │ Open Open a file Ctrl+O │ +│ │ Close Close a file Ctrl+C │ +│ │ Save Save a file Ctrl+S │ +│ │ Save As Save a file as Ctrl+A │ +│ │ Delete Delete a file Ctrl+A │ +│ └──────────────────────────────────┘ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); + + // Need to fool MainLoop into thinking it's running + Application.MainLoop.Running = true; + bool firstIteration = true; + Application.RunIteration (ref rsDialog, firstIteration); + Assert.Equal (items [0], menu.Menus [0].Title); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ New │ │ +│ │ │ │ +│ └─────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + for (var i = 0; i < items.Count; i++) + { + menu.OpenMenu (); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); + + Application.RunIteration (ref rsDialog); + Assert.Equal (items [i], menu.Menus [0].Title); + } + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); + menu.OpenMenu (); + Application.RunIteration (ref rsDialog); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ Delete │ │ +│ │ ┌─────────────── +│ └─│ New Create +│ │ Open O +│ │ Close Cl +│ │ Save S +│ │ Save As Save +│ │ Delete Del +│ └─────────────── +│ │ +│ │ +└──────────────────┘", + output + ); + + Application.End (rsDialog); + Application.End (rsTop); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void Draw_A_Menu_Over_A_Top_Dialog () + { + ((FakeDriver)Application.Driver).SetBufferSize (40, 15); + + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultShadow = ShadowStyle.None; + Button.DefaultShadow = ShadowStyle.None; + + Assert.Equal (new (0, 0, 40, 15), View.GetClip ()!.GetBounds()); + DriverAssert.AssertDriverContentsWithFrameAre (@"", output); + + List items = new () + { + "New", + "Open", + "Close", + "Save", + "Save As", + "Delete" + }; + var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; + var menu = new MenuBar { X = Pos.Center (), Width = 10 }; + + menu.Menus = new MenuBarItem [] + { + new ( + "File", + new MenuItem [] + { + new ( + items [0], + "Create a new file", + () => ChangeMenuTitle ("New"), + null, + null, + KeyCode.CtrlMask | KeyCode.N + ), + new ( + items [1], + "Open a file", + () => ChangeMenuTitle ("Open"), + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + new ( + items [2], + "Close a file", + () => ChangeMenuTitle ("Close"), + null, + null, + KeyCode.CtrlMask | KeyCode.C + ), + new ( + items [3], + "Save a file", + () => ChangeMenuTitle ("Save"), + null, + null, + KeyCode.CtrlMask | KeyCode.S + ), + new ( + items [4], + "Save a file as", + () => ChangeMenuTitle ("Save As"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ), + new ( + items [5], + "Delete a file", + () => ChangeMenuTitle ("Delete"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ) + } + ) + }; + dialog.Add (menu); + + void ChangeMenuTitle (string title) + { + menu.Menus [0].Title = title; + menu.SetNeedsDraw (); + } + + RunState rs = Application.Begin (dialog); + Application.RunIteration (ref rs); + + Assert.Equal (new (2, 2, 15, 4), dialog.Frame); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ File │ + │ │ + └─────────────┘", + output + ); + + Assert.Equal ("File", menu.Menus [0].Title); + menu.OpenMenu (); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ File │ + │ ┌──────────────────────────────────┐ + └─│ New Create a new file Ctrl+N │ + │ Open Open a file Ctrl+O │ + │ Close Close a file Ctrl+C │ + │ Save Save a file Ctrl+S │ + │ Save As Save a file as Ctrl+A │ + │ Delete Delete a file Ctrl+A │ + └──────────────────────────────────┘", + output + ); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); + + // Need to fool MainLoop into thinking it's running + Application.MainLoop.Running = true; + Application.RunIteration (ref rs); + Assert.Equal (items [0], menu.Menus [0].Title); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ New │ + │ │ + └─────────────┘", + output + ); + + for (var i = 1; i < items.Count; i++) + { + menu.OpenMenu (); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); + + Application.RunIteration (ref rs); + Assert.Equal (items [i], menu.Menus [0].Title); + } + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); + menu.OpenMenu (); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ Delete │ + │ ┌─────────────── + └─│ New Create + │ Open O + │ Close Cl + │ Save S + │ Save As Save + │ Delete Del + └───────────────", + output + ); + + Application.End (rs); + dialog.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Negative_Positions () + { + var menu = new MenuBar + { + X = -1, + Y = -1, + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + menu.Layout (); + + Assert.Equal (new (-1, -1), new Point (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" +──────┐ + One │ + Two │ +──────┘ +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 0, 7, 4), pos); + + menu.CloseAllMenus (); + menu.Frame = new (-1, -2, menu.Frame.Width, menu.Frame.Height); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" + One │ + Two │ +──────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 7, 3), pos); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 5); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" +┌────── +│ One +│ Two +└────── +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 1, 7, 4), pos); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 3); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" +┌────── +│ One +│ Two +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 0, 7, 3), pos); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Negative_Positions_Disabled_Border () + { + var menu = new MenuBar + { + X = -2, + Y = -1, + MenusBorderStyle = LineStyle.None, + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + menu.Layout (); + + Assert.Equal (new (-2, -1), new Point (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" +ne +wo +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + menu.CloseAllMenus (); + menu.Frame = new (-2, -2, menu.Frame.Width, menu.Frame.Height); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" +wo +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 2); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" + On + Tw +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 1); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" + On +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Positive_Positions () + { + var menu = new MenuBar + { + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" +┌──────┐ +│ One │ +│ Two │ +└──────┘ +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 1, 8, 4), pos); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Positive_Positions_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" + One + Two +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + top.Dispose (); + } + + [Fact] + public void Exceptions () + { + Assert.Throws (() => new MenuBarItem ("Test", (MenuItem [])null)); + Assert.Throws (() => new MenuBarItem ("Test", (List)null)); + } + + [Fact] + [AutoInitShutdown] + public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKeyPressed () + { + var newAction = false; + var copyAction = false; + + var menu = new MenuBar + { + Menus = + [ + new ("_File", new MenuItem [] { new ("_New", "", () => newAction = true) }), + new ( + "_Edit", + new MenuItem [] { new ("_Copy", "", () => copyAction = true) } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.False (newAction); + Assert.False (copyAction); + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + + string expected = @" + File Edit +"; + + var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 11, 1), pos); + + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); + Application.MainLoop.RunIteration (); + Assert.False (newAction); // not yet, hot keys don't work if the item is not visible + + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.F))); + Application.MainLoop.RunIteration (); + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); + Application.MainLoop.RunIteration (); + Assert.True (newAction); + Application.Top.Draw (); + + expected = @" + File Edit +"; + + Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + + expected = @" + File Edit +"; + + pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 11, 1), pos); + + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.C))); + Application.MainLoop.RunIteration (); + Assert.True (copyAction); +#endif + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void HotKey_MenuBar_ProcessKeyPressed_Menu_ProcessKey () + { + var newAction = false; + var copyAction = false; + + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] { new ("Copy", "", null) } + ) + ] + }; + + // The real menu + var menu = new MenuBar + { + Menus = + [ + new ( + "_" + expectedMenu.Menus [0].Title, + new MenuItem [] + { + new ( + "_" + expectedMenu.Menus [0].Children [0].Title, + "", + () => newAction = true + ) + } + ), + new ( + "_" + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + "_" + + expectedMenu.Menus [1] + .Children [0] + .Title, + "", + () => copyAction = true + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.False (newAction); + Assert.False (copyAction); + + Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); + Application.MainLoop.RunIteration (); + Assert.True (newAction); + + Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); + Application.MainLoop.RunIteration (); + Assert.True (copyAction); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Key_Open_And_Close_The_MenuBar () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (top.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + Assert.True (top.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + + menu.Key = Key.F10.WithShift; + Assert.False (top.NewKeyDownEvent (Key.F9)); + Assert.False (menu.IsMenuOpen); + + Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); + Assert.True (menu.IsMenuOpen); + Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Theory] + [AutoInitShutdown] + [InlineData ("_File", "_New", "", KeyCode.Space | KeyCode.CtrlMask)] + [InlineData ("Closed", "None", "", KeyCode.Space | KeyCode.CtrlMask, KeyCode.Space | KeyCode.CtrlMask)] + [InlineData ("_File", "_New", "", KeyCode.F9)] + [InlineData ("Closed", "None", "", KeyCode.F9, KeyCode.F9)] + [InlineData ("_File", "_Open", "", KeyCode.F9, KeyCode.CursorDown)] + [InlineData ("_File", "_Save", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown)] + [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown, KeyCode.CursorDown)] + [InlineData ( + "_File", + "_New", + "", + KeyCode.F9, + KeyCode.CursorDown, + KeyCode.CursorDown, + KeyCode.CursorDown, + KeyCode.CursorDown + )] + [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorUp)] + [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorUp)] + [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorUp, KeyCode.CursorDown)] + [InlineData ("Closed", "None", "Open", KeyCode.F9, KeyCode.CursorDown, KeyCode.Enter)] + [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorRight)] + [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorLeft)] + [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorLeft, KeyCode.CursorLeft)] + [InlineData ("_Edit", "_Select All", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorUp)] + [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorDown, KeyCode.CursorLeft)] + [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight)] + [InlineData ("Closed", "None", "New", KeyCode.F9, KeyCode.Enter)] + [InlineData ("Closed", "None", "Quit", KeyCode.F9, KeyCode.CursorUp, KeyCode.Enter)] + [InlineData ("Closed", "None", "Copy", KeyCode.F9, KeyCode.CursorRight, KeyCode.Enter)] + [InlineData ( + "Closed", + "None", + "Find", + KeyCode.F9, + KeyCode.CursorRight, + KeyCode.CursorUp, + KeyCode.CursorUp, + KeyCode.Enter + )] + [InlineData ( + "Closed", + "None", + "Replace", + KeyCode.F9, + KeyCode.CursorRight, + KeyCode.CursorUp, + KeyCode.CursorUp, + KeyCode.CursorDown, + KeyCode.Enter + )] + [InlineData ( + "_Edit", + "F_ind", + "", + KeyCode.F9, + KeyCode.CursorRight, + KeyCode.CursorUp, + KeyCode.CursorUp, + KeyCode.CursorLeft, + KeyCode.Enter + )] + [InlineData ("Closed", "None", "About", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight, KeyCode.Enter)] + + //// Hotkeys + [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F)] + [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.ShiftMask | KeyCode.F)] + [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.Esc)] + [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.F)] + [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.O)] + [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F, KeyCode.ShiftMask | KeyCode.O)] + [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.O)] + [InlineData ("_Edit", "_Copy", "", KeyCode.AltMask | KeyCode.E)] + [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.F)] + [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.AltMask | KeyCode.F)] + [InlineData ("Closed", "None", "Replace", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.R)] + [InlineData ("Closed", "None", "Copy", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)] + [InlineData ("_Edit", "_1st", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3)] + [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D1)] + [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.Enter)] + [InlineData ("Closed", "None", "2", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D2)] + [InlineData ("_Edit", "_5th", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)] + [InlineData ("Closed", "None", "5", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D4, KeyCode.D5)] + [InlineData ("Closed", "None", "About", KeyCode.AltMask | KeyCode.A)] + public void KeyBindings_Navigation_Commands ( + string expectedBarTitle, + string expectedItemTitle, + string expectedAction, + params KeyCode [] keys + ) + { + var miAction = ""; + MenuItem mbiCurrent = null; + MenuItem miCurrent = null; + + var menu = new MenuBar (); + + Func fn = s => + { + miAction = s as string; + + return true; + }; + menu.EnableForDesign (ref fn); + + menu.Key = KeyCode.F9; + menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; + menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; + + menu.MenuClosing += (s, e) => + { + mbiCurrent = null; + miCurrent = null; + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + foreach (Key key in keys) + { + top.NewKeyDownEvent (key); + Application.MainLoop.RunIteration (); + } + + Assert.Equal (expectedBarTitle, mbiCurrent != null ? mbiCurrent.Title : "Closed"); + Assert.Equal (expectedItemTitle, miCurrent != null ? miCurrent.Title : "None"); + Assert.Equal (expectedAction, miAction); + top.Dispose (); + } + + [Theory] + [AutoInitShutdown] + [InlineData ("New", KeyCode.CtrlMask | KeyCode.N)] + [InlineData ("Quit", KeyCode.CtrlMask | KeyCode.Q)] + [InlineData ("Copy", KeyCode.CtrlMask | KeyCode.C)] + [InlineData ("Replace", KeyCode.CtrlMask | KeyCode.H)] + [InlineData ("1", KeyCode.F1)] + [InlineData ("5", KeyCode.CtrlMask | KeyCode.D5)] + public void KeyBindings_Shortcut_Commands (string expectedAction, params KeyCode [] keys) + { + var miAction = ""; + MenuItem mbiCurrent = null; + MenuItem miCurrent = null; + + var menu = new MenuBar (); + + bool FnAction (string s) + { + miAction = s; + + return true; + } + + // Declare a variable for the function + Func fnActionVariable = FnAction; + + menu.EnableForDesign (ref fnActionVariable); + + menu.Key = KeyCode.F9; + menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; + menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; + + menu.MenuClosing += (s, e) => + { + mbiCurrent = null; + miCurrent = null; + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + foreach (KeyCode key in keys) + { + Assert.True (top.NewKeyDownEvent (new (key))); + Application.MainLoop!.RunIteration (); + } + + Assert.Equal (expectedAction, miAction); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Menu_With_Separator () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "_Open", + "Open a file", + () => { }, + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + null, + new ("_Quit", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + menu.OpenMenu (); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File +┌────────────────────────────┐ +│ Open Open a file Ctrl+O │ +├────────────────────────────┤ +│ Quit │ +└────────────────────────────┘", + output + ); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Menu_With_Separator_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "_Open", + "Open a file", + () => { }, + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + null, + new ("_Quit", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + menu.OpenMenu (); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File + Open Open a file Ctrl+O +──────────────────────────── + Quit ", + output + ); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("Open", "", null) }), + new ( + "Edit", + new MenuItem [] { new ("Copy", "", null) } + ) + ] + }; + + // Test without HotKeys first + var menu = new MenuBar + { + Menus = + [ + new ( + "_" + expectedMenu.Menus [0].Title, + new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new ( + "_" + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + "_" + + expectedMenu.Menus [1] + .Children [0] + .Title, + "", + null + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.IsMenuOpen); + top.Draw (); + + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.False (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_With_Top_Init () + { + var win = new Window (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + win.Add (menu); + Toplevel top = new (); + top.Add (win); + Application.Begin (top); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (win.NewKeyDownEvent (menu.Key)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + View.SetClipToScreen (); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_With_Top_Init_With_Parameterless_Run () + { + var win = new Window (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + win.Add (menu); + Toplevel top = new (); + top.Add (win); + + Application.Iteration += (s, a) => + { + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (win.NewKeyDownEvent (menu.Key)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + View.SetClipToScreen (); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Application.RequestStop (); + }; + + Application.Run (top); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init () + { + var win = new Window (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + win.Add (menu); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + RunState rs = Application.Begin (win); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (win.NewKeyDownEvent (menu.Key)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + win.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init_With_Run_T () + { + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + + Application.Iteration += (s, a) => + { + Toplevel top = Application.Top; + Application.LayoutAndDraw(); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (top.NewKeyDownEvent (Key.F9)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (top.SubViews.ElementAt (0).NewKeyDownEvent (Key.CursorRight)); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True ( + ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) + ); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True ( + ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) + ); + View.SetClipToScreen (); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Application.RequestStop (); + }; + + Application.Run ().Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("12", "", null) }), + new ( + "Edit", + new MenuItem [] { new ("Copy", "", null) } + ) + ] + }; + + // Test without HotKeys first + var menu = new MenuBar + { + Menus = + [ + new ( + expectedMenu.Menus [0].Title, + new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new ( + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + expectedMenu.Menus [1].Children [0].Title, + "", + null + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + // Open first + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + // Open second + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + // Close menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + top.Remove (menu); + + // Now test WITH HotKeys + menu = new () + { + Menus = + [ + new ( + "_" + expectedMenu.Menus [0].Title, + new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new ( + "_" + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + "_" + expectedMenu.Menus [1].Children [0].Title, + "", + null + ) + } + ) + ] + }; + + top.Add (menu); + + // Open first + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + // Open second + Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + Application.Top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + // Close menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_Submenus_Alignment_Correct () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "Really Long Sub Menu", + "", + null + ) + } + ), + new ( + "123", + new MenuItem [] { new ("Copy", "", null) } + ), + new ( + "Format", + new MenuItem [] { new ("Word Wrap", "", null) } + ), + new ( + "Help", + new MenuItem [] { new ("About", "", null) } + ), + new ( + "1", + new MenuItem [] { new ("2", "", null) } + ), + new ( + "3", + new MenuItem [] { new ("2", "", null) } + ), + new ( + "Last one", + new MenuItem [] { new ("Test", "", null) } + ) + ] + }; + + MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; + + for (var i = 0; i < expectedMenu.Menus.Length; i++) + { + items [i] = new ( + expectedMenu.Menus [i].Title, + new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } + ); + } + + var menu = new MenuBar { Menus = items }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + for (var i = 0; i < expectedMenu.Menus.Length; i++) + { + menu.OpenMenu (i); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (i), output); + } + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_With_Action_But_Without_MenuItems_Not_Throw () + { + var menu = new MenuBar + { + Menus = + [ + new () { Title = "Test 1", Action = () => { } }, + + new () { Title = "Test 2", Action = () => { } } + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + Assert.True ( + Application.OnKeyUp ( + new KeyEventArgs ( + Key.AltMask + ) + ) + ); // changed to true because Alt activates menu bar +#endif + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBarItem_Children_Null_Does_Not_Throw () + { + var menu = new MenuBar + { + Menus = + [ + new ("Test", "", null) + ] + }; + var top = new Toplevel (); + top.Add (menu); + + Exception exception = Record.Exception (() => menu.NewKeyDownEvent (Key.Space)); + Assert.Null (exception); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuOpened_On_Disabled_MenuItem () + { + MenuItem parent = null; + MenuItem miCurrent = null; + Menu mCurrent = null; + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new MenuBarItem ( + "_New", + new MenuItem [] + { + new ( + "_New doc", + "Creates new doc.", + null, + () => false + ) + } + ), + null, + new ("_Save", "Saves the file.", null) + } + ) + ] + }; + + menu.MenuOpened += (s, e) => + { + parent = e.Parent; + miCurrent = e.MenuItem; + mCurrent = menu._openMenu; + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + // open the menu + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_New", miCurrent.Title); + + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_New", miCurrent.Title); + + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_New", miCurrent.Title); + + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_Save", miCurrent.Title); + + // close the menu + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + Assert.False (menu.IsMenuOpen); + + // open the menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + + // The _New doc is enabled but the sub-menu isn't enabled. Is show but can't be selected and executed + Assert.Equal ("_New", parent.Title); + Assert.Equal ("_New", miCurrent.Parent.Title); + Assert.Equal ("_New doc", miCurrent.Title); + + Assert.True (mCurrent.NewKeyDownEvent (Key.CursorDown)); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_Save", miCurrent.Title); + + Assert.True (mCurrent.NewKeyDownEvent (Key.CursorUp)); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Null (miCurrent); + + // close the menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuOpening_MenuOpened_MenuClosing_Events () + { + var miAction = ""; + var isMenuClosed = true; + var cancelClosing = false; + + var menu = new MenuBar + { + Menus = + [ + new ("_File", new MenuItem [] { new ("_New", "Creates new file.", New) }) + ] + }; + + menu.MenuOpening += (s, e) => + { + Assert.Equal ("_File", e.CurrentMenu.Title); + Assert.Equal ("_New", e.CurrentMenu.Children [0].Title); + Assert.Equal ("Creates new file.", e.CurrentMenu.Children [0].Help); + Assert.Equal (New, e.CurrentMenu.Children [0].Action); + e.CurrentMenu.Children [0].Action (); + Assert.Equal ("New", miAction); + + e.NewMenuBarItem = new ( + "_Edit", + new MenuItem [] { new ("_Copy", "Copies the selection.", Copy) } + ); + }; + + menu.MenuOpened += (s, e) => + { + MenuItem mi = e.MenuItem; + + Assert.Equal ("_Edit", mi.Parent.Title); + Assert.Equal ("_Copy", mi.Title); + Assert.Equal ("Copies the selection.", mi.Help); + Assert.Equal (Copy, mi.Action); + mi.Action (); + Assert.Equal ("Copy", miAction); + }; + + menu.MenuClosing += (s, e) => + { + Assert.False (isMenuClosed); + + if (cancelClosing) + { + e.Cancel = true; + isMenuClosed = false; + } + else + { + isMenuClosed = true; + } + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + isMenuClosed = !menu.IsMenuOpen; + Assert.False (isMenuClosed); + top.Draw (); + + var expected = @" +Edit +┌──────────────────────────────┐ +│ Copy Copies the selection. │ +└──────────────────────────────┘ +"; + DriverAssert.AssertDriverContentsAre (expected, output); + + cancelClosing = true; + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + Assert.False (isMenuClosed); + View.SetClipToScreen (); + top.Draw (); + + expected = @" +Edit +┌──────────────────────────────┐ +│ Copy Copies the selection. │ +└──────────────────────────────┘ +"; + DriverAssert.AssertDriverContentsAre (expected, output); + + cancelClosing = false; + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + Assert.True (isMenuClosed); + View.SetClipToScreen (); + top.Draw (); + + expected = @" +Edit +"; + DriverAssert.AssertDriverContentsAre (expected, output); + + void New () { miAction = "New"; } + + void Copy () { miAction = "Copy"; } + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MouseEvent_Test () + { + MenuItem miCurrent = null; + Menu mCurrent = null; + + var menuBar = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] { new ("_New", "", null), new ("_Open", "", null), new ("_Save", "", null) } + ), + new ( + "_Edit", + new MenuItem [] { new ("_Copy", "", null), new ("C_ut", "", null), new ("_Paste", "", null) } + ) + ] + }; + + menuBar.MenuOpened += (s, e) => + { + miCurrent = e.MenuItem; + mCurrent = menuBar.OpenCurrentMenu; + }; + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + // Click on Edit + Assert.True ( + menuBar.NewMouseEvent ( + new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menuBar } + ) + ); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("_Edit", miCurrent.Parent.Title); + Assert.Equal ("_Copy", miCurrent.Title); + + // Click on Paste + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("_Edit", miCurrent.Parent.Title); + Assert.Equal ("_Paste", miCurrent.Title); + + for (var i = 4; i >= -1; i--) + { + Application.RaiseMouseEvent ( + new () { ScreenPosition = new (10, i), Flags = MouseFlags.ReportMousePosition } + ); + + Assert.True (menuBar.IsMenuOpen); + Menu menu = (Menu)top.SubViews.First (v => v is Menu); + + if (i is < 0 or > 0) + { + Assert.Equal (menu, Application.MouseGrabView); + } + else + { + Assert.Equal (menuBar, Application.MouseGrabView); + } + + Assert.Equal ("_Edit", miCurrent.Parent.Title); + + if (i == 4) + { + Assert.Equal ("_Paste", miCurrent.Title); + } + else if (i == 3) + { + Assert.Equal ("C_ut", miCurrent.Title); + } + else if (i == 2) + { + Assert.Equal ("_Copy", miCurrent.Title); + } + else + { + Assert.Equal ("_Copy", miCurrent.Title); + } + } + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard () + { + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ("Edit", Array.Empty ()), + new ( + "Format", + new MenuItem [] { new ("Wrap", "", null) } + ) + ] + }; + + MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; + + for (var i = 0; i < expectedMenu.Menus.Length; i++) + { + items [i] = new ( + expectedMenu.Menus [i].Title, + expectedMenu.Menus [i].Children.Length > 0 + ? new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } + : Array.Empty () + ); + } + + var menu = new MenuBar { Menus = items }; + + var tf = new TextField { Y = 2, Width = 10 }; + var top = new Toplevel (); + top.Add (menu, tf); + + Application.Begin (top); + Assert.True (tf.HasFocus); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + // Right - Edit has no sub menu; this tests that no sub menu shows + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + Assert.Equal (1, menu._selected); + Assert.Equal (-1, menu._selectedSub); + Assert.Null (menu._openSubMenu); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + // Right - Format + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); + + // Left - Edit + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (Application.RaiseKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + Assert.True (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse () + { + // File Edit Format + //┌──────┐ ┌───────┐ + //│ New │ │ Wrap │ + //└──────┘ └───────┘ + + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ("Edit", new MenuItem [] { }), + new ( + "Format", + new MenuItem [] { new ("Wrap", "", null) } + ) + ] + }; + + var menu = new MenuBar + { + Menus = + [ + new ( + expectedMenu.Menus [0].Title, + new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new (expectedMenu.Menus [1].Title, new MenuItem [] { }), + new ( + expectedMenu.Menus [2].Title, + new MenuItem [] + { + new ( + expectedMenu.Menus [2].Children [0].Title, + "", + null + ) + } + ) + ] + }; + + var tf = new TextField { Y = 2, Width = 10 }; + var top = new Toplevel (); + top.Add (menu, tf); + Application.Begin (top); + + Assert.True (tf.HasFocus); + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.False (menu.IsMenuOpen); + Assert.True (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + public void RemoveAndThenAddMenuBar_ShouldNotChangeWidth () + { + MenuBar menuBar; + MenuBar menuBar2; + + // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, + // TODO: Change this to Window + var w = new View (); + menuBar2 = new (); + menuBar = new (); + w.Width = Dim.Fill (); + w.Height = Dim.Fill (); + w.X = 0; + w.Y = 0; + + w.Visible = true; + + // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, + // TODO: uncomment this. + //w.Modal = false; + w.Title = ""; + menuBar.Width = Dim.Fill (); + menuBar.Height = 1; + menuBar.X = 0; + menuBar.Y = 0; + menuBar.Visible = true; + w.Add (menuBar); + + menuBar2.Width = Dim.Fill (); + menuBar2.Height = 1; + menuBar2.X = 0; + menuBar2.Y = 4; + menuBar2.Visible = true; + w.Add (menuBar2); + + MenuBar [] menuBars = w.SubViews.OfType ().ToArray (); + Assert.Equal (2, menuBars.Length); + + Assert.Equal (Dim.Fill (), menuBars [0].Width); + Assert.Equal (Dim.Fill (), menuBars [1].Width); + + // Goes wrong here + w.Remove (menuBar); + w.Remove (menuBar2); + + w.Add (menuBar); + w.Add (menuBar2); + + // These assertions fail + Assert.Equal (Dim.Fill (), menuBars [0].Width); + Assert.Equal (Dim.Fill (), menuBars [1].Width); + } + + [Fact] + [AutoInitShutdown] + public void Resizing_Close_Menus () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "Open", + "Open a file", + () => { }, + null, + null, + KeyCode.CtrlMask | KeyCode.O + ) + } + ) + ] + }; + var top = new Toplevel (); + top.Add (menu); + RunState rs = Application.Begin (top); + + menu.OpenMenu (); + var firstIteration = false; + Application.RunIteration (ref rs, firstIteration); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File +┌────────────────────────────┐ +│ Open Open a file Ctrl+O │ +└────────────────────────────┘", + output + ); + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); + firstIteration = false; + Application.RunIteration (ref rs, firstIteration); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File", + output + ); + + Application.End (rs); + top.Dispose (); + } + + [Fact] + public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] { new ("_New", "", null), null, new ("_Quit", "", null) } + ) + ] + }; + Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt)); + } + + [Fact] + public void SetMenus_With_Same_HotKey_Does_Not_Throws () + { + var mb = new MenuBar (); + + var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); + + mb.Menus = new [] { i1 }; + mb.Menus = new [] { i1 }; + + Assert.Equal (Key.H, mb.Menus [0].HotKey); + } + + [Fact] + [AutoInitShutdown] + public void ShortCut_Activates () + { + var saveAction = false; + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ( + "_Save", + "Saves the file.", + () => { saveAction = true; }, + null, + null, + (KeyCode)Key.S.WithCtrl + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Application.RaiseKeyDownEvent (Key.S.WithCtrl); + Application.MainLoop.RunIteration (); + + Assert.True (saveAction); + top.Dispose (); + } + + [Fact] + public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () + { + var menuBar = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ("New", "Create New", null, null, null, Key.A.WithCtrl) + } + ) + ] + }; + + Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); + + menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; + + Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _)); + } + + [Fact] + public void UseKeysUpDownAsKeysLeftRight_And_UseSubMenusSingleFrame_Cannot_Be_Both_True () + { + var menu = new MenuBar (); + Assert.False (menu.UseKeysUpDownAsKeysLeftRight); + Assert.False (menu.UseSubMenusSingleFrame); + + menu.UseKeysUpDownAsKeysLeftRight = true; + Assert.True (menu.UseKeysUpDownAsKeysLeftRight); + Assert.False (menu.UseSubMenusSingleFrame); + + menu.UseSubMenusSingleFrame = true; + Assert.False (menu.UseKeysUpDownAsKeysLeftRight); + Assert.True (menu.UseSubMenusSingleFrame); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_False_By_Keyboard () + { + var menu = new MenuBar + { + Menus = new MenuBarItem [] + { + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ("Sub-Menu 1", "", null), + new ("Sub-Menu 2", "", null) + } + ), + new ("Three", "", null) + } + ) + } + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│┌─────────────┐ +│ Three ││ Sub-Menu 1 │ +└────────┘│ Sub-Menu 2 │ + └─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_False_By_Mouse () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + menu.NewMouseEvent ( + new () + { + Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) + } + ); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│┌─────────────┐ +│ Three ││ Sub-Menu 1 │ +└────────┘│ Sub-Menu 2 │ + └─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 25, 7), pos); + + Assert.False ( + menu.NewMouseEvent ( + new () + { + Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) + } + ) + ); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + menu.NewMouseEvent ( + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + ); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_False_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + menu.UseKeysUpDownAsKeysLeftRight = true; + menu.BeginInit (); + menu.EndInit (); + + menu.OpenMenu (); + menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); + Assert.True (menu.IsMenuOpen); + + menu.Draw (); + menu._openMenu.Draw (); + + var expected = @" + Numbers + One + Two ► + Three "; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); + menu.Draw (); + menu._openMenu.Draw (); + menu.OpenCurrentMenu.Draw (); + + expected = @" + Numbers + One + Two ► Sub-Menu 1 + Three Sub-Menu 2"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_By_Keyboard () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + menu.UseSubMenusSingleFrame = true; + Assert.True (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Enter)); + top.Draw (); + + expected = @" + Numbers +┌─────────────┐ +│◄ Two │ +├─────────────┤ +│ Sub-Menu 1 │ +│ Sub-Menu 2 │ +└─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 15, 7), pos); + + Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.Enter)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_By_Mouse () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + menu.UseSubMenusSingleFrame = true; + Assert.True (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.False (menu.NewMouseEvent (new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) })); + top.Draw (); + + expected = @" + Numbers +┌─────────────┐ +│◄ Two │ +├─────────────┤ +│ Sub-Menu 1 │ +│ Sub-Menu 2 │ +└─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 15, 7), pos); + + menu.NewMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) }); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.False (menu.NewMouseEvent (new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top })); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + menu.UseSubMenusSingleFrame = true; + menu.BeginInit (); + menu.EndInit (); + + menu.OpenMenu (); + Assert.True (menu.IsMenuOpen); + + menu.Draw (); + menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); + menu._openMenu.Draw (); + + var expected = @" + Numbers + One + Two ► + Three "; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + menu.Draw (); + menu._openMenu.Draw (); + menu.OpenCurrentMenu.Draw (); + + expected = @" + Numbers +◄ Two +───────────── + Sub-Menu 1 + Sub-Menu 2 "; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_Without_Border () + { + var menu = new MenuBar + { + UseSubMenusSingleFrame = true, + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.True (menu.UseSubMenusSingleFrame); + Assert.Equal (LineStyle.None, menu.MenusBorderStyle); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + top.Draw (); + + expected = @" + Numbers + One + Two ► + Three +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 4), pos); + + menu.NewMouseEvent ( + new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) } + ); + top.Draw (); + + expected = @" + Numbers +◄ Two +───────────── + Sub-Menu 1 + Sub-Menu 2 +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 13, 5), pos); + + menu.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) } + ); + top.Draw (); + + expected = @" + Numbers + One + Two ► + Three +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 4), pos); + + menu.NewMouseEvent ( + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + ); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Visible_False_Key_Does_Not_Open_And_Close_All_Opened_Menus () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.Visible); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + + menu.Visible = false; + Assert.False (menu.IsMenuOpen); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanFocus_True_Key_Esc_Exit_Toplevel_If_IsMenuOpen_False () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ], + CanFocus = true + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.CanFocus); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + + Assert.True (menu.NewKeyDownEvent (Key.Esc)); + Assert.False (menu.IsMenuOpen); + + Assert.False (menu.NewKeyDownEvent (Key.Esc)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + // Defines the expected strings for a Menu. Currently supports + // - MenuBar with any number of MenuItems + // - Each top-level MenuItem can have a SINGLE sub-menu + // + // TODO: Enable multiple sub-menus + // TODO: Enable checked sub-menus + // TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?) + // + // E.g: + // + // File Edit + // New Copy + public class ExpectedMenuBar : MenuBar + { + private FakeDriver _d = (FakeDriver)Application.Driver; + + // The expected strings when the menu is closed + public string ClosedMenuText => MenuBarText + "\n"; + + public string ExpectedBottomRow (int i) + { + return $"{Glyphs.LLCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.LRCorner} \n"; + } + + // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) + public string ExpectedMenuItemRow (int i) { return $"{Glyphs.VLine} {Menus [i].Children [0].Title} {Glyphs.VLine} \n"; } + + // The full expected string for an open sub menu + public string ExpectedSubMenuOpen (int i) + { + return ClosedMenuText + + (Menus [i].Children.Length > 0 + ? ExpectedPadding (i) + + ExpectedTopRow (i) + + ExpectedPadding (i) + + ExpectedMenuItemRow (i) + + ExpectedPadding (i) + + ExpectedBottomRow (i) + : ""); + } + + // Define expected menu frame + // "┌──────┐" + // "│ New │" + // "└──────┘" + // + // The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated + // 1 space before the Title and 2 spaces after the Title/Check/Help + public string ExpectedTopRow (int i) + { + return $"{Glyphs.ULCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.URCorner} \n"; + } + + // Each MenuBar title has a 1 space pad on each side + // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs + public string MenuBarText + { + get + { + var txt = string.Empty; + + foreach (MenuBarItem m in Menus) + { + txt += " " + m.Title + " "; + } + + return txt; + } + } + + // Padding for the X of the sub menu Frame + // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created + private string ExpectedPadding (int i) + { + var n = 0; + + while (i > 0) + { + n += Menus [i - 1].TitleLength + 2; + i--; + } + + return new (' ', n); + } + } + + private class CustomWindow : Window + { + public CustomWindow () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + Add (menu); + } + } +} diff --git a/Tests/UnitTests/Views/MenuTests.cs b/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs similarity index 97% rename from Tests/UnitTests/Views/MenuTests.cs rename to Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs index d00c4a375..2c8386004 100644 --- a/Tests/UnitTests/Views/MenuTests.cs +++ b/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs @@ -4,10 +4,10 @@ namespace Terminal.Gui.ViewsTests; -public class MenuTests +public class Menuv1Tests { private readonly ITestOutputHelper _output; - public MenuTests (ITestOutputHelper output) { _output = output; } + public Menuv1Tests (ITestOutputHelper output) { _output = output; } // TODO: Create more low-level unit tests for Menu and MenuItem diff --git a/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs b/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs new file mode 100644 index 000000000..a4feda11d --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs @@ -0,0 +1,124 @@ + +namespace Terminal.Gui.ViewsTests; + +public class FlagSelectorTests +{ + [Fact] + public void Initialization_ShouldSetDefaults() + { + var flagSelector = new FlagSelector(); + + Assert.True(flagSelector.CanFocus); + Assert.Equal(Dim.Auto(DimAutoStyle.Content), flagSelector.Width); + Assert.Equal(Dim.Auto(DimAutoStyle.Content), flagSelector.Height); + Assert.Equal(Orientation.Vertical, flagSelector.Orientation); + } + + [Fact] + public void SetFlags_WithDictionary_ShouldSetFlags() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + + Assert.Equal(flags, flagSelector.Flags); + } + + [Fact] + public void SetFlags_WithEnum_ShouldSetFlags() + { + var flagSelector = new FlagSelector(); + + flagSelector.SetFlags(); + + var expectedFlags = Enum.GetValues() + .ToDictionary(f => Convert.ToUInt32(f), f => f.ToString()); + + Assert.Equal(expectedFlags, flagSelector.Flags); + } + + [Fact] + public void SetFlags_WithEnumAndCustomNames_ShouldSetFlags() + { + var flagSelector = new FlagSelector(); + + flagSelector.SetFlags(f => f switch + { + FlagSelectorStyles.ShowNone => "Show None Value", + FlagSelectorStyles.ShowValueEdit => "Show Value Editor", + FlagSelectorStyles.All => "Everything", + _ => f.ToString() + }); + + var expectedFlags = Enum.GetValues() + .ToDictionary(f => Convert.ToUInt32(f), f => f switch + { + FlagSelectorStyles.ShowNone => "Show None Value", + FlagSelectorStyles.ShowValueEdit => "Show Value Editor", + FlagSelectorStyles.All => "Everything", + _ => f.ToString() + }); + + Assert.Equal(expectedFlags, flagSelector.Flags); + } + + [Fact] + public void Value_Set_ShouldUpdateCheckedState() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + flagSelector.Value = 1; + + var checkBox = flagSelector.SubViews.OfType().First(cb => (uint)cb.Data == 1); + Assert.Equal(CheckState.Checked, checkBox.CheckedState); + + checkBox = flagSelector.SubViews.OfType().First(cb => (uint)cb.Data == 2); + Assert.Equal(CheckState.UnChecked, checkBox.CheckedState); + } + + [Fact] + public void Styles_Set_ShouldCreateSubViews() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + flagSelector.Styles = FlagSelectorStyles.ShowNone; + + Assert.Contains(flagSelector.SubViews, sv => sv is CheckBox cb && cb.Title == "None"); + } + + [Fact] + public void ValueChanged_Event_ShouldBeRaised() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + bool eventRaised = false; + flagSelector.ValueChanged += (sender, args) => eventRaised = true; + + flagSelector.Value = 1; + + Assert.True(eventRaised); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs new file mode 100644 index 000000000..238a39c64 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs @@ -0,0 +1,22 @@ +using Xunit.Abstractions; + +//using static Terminal.Gui.ViewTests.MenuTests; + +namespace Terminal.Gui.ViewsTests; + +public class MenuBarItemTests () +{ + [Fact] + public void Constructors_Defaults () + { + var menuBarItem = new MenuBarItemv2 (); + Assert.Null (menuBarItem.PopoverMenu); + Assert.Null (menuBarItem.TargetView); + + menuBarItem = new MenuBarItemv2 (targetView: null, command: Command.NotBound, commandText: null, popoverMenu: null); + Assert.Null (menuBarItem.PopoverMenu); + Assert.Null (menuBarItem.TargetView); + + + } +} diff --git a/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs new file mode 100644 index 000000000..0e7cf4140 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs @@ -0,0 +1,14 @@ +using Xunit.Abstractions; + +//using static Terminal.Gui.ViewTests.MenuTests; + +namespace Terminal.Gui.ViewsTests; + +public class MenuItemTests () +{ + [Fact] + public void Constructors_Defaults () + { + + } +} diff --git a/Tests/UnitTestsParallelizable/Views/MenuTests.cs b/Tests/UnitTestsParallelizable/Views/MenuTests.cs new file mode 100644 index 000000000..f29d7273f --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/MenuTests.cs @@ -0,0 +1,17 @@ +using Xunit.Abstractions; + +//using static Terminal.Gui.ViewTests.MenuTests; + +namespace Terminal.Gui.ViewsTests; + +public class MenuTests () +{ + [Fact] + public void Constructors_Defaults () + { + var menu = new Menuv2 { }; + Assert.Empty (menu.Title); + Assert.Empty (menu.Text); + } + +} diff --git a/UICatalog/KeyBindingsDialog.cs b/UICatalog/KeyBindingsDialog.cs deleted file mode 100644 index 73ce9c6af..000000000 --- a/UICatalog/KeyBindingsDialog.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Terminal.Gui; - -namespace UICatalog; - -internal class KeyBindingsDialog : Dialog -{ - // TODO: Update to use Key instead of KeyCode - private static readonly Dictionary CurrentBindings = new (); - - private readonly ObservableCollection _commands; - private readonly ListView _commandsListView; - private readonly Label _keyLabel; - - public KeyBindingsDialog () - { - Title = "Keybindings"; - - //Height = Dim.Percent (80); - //Width = Dim.Percent (80); - if (ViewTracker.Instance == null) - { - ViewTracker.Initialize (); - } - - // known commands that views can support - _commands = new (Enum.GetValues (typeof (Command)).Cast ().ToArray ()); - - _commandsListView = new ListView - { - Width = Dim.Percent (50), - Height = Dim.Fill (Dim.Func (() => IsInitialized ? SubViews.First (view => view.Y.Has (out _)).Frame.Height : 1)), - Source = new ListWrapper (_commands), - SelectedItem = 0 - }; - - Add (_commandsListView); - - _keyLabel = new Label { Text = "Key: None", Width = Dim.Fill (), X = Pos.Percent (50), Y = 0 }; - Add (_keyLabel); - - var btnChange = new Button { X = Pos.Percent (50), Y = 1, Text = "Ch_ange" }; - Add (btnChange); - btnChange.Accepting += RemapKey; - - var close = new Button { Text = "Ok" }; - - close.Accepting += (s, e) => - { - Application.RequestStop (); - ViewTracker.Instance.StartUsingNewKeyMap (CurrentBindings); - }; - AddButton (close); - - var cancel = new Button { Text = "Cancel" }; - cancel.Accepting += (s, e) => Application.RequestStop (); - AddButton (cancel); - - // Register event handler as the last thing in constructor to prevent early calls - // before it is even shown (e.g. OnHasFocusChanging) - _commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged; - - // Setup to show first ListView entry - SetTextBoxToShowBinding (_commands.First ()); - } - - private void CommandsListView_SelectedItemChanged (object sender, ListViewItemEventArgs obj) { SetTextBoxToShowBinding ((Command)obj.Value); } - - private void RemapKey (object sender, EventArgs e) - { - Command cmd = _commands [_commandsListView.SelectedItem]; - KeyCode? key = null; - - // prompt user to hit a key - var dlg = new Dialog { Title = "Enter Key" }; - - dlg.KeyDown += (s, k) => - { - key = k.KeyCode; - Application.RequestStop (); - }; - Application.Run (dlg); - dlg.Dispose (); - - if (key.HasValue) - { - CurrentBindings [cmd] = key.Value; - SetTextBoxToShowBinding (cmd); - } - } - - private void SetTextBoxToShowBinding (Command cmd) - { - if (CurrentBindings.ContainsKey (cmd)) - { - _keyLabel.Text = "Key: " + CurrentBindings [cmd]; - } - else - { - _keyLabel.Text = "Key: None"; - } - - SetNeedsDraw (); - } - - /// Tracks views as they are created in UICatalog so that their keybindings can be managed. - private class ViewTracker - { - /// All views seen so far and a bool to indicate if we have applied keybindings to them - private readonly Dictionary _knownViews = new (); - - private readonly object _lockKnownViews = new (); - private Dictionary _keybindings; - - private ViewTracker (View top) - { - RecordView (top); - - // Refresh known windows - Application.AddTimeout ( - TimeSpan.FromMilliseconds (100), - () => - { - lock (_lockKnownViews) - { - RecordView (Application.Top); - - ApplyKeyBindingsToAllKnownViews (); - } - - return true; - } - ); - } - - public static ViewTracker Instance { get; private set; } - internal static void Initialize () { Instance = new ViewTracker (Application.Top); } - - internal void StartUsingNewKeyMap (Dictionary currentBindings) - { - lock (_lockKnownViews) - { - // change our knowledge of what keys to bind - _keybindings = currentBindings; - - // Mark that we have not applied the key bindings yet to any views - foreach (View view in _knownViews.Keys) - { - _knownViews [view] = false; - } - } - } - - private void ApplyKeyBindingsToAllKnownViews () - { - if (_keybindings == null) - { - return; - } - - // Key is the view Value is whether we have already done it - foreach (KeyValuePair viewDone in _knownViews) - { - View view = viewDone.Key; - bool done = viewDone.Value; - - if (done) - { - // we have already applied keybindings to this view - continue; - } - - HashSet supported = new (view.GetSupportedCommands ()); - - foreach (KeyValuePair kvp in _keybindings) - { - // if the view supports the keybinding - if (supported.Contains (kvp.Key)) - { - // if the key was bound to any other commands clear that - view.KeyBindings.Remove (kvp.Value); - view.KeyBindings.Add (kvp.Value, kvp.Key); - } - - // mark that we have done this view so don't need to set keybindings again on it - _knownViews [view] = true; - } - } - } - - private void RecordView (View view) - { - if (!_knownViews.ContainsKey (view)) - { - _knownViews.Add (view, false); - } - - // may already have subviews that were added to it - // before we got to it - foreach (View sub in view.SubViews) - { - RecordView (sub); - } - - view.SubViewAdded += (s, e) => RecordView (e.SubView); - } - } -} diff --git a/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 028dedfba..60bdb954f 100644 --- a/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -146,13 +146,13 @@ public class CharacterMap : Scenario top.Add (_categoryList); - var menu = new MenuBar + var menu = new MenuBarv2 { Menus = [ new ( "_File", - new MenuItem [] + new MenuItemv2 [] { new ( "_Quit", @@ -163,7 +163,7 @@ public class CharacterMap : Scenario ), new ( "_Options", - new [] { CreateMenuShowWidth () } + new MenuItemv2 [] { CreateMenuShowWidth () } ) ] }; @@ -305,16 +305,19 @@ public class CharacterMap : Scenario ); } - private MenuItem CreateMenuShowWidth () + private MenuItemv2 CreateMenuShowWidth () { - var item = new MenuItem { Title = "_Show Glyph Width" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _charMap?.ShowGlyphWidths; + CheckBox cb = new () + { + Title = "_Show Glyph Width", + CheckedState = _charMap!.ShowGlyphWidths ? CheckState.Checked : CheckState.None + }; + var item = new MenuItemv2 { CommandView = cb }; item.Action += () => { if (_charMap is { }) { - _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked)!; + _charMap.ShowGlyphWidths = cb.CheckedState == CheckState.Checked; } }; diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index ca1d69457..9312ff7f8 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -1,5 +1,4 @@ -using System.IO; -using System.Text; +using System.Text; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -14,7 +13,7 @@ public class HexEditor : Scenario { private string _fileName = "demo.bin"; private HexView _hexView; - private MenuItem _miAllowEdits; + private MenuItemv2 _miAllowEdits; private bool _saved = true; private Shortcut _scAddress; private Shortcut _scInfo; @@ -48,13 +47,13 @@ public class HexEditor : Scenario app.Add (_hexView); - var menu = new MenuBar + var menu = new MenuBarv2 { Menus = [ new ( "_File", - new MenuItem [] + new MenuItemv2 [] { new ("_New", "", () => New ()), new ("_Open", "", () => Open ()), @@ -65,7 +64,7 @@ public class HexEditor : Scenario ), new ( "_Edit", - new MenuItem [] + new MenuItemv2 [] { new ("_Copy", "", () => Copy ()), new ("C_ut", "", () => Cut ()), @@ -74,7 +73,7 @@ public class HexEditor : Scenario ), new ( "_Options", - new [] + new MenuItemv2 [] { _miAllowEdits = new ( "_AllowEdits", @@ -82,14 +81,19 @@ public class HexEditor : Scenario () => ToggleAllowEdits () ) { - Checked = _hexView.AllowEdits, - CheckType = MenuItemCheckStyle - .Checked + } } ) ] }; + + CheckBox cb = new CheckBox () + { + Title = _miAllowEdits.Title, + CheckedState = _hexView.AllowEdits ? CheckState.Checked : CheckState.None, + }; + _miAllowEdits.CommandView = cb; app.Add (menu); var addressWidthUpDown = new NumericUpDown @@ -285,5 +289,14 @@ public class HexEditor : Scenario } } - private void ToggleAllowEdits () { _hexView.AllowEdits = (bool)(_miAllowEdits.Checked = !_miAllowEdits.Checked); } + private void ToggleAllowEdits () + { + CheckBox? cb = _miAllowEdits.CommandView as CheckBox; + if (cb is null) + { + return; + } + + _hexView.AllowEdits = cb.CheckedState == CheckState.Checked; + } } diff --git a/UICatalog/Scenarios/MenusV2.cs b/UICatalog/Scenarios/MenusV2.cs index 9239f527b..9a34ea410 100644 --- a/UICatalog/Scenarios/MenusV2.cs +++ b/UICatalog/Scenarios/MenusV2.cs @@ -439,7 +439,7 @@ public class MenusV2 : Scenario _menuBgColorCp.ColorChanged += (sender, args) => { - menu.ColorScheme = menu.ColorScheme with + menu.ColorScheme = menu.ColorScheme! with { Normal = new (menu.ColorScheme.Normal.Foreground, args.CurrentValue) }; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 4334aabb4..f41f5e354 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -1,7 +1,4 @@ global using Attribute = Terminal.Gui.Attribute; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Parsing; @@ -9,29 +6,25 @@ using System.Data; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; -using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; -using static Terminal.Gui.ConfigurationManager; -using Command = Terminal.Gui.Command; using Serilog; using Serilog.Core; using Serilog.Events; -using ILogger = Microsoft.Extensions.Logging.ILogger; -using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; using Terminal.Gui; +using static Terminal.Gui.ConfigurationManager; +using Command = Terminal.Gui.Command; +using ILogger = Microsoft.Extensions.Logging.ILogger; #nullable enable namespace UICatalog; /// -/// UI Catalog is a comprehensive sample library and test app for Terminal.Gui. It provides a simple UI for adding to the +/// UI Catalog is a comprehensive sample library and test app for Terminal.Gui. It provides a simple UI for adding to +/// the /// catalog of scenarios. /// /// @@ -50,85 +43,14 @@ namespace UICatalog; /// /// /// -public class UICatalogApp +public class UICatalog { - private static int _cachedCategoryIndex; - - // When a scenario is run, the main app is killed. These items - // are therefore cached so that when the scenario exits the - // main app UI can be restored to previous state - private static int _cachedScenarioIndex; - private static string? _cachedTheme = string.Empty; - private static ObservableCollection? _categories; - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private static readonly FileSystemWatcher _currentDirWatcher = new (); - - private static ViewDiagnosticFlags _diagnosticFlags; private static string _forceDriver = string.Empty; - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private static readonly FileSystemWatcher _homeDirWatcher = new (); - - private static bool _isFirstRunning = true; - private static Options _options; - private static ObservableCollection? _scenarios; - - private const string LOGFILE_LOCATION = "logs"; - private static string _logFilePath = string.Empty; - private static readonly LoggingLevelSwitch _logLevelSwitch = new (); - - // If set, holds the scenario the user selected - private static Scenario? _selectedScenario; - private static MenuBarItem? _themeMenuBarItem; - private static MenuItem []? _themeMenuItems; - private static string _topLevelColorScheme = string.Empty; - - [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true)] - [JsonPropertyName ("UICatalog.StatusBar")] - public static bool ShowStatusBar { get; set; } = true; - - /// - /// Gets the message displayed in the About Box. `public` so it can be used from Unit tests. - /// - /// - public static string GetAboutBoxMessage () - { - // NOTE: Do not use multiline verbatim strings here. - // WSL gets all confused. - StringBuilder msg = new (); - msg.AppendLine ("UI Catalog: A comprehensive sample library and test app for"); - msg.AppendLine (); - - msg.AppendLine ( - """ - _______ _ _ _____ _ - |__ __| (_) | | / ____| (_) - | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ - | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | - | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | - |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| - """); - msg.AppendLine (); - msg.AppendLine ("v2 - Pre-Alpha"); - msg.AppendLine (); - msg.AppendLine ("https://github.com/gui-cs/Terminal.Gui"); - - return msg.ToString (); - } - - private static void ConfigFileChanged (object sender, FileSystemEventArgs e) - { - if (Application.Top == null) - { - return; - } - - // TODO: This is a hack. Figure out how to ensure that the file is fully written before reading it. - //Thread.Sleep (500); - Load (); - Apply (); - } + public static string LogFilePath { get; set; } = string.Empty; + public static LoggingLevelSwitch LogLevelSwitch { get; } = new (); + public const string LOGFILE_LOCATION = "logs"; + public static UICatalogCommandLineOptions Options { get; set; } private static int Main (string [] args) { @@ -139,18 +61,18 @@ public class UICatalogApp CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); } - _scenarios = Scenario.GetScenarios (); - _categories = Scenario.GetAllCategories (); + UICatalogTopLevel.CachedScenarios = Scenario.GetScenarios (); + UICatalogTopLevel.CachedCategories = Scenario.GetAllCategories (); // Process command line args // If no driver is provided, the default driver is used. Option driverOption = new Option ("--driver", "The IConsoleDriver to use.").FromAmong ( Application.GetDriverTypes () - .Where (d=>!typeof (IConsoleDriverFacade).IsAssignableFrom (d)) - .Select (d => d!.Name) - .Union (["v2","v2win","v2net"]) - .ToArray () + .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d)) + .Select (d => d!.Name) + .Union (["v2", "v2win", "v2net"]) + .ToArray () ); driverOption.AddAlias ("-d"); driverOption.AddAlias ("--d"); @@ -171,11 +93,12 @@ public class UICatalogApp resultsFile.AddAlias ("--f"); // what's the app name? - _logFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}"; - Option debugLogLevel = new Option ("--debug-log-level", $"The level to use for logging (debug console and {_logFilePath})").FromAmong ( + LogFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}"; + + Option debugLogLevel = new Option ("--debug-log-level", $"The level to use for logging (debug console and {LogFilePath})").FromAmong ( Enum.GetNames () ); - debugLogLevel.SetDefaultValue("Warning"); + debugLogLevel.SetDefaultValue ("Warning"); debugLogLevel.AddAlias ("-dl"); debugLogLevel.AddAlias ("--dl"); @@ -185,9 +108,9 @@ public class UICatalogApp "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.", getDefaultValue: () => "none" ).FromAmong ( - _scenarios.Select (s => s.GetName ()) - .Append ("none") - .ToArray () + UICatalogTopLevel.CachedScenarios.Select (s => s.GetName ()) + .Append ("none") + .ToArray () ); var rootCommand = new RootCommand ("A comprehensive sample library and test app for Terminal.Gui") @@ -198,7 +121,7 @@ public class UICatalogApp rootCommand.SetHandler ( context => { - var options = new Options + var options = new UICatalogCommandLineOptions { Scenario = context.ParseResult.GetValueForArgument (scenarioArgument), Driver = context.ParseResult.GetValueForOption (driverOption) ?? string.Empty, @@ -210,7 +133,7 @@ public class UICatalogApp }; // See https://github.com/dotnet/command-line-api/issues/796 for the rationale behind this hackery - _options = options; + Options = options; } ); @@ -227,16 +150,16 @@ public class UICatalogApp return 0; } - Scenario.BenchmarkTimeout = _options.BenchmarkTimeout; + Scenario.BenchmarkTimeout = Options.BenchmarkTimeout; Logging.Logger = CreateLogger (); - UICatalogMain (_options); + UICatalogMain (Options); return 0; } - private static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel) + public static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel) { return logLevel switch { @@ -254,13 +177,14 @@ public class UICatalogApp private static ILogger CreateLogger () { // Configure Serilog to write logs to a file - _logLevelSwitch.MinimumLevel = LogLevelToLogEventLevel(Enum.Parse (_options.DebugLogLevel)); + LogLevelSwitch.MinimumLevel = LogLevelToLogEventLevel (Enum.Parse (Options.DebugLogLevel)); + Log.Logger = new LoggerConfiguration () - .MinimumLevel.ControlledBy (_logLevelSwitch) + .MinimumLevel.ControlledBy (LogLevelSwitch) .Enrich.FromLogContext () // Enables dynamic enrichment .WriteTo.Debug () .WriteTo.File ( - _logFilePath, + LogFilePath, rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger (); @@ -278,35 +202,6 @@ public class UICatalogApp return loggerFactory.CreateLogger ("Global Logger"); } - public static void OpenUrl (string url) - { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) - { - url = url.Replace ("&", "^&"); - Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); - } - else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) - { - using var process = new Process - { - StartInfo = new () - { - FileName = "xdg-open", - Arguments = url, - RedirectStandardError = true, - RedirectStandardOutput = true, - CreateNoWindow = true, - UseShellExecute = false - } - }; - process.Start (); - } - else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) - { - Process.Start ("open", url); - } - } - /// /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is /// killed and the Scenario is run as though it were Application.Top. When the Scenario exits, this function exits. @@ -322,25 +217,31 @@ public class UICatalogApp Application.Init (driverName: _forceDriver); - if (_cachedTheme is null) + if (string.IsNullOrWhiteSpace (UICatalogTopLevel.CachedTheme)) { - _cachedTheme = Themes?.Theme; + UICatalogTopLevel.CachedTheme = Themes?.Theme; } else { - Themes!.Theme = _cachedTheme; + Themes!.Theme = UICatalogTopLevel.CachedTheme; Apply (); } Application.Run ().Dispose (); Application.Shutdown (); - return _selectedScenario!; + return UICatalogTopLevel.CachedSelectedScenario!; } + [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] + private static readonly FileSystemWatcher _currentDirWatcher = new (); + + [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] + private static readonly FileSystemWatcher _homeDirWatcher = new (); + private static void StartConfigFileWatcher () { - // Setup a file system watcher for `./.tui/` + // Set up a file system watcher for `./.tui/` _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite; string assemblyLocation = Assembly.GetExecutingAssembly ().Location; @@ -364,7 +265,7 @@ public class UICatalogApp _currentDirWatcher.Path = tuiDir; _currentDirWatcher.Filter = "*config.json"; - // Setup a file system watcher for `~/.tui/` + // Set up a file system watcher for `~/.tui/` _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite; var f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); tuiDir = Path.Combine (f.FullName, ".tui"); @@ -399,7 +300,18 @@ public class UICatalogApp _homeDirWatcher.Created -= ConfigFileChanged; } - private static void UICatalogMain (Options options) + private static void ConfigFileChanged (object sender, FileSystemEventArgs e) + { + if (Application.Top == null) + { + return; + } + + Load (); + Apply (); + } + + private static void UICatalogMain (UICatalogCommandLineOptions options) { StartConfigFileWatcher (); @@ -411,17 +323,15 @@ public class UICatalogApp // run it and exit when done. if (options.Scenario != "none") { - _topLevelColorScheme = "Base"; - - int item = _scenarios!.IndexOf ( - _scenarios!.FirstOrDefault ( + int item = UICatalogTopLevel.CachedScenarios!.IndexOf ( + UICatalogTopLevel.CachedScenarios!.FirstOrDefault ( s => s.GetName () .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase) )!); - _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ())!; + UICatalogTopLevel.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogTopLevel.CachedScenarios [item].GetType ())!; - BenchmarkResults? results = RunScenario (_selectedScenario, options.Benchmark); + BenchmarkResults? results = RunScenario (UICatalogTopLevel.CachedSelectedScenario, options.Benchmark); if (results is { }) { @@ -450,12 +360,13 @@ public class UICatalogApp while (RunUICatalogTopLevel () is { } scenario) { VerifyObjectsWereDisposed (); - Themes!.Theme = _cachedTheme!; + Themes!.Theme = UICatalogTopLevel.CachedTheme!; Apply (); - scenario.TopLevelColorScheme = _topLevelColorScheme; + scenario.TopLevelColorScheme = UICatalogTopLevel.CachedTopLevelColorScheme!; #if DEBUG_IDISPOSABLE View.DebugIDisposable = true; + // Measure how long it takes for the app to shut down var sw = new Stopwatch (); string scenarioName = scenario.GetName (); @@ -501,7 +412,7 @@ public class UICatalogApp } Application.Init (driverName: _forceDriver); - scenario.TopLevelColorScheme = _topLevelColorScheme; + scenario.TopLevelColorScheme = UICatalogTopLevel.CachedTopLevelColorScheme!; if (benchmark) { @@ -527,11 +438,11 @@ public class UICatalogApp private static void BenchmarkAllScenarios () { - List resultsList = new (); + List resultsList = []; var maxScenarios = 5; - foreach (Scenario s in _scenarios!) + foreach (Scenario s in UICatalogTopLevel.CachedScenarios!) { resultsList.Add (RunScenario (s, true)!); maxScenarios--; @@ -542,144 +453,146 @@ public class UICatalogApp } } - if (resultsList.Count > 0) + if (resultsList.Count <= 0) { - if (!string.IsNullOrEmpty (_options.ResultsFile)) - { - string output = JsonSerializer.Serialize ( - resultsList, - new JsonSerializerOptions - { - WriteIndented = true - }); - - using StreamWriter file = File.CreateText (_options.ResultsFile); - file.Write (output); - file.Close (); - - return; - } - - Application.Init (); - - var benchmarkWindow = new Window - { - Title = "Benchmark Results" - }; - - if (benchmarkWindow.Border is { }) - { - benchmarkWindow.Border.Thickness = new (0, 0, 0, 0); - } - - TableView resultsTableView = new () - { - Width = Dim.Fill (), - Height = Dim.Fill () - }; - - // TableView provides many options for table headers. For simplicity we turn all - // of these off. By enabling FullRowSelect and turning off headers, TableView looks just - // like a ListView - resultsTableView.FullRowSelect = true; - resultsTableView.Style.ShowHeaders = true; - resultsTableView.Style.ShowHorizontalHeaderOverline = false; - resultsTableView.Style.ShowHorizontalHeaderUnderline = true; - resultsTableView.Style.ShowHorizontalBottomline = false; - resultsTableView.Style.ShowVerticalCellLines = true; - resultsTableView.Style.ShowVerticalHeaderLines = true; - - /* By default TableView lays out columns at render time and only - * measures y rows of data at a time. Where y is the height of the - * console. This is for the following reasons: - * - * - Performance, when tables have a large amount of data - * - Defensive, prevents a single wide cell value pushing other - * columns off screen (requiring horizontal scrolling - * - * In the case of UICatalog here, such an approach is overkill so - * we just measure all the data ourselves and set the appropriate - * max widths as ColumnStyles - */ - //int longestName = _scenarios!.Max (s => s.GetName ().Length); - - //resultsTableView.Style.ColumnStyles.Add ( - // 0, - // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } - // ); - //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); - //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem; - - // TableView typically is a grid where nav keys are biased for moving left/right. - resultsTableView.KeyBindings.Remove (Key.Home); - resultsTableView.KeyBindings.Add (Key.Home, Command.Start); - resultsTableView.KeyBindings.Remove (Key.End); - resultsTableView.KeyBindings.Add (Key.End, Command.End); - - // Ideally, TableView.MultiSelect = false would turn off any keybindings for - // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for - // a shortcut to About. - resultsTableView.MultiSelect = false; - - var dt = new DataTable (); - - dt.Columns.Add (new DataColumn ("Scenario", typeof (string))); - dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan))); - dt.Columns.Add (new DataColumn ("Refreshed", typeof (int))); - dt.Columns.Add (new DataColumn ("LaidOut", typeof (int))); - dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int))); - dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int))); - dt.Columns.Add (new DataColumn ("Updated", typeof (int))); - dt.Columns.Add (new DataColumn ("Iterations", typeof (int))); - - foreach (BenchmarkResults r in resultsList) - { - dt.Rows.Add ( - r.Scenario, - r.Duration, - r.RefreshedCount, - r.LaidOutCount, - r.ClearedContentCount, - r.DrawCompleteCount, - r.UpdatedCount, - r.IterationCount - ); - } - - BenchmarkResults totalRow = new () - { - Scenario = "TOTAL", - Duration = new (resultsList.Sum (r => r.Duration.Ticks)), - RefreshedCount = resultsList.Sum (r => r.RefreshedCount), - LaidOutCount = resultsList.Sum (r => r.LaidOutCount), - ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount), - DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount), - UpdatedCount = resultsList.Sum (r => r.UpdatedCount), - IterationCount = resultsList.Sum (r => r.IterationCount) - }; - - dt.Rows.Add ( - totalRow.Scenario, - totalRow.Duration, - totalRow.RefreshedCount, - totalRow.LaidOutCount, - totalRow.ClearedContentCount, - totalRow.DrawCompleteCount, - totalRow.UpdatedCount, - totalRow.IterationCount - ); - - dt.DefaultView.Sort = "Duration"; - DataTable sortedCopy = dt.DefaultView.ToTable (); - - resultsTableView.Table = new DataTableSource (sortedCopy); - - benchmarkWindow.Add (resultsTableView); - - Application.Run (benchmarkWindow); - benchmarkWindow.Dispose (); - Application.Shutdown (); + return; } + + if (!string.IsNullOrEmpty (Options.ResultsFile)) + { + string output = JsonSerializer.Serialize ( + resultsList, + new JsonSerializerOptions + { + WriteIndented = true + }); + + using StreamWriter file = File.CreateText (Options.ResultsFile); + file.Write (output); + file.Close (); + + return; + } + + Application.Init (); + + var benchmarkWindow = new Window + { + Title = "Benchmark Results" + }; + + if (benchmarkWindow.Border is { }) + { + benchmarkWindow.Border.Thickness = new (0, 0, 0, 0); + } + + TableView resultsTableView = new () + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + // TableView provides many options for table headers. For simplicity we turn all + // of these off. By enabling FullRowSelect and turning off headers, TableView looks just + // like a ListView + resultsTableView.FullRowSelect = true; + resultsTableView.Style.ShowHeaders = true; + resultsTableView.Style.ShowHorizontalHeaderOverline = false; + resultsTableView.Style.ShowHorizontalHeaderUnderline = true; + resultsTableView.Style.ShowHorizontalBottomline = false; + resultsTableView.Style.ShowVerticalCellLines = true; + resultsTableView.Style.ShowVerticalHeaderLines = true; + + /* By default, TableView lays out columns at render time and only + * measures y rows of data at a time. Where y is the height of the + * console. This is for the following reasons: + * + * - Performance, when tables have a large amount of data + * - Defensive, prevents a single wide cell value pushing other + * columns off-screen (requiring horizontal scrolling + * + * In the case of UICatalog here, such an approach is overkill so + * we just measure all the data ourselves and set the appropriate + * max widths as ColumnStyles + */ + //int longestName = _scenarios!.Max (s => s.GetName ().Length); + + //resultsTableView.Style.ColumnStyles.Add ( + // 0, + // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } + // ); + //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); + //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem; + + // TableView typically is a grid where nav keys are biased for moving left/right. + resultsTableView.KeyBindings.Remove (Key.Home); + resultsTableView.KeyBindings.Add (Key.Home, Command.Start); + resultsTableView.KeyBindings.Remove (Key.End); + resultsTableView.KeyBindings.Add (Key.End, Command.End); + + // Ideally, TableView.MultiSelect = false would turn off any keybindings for + // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for + // a shortcut to About. + resultsTableView.MultiSelect = false; + + var dt = new DataTable (); + + dt.Columns.Add (new DataColumn ("Scenario", typeof (string))); + dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan))); + dt.Columns.Add (new DataColumn ("Refreshed", typeof (int))); + dt.Columns.Add (new DataColumn ("LaidOut", typeof (int))); + dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int))); + dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int))); + dt.Columns.Add (new DataColumn ("Updated", typeof (int))); + dt.Columns.Add (new DataColumn ("Iterations", typeof (int))); + + foreach (BenchmarkResults r in resultsList) + { + dt.Rows.Add ( + r.Scenario, + r.Duration, + r.RefreshedCount, + r.LaidOutCount, + r.ClearedContentCount, + r.DrawCompleteCount, + r.UpdatedCount, + r.IterationCount + ); + } + + BenchmarkResults totalRow = new () + { + Scenario = "TOTAL", + Duration = new (resultsList.Sum (r => r.Duration.Ticks)), + RefreshedCount = resultsList.Sum (r => r.RefreshedCount), + LaidOutCount = resultsList.Sum (r => r.LaidOutCount), + ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount), + DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount), + UpdatedCount = resultsList.Sum (r => r.UpdatedCount), + IterationCount = resultsList.Sum (r => r.IterationCount) + }; + + dt.Rows.Add ( + totalRow.Scenario, + totalRow.Duration, + totalRow.RefreshedCount, + totalRow.LaidOutCount, + totalRow.ClearedContentCount, + totalRow.DrawCompleteCount, + totalRow.UpdatedCount, + totalRow.IterationCount + ); + + dt.DefaultView.Sort = "Duration"; + DataTable sortedCopy = dt.DefaultView.ToTable (); + + resultsTableView.Table = new DataTableSource (sortedCopy); + + benchmarkWindow.Add (resultsTableView); + + Application.Run (benchmarkWindow); + benchmarkWindow.Dispose (); + Application.Shutdown (); } private static void VerifyObjectsWereDisposed () @@ -711,848 +624,4 @@ public class UICatalogApp RunState.Instances.Clear (); #endif } - - /// - /// 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 ListView? CategoryList; - public MenuItem? MiForce16Colors; - public MenuItem? MiIsMenuBorderDisabled; - public MenuItem? MiIsMouseDisabled; - public MenuItem? MiUseSubMenusSingleFrame; - - public Shortcut? ShForce16Colors; - - //public Shortcut? ShDiagnostics; - public Shortcut? ShVersion; - - // UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how - // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView - // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. - public TableView ScenarioList; - - private readonly StatusBar? _statusBar; - - private readonly CollectionNavigator _scenarioCollectionNav = new (); - - public UICatalogTopLevel () - { - _diagnosticFlags = Diagnostics; - - _themeMenuItems = CreateThemeMenuItems (); - _themeMenuBarItem = new ("_Themes", _themeMenuItems!); - - MenuBar menuBar = new () - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Quit", - "Quit UI Catalog", - RequestStop - ) - } - ), - _themeMenuBarItem, - new ("Diag_nostics", CreateDiagnosticMenuItems ()), - new ("_Logging", CreateLoggingMenuItems ()), - new ( - "_Help", - new MenuItem [] - { - new ( - "_Documentation", - "", - () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"), - null, - null, - (KeyCode)Key.F1 - ), - new ( - "_README", - "", - () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), - null, - null, - (KeyCode)Key.F2 - ), - new ( - "_About...", - "About UI Catalog", - () => MessageBox.Query ( - "", - GetAboutBoxMessage (), - wrapMessage: false, - buttons: "_Ok" - ), - null, - null, - (KeyCode)Key.A.WithCtrl - ) - } - ) - ] - }; - - _statusBar = new () - { - Visible = ShowStatusBar, - AlignmentModes = AlignmentModes.IgnoreFirstOrLast, - CanFocus = false - }; - _statusBar.Height = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: Dim.Func (() => _statusBar.Visible ? 1 : 0), maximumContentDim: Dim.Func (() => _statusBar.Visible ? 1 : 0)); - - ShVersion = new () - { - Title = "Version Info", - CanFocus = false - }; - - var statusBarShortcut = new Shortcut - { - Key = Key.F10, - Title = "Show/Hide Status Bar", - CanFocus = false - }; - - statusBarShortcut.Accepting += (sender, args) => - { - _statusBar.Visible = !_statusBar.Visible; - args.Cancel = true; - }; - - ShForce16Colors = new () - { - CanFocus = false, - CommandView = new CheckBox - { - Title = "16 color mode", - CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, - CanFocus = false - }, - HelpText = "", - BindKeyToApplication = true, - Key = Key.F7 - }; - - ((CheckBox)ShForce16Colors.CommandView).CheckedStateChanging += (sender, args) => - { - Application.Force16Colors = args.NewValue == CheckState.Checked; - MiForce16Colors!.Checked = Application.Force16Colors; - Application.LayoutAndDraw (); - }; - - _statusBar.Add ( - new Shortcut - { - CanFocus = false, - Title = "Quit", - Key = Application.QuitKey - }, - statusBarShortcut, - ShForce16Colors, - - //ShDiagnostics, - ShVersion - ); - - // Create the Category list view. This list never changes. - CategoryList = new () - { - X = 0, - Y = Pos.Bottom (menuBar), - Width = Dim.Auto (), - Height = Dim.Fill ( - Dim.Func ( - () => - { - if (_statusBar.NeedsLayout) - { - throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); - - //_statusBar.Layout (); - } - - return _statusBar.Frame.Height; - })), - AllowsMarking = false, - CanFocus = true, - Title = "_Categories", - BorderStyle = LineStyle.Rounded, - SuperViewRendersLineCanvas = true, - Source = new ListWrapper (_categories) - }; - CategoryList.OpenSelectedItem += (s, a) => { ScenarioList!.SetFocus (); }; - CategoryList.SelectedItemChanged += CategoryView_SelectedChanged; - - // This enables the scrollbar by causing lazy instantiation to happen - CategoryList.VerticalScrollBar.AutoShow = true; - - // Create the scenario list. The contents of the scenario list changes whenever the - // Category list selection changes (to show just the scenarios that belong to the selected - // category). - ScenarioList = new () - { - X = Pos.Right (CategoryList) - 1, - Y = Pos.Bottom (menuBar), - Width = Dim.Fill (), - Height = Dim.Fill ( - Dim.Func ( - () => - { - if (_statusBar.NeedsLayout) - { - throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); - - //_statusBar.Layout (); - } - - return _statusBar.Frame.Height; - })), - - //AllowsMarking = false, - CanFocus = true, - Title = "_Scenarios", - BorderStyle = CategoryList.BorderStyle, - SuperViewRendersLineCanvas = true - }; - - //ScenarioList.VerticalScrollBar.AutoHide = false; - //ScenarioList.HorizontalScrollBar.AutoHide = false; - - // TableView provides many options for table headers. For simplicity we turn all - // of these off. By enabling FullRowSelect and turning off headers, TableView looks just - // like a ListView - ScenarioList.FullRowSelect = true; - ScenarioList.Style.ShowHeaders = false; - ScenarioList.Style.ShowHorizontalHeaderOverline = false; - ScenarioList.Style.ShowHorizontalHeaderUnderline = false; - ScenarioList.Style.ShowHorizontalBottomline = false; - ScenarioList.Style.ShowVerticalCellLines = false; - ScenarioList.Style.ShowVerticalHeaderLines = false; - - /* By default TableView lays out columns at render time and only - * measures y rows of data at a time. Where y is the height of the - * console. This is for the following reasons: - * - * - Performance, when tables have a large amount of data - * - Defensive, prevents a single wide cell value pushing other - * columns off screen (requiring horizontal scrolling - * - * In the case of UICatalog here, such an approach is overkill so - * we just measure all the data ourselves and set the appropriate - * max widths as ColumnStyles - */ - int longestName = _scenarios!.Max (s => s.GetName ().Length); - - ScenarioList.Style.ColumnStyles.Add ( - 0, - new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } - ); - ScenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); - ScenarioList.CellActivated += ScenarioView_OpenSelectedItem; - - // TableView typically is a grid where nav keys are biased for moving left/right. - ScenarioList.KeyBindings.Remove (Key.Home); - ScenarioList.KeyBindings.Add (Key.Home, Command.Start); - ScenarioList.KeyBindings.Remove (Key.End); - ScenarioList.KeyBindings.Add (Key.End, Command.End); - - // Ideally, TableView.MultiSelect = false would turn off any keybindings for - // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for - // a shortcut to About. - ScenarioList.MultiSelect = false; - ScenarioList.KeyBindings.Remove (Key.A.WithCtrl); - - Add (menuBar); - Add (CategoryList); - Add (ScenarioList); - Add (_statusBar); - - Loaded += LoadedHandler; - Unloaded += UnloadedHandler; - - // Restore previous selections - CategoryList.SelectedItem = _cachedCategoryIndex; - ScenarioList.SelectedRow = _cachedScenarioIndex; - - Applied += ConfigAppliedHandler; - } - - public void ConfigChanged () - { - if (MenuBar == null) - { - // View is probably disposed - return; - } - - if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) - { - _topLevelColorScheme = "Base"; - } - - _cachedTheme = Themes?.Theme; - - _themeMenuItems = CreateThemeMenuItems (); - _themeMenuBarItem!.Children = _themeMenuItems; - - foreach (MenuItem mi in _themeMenuItems!) - { - if (mi is { Parent: null }) - { - mi.Parent = _themeMenuBarItem; - } - } - - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - - MenuBar!.Menus [0].Children! [0]!.ShortcutKey = Application.QuitKey; - - ((Shortcut)_statusBar!.SubViews.ElementAt (0)).Key = Application.QuitKey; - _statusBar.Visible = ShowStatusBar; - - MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; - - ((CheckBox)ShForce16Colors!.CommandView!).CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - - Application.Top!.SetNeedsDraw (); - } - - public MenuItem []? CreateThemeMenuItems () - { - List menuItems = CreateForce16ColorItems ().ToList (); - menuItems.Add (null!); - - var schemeCount = 0; - - foreach (KeyValuePair theme in Themes!) - { - var item = new MenuItem - { - Title = theme.Key == "Dark" ? $"{theme.Key.Substring (0, 3)}_{theme.Key.Substring (3, 1)}" : $"_{theme.Key}", - ShortcutKey = new Key ((KeyCode)((uint)KeyCode.D1 + schemeCount++)) - .WithCtrl - }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = theme.Key == _cachedTheme; // CM.Themes.Theme; - - item.Action += () => - { - Themes.Theme = _cachedTheme = theme.Key; - Apply (); - }; - menuItems.Add (item); - } - - List schemeMenuItems = new (); - - foreach (KeyValuePair sc in Colors.ColorSchemes) - { - var item = new MenuItem { Title = $"_{sc.Key}", Data = sc.Key }; - item.CheckType |= MenuItemCheckStyle.Radio; - item.Checked = sc.Key == _topLevelColorScheme; - - item.Action += () => - { - _topLevelColorScheme = (string)item.Data; - - foreach (MenuItem schemeMenuItem in schemeMenuItems) - { - schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; - } - - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - }; - item.ShortcutKey = ((Key)sc.Key [0].ToString ().ToLower ()).WithCtrl; - schemeMenuItems.Add (item); - } - - menuItems.Add (null!); - var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ()); - menuItems.Add (mbi); - - return menuItems.ToArray (); - } - - private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) - { - string item = _categories! [e!.Item]; - ObservableCollection newlist; - - if (e.Item == 0) - { - // First category is "All" - newlist = _scenarios!; - } - else - { - newlist = new (_scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ()); - } - - ScenarioList.Table = new EnumerableTableSource ( - newlist, - new () - { - { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () } - } - ); - - // Create a collection of just the scenario names (the 1st column in our TableView) - // for CollectionNavigator. - List firstColumnList = new (); - - for (var i = 0; i < ScenarioList.Table.Rows; i++) - { - firstColumnList.Add (ScenarioList.Table [i, 0]); - } - - _scenarioCollectionNav.Collection = firstColumnList; - } - - private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); } - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private MenuItem [] CreateDiagnosticFlagsMenuItems () - { - const string OFF = "View Diagnostics: _Off"; - const string RULER = "View Diagnostics: _Ruler"; - const string THICKNESS = "View Diagnostics: _Thickness"; - const string HOVER = "View Diagnostics: _Hover"; - const string DRAWINDICATOR = "View Diagnostics: _DrawIndicator"; - var index = 0; - - List menuItems = new (); - - foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) - { - var item = new MenuItem - { - Title = GetDiagnosticsTitle (diag), ShortcutKey = new Key (index.ToString () [0]).WithAlt - }; - index++; - item.CheckType |= MenuItemCheckStyle.Checked; - - if (GetDiagnosticsTitle (ViewDiagnosticFlags.Off) == item.Title) - { - item.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.DrawIndicator); - } - else - { - item.Checked = _diagnosticFlags.HasFlag (diag); - } - - item.Action += () => - { - string t = GetDiagnosticsTitle (ViewDiagnosticFlags.Off); - - if (item.Title == t && item.Checked == false) - { - _diagnosticFlags &= ~(ViewDiagnosticFlags.Thickness - | ViewDiagnosticFlags.Ruler - | ViewDiagnosticFlags.Hover - | ViewDiagnosticFlags.DrawIndicator); - item.Checked = true; - } - else if (item.Title == t && item.Checked == true) - { - _diagnosticFlags |= ViewDiagnosticFlags.Thickness - | ViewDiagnosticFlags.Ruler - | ViewDiagnosticFlags.Hover - | ViewDiagnosticFlags.DrawIndicator; - item.Checked = false; - } - else - { - Enum f = GetDiagnosticsEnumValue (item.Title); - - if (_diagnosticFlags.HasFlag (f)) - { - SetDiagnosticsFlag (f, false); - } - else - { - SetDiagnosticsFlag (f, true); - } - } - - foreach (MenuItem menuItem in menuItems) - { - if (menuItem.Title == t) - { - menuItem.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.DrawIndicator); - } - else if (menuItem.Title != t) - { - menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); - } - } - - Diagnostics = _diagnosticFlags; - }; - menuItems.Add (item); - } - - return menuItems.ToArray (); - - string GetDiagnosticsTitle (Enum diag) - { - return Enum.GetName (_diagnosticFlags.GetType (), diag) switch - { - "Off" => OFF, - "Ruler" => RULER, - "Thickness" => THICKNESS, - "Hover" => HOVER, - "DrawIndicator" => DRAWINDICATOR, - _ => "" - }; - } - - Enum GetDiagnosticsEnumValue (string? title) - { - return title switch - { - RULER => ViewDiagnosticFlags.Ruler, - THICKNESS => ViewDiagnosticFlags.Thickness, - HOVER => ViewDiagnosticFlags.Hover, - DRAWINDICATOR => ViewDiagnosticFlags.DrawIndicator, - _ => null! - }; - } - - void SetDiagnosticsFlag (Enum diag, bool add) - { - switch (diag) - { - case ViewDiagnosticFlags.Ruler: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.Ruler; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.Ruler; - } - - break; - case ViewDiagnosticFlags.Thickness: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.Thickness; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.Thickness; - } - - break; - case ViewDiagnosticFlags.Hover: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.Hover; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.Hover; - } - - break; - case ViewDiagnosticFlags.DrawIndicator: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.DrawIndicator; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.DrawIndicator; - } - - break; - default: - _diagnosticFlags = default (ViewDiagnosticFlags); - - break; - } - } - } - - private List CreateDiagnosticMenuItems () - { - List menuItems = new () - { - CreateDiagnosticFlagsMenuItems (), - new MenuItem [] { null! }, - CreateDisabledEnabledMouseItems (), - CreateDisabledEnabledMenuBorder (), - CreateDisabledEnableUseSubMenusSingleFrame (), - CreateKeyBindingsMenuItems () - }; - - return menuItems; - } - - private List CreateLoggingMenuItems () - { - List menuItems = new () - { - CreateLoggingFlagsMenuItems ()! - }; - - return menuItems; - } - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private MenuItem? [] CreateLoggingFlagsMenuItems () - { - string [] logLevelMenuStrings = Enum.GetNames ().Select (n => n = "_" + n).ToArray (); - LogLevel [] logLevels = Enum.GetValues (); - - List menuItems = new (); - - foreach (LogLevel logLevel in logLevels) - { - var item = new MenuItem - { - Title = logLevelMenuStrings [(int)logLevel] - }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = Enum.Parse (_options.DebugLogLevel) == logLevel; - - item.Action += () => - { - foreach (MenuItem? menuItem in menuItems.Where (mi => mi is { } && logLevelMenuStrings.Contains (mi.Title))) - { - menuItem!.Checked = false; - } - - if (item.Title == logLevelMenuStrings [(int)logLevel] && item.Checked == false) - { - _options.DebugLogLevel = Enum.GetName (logLevel)!; - _logLevelSwitch.MinimumLevel = LogLevelToLogEventLevel (Enum.Parse (_options.DebugLogLevel)); - item.Checked = true; - } - - Diagnostics = _diagnosticFlags; - }; - menuItems.Add (item); - } - - // add a separator - menuItems.Add (null!); - - menuItems.Add ( - new ( - $"_Open Log Folder", - "", - () => OpenUrl (LOGFILE_LOCATION), - null, - null, - null - )); - - return menuItems.ToArray ()!; - } - - // TODO: This should be an ConfigurationManager setting - private MenuItem [] CreateDisabledEnabledMenuBorder () - { - List menuItems = new (); - MiIsMenuBorderDisabled = new () { Title = "Disable Menu _Border" }; - - MiIsMenuBorderDisabled.ShortcutKey = - new Key (MiIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt.WithCtrl.NoShift; - MiIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked; - - MiIsMenuBorderDisabled.Action += () => - { - MiIsMenuBorderDisabled.Checked = (bool)!MiIsMenuBorderDisabled.Checked!; - - MenuBar!.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked - ? LineStyle.Single - : LineStyle.None; - }; - menuItems.Add (MiIsMenuBorderDisabled); - - return menuItems.ToArray (); - } - - private MenuItem [] CreateDisabledEnabledMouseItems () - { - List menuItems = new (); - MiIsMouseDisabled = new () { Title = "_Disable Mouse" }; - - MiIsMouseDisabled.ShortcutKey = - new Key (MiIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl.NoShift; - MiIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; - - MiIsMouseDisabled.Action += () => - { - MiIsMouseDisabled.Checked = - Application.IsMouseDisabled = (bool)!MiIsMouseDisabled.Checked!; - }; - menuItems.Add (MiIsMouseDisabled); - - return menuItems.ToArray (); - } - - // TODO: This should be an ConfigurationManager setting - private MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame () - { - List menuItems = new (); - MiUseSubMenusSingleFrame = new () { Title = "Enable _Sub-Menus Single Frame" }; - - MiUseSubMenusSingleFrame.ShortcutKey = KeyCode.CtrlMask - | KeyCode.AltMask - | (KeyCode)MiUseSubMenusSingleFrame!.Title!.Substring (8, 1) [ - 0]; - MiUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked; - - MiUseSubMenusSingleFrame.Action += () => - { - MiUseSubMenusSingleFrame.Checked = (bool)!MiUseSubMenusSingleFrame.Checked!; - MenuBar!.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked; - }; - menuItems.Add (MiUseSubMenusSingleFrame); - - return menuItems.ToArray (); - } - - private MenuItem [] CreateForce16ColorItems () - { - List menuItems = new (); - - MiForce16Colors = new () - { - Title = "Force _16 Colors", - ShortcutKey = Key.F6, - Checked = Application.Force16Colors, - CanExecute = () => Application.Driver?.SupportsTrueColor ?? false - }; - MiForce16Colors.CheckType |= MenuItemCheckStyle.Checked; - - MiForce16Colors.Action += () => - { - MiForce16Colors.Checked = Application.Force16Colors = (bool)!MiForce16Colors.Checked!; - - ((CheckBox)ShForce16Colors!.CommandView!).CheckedState = - Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - Application.LayoutAndDraw (); - }; - menuItems.Add (MiForce16Colors); - - return menuItems.ToArray (); - } - - private MenuItem [] CreateKeyBindingsMenuItems () - { - List menuItems = new (); - var item = new MenuItem { Title = "_Key Bindings", Help = "Change which keys do what" }; - - item.Action += () => - { - var dlg = new KeyBindingsDialog (); - Application.Run (dlg); - dlg.Dispose (); - }; - - menuItems.Add (null!); - menuItems.Add (item); - - return menuItems.ToArray (); - } - - private void LoadedHandler (object? sender, EventArgs? args) - { - ConfigChanged (); - - MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; - - if (ShVersion is { }) - { - ShVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver!.GetVersionInfo ()}"; - } - - if (_selectedScenario != null) - { - _selectedScenario = null; - _isFirstRunning = false; - } - - if (!_isFirstRunning) - { - ScenarioList.SetFocus (); - } - - if (_statusBar is { }) - { - _statusBar.VisibleChanged += (s, e) => { ShowStatusBar = _statusBar.Visible; }; - } - - Loaded -= LoadedHandler; - CategoryList!.EnsureSelectedItemVisible (); - ScenarioList.EnsureSelectedCellIsVisible (); - } - - /// Launches the selected scenario, setting the global _selectedScenario - /// - private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) - { - if (_selectedScenario is null) - { - // Save selected item state - _cachedCategoryIndex = CategoryList!.SelectedItem; - _cachedScenarioIndex = ScenarioList.SelectedRow; - - // Create new instance of scenario (even though Scenarios contains instances) - var selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0]; - - _selectedScenario = (Scenario)Activator.CreateInstance ( - _scenarios!.FirstOrDefault ( - s => s.GetName () - == selectedScenarioName - )! - .GetType () - )!; - - // Tell the main app to stop - Application.RequestStop (); - } - } - - private void UnloadedHandler (object? sender, EventArgs? args) - { - Applied -= ConfigAppliedHandler; - Unloaded -= UnloadedHandler; - Dispose (); - } - } - - private struct Options - { - public string Driver; - - public string Scenario; - - public uint BenchmarkTimeout; - - public bool Benchmark; - - public string ResultsFile; - - public string DebugLogLevel; - /* etc. */ - } } diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index f0b5a8760..d3a11147d 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -1,6 +1,6 @@  - UICatalog.UICatalogApp + UICatalog.UICatalog Exe diff --git a/UICatalog/UICatalogCommandLineOptions.cs b/UICatalog/UICatalogCommandLineOptions.cs new file mode 100644 index 000000000..c39b9f2b2 --- /dev/null +++ b/UICatalog/UICatalogCommandLineOptions.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace UICatalog; + +public struct UICatalogCommandLineOptions +{ + public string Driver { get; set; } + + public string Scenario { get; set; } + + public uint BenchmarkTimeout { get; set; } + + public bool Benchmark { get; set; } + + public string ResultsFile { get; set; } + + public string DebugLogLevel { get; set; } + /* etc. */ +} diff --git a/UICatalog/UICatalogTopLevel.cs b/UICatalog/UICatalogTopLevel.cs new file mode 100644 index 000000000..d6f65cc6f --- /dev/null +++ b/UICatalog/UICatalogTopLevel.cs @@ -0,0 +1,721 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Terminal.Gui; +using static Terminal.Gui.ConfigurationManager; +using Command = Terminal.Gui.Command; +using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; + +#nullable enable + +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 +{ + // When a scenario is run, the main app is killed. The static + // members are cached so that when the scenario exits the + // main app UI can be restored to previous state + + // Theme Management + public static string? CachedTheme { get; set; } + + public static string? CachedTopLevelColorScheme { get; set; } + + // Diagnostics + private static ViewDiagnosticFlags _diagnosticFlags; + + public UICatalogTopLevel () + { + _diagnosticFlags = Diagnostics; + + _menuBar = CreateMenuBar (); + _statusBar = CreateStatusBar (); + _categoryList = CreateCategoryList (); + _scenarioList = CreateScenarioList (); + + Add (_menuBar, _categoryList, _scenarioList, _statusBar); + + Loaded += LoadedHandler; + Unloaded += UnloadedHandler; + + // Restore previous selections + _categoryList.SelectedItem = _cachedCategoryIndex; + _scenarioList.SelectedRow = _cachedScenarioIndex; + + Applied += ConfigAppliedHandler; + } + + + private static bool _isFirstRunning = true; + + private void LoadedHandler (object? sender, EventArgs? args) + { + ConfigChanged (); + + if (_disableMouseCb is { }) + { + _disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; + } + + if (_shVersion is { }) + { + _shVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver!.GetVersionInfo ()}"; + } + + if (CachedSelectedScenario != null) + { + CachedSelectedScenario = null; + _isFirstRunning = false; + } + + if (!_isFirstRunning) + { + _scenarioList.SetFocus (); + } + + if (_statusBar is { }) + { + _statusBar.VisibleChanged += (s, e) => { ShowStatusBar = _statusBar.Visible; }; + } + + Loaded -= LoadedHandler; + _categoryList!.EnsureSelectedItemVisible (); + _scenarioList.EnsureSelectedCellIsVisible (); + } + + private void UnloadedHandler (object? sender, EventArgs? args) + { + Applied -= ConfigAppliedHandler; + Unloaded -= UnloadedHandler; + Dispose (); + } + + #region MenuBar + + private readonly MenuBarv2? _menuBar; + private CheckBox? _force16ColorsMenuItemCb; + private RadioGroup? _themesRg; + private RadioGroup? _topSchemeRg; + private RadioGroup? _logLevelRg; + private FlagSelector? _diagnosticFlagsSelector; + private CheckBox? _disableMouseCb; + + private MenuBarv2 CreateMenuBar () + { + MenuBarv2 menuBar = new ( + [ + new ( + "_File", + [ + new MenuItemv2 ( + "_Quit", + "Quit UI Catalog", + RequestStop + ) + ]), + new ("_Themes", CreateThemeMenuItems ()), + new ("Diag_nostics", CreateDiagnosticMenuItems ()), + new ("_Logging", CreateLoggingMenuItems ()), + new ( + "_Help", + [ + new MenuItemv2 ( + "_Documentation", + "", + () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"), + Key.F1 + ), + new MenuItemv2 ( + "_README", + "", + () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), + Key.F2 + ), + new MenuItemv2 ( + "_About...", + "About UI Catalog", + () => MessageBox.Query ( + "", + GetAboutBoxMessage (), + wrapMessage: false, + buttons: "_Ok" + ), + Key.A.WithCtrl + ) + ]) + ]); + + return menuBar; + + View [] CreateThemeMenuItems () + { + List menuItems = []; + + _force16ColorsMenuItemCb = new () + { + Title = "Force _16 Colors", + CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked + }; + + _force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) => + { + Application.Force16Colors = args.CurrentValue == CheckState.Checked; + + _force16ColorsShortcutCb!.CheckedState = args.CurrentValue; + Application.LayoutAndDraw (); + }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _force16ColorsMenuItemCb + }); + + menuItems.Add (new Line ()); + + _themesRg = new (); + + _themesRg.SelectedItemChanged += (_, args) => + { + Themes!.Theme = Themes!.Keys.ToArray () [args.SelectedItem]; + CachedTheme = Themes!.Keys.ToArray () [args.SelectedItem]; + Apply (); + SetNeedsDraw (); + }; + + var menuItem = new MenuItemv2 + { + CommandView = _themesRg, + HelpText = "Cycle Through Themes", + Key = Key.T.WithCtrl + }; + menuItems.Add (menuItem); + + menuItems.Add (new Line ()); + + _topSchemeRg = new (); + + _topSchemeRg.SelectedItemChanged += (_, args) => + { + CachedTopLevelColorScheme = Colors.ColorSchemes.Keys.ToArray () [args.SelectedItem]; + ColorScheme = Colors.ColorSchemes [CachedTopLevelColorScheme]; + SetNeedsDraw (); + }; + + menuItem = new () + { + Title = "Color Scheme for Application._Top", + SubMenu = new ( + [ + new () + { + CommandView = _topSchemeRg, + HelpText = "Cycle Through Color Schemes", + Key = Key.S.WithCtrl + } + ]) + }; + menuItems.Add (menuItem); + + UpdateThemesMenu (); + + return menuItems.ToArray (); + } + + View [] CreateDiagnosticMenuItems () + { + List menuItems = []; + + _diagnosticFlagsSelector = new () + { + CanFocus = false, + Styles = FlagSelectorStyles.ShowNone, + HighlightStyle = HighlightStyle.None + }; + _diagnosticFlagsSelector.SetFlags (); + + _diagnosticFlagsSelector.ValueChanged += (sender, args) => + { + _diagnosticFlags = (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value; + Diagnostics = _diagnosticFlags; + }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _diagnosticFlagsSelector, + HelpText = "View Diagnostics" + }); + + menuItems.Add (new Line ()); + + _disableMouseCb = new () + { + Title = "_Disable Mouse", + CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked + }; + + _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.CurrentValue == CheckState.Checked; }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _disableMouseCb, + HelpText = "Disable Mouse" + }); + + return menuItems.ToArray (); + } + + View [] CreateLoggingMenuItems () + { + List menuItems = []; + + LogLevel [] logLevels = Enum.GetValues (); + + _logLevelRg = new () + { + AssignHotKeysToRadioLabels = true, + RadioLabels = Enum.GetNames (), + SelectedItem = logLevels.ToList ().IndexOf (Enum.Parse (UICatalog.Options.DebugLogLevel)) + }; + + _logLevelRg.SelectedItemChanged += (_, args) => + { + UICatalog.Options = UICatalog.Options with { DebugLogLevel = Enum.GetName (logLevels [args.SelectedItem])! }; + + UICatalog.LogLevelSwitch.MinimumLevel = + UICatalog.LogLevelToLogEventLevel (Enum.Parse (UICatalog.Options.DebugLogLevel)); + }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _logLevelRg, + HelpText = "Cycle Through Log Levels", + Key = Key.L.WithCtrl + }); + + // add a separator + menuItems.Add (new Line ()); + + menuItems.Add ( + new MenuItemv2 ( + "_Open Log Folder", + string.Empty, + () => OpenUrl (UICatalog.LOGFILE_LOCATION) + )); + + return menuItems.ToArray ()!; + } + + } + + private void UpdateThemesMenu () + { + if (_themesRg is null) + { + return; + } + + _themesRg.AssignHotKeysToRadioLabels = true; + _themesRg.UsedHotKeys.Clear (); + _themesRg.RadioLabels = Themes!.Keys.ToArray (); + _themesRg.SelectedItem = Themes.Keys.ToList ().IndexOf (CachedTheme!.Replace ("_", string.Empty)); + + if (_topSchemeRg is null) + { + return; + } + + _topSchemeRg.AssignHotKeysToRadioLabels = true; + _topSchemeRg.UsedHotKeys.Clear (); + int selected = _topSchemeRg.SelectedItem; + _topSchemeRg.RadioLabels = Colors.ColorSchemes.Keys.ToArray (); + _topSchemeRg.SelectedItem = selected; + + if (CachedTopLevelColorScheme is null || !Colors.ColorSchemes.ContainsKey (CachedTopLevelColorScheme)) + { + CachedTopLevelColorScheme = "Base"; + } + + _topSchemeRg.SelectedItem = Array.IndexOf (Colors.ColorSchemes.Keys.ToArray (), CachedTopLevelColorScheme); + } + + #endregion MenuBar + + #region Scenario List + + private readonly TableView _scenarioList; + + private static int _cachedScenarioIndex; + + public static ObservableCollection? CachedScenarios { get; set; } + + // UI Catalog uses TableView for the scenario list instead of a ListView to demonstrate how + // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView + // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. + private readonly CollectionNavigator _scenarioCollectionNav = new (); + + // If set, holds the scenario the user selected to run + public static Scenario? CachedSelectedScenario { get; set; } + + private TableView CreateScenarioList () + { + // Create the scenario list. The contents of the scenario list changes whenever the + // Category list selection changes (to show just the scenarios that belong to the selected + // category). + TableView scenarioList = new () + { + X = Pos.Right (_categoryList!) - 1, + Y = Pos.Bottom (_menuBar!), + Width = Dim.Fill (), + Height = Dim.Fill ( + Dim.Func ( + () => + { + if (_statusBar!.NeedsLayout) + { + throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); + + //_statusBar.Layout (); + } + + return _statusBar.Frame.Height; + })), + + //AllowsMarking = false, + CanFocus = true, + Title = "_Scenarios", + BorderStyle = _categoryList!.BorderStyle, + SuperViewRendersLineCanvas = true + }; + + // TableView provides many options for table headers. For simplicity, we turn all + // of these off. By enabling FullRowSelect and turning off headers, TableView looks just + // like a ListView + scenarioList.FullRowSelect = true; + scenarioList.Style.ShowHeaders = false; + scenarioList.Style.ShowHorizontalHeaderOverline = false; + scenarioList.Style.ShowHorizontalHeaderUnderline = false; + scenarioList.Style.ShowHorizontalBottomline = false; + scenarioList.Style.ShowVerticalCellLines = false; + scenarioList.Style.ShowVerticalHeaderLines = false; + + /* By default, TableView lays out columns at render time and only + * measures y rows of data at a time. Where y is the height of the + * console. This is for the following reasons: + * + * - Performance, when tables have a large amount of data + * - Defensive, prevents a single wide cell value pushing other + * columns off-screen (requiring horizontal scrolling + * + * In the case of UICatalog here, such an approach is overkill so + * we just measure all the data ourselves and set the appropriate + * max widths as ColumnStyles + */ + int longestName = CachedScenarios!.Max (s => s.GetName ().Length); + + scenarioList.Style.ColumnStyles.Add ( + 0, + new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } + ); + scenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); + scenarioList.CellActivated += ScenarioView_OpenSelectedItem; + + // TableView typically is a grid where nav keys are biased for moving left/right. + scenarioList.KeyBindings.Remove (Key.Home); + scenarioList.KeyBindings.Add (Key.Home, Command.Start); + scenarioList.KeyBindings.Remove (Key.End); + scenarioList.KeyBindings.Add (Key.End, Command.End); + + // Ideally, TableView.MultiSelect = false would turn off any keybindings for + // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for + // a shortcut to About. + scenarioList.MultiSelect = false; + scenarioList.KeyBindings.Remove (Key.A.WithCtrl); + + return scenarioList; + } + + + /// Launches the selected scenario, setting the global _selectedScenario + /// + private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) + { + if (CachedSelectedScenario is null) + { + // Save selected item state + _cachedCategoryIndex = _categoryList!.SelectedItem; + _cachedScenarioIndex = _scenarioList.SelectedRow; + + // Create new instance of scenario (even though Scenarios contains instances) + var selectedScenarioName = (string)_scenarioList.Table [_scenarioList.SelectedRow, 0]; + + CachedSelectedScenario = (Scenario)Activator.CreateInstance ( + CachedScenarios!.FirstOrDefault ( + s => s.GetName () + == selectedScenarioName + )! + .GetType () + )!; + + // Tell the main app to stop + Application.RequestStop (); + } + } + + #endregion Scenario List + + #region Category List + + private readonly ListView? _categoryList; + private static int _cachedCategoryIndex; + public static ObservableCollection? CachedCategories { get; set; } + + private ListView CreateCategoryList () + { + // Create the Category list view. This list never changes. + ListView categoryList = new () + { + X = 0, + Y = Pos.Bottom (_menuBar!), + Width = Dim.Auto (), + Height = Dim.Fill ( + Dim.Func ( + () => + { + if (_statusBar!.NeedsLayout) + { + throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); + + //_statusBar.Layout (); + } + + return _statusBar.Frame.Height; + })), + AllowsMarking = false, + CanFocus = true, + Title = "_Categories", + BorderStyle = LineStyle.Rounded, + SuperViewRendersLineCanvas = true, + Source = new ListWrapper (CachedCategories) + }; + categoryList.OpenSelectedItem += (s, a) => { _scenarioList!.SetFocus (); }; + categoryList.SelectedItemChanged += CategoryView_SelectedChanged; + + // This enables the scrollbar by causing lazy instantiation to happen + categoryList.VerticalScrollBar.AutoShow = true; + + return categoryList; + } + + private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) + { + string item = CachedCategories! [e!.Item]; + ObservableCollection newScenarioList; + + if (e.Item == 0) + { + // First category is "All" + newScenarioList = CachedScenarios!; + } + else + { + newScenarioList = new (CachedScenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ()); + } + + _scenarioList.Table = new EnumerableTableSource ( + newScenarioList, + new () + { + { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () } + } + ); + + // Create a collection of just the scenario names (the 1st column in our TableView) + // for CollectionNavigator. + List firstColumnList = []; + + for (var i = 0; i < _scenarioList.Table.Rows; i++) + { + firstColumnList.Add (_scenarioList.Table [i, 0]); + } + + _scenarioCollectionNav.Collection = firstColumnList; + } + + #endregion Category List + + #region StatusBar + + private readonly StatusBar? _statusBar; + + [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true)] + [JsonPropertyName ("UICatalog.StatusBar")] + public static bool ShowStatusBar { get; set; } = true; + + private Shortcut? _shVersion; + private CheckBox? _force16ColorsShortcutCb; + + private StatusBar CreateStatusBar () + { + StatusBar statusBar = new () + { + Visible = ShowStatusBar, + AlignmentModes = AlignmentModes.IgnoreFirstOrLast, + CanFocus = false + }; + + // ReSharper disable All + statusBar.Height = Dim.Auto ( + DimAutoStyle.Auto, + minimumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0), + maximumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0)); + // ReSharper restore All + + _shVersion = new () + { + Title = "Version Info", + CanFocus = false + }; + + var statusBarShortcut = new Shortcut + { + Key = Key.F10, + Title = "Show/Hide Status Bar", + CanFocus = false + }; + + statusBarShortcut.Accepting += (sender, args) => + { + statusBar.Visible = !_statusBar!.Visible; + args.Cancel = true; + }; + + _force16ColorsShortcutCb = new () + { + Title = "16 color mode", + CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + CanFocus = false + }; + + _force16ColorsShortcutCb.CheckedStateChanging += (sender, args) => + { + Application.Force16Colors = args.NewValue == CheckState.Checked; + _force16ColorsMenuItemCb!.CheckedState = args.NewValue; + Application.LayoutAndDraw (); + }; + + statusBar.Add ( + new Shortcut + { + CanFocus = false, + Title = "Quit", + Key = Application.QuitKey + }, + statusBarShortcut, + new Shortcut + { + CanFocus = false, + CommandView = _force16ColorsShortcutCb, + HelpText = "", + BindKeyToApplication = true, + Key = Key.F7 + }, + _shVersion + ); + + return statusBar; + } + + #endregion StatusBar + + #region Configuration Manager + public void ConfigChanged () + { + CachedTheme = Themes?.Theme; + + UpdateThemesMenu (); + + ColorScheme = Colors.ColorSchemes [CachedTopLevelColorScheme!]; + + ((Shortcut)_statusBar!.SubViews.ElementAt (0)).Key = Application.QuitKey; + _statusBar.Visible = ShowStatusBar; + + _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + + Application.Top!.SetNeedsDraw (); + } + + private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); } + + #endregion Configuration Manager + + /// + /// Gets the message displayed in the About Box. `public` so it can be used from Unit tests. + /// + /// + public static string GetAboutBoxMessage () + { + // NOTE: Do not use multiline verbatim strings here. + // WSL gets all confused. + StringBuilder msg = new (); + msg.AppendLine ("UI Catalog: A comprehensive sample library and test app for"); + msg.AppendLine (); + + msg.AppendLine ( + """ + _______ _ _ _____ _ + |__ __| (_) | | / ____| (_) + | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ + | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | + | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | + |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| + """); + msg.AppendLine (); + msg.AppendLine ("v2 - Pre-Alpha"); + msg.AppendLine (); + msg.AppendLine ("https://github.com/gui-cs/Terminal.Gui"); + + return msg.ToString (); + } + + public static void OpenUrl (string url) + { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + url = url.Replace ("&", "^&"); + Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) + { + using var process = new Process + { + StartInfo = new () + { + FileName = "xdg-open", + Arguments = url, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + UseShellExecute = false + } + }; + process.Start (); + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) + { + Process.Start ("open", url); + } + } +}