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 GetMenuItemsWithTitle (string title) { List menuItems = new (); if (string.IsNullOrEmpty (title)) { return menuItems; } foreach (MenuBarItem mbi in SubViews.OfType ()) { if (mbi.PopoverMenu is { }) { menuItems.AddRange (mbi.PopoverMenu.GetMenuItemsOfAllSubMenus ()); } } return menuItems.Where (mi => mi.Title == title); } /// public bool EnableForDesign (ref TContext targetView) where TContext : notnull { // Note: This menu is used by unit tests. If you modify it, you'll likely have to update // unit tests. if (targetView is View target) { App ??= target.App; } Id = "DemoBar"; var bordersCb = new CheckBox { Title = "_Borders", CheckedState = CheckState.Checked }; var autoSaveCb = new CheckBox { Title = "_Auto Save" }; var enableOverwriteCb = new CheckBox { Title = "Enable _Overwrite" }; var mutuallyExclusiveOptionsSelector = new OptionSelector { Labels = ["G_ood", "_Bad", "U_gly"], Value = 0 }; var menuBgColorCp = new ColorPicker { Width = 30 }; menuBgColorCp.ColorChanged += (sender, args) => { // BUGBUG: This is weird. SetScheme ( GetScheme () with { Normal = new ( GetAttributeForRole (VisualRole.Normal).Foreground, args.Result, GetAttributeForRole (VisualRole.Normal).Style) }); }; Add ( new MenuBarItem ( "_File", [ new MenuItem (targetView as View, Command.New), new MenuItem (targetView as View, Command.Open), new MenuItem (targetView as View, Command.Save), new MenuItem (targetView as View, Command.SaveAs), new Line (), new MenuItem { Title = "_File Options", SubMenu = new ( [ new () { Id = "AutoSave", Text = "(no Command)", Key = Key.F10, CommandView = autoSaveCb }, new () { Text = "Overwrite", Id = "Overwrite", Key = Key.W.WithCtrl, CommandView = enableOverwriteCb, Command = Command.EnableOverwrite, TargetView = targetView as View }, new () { Title = "_File Settings...", HelpText = "More file settings", Action = () => MessageBox.Query ( "File Settings", "This is the File Settings Dialog\n", "_Ok", "_Cancel") } ] ) }, new Line (), new MenuItem { Title = "_Preferences", SubMenu = new ( [ new MenuItem { CommandView = bordersCb, HelpText = "Toggle Menu Borders", Action = ToggleMenuBorders }, new MenuItem { HelpText = "3 Mutually Exclusive Options", CommandView = mutuallyExclusiveOptionsSelector, Key = Key.F7 }, new Line (), new MenuItem { HelpText = "MenuBar BG Color", CommandView = menuBgColorCp, Key = Key.F8 } ] ) }, new Line (), new MenuItem { TargetView = targetView as View, Key = Application.QuitKey, Command = Command.Quit } ] ) ); Add ( new MenuBarItem ( "_Edit", [ new MenuItem (targetView as View, Command.Cut), new MenuItem (targetView as View, Command.Copy), new MenuItem (targetView as View, Command.Paste), new Line (), new MenuItem (targetView as View, Command.SelectAll), new Line (), new MenuItem { Title = "_Details", SubMenu = new (ConfigureDetailsSubMenu ()) } ] ) ); Add ( new MenuBarItem ( "_Help", [ new MenuItem { Title = "_Online Help...", Action = () => MessageBox.Query ("Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok") }, new MenuItem { Title = "About...", Action = () => MessageBox.Query ("About", "Something About Mary.", "Ok") } ] ) ); return true; void ToggleMenuBorders () { foreach (MenuBarItem mbi in SubViews.OfType ()) { if (mbi is not { PopoverMenu: { } }) { continue; } foreach (Menu? subMenu in mbi.PopoverMenu.GetAllSubMenus ()) { if (bordersCb.CheckedState == CheckState.Checked) { subMenu.Border!.Thickness = new (1); } else { subMenu.Border!.Thickness = new (0); } } } } MenuItem [] ConfigureDetailsSubMenu () { var detail = new MenuItem { Title = "_Detail 1", Text = "Some detail #1" }; var nestedSubMenu = new MenuItem { Title = "_Moar Details", SubMenu = new (ConfigureMoreDetailsSubMenu ()) }; var editMode = new MenuItem { Text = "App Binding to Command.Edit", Id = "EditMode", Command = Command.Edit, CommandView = new CheckBox { Title = "E_dit Mode" } }; return [detail, nestedSubMenu, null!, editMode]; View [] ConfigureMoreDetailsSubMenu () { var deeperDetail = new MenuItem { Title = "_Deeper Detail", Text = "Deeper Detail", Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } }; var belowLineDetail = new MenuItem { Title = "_Even more detail", Text = "Below the line" }; // This ensures the checkbox state toggles when the hotkey of Title is pressed. //shortcut4.Accepting += (sender, args) => args.Cancel = true; return [deeperDetail, new Line (), belowLineDetail]; } } } /// protected override void Dispose (bool disposing) { base.Dispose (disposing); if (disposing) { SuperViewChanged += OnSuperViewChanged; ConfigurationManager.Applied -= OnConfigurationManagerApplied; } } }