using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui.Views;
///
/// A horizontal list of s. Each can have a
/// that is shown when the is selected.
///
///
/// MenuBars may be hosted by any View and will, by default, be positioned the full width across the top of the View's
/// Viewport.
///
public class MenuBar : Menu, IDesignable
{
///
public MenuBar () : this ([]) { }
///
public MenuBar (IEnumerable menuBarItems) : base (menuBarItems)
{
CanFocus = false;
TabStop = TabBehavior.TabGroup;
Y = 0;
Width = Dim.Fill ();
Height = Dim.Auto ();
Orientation = Orientation.Horizontal;
Key = DefaultKey;
AddCommand (
Command.HotKey,
(ctx) =>
{
// Logging.Debug ($"{Title} - Command.HotKey");
if (RaiseHandlingHotKey (ctx) is true)
{
return true;
}
if (HideActiveItem ())
{
return true;
}
if (SubViews.OfType ().FirstOrDefault (mbi => mbi.PopoverMenu is { }) is { } first)
{
Active = true;
ShowItem (first);
return true;
}
return false;
});
// If we're not focused, Key activates/deactivates
HotKeyBindings.Add (Key, Command.HotKey);
KeyBindings.Add (Key, Command.Quit);
KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit);
AddCommand (
Command.Quit,
ctx =>
{
// Logging.Debug ($"{Title} - Command.Quit");
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);
AddCommand (Command.Left, MoveLeft);
KeyBindings.Add (Key.CursorLeft, Command.Left);
BorderStyle = DefaultBorderStyle;
ConfigurationManager.Applied += OnConfigurationManagerApplied;
SuperViewChanged += OnSuperViewChanged;
return;
bool? MoveLeft (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); }
bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); }
}
private void OnSuperViewChanged (object? sender, SuperViewChangedEventArgs e)
{
if (SuperView is null)
{
// BUGBUG: This is a hack for avoiding a race condition in ConfigurationManager.Apply
// BUGBUG: For some reason in some unit tests, when Top is disposed, MenuBar.Dispose does not get called.
// BUGBUG: Yet, the MenuBar does get Removed from Top (and it's SuperView set to null).
// BUGBUG: Related: https://github.com/gui-cs/Terminal.Gui/issues/4021
ConfigurationManager.Applied -= OnConfigurationManagerApplied;
}
}
private void OnConfigurationManagerApplied (object? sender, ConfigurationManagerEventArgs e) { BorderStyle = DefaultBorderStyle; }
///
protected override bool OnBorderStyleChanged ()
{
//HideActiveItem ();
return base.OnBorderStyleChanged ();
}
///
/// Gets or sets the default Border Style for the MenuBar. The default is .
///
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.None;
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 MenuBarItem []? Menus
{
set
{
RemoveAll ();
if (value is null)
{
return;
}
foreach (MenuBarItem mbi in value)
{
Add (mbi);
}
}
}
///
protected override void OnSubViewAdded (View view)
{
base.OnSubViewAdded (view);
if (view is MenuBarItem mbi)
{
mbi.Accepted += OnMenuBarItemAccepted;
mbi.PopoverMenuOpenChanged += OnMenuBarItemPopoverMenuOpenChanged;
}
}
///
protected override void OnSubViewRemoved (View view)
{
base.OnSubViewRemoved (view);
if (view is MenuBarItem mbi)
{
mbi.Accepted -= OnMenuBarItemAccepted;
mbi.PopoverMenuOpenChanged -= OnMenuBarItemPopoverMenuOpenChanged;
}
}
private void OnMenuBarItemPopoverMenuOpenChanged (object? sender, EventArgs e)
{
if (sender is MenuBarItem mbi)
{
if (e.Value)
{
Active = true;
}
}
}
private void OnMenuBarItemAccepted (object? sender, CommandEventArgs e)
{
// Logging.Debug ($"{Title} ({e.Context?.Source?.Title}) Command: {e.Context?.Command}");
RaiseAccepted (e.Context);
}
/// Raised when is changed.
public event EventHandler? KeyChanged;
/// The default key for activating menu bars.
[ConfigurationProperty (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.OfType ().Count (sv => sv is { PopoverMenuOpen: true }) > 0; }
private bool _active;
///
/// Gets or sets whether the menu bar is active or not. When active, the MenuBar can focus and moving the mouse
/// over a MenuBarItem will switch focus to that item. Use to determine if a PopoverMenu of
/// a MenuBarItem is open.
///
///
public bool Active
{
get => _active;
internal set
{
if (_active == value)
{
return;
}
_active = value;
// Logging.Debug ($"Active set to {_active} - CanFocus: {CanFocus}, HasFocus: {HasFocus}");
if (!_active)
{
// Hide open Popovers
HideActiveItem ();
}
CanFocus = value;
// Logging.Debug ($"Set CanFocus: {CanFocus}, HasFocus: {HasFocus}");
}
}
///
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
// Logging.Debug ($"CanFocus = {CanFocus}, HasFocus = {HasFocus}");
if (!HasFocus)
{
Active = true;
}
return base.OnMouseEnter (eventArgs);
}
///
protected override void OnMouseLeave ()
{
// Logging.Debug ($"CanFocus = {CanFocus}, HasFocus = {HasFocus}");
if (!IsOpen ())
{
Active = false;
}
base.OnMouseLeave ();
}
///
protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
{
// Logging.Debug ($"CanFocus = {CanFocus}, HasFocus = {HasFocus}");
if (!newHasFocus)
{
Active = false;
}
}
///
protected override void OnSelectedMenuItemChanged (MenuItem? selected)
{
// Logging.Debug ($"{Title} ({selected?.Title}) - IsOpen: {IsOpen ()}");
if (IsOpen () && selected is MenuBarItem { PopoverMenuOpen: false } selectedMenuBarItem)
{
ShowItem (selectedMenuBarItem);
}
}
///
public override void EndInit ()
{
base.EndInit ();
if (Border is { })
{
Border.Thickness = new (0);
Border.LineStyle = LineStyle.None;
}
// TODO: This needs to be done whenever a menuitem in any MenuBarItem changes
foreach (MenuBarItem? mbi in SubViews.Select (s => s as MenuBarItem))
{
App?.Popover?.Register (mbi?.PopoverMenu);
}
}
///
protected override bool OnAccepting (CommandEventArgs args)
{
// Logging.Debug ($"{Title} ({args.Context?.Source?.Title})");
// TODO: Ensure sourceMenuBar is actually one of our bar items
if (Visible && Enabled && args.Context?.Source is MenuBarItem { PopoverMenuOpen: false } sourceMenuBarItem)
{
if (!CanFocus)
{
Debug.Assert (!Active);
// We are not Active; change that
Active = true;
ShowItem (sourceMenuBarItem);
if (!sourceMenuBarItem.HasFocus)
{
sourceMenuBarItem.SetFocus ();
}
}
else
{
Debug.Assert (Active);
ShowItem (sourceMenuBarItem);
}
return true;
}
return false;
}
///
protected override void OnAccepted (CommandEventArgs args)
{
// Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command}");
base.OnAccepted (args);
if (SubViews.OfType ().Contains (args.Context?.Source))
{
return;
}
Active = false;
}
///
/// Shows the specified popover, but only if the menu bar is active.
///
///
private void ShowItem (MenuBarItem? menuBarItem)
{
// Logging.Debug ($"{Title} - {menuBarItem?.Id}");
if (!Active || !Visible)
{
// Logging.Debug ($"{Title} - {menuBarItem?.Id} - Not Active, not showing.");
return;
}
// 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 Application Popover is part of this MenuBar, hide it.
if (App?.Popover?.GetActivePopover () is PopoverMenu popoverMenu
&& popoverMenu.Root?.SuperMenuItem?.SuperView == this)
{
// Logging.Debug ($"{Title} - Calling App?.Popover?.Hide ({popoverMenu.Title})");
App?.Popover.Hide (popoverMenu);
}
if (menuBarItem is null)
{
// Logging.Debug ($"{Title} - menuBarItem is null.");
return;
}
Active = true;
menuBarItem.SetFocus ();
if (menuBarItem.PopoverMenu?.Root is { })
{
menuBarItem.PopoverMenu.Root.SuperMenuItem = menuBarItem;
menuBarItem.PopoverMenu.Root.SchemeName = SchemeName;
}
// Logging.Debug ($"{Title} - \"{menuBarItem.PopoverMenu?.Title}\".MakeVisible");
if (menuBarItem.PopoverMenu is { })
{
menuBarItem.PopoverMenu.App ??= App;
menuBarItem.PopoverMenu.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom));
}
menuBarItem.Accepting += OnMenuItemAccepted;
return;
void OnMenuItemAccepted (object? sender, EventArgs args)
{
// Logging.Debug ($"{Title} - OnMenuItemAccepted");
if (menuBarItem.PopoverMenu is { })
{
menuBarItem.PopoverMenu.VisibleChanged -= OnMenuItemAccepted;
}
if (Active && menuBarItem.PopoverMenu is { Visible: false })
{
Active = false;
HasFocus = false;
}
}
}
private MenuBarItem? GetActiveItem () { return SubViews.OfType ().FirstOrDefault (sv => sv is { PopoverMenu: { Visible: true } }); }
///
/// 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 (MenuBarItem? activeItem)
{
// Logging.Debug ($"{Title} ({activeItem?.Title}) - Active: {Active}, CanFocus: {CanFocus}, HasFocus: {HasFocus}");
if (activeItem is null || !activeItem.PopoverMenu!.Visible)
{
// Logging.Debug ($"{Title} No active item.");
return false;
}
// IMPORTANT: Set Visible false before setting Active to false (Active changes Can/HasFocus)
activeItem.PopoverMenu!.Visible = false;
Active = false;
HasFocus = false;
return true;
}
///
/// Gets all menu items with the specified Title, anywhere in the menu hierarchy.
///
///
///
public IEnumerable