ContextMenu.cs 7.4 KB

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