ContextMenu.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. private static MenuBar menuBar;
  25. private Key key = Key.F10 | Key.ShiftMask;
  26. private MouseFlags mouseFlags = MouseFlags.Button3Clicked;
  27. private Toplevel container;
  28. /// <summary>
  29. /// Initializes a context menu with no menu items.
  30. /// </summary>
  31. public ContextMenu () : this (0, 0, new MenuBarItem ()) { }
  32. /// <summary>
  33. /// Initializes a context menu, with a <see cref="View"/> specifiying the parent/hose of the menu.
  34. /// </summary>
  35. /// <param name="host">The host view.</param>
  36. /// <param name="menuItems">The menu items for the context menu.</param>
  37. public ContextMenu (View host, MenuBarItem menuItems) :
  38. this (host.Frame.X, host.Frame.Y, menuItems)
  39. {
  40. Host = host;
  41. }
  42. /// <summary>
  43. /// Initializes a context menu with menu items at a specific screen location.
  44. /// </summary>
  45. /// <param name="x">The left position (screen relative).</param>
  46. /// <param name="y">The top position (screen relative).</param>
  47. /// <param name="menuItems">The menu items.</param>
  48. public ContextMenu (int x, int y, MenuBarItem menuItems)
  49. {
  50. if (IsShow) {
  51. Hide ();
  52. }
  53. MenuItems = menuItems;
  54. Position = new Point (x, y);
  55. }
  56. private void MenuBar_MenuAllClosed ()
  57. {
  58. Dispose ();
  59. }
  60. /// <summary>
  61. /// Disposes the context menu object.
  62. /// </summary>
  63. public void Dispose ()
  64. {
  65. if (IsShow) {
  66. menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
  67. menuBar.Dispose ();
  68. menuBar = null;
  69. IsShow = false;
  70. }
  71. if (container != null) {
  72. container.Closing -= Container_Closing;
  73. container.Resized -= Container_Resized;
  74. }
  75. }
  76. /// <summary>
  77. /// Shows (opens) the ContextMenu, displaying the <see cref="MenuItem"/>s it contains.
  78. /// </summary>
  79. public void Show ()
  80. {
  81. if (menuBar != null) {
  82. Hide ();
  83. }
  84. container = Application.Current;
  85. container.Closing += Container_Closing;
  86. container.Resized += Container_Resized;
  87. var frame = container.Frame;
  88. var position = Position;
  89. if (Host != null) {
  90. Host.ViewToScreen (container.Frame.X, container.Frame.Y, out int x, out int y);
  91. var pos = new Point (x, y);
  92. pos.Y += Host.Frame.Height - 1;
  93. if (position != pos) {
  94. Position = position = pos;
  95. }
  96. }
  97. var rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children);
  98. if (rect.Right >= frame.Right) {
  99. if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero) {
  100. position.X = frame.Right - rect.Width;
  101. } else if (ForceMinimumPosToZero) {
  102. position.X = 0;
  103. }
  104. } else if (ForceMinimumPosToZero && position.X < 0) {
  105. position.X = 0;
  106. }
  107. if (rect.Bottom >= frame.Bottom) {
  108. if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero) {
  109. if (Host == null) {
  110. position.Y = frame.Bottom - rect.Height - 1;
  111. } else {
  112. Host.ViewToScreen (container.Frame.X, container.Frame.Y, out int x, out int y);
  113. var pos = new Point (x, y);
  114. position.Y = pos.Y - rect.Height - 1;
  115. }
  116. } else if (ForceMinimumPosToZero) {
  117. position.Y = 0;
  118. }
  119. } else if (ForceMinimumPosToZero && position.Y < 0) {
  120. position.Y = 0;
  121. }
  122. menuBar = new MenuBar (new [] { MenuItems }) {
  123. X = position.X,
  124. Y = position.Y,
  125. Width = 0,
  126. Height = 0,
  127. UseSubMenusSingleFrame = UseSubMenusSingleFrame,
  128. Key = Key
  129. };
  130. menuBar.isContextMenuLoading = true;
  131. menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
  132. IsShow = true;
  133. menuBar.OpenMenu ();
  134. }
  135. private void Container_Resized (Size obj)
  136. {
  137. if (IsShow) {
  138. Show ();
  139. }
  140. }
  141. private void Container_Closing (ToplevelClosingEventArgs obj)
  142. {
  143. Hide ();
  144. }
  145. /// <summary>
  146. /// Hides (closes) the ContextMenu.
  147. /// </summary>
  148. public void Hide ()
  149. {
  150. menuBar.CleanUp ();
  151. Dispose ();
  152. }
  153. /// <summary>
  154. /// Event invoked when the <see cref="ContextMenu.Key"/> is changed.
  155. /// </summary>
  156. public event Action<Key> KeyChanged;
  157. /// <summary>
  158. /// Event invoked when the <see cref="ContextMenu.MouseFlags"/> is changed.
  159. /// </summary>
  160. public event Action<MouseFlags> MouseFlagsChanged;
  161. /// <summary>
  162. /// Gets or sets the menu position.
  163. /// </summary>
  164. public Point Position { get; set; }
  165. /// <summary>
  166. /// Gets or sets the menu items for this context menu.
  167. /// </summary>
  168. public MenuBarItem MenuItems { get; set; }
  169. /// <summary>
  170. /// <see cref="Gui.Key"/> specifies they keyboard key that will activate the context menu with the keyboard.
  171. /// </summary>
  172. public Key Key {
  173. get => key;
  174. set {
  175. var oldKey = key;
  176. key = value;
  177. KeyChanged?.Invoke (oldKey);
  178. }
  179. }
  180. /// <summary>
  181. /// <see cref="Gui.MouseFlags"/> specifies the mouse action used to activate the context menu by mouse.
  182. /// </summary>
  183. public MouseFlags MouseFlags {
  184. get => mouseFlags;
  185. set {
  186. var oldFlags = mouseFlags;
  187. mouseFlags = value;
  188. MouseFlagsChanged?.Invoke (oldFlags);
  189. }
  190. }
  191. /// <summary>
  192. /// Gets whether the ContextMenu is showing or not.
  193. /// </summary>
  194. public static bool IsShow { get; private set; }
  195. /// <summary>
  196. /// The host <see cref="View "/> which position will be used,
  197. /// otherwise if it's null the container will be used.
  198. /// </summary>
  199. public View Host { get; set; }
  200. /// <summary>
  201. /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position
  202. /// is less than zero. The default is <see langword="true"/> which means the context menu will be forced to the right.
  203. /// If set to <see langword="false"/>, the context menu will be clipped on the left if x is less than zero.
  204. /// </summary>
  205. public bool ForceMinimumPosToZero { get; set; } = true;
  206. /// <summary>
  207. /// Gets the <see cref="Gui.MenuBar"/> that is hosting this context menu.
  208. /// </summary>
  209. public MenuBar MenuBar { get => menuBar; }
  210. /// <summary>
  211. /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If <see langword="true"/>, the ContextMenu
  212. /// and any sub-menus that would normally cascade will be displayed within a single frame. If <see langword="false"/> (the default),
  213. /// sub-menus will cascade using separate frames for each level of the menu hierarchy.
  214. /// </summary>
  215. public bool UseSubMenusSingleFrame { get; set; }
  216. }
  217. }