namespace Terminal.Gui.Views; /// /// A -derived object to be used as a vertically-oriented menu. Each subview is a . /// public class Menu : Bar { /// public Menu () : this ([]) { } /// public Menu (IEnumerable? menuItems) : this (menuItems?.Cast ()) { } /// public Menu (IEnumerable? shortcuts) : base (shortcuts) { // Do this to support debugging traces where Title gets set base.HotKeySpecifier = (Rune)'\xffff'; Orientation = Orientation.Vertical; Width = Dim.Auto (); Height = Dim.Auto (DimAutoStyle.Content, 1); SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu); if (Border is { }) { Border.Settings &= ~BorderSettings.Title; } BorderStyle = DefaultBorderStyle; ConfigurationManager.Applied += OnConfigurationManagerApplied; } private void OnConfigurationManagerApplied (object? sender, ConfigurationManagerEventArgs e) { if (SuperView is { }) { BorderStyle = DefaultBorderStyle; } } /// /// Gets or sets the default Border Style for Menus. The default is . /// [ConfigurationProperty (Scope = typeof (ThemeScope))] public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.None; /// /// Gets or sets the menu item that opened this menu as a sub-menu. /// public MenuItem? SuperMenuItem { get; set; } /// protected override void OnVisibleChanged () { if (Visible) { SelectedMenuItem = SubViews.Where (mi => mi is MenuItem).ElementAtOrDefault (0) as MenuItem; } } /// protected override void OnSubViewAdded (View view) { base.OnSubViewAdded (view); switch (view) { case MenuItem menuItem: { menuItem.CanFocus = true; AddCommand (menuItem.Command, (ctx) => { RaiseAccepted (ctx); return true; }); menuItem.Accepted += MenuItemOnAccepted; break; void MenuItemOnAccepted (object? sender, CommandEventArgs e) { // Logging.Debug ($"MenuItemOnAccepted: Calling RaiseAccepted {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); line.Width = Dim.Fill ()! + Dim.Func (_ => Border!.Thickness.Right); break; } } /// protected override bool OnAccepting (CommandEventArgs args) { // When the user accepts a menuItem, Menu.RaiseAccepting is called, and we intercept that here. // Logging.Debug ($"{Title} - {args.Context?.Source?.Title} Command: {args.Context?.Command}"); // TODO: Consider having PopoverMenu subscribe to Accepting instead of us overriding OnAccepting here // TODO: Doing so would be better encapsulation and might allow us to remove the SuperMenuItem property. if (SuperView is { }) { // Logging.Debug ($"{Title} - SuperView is null"); //return false; } // Logging.Debug ($"{Title} - {args.Context}"); if (args.Context is CommandContext { Binding.Key: { } } keyCommandContext && keyCommandContext.Binding.Key == Application.QuitKey) { // Special case QuitKey if we are Visible - This supports a MenuItem with Key = Application.QuitKey/Command = Command.Quit // And causes just the menu to quit. // Logging.Debug ($"{Title} - Returning true - Application.QuitKey/Command = Command.Quit"); return true; } // Because we may not have a SuperView (if we are in a PopoverMenu), we need to propagate // Command.Accept to the SuperMenuItem if it exists. if (SuperView is null && SuperMenuItem is { }) { // Logging.Debug ($"{Title} - Invoking Accept on SuperMenuItem: {SuperMenuItem?.Title}..."); return SuperMenuItem?.InvokeCommand (Command.Accept, args.Context) is true; } return false; } // TODO: Consider moving Accepted to Bar? /// /// Raises the / event indicating an item in this menu (or submenu) /// was accepted. This is used to determine when to hide the menu. /// /// /// protected void RaiseAccepted (ICommandContext? ctx) { //Logging.Trace ($"RaiseAccepted: {ctx}"); CommandEventArgs args = new () { Context = ctx }; OnAccepted (args); Accepted?.Invoke (this, args); } /// /// Called when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu. /// /// /// /// protected virtual void OnAccepted (CommandEventArgs args) { } /// /// Raised when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu. /// /// /// /// See for more information. /// /// public event EventHandler? Accepted; /// protected override void OnFocusedChanged (View? previousFocused, View? focused) { base.OnFocusedChanged (previousFocused, focused); SelectedMenuItem = focused as MenuItem; RaiseSelectedMenuItemChanged (SelectedMenuItem); } /// /// Gets or set the currently selected menu item. This is a helper that /// tracks . /// public MenuItem? SelectedMenuItem { get => Focused as MenuItem; set { if (value == Focused) { return; } // Note we DO NOT set focus here; This property tracks Focused } } internal void RaiseSelectedMenuItemChanged (MenuItem? selected) { // Logging.Debug ($"{Title} ({selected?.Title})"); OnSelectedMenuItemChanged (selected); SelectedMenuItemChanged?.Invoke (this, selected); } /// /// Called when the selected menu item has changed. /// /// protected virtual void OnSelectedMenuItemChanged (MenuItem? selected) { } /// /// Raised when the selected menu item has changed. /// public event EventHandler? SelectedMenuItemChanged; /// protected override void Dispose (bool disposing) { base.Dispose (disposing); if (disposing) { ConfigurationManager.Applied -= OnConfigurationManagerApplied; } } }