MouseImpl.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. using System.ComponentModel;
  2. namespace Terminal.Gui.App;
  3. /// <summary>
  4. /// INTERNAL: Implements <see cref="IMouse"/> to manage mouse event handling and state.
  5. /// <para>
  6. /// This class holds all mouse-related state that was previously in the static <see cref="App"/> class,
  7. /// enabling better testability and parallel test execution.
  8. /// </para>
  9. /// </summary>
  10. internal class MouseImpl : IMouse, IDisposable
  11. {
  12. /// <summary>
  13. /// Initializes a new instance of the <see cref="MouseImpl"/> class and subscribes to Application configuration property events.
  14. /// </summary>
  15. public MouseImpl ()
  16. {
  17. // Initialize from Application static property (ConfigurationManager may have set this before we were created)
  18. IsMouseDisabled = Application.IsMouseDisabled;
  19. // Subscribe to Application static property change events
  20. Application.IsMouseDisabledChanged += OnIsMouseDisabledChanged;
  21. }
  22. /// <inheritdoc/>
  23. public IApplication? App { get; set; }
  24. /// <inheritdoc/>
  25. public Point? LastMousePosition { get; set; }
  26. /// <inheritdoc/>
  27. public bool IsMouseDisabled { get; set; }
  28. /// <inheritdoc/>
  29. public List<View?> CachedViewsUnderMouse { get; } = [];
  30. /// <inheritdoc/>
  31. public event EventHandler<MouseEventArgs>? MouseEvent;
  32. // Mouse grab functionality merged from MouseGrabHandler
  33. /// <inheritdoc/>
  34. public View? MouseGrabView { get; private set; }
  35. /// <inheritdoc/>
  36. public event EventHandler<GrabMouseEventArgs>? GrabbingMouse;
  37. /// <inheritdoc/>
  38. public event EventHandler<GrabMouseEventArgs>? UnGrabbingMouse;
  39. /// <inheritdoc/>
  40. public event EventHandler<ViewEventArgs>? GrabbedMouse;
  41. /// <inheritdoc/>
  42. public event EventHandler<ViewEventArgs>? UnGrabbedMouse;
  43. /// <inheritdoc/>
  44. public void RaiseMouseEvent (MouseEventArgs mouseEvent)
  45. {
  46. //Debug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
  47. if (App?.Initialized is true)
  48. {
  49. // LastMousePosition is only set if the application is initialized.
  50. LastMousePosition = mouseEvent.ScreenPosition;
  51. }
  52. if (IsMouseDisabled)
  53. {
  54. return;
  55. }
  56. // The position of the mouse is the same as the screen position at the application level.
  57. //Debug.Assert (mouseEvent.Position == mouseEvent.ScreenPosition);
  58. mouseEvent.Position = mouseEvent.ScreenPosition;
  59. List<View?>? currentViewsUnderMouse = App?.TopRunnable?.GetViewsUnderLocation (mouseEvent.ScreenPosition, ViewportSettingsFlags.TransparentMouse);
  60. View? deepestViewUnderMouse = currentViewsUnderMouse?.LastOrDefault ();
  61. if (deepestViewUnderMouse is { })
  62. {
  63. #if DEBUG_IDISPOSABLE
  64. if (View.EnableDebugIDisposableAsserts && deepestViewUnderMouse.WasDisposed)
  65. {
  66. throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName);
  67. }
  68. #endif
  69. mouseEvent.View = deepestViewUnderMouse;
  70. }
  71. MouseEvent?.Invoke (null, mouseEvent);
  72. if (mouseEvent.Handled)
  73. {
  74. return;
  75. }
  76. // Dismiss the Popover if the user presses mouse outside of it
  77. if (mouseEvent.IsPressed
  78. && App?.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover
  79. && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false)
  80. {
  81. ApplicationPopover.HideWithQuitCommand (visiblePopover);
  82. // Recurse once so the event can be handled below the popover
  83. RaiseMouseEvent (mouseEvent);
  84. return;
  85. }
  86. if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent))
  87. {
  88. return;
  89. }
  90. // May be null before the prior condition or the condition may set it as null.
  91. // So, the checking must be outside the prior condition.
  92. if (deepestViewUnderMouse is null)
  93. {
  94. return;
  95. }
  96. // if the mouse is outside the Application.TopRunnable or Popover hierarchy, we don't want to
  97. // send the mouse event to the deepest view under the mouse.
  98. if (!View.IsInHierarchy (App?.TopRunnable, deepestViewUnderMouse, true) && !View.IsInHierarchy (App?.Popover?.GetActivePopover () as View, deepestViewUnderMouse, true))
  99. {
  100. return;
  101. }
  102. // Create a view-relative mouse event to send to the view that is under the mouse.
  103. MouseEventArgs viewMouseEvent;
  104. if (deepestViewUnderMouse is Adornment adornment)
  105. {
  106. Point frameLoc = adornment.ScreenToFrame (mouseEvent.ScreenPosition);
  107. viewMouseEvent = new ()
  108. {
  109. Position = frameLoc,
  110. Flags = mouseEvent.Flags,
  111. ScreenPosition = mouseEvent.ScreenPosition,
  112. View = deepestViewUnderMouse
  113. };
  114. }
  115. else if (deepestViewUnderMouse.ViewportToScreen (Rectangle.Empty with { Size = deepestViewUnderMouse.Viewport.Size }).Contains (mouseEvent.ScreenPosition))
  116. {
  117. Point viewportLocation = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition);
  118. viewMouseEvent = new ()
  119. {
  120. Position = viewportLocation,
  121. Flags = mouseEvent.Flags,
  122. ScreenPosition = mouseEvent.ScreenPosition,
  123. View = deepestViewUnderMouse
  124. };
  125. }
  126. else
  127. {
  128. // The mouse was outside any View's Viewport.
  129. // Debug.Fail ("This should never happen. If it does please file an Issue!!");
  130. return;
  131. }
  132. if (currentViewsUnderMouse is { })
  133. {
  134. RaiseMouseEnterLeaveEvents (viewMouseEvent.ScreenPosition, currentViewsUnderMouse);
  135. }
  136. while (deepestViewUnderMouse.NewMouseEvent (viewMouseEvent) is not true && MouseGrabView is not { })
  137. {
  138. if (deepestViewUnderMouse is Adornment adornmentView)
  139. {
  140. deepestViewUnderMouse = adornmentView.Parent?.SuperView;
  141. }
  142. else
  143. {
  144. deepestViewUnderMouse = deepestViewUnderMouse.SuperView;
  145. }
  146. if (deepestViewUnderMouse is null)
  147. {
  148. break;
  149. }
  150. Point boundsPoint = deepestViewUnderMouse.ScreenToViewport (mouseEvent.ScreenPosition);
  151. viewMouseEvent = new ()
  152. {
  153. Position = boundsPoint,
  154. Flags = mouseEvent.Flags,
  155. ScreenPosition = mouseEvent.ScreenPosition,
  156. View = deepestViewUnderMouse
  157. };
  158. }
  159. }
  160. /// <inheritdoc/>
  161. public void RaiseMouseEnterLeaveEvents (Point screenPosition, List<View?> currentViewsUnderMouse)
  162. {
  163. // Tell any views that are no longer under the mouse that the mouse has left
  164. List<View?> viewsToLeave = CachedViewsUnderMouse.Where (v => v is { } && !currentViewsUnderMouse.Contains (v)).ToList ();
  165. foreach (View? view in viewsToLeave)
  166. {
  167. if (view is null)
  168. {
  169. continue;
  170. }
  171. view.NewMouseLeaveEvent ();
  172. CachedViewsUnderMouse.Remove (view);
  173. }
  174. // Tell any views that are now under the mouse that the mouse has entered and add them to the list
  175. foreach (View? view in currentViewsUnderMouse)
  176. {
  177. if (view is null)
  178. {
  179. continue;
  180. }
  181. if (CachedViewsUnderMouse.Contains (view))
  182. {
  183. continue;
  184. }
  185. CachedViewsUnderMouse.Add (view);
  186. var raise = false;
  187. if (view is Adornment { Parent: { } } adornmentView)
  188. {
  189. Point superViewLoc = adornmentView.Parent.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition;
  190. raise = adornmentView.Contains (superViewLoc);
  191. }
  192. else
  193. {
  194. Point superViewLoc = view.SuperView?.ScreenToViewport (screenPosition) ?? screenPosition;
  195. raise = view.Contains (superViewLoc);
  196. }
  197. if (!raise)
  198. {
  199. continue;
  200. }
  201. CancelEventArgs eventArgs = new System.ComponentModel.CancelEventArgs ();
  202. bool? cancelled = view.NewMouseEnterEvent (eventArgs);
  203. if (cancelled is true || eventArgs.Cancel)
  204. {
  205. break;
  206. }
  207. }
  208. }
  209. /// <inheritdoc/>
  210. public void ResetState ()
  211. {
  212. // Do not clear LastMousePosition; Popover's require it to stay set with last mouse pos.
  213. CachedViewsUnderMouse.Clear ();
  214. MouseEvent = null;
  215. MouseGrabView = null;
  216. }
  217. // Mouse grab functionality merged from MouseGrabHandler
  218. /// <inheritdoc/>
  219. public void GrabMouse (View? view)
  220. {
  221. if (view is null || RaiseGrabbingMouseEvent (view))
  222. {
  223. return;
  224. }
  225. RaiseGrabbedMouseEvent (view);
  226. // MouseGrabView is only set if the application is initialized.
  227. MouseGrabView = view;
  228. }
  229. /// <inheritdoc/>
  230. public void UngrabMouse ()
  231. {
  232. if (MouseGrabView is null)
  233. {
  234. return;
  235. }
  236. #if DEBUG_IDISPOSABLE
  237. if (View.EnableDebugIDisposableAsserts)
  238. {
  239. ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
  240. }
  241. #endif
  242. if (!RaiseUnGrabbingMouseEvent (MouseGrabView))
  243. {
  244. View view = MouseGrabView;
  245. MouseGrabView = null;
  246. RaiseUnGrabbedMouseEvent (view);
  247. }
  248. }
  249. /// <exception cref="Exception">A delegate callback throws an exception.</exception>
  250. private bool RaiseGrabbingMouseEvent (View? view)
  251. {
  252. if (view is null)
  253. {
  254. return false;
  255. }
  256. GrabMouseEventArgs evArgs = new (view);
  257. GrabbingMouse?.Invoke (view, evArgs);
  258. return evArgs.Cancel;
  259. }
  260. /// <exception cref="Exception">A delegate callback throws an exception.</exception>
  261. private bool RaiseUnGrabbingMouseEvent (View? view)
  262. {
  263. if (view is null)
  264. {
  265. return false;
  266. }
  267. GrabMouseEventArgs evArgs = new (view);
  268. UnGrabbingMouse?.Invoke (view, evArgs);
  269. return evArgs.Cancel;
  270. }
  271. /// <exception cref="Exception">A delegate callback throws an exception.</exception>
  272. private void RaiseGrabbedMouseEvent (View? view)
  273. {
  274. if (view is null)
  275. {
  276. return;
  277. }
  278. GrabbedMouse?.Invoke (view, new (view));
  279. }
  280. /// <exception cref="Exception">A delegate callback throws an exception.</exception>
  281. private void RaiseUnGrabbedMouseEvent (View? view)
  282. {
  283. if (view is null)
  284. {
  285. return;
  286. }
  287. UnGrabbedMouse?.Invoke (view, new (view));
  288. }
  289. /// <summary>
  290. /// Handles mouse grab logic for a mouse event.
  291. /// </summary>
  292. /// <param name="deepestViewUnderMouse">The deepest view under the mouse.</param>
  293. /// <param name="mouseEvent">The mouse event to handle.</param>
  294. /// <returns><see langword="true"/> if the event was handled by the grab handler; otherwise <see langword="false"/>.</returns>
  295. public bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEventArgs mouseEvent)
  296. {
  297. if (MouseGrabView is { })
  298. {
  299. #if DEBUG_IDISPOSABLE
  300. if (View.EnableDebugIDisposableAsserts && MouseGrabView.WasDisposed)
  301. {
  302. throw new ObjectDisposedException (MouseGrabView.GetType ().FullName);
  303. }
  304. #endif
  305. // If the mouse is grabbed, send the event to the view that grabbed it.
  306. // The coordinates are relative to the Bounds of the view that grabbed the mouse.
  307. Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.ScreenPosition);
  308. MouseEventArgs viewRelativeMouseEvent = new ()
  309. {
  310. Position = frameLoc,
  311. Flags = mouseEvent.Flags,
  312. ScreenPosition = mouseEvent.ScreenPosition,
  313. View = MouseGrabView // Always set to the grab view. See Issue #4370
  314. };
  315. //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
  316. if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) is true || viewRelativeMouseEvent.IsSingleClicked)
  317. {
  318. return true;
  319. }
  320. // ReSharper disable once ConditionIsAlwaysTrueOrFalse
  321. if (MouseGrabView is null && deepestViewUnderMouse is Adornment)
  322. {
  323. // The view that grabbed the mouse has been disposed
  324. return true;
  325. }
  326. }
  327. return false;
  328. }
  329. // Event handler for Application static property changes
  330. private void OnIsMouseDisabledChanged (object? sender, ValueChangedEventArgs<bool> e)
  331. {
  332. IsMouseDisabled = e.NewValue;
  333. }
  334. /// <inheritdoc/>
  335. public void Dispose ()
  336. {
  337. // Unsubscribe from Application static property change events
  338. Application.IsMouseDisabledChanged -= OnIsMouseDisabledChanged;
  339. }
  340. }