ViewMouse.cs 19 KB

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