ViewMouse.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. using System.ComponentModel;
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Describes the highlight style of a view.
  5. /// </summary>
  6. [Flags]
  7. public enum HighlightStyle
  8. {
  9. /// <summary>
  10. /// No highlight.
  11. /// </summary>
  12. None = 0,
  13. /// <summary>
  14. /// The mouse is hovering over the view.
  15. /// </summary>
  16. Hover = 1,
  17. /// <summary>
  18. /// The mouse is pressed within the <see cref="View.Bounds"/>.
  19. /// </summary>
  20. Pressed = 2,
  21. /// <summary>
  22. /// The mouse is pressed but moved outside the <see cref="View.Bounds"/>.
  23. /// </summary>
  24. PressedOutside = 4
  25. }
  26. /// <summary>
  27. /// Event arguments for the <see cref="View.Highlight"/> event.
  28. /// </summary>
  29. public class HighlightEventArgs : CancelEventArgs
  30. {
  31. public HighlightEventArgs (HighlightStyle style)
  32. {
  33. HighlightStyle = style;
  34. }
  35. /// <summary>
  36. /// The highlight style.
  37. /// </summary>
  38. public HighlightStyle HighlightStyle { get; }
  39. }
  40. public partial class View
  41. {
  42. /// <summary>
  43. /// Gets or sets whether the <see cref="View"/> will be highlighted visually while the mouse button is
  44. /// pressed.
  45. /// </summary>
  46. public HighlightStyle HighlightStyle { get; set; }
  47. /// <summary>Gets or sets whether the <see cref="View"/> wants continuous button pressed events.</summary>
  48. public virtual bool WantContinuousButtonPressed { get; set; }
  49. /// <summary>Gets or sets whether the <see cref="View"/> wants mouse position reports.</summary>
  50. /// <value><see langword="true"/> if mouse position reports are wanted; otherwise, <see langword="false"/>.</value>
  51. public virtual bool WantMousePositionReports { get; set; }
  52. /// <summary>
  53. /// Called by <see cref="Application.OnMouseEvent"/> when the mouse enters <see cref="Bounds"/>. The view will
  54. /// then receive mouse events until <see cref="NewMouseLeaveEvent"/> is called indicating the mouse has left
  55. /// the view.
  56. /// </summary>
  57. /// <remarks>
  58. /// <para>
  59. /// A view must be both enabled and visible to receive mouse events.
  60. /// </para>
  61. /// <para>
  62. /// This method calls <see cref="OnMouseEnter"/> to fire the event.
  63. /// </para>
  64. /// <para>
  65. /// See <see cref="SetHighlight"/> for more information.
  66. /// </para>
  67. /// </remarks>
  68. /// <param name="mouseEvent"></param>
  69. /// <returns><see langword="true"/> if the event was handled, <see langword="false"/> otherwise.</returns>
  70. internal bool? NewMouseEnterEvent (MouseEvent mouseEvent)
  71. {
  72. if (!Enabled)
  73. {
  74. return true;
  75. }
  76. if (!CanBeVisible (this))
  77. {
  78. return false;
  79. }
  80. return OnMouseEnter (mouseEvent);
  81. }
  82. /// <summary>
  83. /// Called by <see cref="NewMouseEvent"/> when the mouse enters <see cref="Bounds"/>. The view will
  84. /// then receive mouse events until <see cref="OnMouseLeave"/> is called indicating the mouse has left
  85. /// the view.
  86. /// </summary>
  87. /// <remarks>
  88. /// <para>
  89. /// Override this method or subscribe to <see cref="MouseEnter"/> to change the default enter behavior.
  90. /// </para>
  91. /// <para>
  92. /// The coordinates are relative to <see cref="View.Bounds"/>.
  93. /// </para>
  94. /// </remarks>
  95. /// <param name="mouseEvent"></param>
  96. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  97. protected internal virtual bool OnMouseEnter (MouseEvent mouseEvent)
  98. {
  99. var args = new MouseEventEventArgs (mouseEvent);
  100. MouseEnter?.Invoke (this, args);
  101. return args.Handled;
  102. }
  103. /// <summary>Event fired when the mouse moves into the View's <see cref="Bounds"/>.</summary>
  104. public event EventHandler<MouseEventEventArgs> MouseEnter;
  105. /// <summary>
  106. /// Called by <see cref="Application.OnMouseEvent"/> when the mouse leaves <see cref="Bounds"/>. The view will
  107. /// then no longer receive mouse events.
  108. /// </summary>
  109. /// <remarks>
  110. /// <para>
  111. /// A view must be both enabled and visible to receive mouse events.
  112. /// </para>
  113. /// <para>
  114. /// This method calls <see cref="OnMouseLeave"/> to fire the event.
  115. /// </para>
  116. /// <para>
  117. /// See <see cref="SetHighlight"/> for more information.
  118. /// </para>
  119. /// </remarks>
  120. /// <param name="mouseEvent"></param>
  121. /// <returns><see langword="true"/> if the event was handled, <see langword="false"/> otherwise.</returns>
  122. internal bool? NewMouseLeaveEvent (MouseEvent mouseEvent)
  123. {
  124. if (!Enabled)
  125. {
  126. return true;
  127. }
  128. if (!CanBeVisible (this))
  129. {
  130. return false;
  131. }
  132. return OnMouseLeave (mouseEvent);
  133. }
  134. /// <summary>
  135. /// Called by <see cref="NewMouseEvent"/> when a mouse leaves <see cref="Bounds"/>. The view will
  136. /// no longer receive mouse events.
  137. /// </summary>
  138. /// <remarks>
  139. /// <para>
  140. /// Override this method or subscribe to <see cref="MouseEnter"/> to change the default leave behavior.
  141. /// </para>
  142. /// <para>
  143. /// The coordinates are relative to <see cref="View.Bounds"/>.
  144. /// </para>
  145. /// </remarks>
  146. /// <param name="mouseEvent"></param>
  147. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  148. protected internal virtual bool OnMouseLeave (MouseEvent mouseEvent)
  149. {
  150. if (!Enabled)
  151. {
  152. return true;
  153. }
  154. if (!CanBeVisible (this))
  155. {
  156. return false;
  157. }
  158. var args = new MouseEventEventArgs (mouseEvent);
  159. MouseLeave?.Invoke (this, args);
  160. return args.Handled;
  161. }
  162. /// <summary>Event fired when the mouse leaves the View's <see cref="Bounds"/>.</summary>
  163. public event EventHandler<MouseEventEventArgs> MouseLeave;
  164. /// <summary>
  165. /// Processes a <see cref="MouseEvent"/>. This method is called by <see cref="Application.OnMouseEvent"/> when a mouse
  166. /// event occurs.
  167. /// </summary>
  168. /// <remarks>
  169. /// <para>
  170. /// A view must be both enabled and visible to receive mouse events.
  171. /// </para>
  172. /// <para>
  173. /// This method calls <see cref="OnMouseEvent"/> to process the event. If the event is not handled, and one of the
  174. /// mouse buttons was clicked, it calls <see cref="OnMouseClick"/> to process the click.
  175. /// </para>
  176. /// <para>
  177. /// See <see cref="SetHighlight"/> and <see cref="DisableHighlight"/> for more information.
  178. /// </para>
  179. /// <para>
  180. /// If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, the <see cref="OnMouseClick"/> event
  181. /// will be invoked repeatedly while the button is pressed.
  182. /// </para>
  183. /// </remarks>
  184. /// <param name="mouseEvent"></param>
  185. /// <returns><see langword="true"/> if the event was handled, <see langword="false"/> otherwise.</returns>
  186. public bool? NewMouseEvent (MouseEvent mouseEvent)
  187. {
  188. if (!Enabled)
  189. {
  190. // A disabled view should not eat mouse events
  191. return false;
  192. }
  193. if (!CanBeVisible (this))
  194. {
  195. return false;
  196. }
  197. if (OnMouseEvent (mouseEvent))
  198. {
  199. // Technically mouseEvent.Handled should already be true if implementers of OnMouseEvent
  200. // follow the rules. But we'll update it just in case.
  201. return mouseEvent.Handled = true;
  202. }
  203. if (HighlightStyle != Gui.HighlightStyle.None || WantContinuousButtonPressed)
  204. {
  205. if (HandlePressed (mouseEvent))
  206. {
  207. return mouseEvent.Handled;
  208. }
  209. if (HandleReleased (mouseEvent))
  210. {
  211. return mouseEvent.Handled;
  212. }
  213. if (HandleClicked (mouseEvent))
  214. {
  215. return mouseEvent.Handled;
  216. }
  217. }
  218. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)
  219. || mouseEvent.Flags.HasFlag (MouseFlags.Button2Clicked)
  220. || mouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)
  221. || mouseEvent.Flags.HasFlag (MouseFlags.Button4Clicked)
  222. || mouseEvent.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  223. || mouseEvent.Flags.HasFlag (MouseFlags.Button2DoubleClicked)
  224. || mouseEvent.Flags.HasFlag (MouseFlags.Button3DoubleClicked)
  225. || mouseEvent.Flags.HasFlag (MouseFlags.Button4DoubleClicked)
  226. || mouseEvent.Flags.HasFlag (MouseFlags.Button1TripleClicked)
  227. || mouseEvent.Flags.HasFlag (MouseFlags.Button2TripleClicked)
  228. || mouseEvent.Flags.HasFlag (MouseFlags.Button3TripleClicked)
  229. || mouseEvent.Flags.HasFlag (MouseFlags.Button4TripleClicked)
  230. )
  231. {
  232. // If it's a click, and we didn't handle it, then we'll call OnMouseClick
  233. // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and
  234. // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked
  235. return OnMouseClick (new (mouseEvent));
  236. }
  237. return false;
  238. }
  239. /// <summary>
  240. /// For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically
  241. /// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
  242. /// </summary>
  243. /// <remarks>
  244. /// <para>
  245. /// Marked internal just to support unit tests
  246. /// </para>
  247. /// </remarks>
  248. /// <param name="mouseEvent"></param>
  249. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  250. private bool HandlePressed (MouseEvent mouseEvent)
  251. {
  252. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)
  253. || mouseEvent.Flags.HasFlag (MouseFlags.Button2Pressed)
  254. || mouseEvent.Flags.HasFlag (MouseFlags.Button3Pressed)
  255. || mouseEvent.Flags.HasFlag (MouseFlags.Button4Pressed))
  256. {
  257. // The first time we get pressed event, grab the mouse and set focus
  258. if (Application.MouseGrabView != this)
  259. {
  260. Application.GrabMouse (this);
  261. if (CanFocus)
  262. {
  263. // Set the focus, but don't invoke Accept
  264. SetFocus ();
  265. }
  266. }
  267. if (Bounds.Contains (mouseEvent.X, mouseEvent.Y))
  268. {
  269. SetHighlight (HighlightStyle.HasFlag(HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None);
  270. }
  271. else
  272. {
  273. SetHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None);
  274. }
  275. if (WantContinuousButtonPressed && Application.MouseGrabView == this)
  276. {
  277. // If this is not the first pressed event, click
  278. return OnMouseClick (new (mouseEvent));
  279. }
  280. return mouseEvent.Handled = true;
  281. }
  282. return false;
  283. }
  284. /// <summary>
  285. /// For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically
  286. /// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
  287. /// </summary>
  288. /// <remarks>
  289. /// Marked internal just to support unit tests
  290. /// </remarks>
  291. /// <param name="mouseEvent"></param>
  292. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  293. internal bool HandleReleased (MouseEvent mouseEvent)
  294. {
  295. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released)
  296. || mouseEvent.Flags.HasFlag (MouseFlags.Button2Released)
  297. || mouseEvent.Flags.HasFlag (MouseFlags.Button3Released)
  298. || mouseEvent.Flags.HasFlag (MouseFlags.Button4Released))
  299. {
  300. if (Application.MouseGrabView == this)
  301. {
  302. SetHighlight (HighlightStyle.None);
  303. }
  304. return mouseEvent.Handled = true;
  305. }
  306. return false;
  307. }
  308. /// <summary>
  309. /// For cases where the view is grabbed and the mouse is clicked, this method handles the click event (typically
  310. /// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStyle"/> are set).
  311. /// </summary>
  312. /// <remarks>
  313. /// Marked internal just to support unit tests
  314. /// </remarks>
  315. /// <param name="mouseEvent"></param>
  316. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  317. internal bool HandleClicked (MouseEvent mouseEvent)
  318. {
  319. if (Application.MouseGrabView == this
  320. && (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)
  321. || mouseEvent.Flags.HasFlag (MouseFlags.Button2Clicked)
  322. || mouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)
  323. || mouseEvent.Flags.HasFlag (MouseFlags.Button4Clicked)))
  324. {
  325. // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab
  326. Application.UngrabMouse ();
  327. SetHighlight (HighlightStyle.None);
  328. // If mouse is still in bounds, click
  329. if (!WantContinuousButtonPressed && Bounds.Contains (mouseEvent.X, mouseEvent.Y))
  330. {
  331. return OnMouseClick (new (mouseEvent));
  332. }
  333. return mouseEvent.Handled = true;
  334. }
  335. return false;
  336. }
  337. [CanBeNull]
  338. private ColorScheme _savedHighlightColorScheme;
  339. /// <summary>
  340. /// Enables the highlight for the view when the mouse is pressed. Called from OnMouseEvent.
  341. /// </summary>
  342. /// <remarks>
  343. /// <para>
  344. /// Set <see cref="HighlightStyle"/> to have the view highlighted based on the mouse.
  345. /// </para>
  346. /// <para>
  347. /// Calls <see cref="OnHighlight"/> which fires the <see cref="Highlight"/> event.
  348. /// </para>
  349. /// <para>
  350. /// Marked internal just to support unit tests
  351. /// </para>
  352. /// </remarks>
  353. internal void SetHighlight (HighlightStyle style)
  354. {
  355. // Enable override via virtual method and/or event
  356. if (OnHighlight (style) == true)
  357. {
  358. return;
  359. }
  360. if (style.HasFlag (HighlightStyle.Pressed) || style.HasFlag (HighlightStyle.PressedOutside))
  361. {
  362. if (_savedHighlightColorScheme is null && ColorScheme is { })
  363. {
  364. _savedHighlightColorScheme ??= ColorScheme;
  365. if (CanFocus)
  366. {
  367. // TODO: Make the inverted color configurable
  368. var cs = new ColorScheme (ColorScheme)
  369. {
  370. // For Buttons etc...
  371. Focus = new (ColorScheme.Normal.Foreground, ColorScheme.Focus.Background),
  372. // For Adornments
  373. Normal = new (ColorScheme.Focus.Foreground, ColorScheme.Normal.Background)
  374. };
  375. ColorScheme = cs;
  376. }
  377. else
  378. {
  379. var cs = new ColorScheme (ColorScheme)
  380. {
  381. // For Buttons etc... that can't focus (like up/down).
  382. Normal = new (ColorScheme.Focus.Background, ColorScheme.Normal.Foreground)
  383. };
  384. ColorScheme = cs;
  385. }
  386. }
  387. }
  388. else
  389. {
  390. // Unhighlight
  391. if (_savedHighlightColorScheme is { })
  392. {
  393. ColorScheme = _savedHighlightColorScheme;
  394. _savedHighlightColorScheme = null;
  395. }
  396. }
  397. }
  398. /// <summary>
  399. /// Fired when the view is highlighted. Set <see cref="CancelEventArgs.Cancel"/> to <see langword="true"/>
  400. /// to implement a custom highlight scheme or prevent the view from being highlighted.
  401. /// </summary>
  402. public event EventHandler<HighlightEventArgs> Highlight;
  403. /// <summary>
  404. /// Called when the view is to be highlighted.
  405. /// </summary>
  406. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  407. protected virtual bool? OnHighlight (HighlightStyle highlight)
  408. {
  409. HighlightEventArgs args = new (highlight);
  410. Highlight?.Invoke (this, args);
  411. return args.Cancel;
  412. }
  413. /// <summary>Called when a mouse event occurs within the view's <see cref="Bounds"/>.</summary>
  414. /// <remarks>
  415. /// <para>
  416. /// The coordinates are relative to <see cref="View.Bounds"/>.
  417. /// </para>
  418. /// </remarks>
  419. /// <param name="mouseEvent"></param>
  420. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  421. protected internal virtual bool OnMouseEvent (MouseEvent mouseEvent)
  422. {
  423. var args = new MouseEventEventArgs (mouseEvent);
  424. MouseEvent?.Invoke (this, args);
  425. return args.Handled;
  426. }
  427. /// <summary>Event fired when a mouse event occurs.</summary>
  428. /// <remarks>
  429. /// <para>
  430. /// The coordinates are relative to <see cref="View.Bounds"/>.
  431. /// </para>
  432. /// </remarks>
  433. public event EventHandler<MouseEventEventArgs> MouseEvent;
  434. /// <summary>Invokes the MouseClick event.</summary>
  435. /// <remarks>
  436. /// <para>
  437. /// Called when the mouse is either clicked or double-clicked. Check
  438. /// <see cref="MouseEvent.Flags"/> to see which button was clicked.
  439. /// </para>
  440. /// </remarks>
  441. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  442. protected bool OnMouseClick (MouseEventEventArgs args)
  443. {
  444. if (!Enabled)
  445. {
  446. // QUESTION: Is this right? Should a disabled view eat mouse clicks?
  447. args.Handled = true;
  448. return true;
  449. }
  450. MouseClick?.Invoke (this, args);
  451. if (args.Handled)
  452. {
  453. return true;
  454. }
  455. if (!HasFocus && CanFocus)
  456. {
  457. args.Handled = true;
  458. SetFocus ();
  459. }
  460. return args.Handled;
  461. }
  462. /// <summary>Event fired when a mouse click occurs.</summary>
  463. /// <remarks>
  464. /// <para>
  465. /// Fired when the mouse is either clicked or double-clicked. Check
  466. /// <see cref="MouseEvent.Flags"/> to see which button was clicked.
  467. /// </para>
  468. /// <para>
  469. /// The coordinates are relative to <see cref="View.Bounds"/>.
  470. /// </para>
  471. /// </remarks>
  472. public event EventHandler<MouseEventEventArgs> MouseClick;
  473. }