ContextMenu.cs 9.0 KB

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