Menu.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. namespace Terminal.Gui.Views;
  2. /// <summary>
  3. /// A <see cref="Bar"/>-derived object to be used as a vertically-oriented menu. Each subview is a <see cref="MenuItem"/>.
  4. /// </summary>
  5. public class Menu : Bar
  6. {
  7. /// <inheritdoc/>
  8. public Menu () : this ([]) { }
  9. /// <inheritdoc/>
  10. public Menu (IEnumerable<MenuItem>? menuItems) : this (menuItems?.Cast<View> ()) { }
  11. /// <inheritdoc/>
  12. public Menu (IEnumerable<View>? shortcuts) : base (shortcuts)
  13. {
  14. // Do this to support debugging traces where Title gets set
  15. base.HotKeySpecifier = (Rune)'\xffff';
  16. Orientation = Orientation.Vertical;
  17. Width = Dim.Auto ();
  18. Height = Dim.Auto (DimAutoStyle.Content, 1);
  19. SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Menu);
  20. if (Border is { })
  21. {
  22. Border.Settings &= ~BorderSettings.Title;
  23. }
  24. BorderStyle = DefaultBorderStyle;
  25. ConfigurationManager.Applied += OnConfigurationManagerApplied;
  26. }
  27. private void OnConfigurationManagerApplied (object? sender, ConfigurationManagerEventArgs e)
  28. {
  29. if (SuperView is { })
  30. {
  31. BorderStyle = DefaultBorderStyle;
  32. }
  33. }
  34. /// <summary>
  35. /// Gets or sets the default Border Style for Menus. The default is <see cref="LineStyle.None"/>.
  36. /// </summary>
  37. [ConfigurationProperty (Scope = typeof (ThemeScope))]
  38. public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.None;
  39. /// <summary>
  40. /// Gets or sets the menu item that opened this menu as a sub-menu.
  41. /// </summary>
  42. public MenuItem? SuperMenuItem { get; set; }
  43. /// <inheritdoc />
  44. protected override void OnVisibleChanged ()
  45. {
  46. if (Visible)
  47. {
  48. SelectedMenuItem = SubViews.Where (mi => mi is MenuItem).ElementAtOrDefault (0) as MenuItem;
  49. }
  50. }
  51. /// <inheritdoc />
  52. protected override void OnSubViewAdded (View view)
  53. {
  54. base.OnSubViewAdded (view);
  55. switch (view)
  56. {
  57. case MenuItem menuItem:
  58. {
  59. menuItem.CanFocus = true;
  60. AddCommand (menuItem.Command, (ctx) =>
  61. {
  62. RaiseAccepted (ctx);
  63. return true;
  64. });
  65. menuItem.Accepted += MenuItemOnAccepted;
  66. break;
  67. void MenuItemOnAccepted (object? sender, CommandEventArgs e)
  68. {
  69. // Logging.Debug ($"MenuItemOnAccepted: Calling RaiseAccepted {e.Context?.Source?.Title}");
  70. RaiseAccepted (e.Context);
  71. }
  72. }
  73. case Line line:
  74. // Grow line so we get auto-join line
  75. line.X = Pos.Func (_ => -Border!.Thickness.Left);
  76. line.Width = Dim.Fill ()! + Dim.Func (_ => Border!.Thickness.Right);
  77. break;
  78. }
  79. }
  80. /// <inheritdoc />
  81. protected override bool OnAccepting (CommandEventArgs args)
  82. {
  83. // When the user accepts a menuItem, Menu.RaiseAccepting is called, and we intercept that here.
  84. // Logging.Debug ($"{Title} - {args.Context?.Source?.Title} Command: {args.Context?.Command}");
  85. // TODO: Consider having PopoverMenu subscribe to Accepting instead of us overriding OnAccepting here
  86. // TODO: Doing so would be better encapsulation and might allow us to remove the SuperMenuItem property.
  87. if (SuperView is { })
  88. {
  89. // Logging.Debug ($"{Title} - SuperView is null");
  90. //return false;
  91. }
  92. // Logging.Debug ($"{Title} - {args.Context}");
  93. if (args.Context is CommandContext<KeyBinding> { Binding.Key: { } } keyCommandContext && keyCommandContext.Binding.Key == Application.QuitKey)
  94. {
  95. // Special case QuitKey if we are Visible - This supports a MenuItem with Key = Application.QuitKey/Command = Command.Quit
  96. // And causes just the menu to quit.
  97. // Logging.Debug ($"{Title} - Returning true - Application.QuitKey/Command = Command.Quit");
  98. return true;
  99. }
  100. // Because we may not have a SuperView (if we are in a PopoverMenu), we need to propagate
  101. // Command.Accept to the SuperMenuItem if it exists.
  102. if (SuperView is null && SuperMenuItem is { })
  103. {
  104. // Logging.Debug ($"{Title} - Invoking Accept on SuperMenuItem: {SuperMenuItem?.Title}...");
  105. return SuperMenuItem?.InvokeCommand (Command.Accept, args.Context) is true;
  106. }
  107. return false;
  108. }
  109. // TODO: Consider moving Accepted to Bar?
  110. /// <summary>
  111. /// Raises the <see cref="OnAccepted"/>/<see cref="Accepted"/> event indicating an item in this menu (or submenu)
  112. /// was accepted. This is used to determine when to hide the menu.
  113. /// </summary>
  114. /// <param name="ctx"></param>
  115. /// <returns></returns>
  116. protected void RaiseAccepted (ICommandContext? ctx)
  117. {
  118. //Logging.Trace ($"RaiseAccepted: {ctx}");
  119. CommandEventArgs args = new () { Context = ctx };
  120. OnAccepted (args);
  121. Accepted?.Invoke (this, args);
  122. }
  123. /// <summary>
  124. /// Called when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu.
  125. /// </summary>
  126. /// <remarks>
  127. /// </remarks>
  128. /// <param name="args"></param>
  129. protected virtual void OnAccepted (CommandEventArgs args) { }
  130. /// <summary>
  131. /// Raised when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu.
  132. /// </summary>
  133. /// <remarks>
  134. /// <para>
  135. /// See <see cref="RaiseAccepted"/> for more information.
  136. /// </para>
  137. /// </remarks>
  138. public event EventHandler<CommandEventArgs>? Accepted;
  139. /// <inheritdoc />
  140. protected override void OnFocusedChanged (View? previousFocused, View? focused)
  141. {
  142. base.OnFocusedChanged (previousFocused, focused);
  143. SelectedMenuItem = focused as MenuItem;
  144. RaiseSelectedMenuItemChanged (SelectedMenuItem);
  145. }
  146. /// <summary>
  147. /// Gets or set the currently selected menu item. This is a helper that
  148. /// tracks <see cref="View.Focused"/>.
  149. /// </summary>
  150. public MenuItem? SelectedMenuItem
  151. {
  152. get => Focused as MenuItem;
  153. set
  154. {
  155. if (value == Focused)
  156. {
  157. return;
  158. }
  159. // Note we DO NOT set focus here; This property tracks Focused
  160. }
  161. }
  162. internal void RaiseSelectedMenuItemChanged (MenuItem? selected)
  163. {
  164. // Logging.Debug ($"{Title} ({selected?.Title})");
  165. OnSelectedMenuItemChanged (selected);
  166. SelectedMenuItemChanged?.Invoke (this, selected);
  167. }
  168. /// <summary>
  169. /// Called when the selected menu item has changed.
  170. /// </summary>
  171. /// <param name="selected"></param>
  172. protected virtual void OnSelectedMenuItemChanged (MenuItem? selected)
  173. {
  174. }
  175. /// <summary>
  176. /// Raised when the selected menu item has changed.
  177. /// </summary>
  178. public event EventHandler<MenuItem?>? SelectedMenuItemChanged;
  179. /// <inheritdoc />
  180. protected override void Dispose (bool disposing)
  181. {
  182. base.Dispose (disposing);
  183. if (disposing)
  184. {
  185. ConfigurationManager.Applied -= OnConfigurationManagerApplied;
  186. }
  187. }
  188. }