ContextMenu.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. namespace Terminal.Gui;
  2. /// <summary>
  3. /// ContextMenu provides a pop-up menu that can be positioned anywhere within a <see cref="View"/>. ContextMenu is
  4. /// analogous to <see cref="MenuBar"/> and, once activated, works like a sub-menu of a <see cref="MenuBarItem"/> (but
  5. /// can be positioned anywhere).
  6. /// <para>
  7. /// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of
  8. /// the ContextMenu frame (either to the right or left, depending on where the ContextMenu is relative to the edge
  9. /// of the screen). By setting <see cref="UseSubMenusSingleFrame"/> to <see langword="true"/>, this behavior can be
  10. /// changed such that all sub-menus are drawn within the ContextMenu frame.
  11. /// </para>
  12. /// <para>
  13. /// ContextMenus can be activated using the Shift-F10 key (by default; use the <see cref="Key"/> to change to
  14. /// another key).
  15. /// </para>
  16. /// <para>
  17. /// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling
  18. /// <see cref="Show()"/>.
  19. /// </para>
  20. /// <para>ContextMenus are located using screen using screen coordinates and appear above all other Views.</para>
  21. /// </summary>
  22. public sealed class ContextMenu : IDisposable
  23. {
  24. private static MenuBar _menuBar;
  25. private Toplevel _container;
  26. private Key _key = DefaultKey;
  27. private MouseFlags _mouseFlags = MouseFlags.Button3Clicked;
  28. /// <summary>Initializes a context menu with no menu items.</summary>
  29. public ContextMenu ()
  30. {
  31. if (IsShow)
  32. {
  33. if (_menuBar.SuperView is { })
  34. {
  35. Hide ();
  36. }
  37. IsShow = false;
  38. }
  39. MenuItems = new MenuBarItem ();
  40. }
  41. /// <summary>The default shortcut key for activating the context menu.</summary>
  42. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
  43. public static Key DefaultKey { get; set; } = Key.F10.WithShift;
  44. /// <summary>
  45. /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position is
  46. /// less than zero. The default is <see langword="true"/> which means the context menu will be forced to the right. If
  47. /// set to <see langword="false"/>, the context menu will be clipped on the left if x is less than zero.
  48. /// </summary>
  49. public bool ForceMinimumPosToZero { get; set; } = true;
  50. /// <summary>The host <see cref="View "/> which position will be used, otherwise if it's null the container will be used.</summary>
  51. public View Host { get; set; }
  52. /// <summary>Gets whether the ContextMenu is showing or not.</summary>
  53. public static bool IsShow { get; private set; }
  54. /// <summary>Specifies the key that will activate the context menu.</summary>
  55. public Key Key
  56. {
  57. get => _key;
  58. set
  59. {
  60. Key oldKey = _key;
  61. _key = value;
  62. KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, _key));
  63. }
  64. }
  65. /// <summary>Gets the <see cref="MenuBar"/> that is hosting this context menu.</summary>
  66. public MenuBar MenuBar => _menuBar;
  67. /// <summary>Gets or sets the menu items for this context menu.</summary>
  68. public MenuBarItem MenuItems { get; set; }
  69. /// <summary><see cref="Gui.MouseFlags"/> specifies the mouse action used to activate the context menu by mouse.</summary>
  70. public MouseFlags MouseFlags
  71. {
  72. get => _mouseFlags;
  73. set
  74. {
  75. MouseFlags oldFlags = _mouseFlags;
  76. _mouseFlags = value;
  77. MouseFlagsChanged?.Invoke (this, new MouseFlagsChangedEventArgs (oldFlags, value));
  78. }
  79. }
  80. /// <summary>Gets or sets the menu position.</summary>
  81. public Point Position { get; set; }
  82. /// <summary>
  83. /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If <see langword="true"/>, the
  84. /// ContextMenu and any sub-menus that would normally cascade will be displayed within a single frame. If
  85. /// <see langword="false"/> (the default), sub-menus will cascade using separate frames for each level of the menu
  86. /// hierarchy.
  87. /// </summary>
  88. public bool UseSubMenusSingleFrame { get; set; }
  89. /// <summary>Disposes the context menu object.</summary>
  90. public void Dispose ()
  91. {
  92. if (IsShow)
  93. {
  94. _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
  95. _menuBar.Dispose ();
  96. _menuBar = null;
  97. IsShow = false;
  98. }
  99. if (_container is { })
  100. {
  101. _container.Closing -= Container_Closing;
  102. _container.Deactivate -= Container_Deactivate;
  103. }
  104. }
  105. /// <summary>Hides (closes) the ContextMenu.</summary>
  106. public void Hide ()
  107. {
  108. _menuBar?.CleanUp ();
  109. Dispose ();
  110. }
  111. /// <summary>Event invoked when the <see cref="ContextMenu.Key"/> is changed.</summary>
  112. public event EventHandler<KeyChangedEventArgs> KeyChanged;
  113. /// <summary>Event invoked when the <see cref="ContextMenu.MouseFlags"/> is changed.</summary>
  114. public event EventHandler<MouseFlagsChangedEventArgs> MouseFlagsChanged;
  115. /// <summary>Shows (opens) the ContextMenu, displaying the <see cref="MenuItem"/>s it contains.</summary>
  116. public void Show ()
  117. {
  118. if (_menuBar is { })
  119. {
  120. Hide ();
  121. }
  122. _container = Application.Current;
  123. _container.Closing += Container_Closing;
  124. _container.Deactivate += Container_Deactivate;
  125. Rectangle frame = Application.Screen;
  126. Point position = Position;
  127. if (Host is { })
  128. {
  129. Point pos = Host.ViewportToScreen (frame).Location;
  130. pos.Y += Host.Frame.Height - 1;
  131. if (position != pos)
  132. {
  133. Position = position = pos;
  134. }
  135. }
  136. Rectangle rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children);
  137. if (rect.Right >= frame.Right)
  138. {
  139. if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero)
  140. {
  141. position.X = frame.Right - rect.Width;
  142. }
  143. else if (ForceMinimumPosToZero)
  144. {
  145. position.X = 0;
  146. }
  147. }
  148. else if (ForceMinimumPosToZero && position.X < 0)
  149. {
  150. position.X = 0;
  151. }
  152. if (rect.Bottom >= frame.Bottom)
  153. {
  154. if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero)
  155. {
  156. if (Host is null)
  157. {
  158. position.Y = frame.Bottom - rect.Height - 1;
  159. }
  160. else
  161. {
  162. Point pos = Host.ViewportToScreen (frame).Location;
  163. position.Y = pos.Y - rect.Height - 1;
  164. }
  165. }
  166. else if (ForceMinimumPosToZero)
  167. {
  168. position.Y = 0;
  169. }
  170. }
  171. else if (ForceMinimumPosToZero && position.Y < 0)
  172. {
  173. position.Y = 0;
  174. }
  175. _menuBar = new MenuBar
  176. {
  177. X = position.X,
  178. Y = position.Y,
  179. Width = 0,
  180. Height = 0,
  181. UseSubMenusSingleFrame = UseSubMenusSingleFrame,
  182. Key = Key,
  183. Menus = [MenuItems]
  184. };
  185. _menuBar._isContextMenuLoading = true;
  186. _menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
  187. _menuBar.BeginInit ();
  188. _menuBar.EndInit ();
  189. IsShow = true;
  190. _menuBar.OpenMenu ();
  191. }
  192. private void Container_Deactivate (object sender, ToplevelEventArgs e) { Hide (); }
  193. private void Container_Closing (object sender, ToplevelClosingEventArgs obj) { Hide (); }
  194. private void MenuBar_MenuAllClosed (object sender, EventArgs e) { Dispose (); }
  195. }