Menuv2.cs 7.5 KB

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