Application.Mouse.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. #nullable enable
  2. using System.ComponentModel;
  3. namespace Terminal.Gui.App;
  4. public static partial class Application // Mouse handling
  5. {
  6. /// <summary>
  7. /// INTERNAL API: Holds the last mouse position.
  8. /// </summary>
  9. internal static Point? LastMousePosition { get; set; }
  10. /// <summary>
  11. /// Gets the most recent position of the mouse.
  12. /// </summary>
  13. public static Point? GetLastMousePosition () { return LastMousePosition; }
  14. /// <summary>Disable or enable the mouse. The mouse is enabled by default.</summary>
  15. [ConfigurationProperty (Scope = typeof (SettingsScope))]
  16. public static bool IsMouseDisabled { get; set; }
  17. /// <summary>
  18. /// Static reference to the current <see cref="IApplication"/> <see cref="IMouseGrabHandler"/>.
  19. /// </summary>
  20. public static IMouseGrabHandler MouseGrabHandler
  21. {
  22. get => ApplicationImpl.Instance.MouseGrabHandler;
  23. set => ApplicationImpl.Instance.MouseGrabHandler = value ??
  24. throw new ArgumentNullException(nameof(value));
  25. }
  26. /// <summary>
  27. /// INTERNAL API: Called when a mouse event is raised by the driver. Determines the view under the mouse and
  28. /// calls the appropriate View mouse event handlers.
  29. /// </summary>
  30. /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
  31. /// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
  32. internal static void RaiseMouseEvent (MouseEventArgs mouseEvent)
  33. {
  34. if (Initialized)
  35. {
  36. // LastMousePosition is a static; only set if the application is initialized.
  37. LastMousePosition = mouseEvent.ScreenPosition;
  38. }
  39. if (IsMouseDisabled)
  40. {
  41. return;
  42. }
  43. // The position of the mouse is the same as the screen position at the application level.
  44. //Debug.Assert (mouseEvent.Position == mouseEvent.ScreenPosition);
  45. mouseEvent.Position = mouseEvent.ScreenPosition;
  46. List<View?> currentViewsUnderMouse = View.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse);
  47. View? deepestViewUnderMouse = currentViewsUnderMouse.LastOrDefault ();
  48. if (deepestViewUnderMouse is { })
  49. {
  50. #if DEBUG_IDISPOSABLE
  51. if (View.EnableDebugIDisposableAsserts && deepestViewUnderMouse.WasDisposed)
  52. {
  53. throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName);
  54. }
  55. #endif
  56. mouseEvent.View = deepestViewUnderMouse;
  57. }
  58. MouseEvent?.Invoke (null, mouseEvent);
  59. if (mouseEvent.Handled)
  60. {
  61. return;
  62. }
  63. // Dismiss the Popover if the user presses mouse outside of it
  64. if (mouseEvent.IsPressed
  65. && Popover?.GetActivePopover () as View is { Visible: true } visiblePopover
  66. && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false)
  67. {
  68. ApplicationPopover.HideWithQuitCommand (visiblePopover);
  69. // Recurse once so the event can be handled below the popover
  70. RaiseMouseEvent (mouseEvent);
  71. return;
  72. }
  73. if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent))
  74. {
  75. return;
  76. }
  77. // May be null before the prior condition or the condition may set it as null.
  78. // So, the checking must be outside the prior condition.
  79. if (deepestViewUnderMouse is null)
  80. {
  81. return;
  82. }
  83. // if the mouse is outside the Application.Top or Application.Popover hierarchy, we don't want to
  84. // send the mouse event to the deepest view under the mouse.
  85. if (!View.IsInHierarchy (Application.Top, deepestViewUnderMouse, true) && !View.IsInHierarchy (Popover?.GetActivePopover () as View, deepestViewUnderMouse, true))
  86. {
  87. return;
  88. }
  89. // Create a view-relative mouse event to send to the view that is under the mouse.
  90. MouseEventArgs viewMouseEvent;
  91. if (deepestViewUnderMouse is Adornment adornment)
  92. {
  93. Point frameLoc = adornment.ScreenToFrame (mouseEvent.ScreenPosition);
  94. viewMouseEvent = new ()
  95. {
  96. Position = frameLoc,
  97. Flags = mouseEvent.Flags,
  98. ScreenPosition = mouseEvent.ScreenPosition,
  99. View = deepestViewUnderMouse
  100. };
  101. }
  102. else if (deepestViewUnderMouse.ViewportToScreen (Rectangle.Empty with { Size = deepestViewUnderMouse.Viewport.Size }).Contains (mouseEvent.ScreenPosition))
  103. {
  104. Point viewportLocation = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition);
  105. viewMouseEvent = new ()
  106. {
  107. Position = viewportLocation,
  108. Flags = mouseEvent.Flags,
  109. ScreenPosition = mouseEvent.ScreenPosition,
  110. View = deepestViewUnderMouse
  111. };
  112. }
  113. else
  114. {
  115. // The mouse was outside any View's Viewport.
  116. // Debug.Fail ("This should never happen. If it does please file an Issue!!");
  117. return;
  118. }
  119. RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse);
  120. while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabHandler.MouseGrabView is not { })
  121. {
  122. if (deepestViewUnderMouse is Adornment adornmentView)
  123. {
  124. deepestViewUnderMouse = adornmentView.Parent?.SuperView;
  125. }
  126. else
  127. {
  128. deepestViewUnderMouse = deepestViewUnderMouse.SuperView;
  129. }
  130. if (deepestViewUnderMouse is null)
  131. {
  132. break;
  133. }
  134. Point boundsPoint = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition);
  135. viewMouseEvent = new ()
  136. {
  137. Position = boundsPoint,
  138. Flags = mouseEvent.Flags,
  139. ScreenPosition = mouseEvent.ScreenPosition,
  140. View = deepestViewUnderMouse
  141. };
  142. }
  143. }
  144. #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved
  145. /// <summary>
  146. /// Raised when a mouse event occurs. Can be cancelled by setting <see cref="HandledEventArgs.Handled"/> to <see langword="true"/>.
  147. /// </summary>
  148. /// <remarks>
  149. /// <para>
  150. /// <see cref="MouseEventArgs.ScreenPosition"/> coordinates are screen-relative.
  151. /// </para>
  152. /// <para>
  153. /// <see cref="MouseEventArgs.View"/> will be the deepest view under the mouse.
  154. /// </para>
  155. /// <para>
  156. /// <see cref="MouseEventArgs.Position"/> coordinates are view-relative. Only valid if <see cref="MouseEventArgs.View"/> is set.
  157. /// </para>
  158. /// <para>
  159. /// Use this even to handle mouse events at the application level, before View-specific handling.
  160. /// </para>
  161. /// </remarks>
  162. public static event EventHandler<MouseEventArgs>? MouseEvent;
  163. #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved
  164. internal static bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent)
  165. {
  166. if (MouseGrabHandler.MouseGrabView is { })
  167. {
  168. #if DEBUG_IDISPOSABLE
  169. if (View.EnableDebugIDisposableAsserts && MouseGrabHandler.MouseGrabView.WasDisposed)
  170. {
  171. throw new ObjectDisposedException (MouseGrabHandler.MouseGrabView.GetType ().FullName);
  172. }
  173. #endif
  174. // If the mouse is grabbed, send the event to the view that grabbed it.
  175. // The coordinates are relative to the Bounds of the view that grabbed the mouse.
  176. Point frameLoc = MouseGrabHandler.MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition);
  177. var viewRelativeMouseEvent = new MouseEventArgs
  178. {
  179. Position = frameLoc,
  180. Flags = mouseEvent.Flags,
  181. ScreenPosition = mouseEvent.ScreenPosition,
  182. View = deepestViewUnderMouse ?? MouseGrabHandler.MouseGrabView
  183. };
  184. //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
  185. if (MouseGrabHandler.MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true)
  186. {
  187. return true;
  188. }
  189. // ReSharper disable once ConditionIsAlwaysTrueOrFalse
  190. if (MouseGrabHandler.MouseGrabView is null && deepestViewUnderMouse is Adornment)
  191. {
  192. // The view that grabbed the mouse has been disposed
  193. return true;
  194. }
  195. }
  196. return false;
  197. }
  198. /// <summary>
  199. /// INTERNAL: Holds the non-<see cref="ViewportSettingsFlags.TransparentMouse"/> views that are currently under the mouse.
  200. /// </summary>
  201. internal static List<View?> CachedViewsUnderMouse { get; } = [];
  202. /// <summary>
  203. /// INTERNAL: Raises the MouseEnter and MouseLeave events for the views that are under the mouse.
  204. /// </summary>
  205. /// <param name="screenPosition">The position of the mouse.</param>
  206. /// <param name="currentViewsUnderMouse">The most recent result from GetViewsUnderLocation().</param>
  207. internal static void RaiseMouseEnterLeaveEvents (Point screenPosition, List<View?> currentViewsUnderMouse)
  208. {
  209. // Tell any views that are no longer under the mouse that the mouse has left
  210. List<View?> viewsToLeave = CachedViewsUnderMouse.Where (v => v is { } && !currentViewsUnderMouse.Contains (v)).ToList ();
  211. foreach (View? view in viewsToLeave)
  212. {
  213. if (view is null)
  214. {
  215. continue;
  216. }
  217. view.NewMouseLeaveEvent ();
  218. CachedViewsUnderMouse.Remove (view);
  219. }
  220. // Tell any views that are now under the mouse that the mouse has entered and add them to the list
  221. foreach (View? view in currentViewsUnderMouse)
  222. {
  223. if (view is null)
  224. {
  225. continue;
  226. }
  227. if (CachedViewsUnderMouse.Contains (view))
  228. {
  229. continue;
  230. }
  231. CachedViewsUnderMouse.Add (view);
  232. var raise = false;
  233. if (view is Adornment { Parent: { } } adornmentView)
  234. {
  235. Point superViewLoc = adornmentView.Parent.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition;
  236. raise = adornmentView.Contains (superViewLoc);
  237. }
  238. else
  239. {
  240. Point superViewLoc = view.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition;
  241. raise = view.Contains (superViewLoc);
  242. }
  243. if (!raise)
  244. {
  245. continue;
  246. }
  247. CancelEventArgs eventArgs = new ();
  248. bool? cancelled = view.NewMouseEnterEvent (eventArgs);
  249. if (cancelled is true || eventArgs.Cancel)
  250. {
  251. break;
  252. }
  253. }
  254. }
  255. }