| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 |
- using System.ComponentModel;
- using System.Diagnostics;
- namespace Terminal.Gui.Views;
- /// <summary>
- /// A horizontal list of <see cref="MenuBarItem"/>s. Each <see cref="MenuBarItem"/> can have a
- /// <see cref="PopoverMenu"/> that is shown when the <see cref="MenuBarItem"/> is selected.
- /// </summary>
- /// <remarks>
- /// MenuBars may be hosted by any View and will, by default, be positioned the full width across the top of the View's
- /// Viewport.
- /// </remarks>
- public class MenuBar : Menu, IDesignable
- {
- /// <inheritdoc/>
- public MenuBar () : this ([]) { }
- /// <inheritdoc/>
- public MenuBar (IEnumerable<MenuBarItem> 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<MenuBarItem> ().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; }
- /// <inheritdoc/>
- protected override bool OnBorderStyleChanged ()
- {
- //HideActiveItem ();
- return base.OnBorderStyleChanged ();
- }
- /// <summary>
- /// Gets or sets the default Border Style for the MenuBar. The default is <see cref="LineStyle.None"/>.
- /// </summary>
- [ConfigurationProperty (Scope = typeof (ThemeScope))]
- public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.None;
- private Key _key = DefaultKey;
- /// <summary>Specifies the key that will activate the context menu.</summary>
- public Key Key
- {
- get => _key;
- set
- {
- Key oldKey = _key;
- _key = value;
- KeyChanged?.Invoke (this, new (oldKey, _key));
- }
- }
- /// <summary>
- /// Sets the Menu Bar Items for this Menu Bar. This will replace any existing Menu Bar Items.
- /// </summary>
- /// <remarks>
- /// <para>
- /// This is a convenience property to help porting from the v1 MenuBar.
- /// </para>
- /// </remarks>
- public MenuBarItem []? Menus
- {
- set
- {
- RemoveAll ();
- if (value is null)
- {
- return;
- }
- foreach (MenuBarItem mbi in value)
- {
- Add (mbi);
- }
- }
- }
- /// <inheritdoc/>
- protected override void OnSubViewAdded (View view)
- {
- base.OnSubViewAdded (view);
- if (view is MenuBarItem mbi)
- {
- mbi.Accepted += OnMenuBarItemAccepted;
- mbi.PopoverMenuOpenChanged += OnMenuBarItemPopoverMenuOpenChanged;
- }
- }
- /// <inheritdoc/>
- 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<bool> 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);
- }
- /// <summary>Raised when <see cref="Key"/> is changed.</summary>
- public event EventHandler<KeyChangedEventArgs>? KeyChanged;
- /// <summary>The default key for activating menu bars.</summary>
- [ConfigurationProperty (Scope = typeof (SettingsScope))]
- public static Key DefaultKey { get; set; } = Key.F9;
- /// <summary>
- /// Gets whether any of the menu bar items have a visible <see cref="PopoverMenu"/>.
- /// </summary>
- /// <exception cref="NotImplementedException"></exception>
- public bool IsOpen () { return SubViews.OfType<MenuBarItem> ().Count (sv => sv is { PopoverMenuOpen: true }) > 0; }
- private bool _active;
- /// <summary>
- /// 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 <see cref="IsOpen"/> to determine if a PopoverMenu of
- /// a MenuBarItem is open.
- /// </summary>
- /// <returns></returns>
- 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}");
- }
- }
- /// <inheritdoc/>
- 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);
- }
- /// <inheritdoc/>
- protected override void OnMouseLeave ()
- {
- // Logging.Debug ($"CanFocus = {CanFocus}, HasFocus = {HasFocus}");
- if (!IsOpen ())
- {
- Active = false;
- }
- base.OnMouseLeave ();
- }
- /// <inheritdoc/>
- protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
- {
- // Logging.Debug ($"CanFocus = {CanFocus}, HasFocus = {HasFocus}");
- if (!newHasFocus)
- {
- Active = false;
- }
- }
- /// <inheritdoc/>
- protected override void OnSelectedMenuItemChanged (MenuItem? selected)
- {
- // Logging.Debug ($"{Title} ({selected?.Title}) - IsOpen: {IsOpen ()}");
- if (IsOpen () && selected is MenuBarItem { PopoverMenuOpen: false } selectedMenuBarItem)
- {
- ShowItem (selectedMenuBarItem);
- }
- }
- /// <inheritdoc/>
- 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);
- }
- }
- /// <inheritdoc/>
- 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;
- }
- /// <inheritdoc/>
- protected override void OnAccepted (CommandEventArgs args)
- {
- // Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command}");
- base.OnAccepted (args);
- if (SubViews.OfType<MenuBarItem> ().Contains (args.Context?.Source))
- {
- return;
- }
- Active = false;
- }
- /// <summary>
- /// Shows the specified popover, but only if the menu bar is active.
- /// </summary>
- /// <param name="menuBarItem"></param>
- 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<MenuBarItem> ().FirstOrDefault (sv => sv is { PopoverMenu: { Visible: true } }); }
- /// <summary>
- /// Hides the popover menu associated with the active menu bar item and updates the focus state.
- /// </summary>
- /// <returns><see langword="true"/> if the popover was hidden</returns>
- public bool HideActiveItem () { return HideItem (GetActiveItem ()); }
- /// <summary>
- /// Hides popover menu associated with the specified menu bar item and updates the focus state.
- /// </summary>
- /// <param name="activeItem"></param>
- /// <returns><see langword="true"/> if the popover was hidden</returns>
- 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;
- }
- /// <summary>
- /// Gets all menu items with the specified Title, anywhere in the menu hierarchy.
- /// </summary>
- /// <param name="title"></param>
- /// <returns></returns>
- public IEnumerable<MenuItem> GetMenuItemsWithTitle (string title)
- {
- List<MenuItem> menuItems = new ();
- if (string.IsNullOrEmpty (title))
- {
- return menuItems;
- }
- foreach (MenuBarItem mbi in SubViews.OfType<MenuBarItem> ())
- {
- if (mbi.PopoverMenu is { })
- {
- menuItems.AddRange (mbi.PopoverMenu.GetMenuItemsOfAllSubMenus ());
- }
- }
- return menuItems.Where (mi => mi.Title == title);
- }
- /// <inheritdoc/>
- public bool EnableForDesign<TContext> (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 (App,
- "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 (App, "Online Help", "https://gui-cs.github.io/Terminal.Gui", "Ok")
- },
- new MenuItem
- {
- Title = "About...",
- Action = () => MessageBox.Query (App, "About", "Something About Mary.", "Ok")
- }
- ]
- )
- );
- return true;
- void ToggleMenuBorders ()
- {
- foreach (MenuBarItem mbi in SubViews.OfType<MenuBarItem> ())
- {
- 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 (App, "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];
- }
- }
- }
- /// <inheritdoc/>
- protected override void Dispose (bool disposing)
- {
- base.Dispose (disposing);
- if (disposing)
- {
- SuperViewChanged += OnSuperViewChanged;
- ConfigurationManager.Applied -= OnConfigurationManagerApplied;
- }
- }
- }
|