Adornment.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. namespace Terminal.Gui;
  2. /// <summary>
  3. /// Adornments are a special form of <see cref="View"/> that appear outside the <see cref="View.Bounds"/>:
  4. /// <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/>. They are defined using the
  5. /// <see cref="Thickness"/> class, which specifies the thickness of the sides of a rectangle.
  6. /// </summary>
  7. /// <remarsk>
  8. /// <para>
  9. /// Each of <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/> has slightly different
  10. /// behavior relative to <see cref="ColorScheme"/>, <see cref="View.SetFocus"/>, keyboard input, and
  11. /// mouse input. Each can be customized by manipulating their Subviews.
  12. /// </para>
  13. /// </remarsk>
  14. public class Adornment : View
  15. {
  16. /// <inheritdoc/>
  17. public Adornment ()
  18. {
  19. /* Do nothing; A parameter-less constructor is required to support all views unit tests. */
  20. }
  21. /// <summary>Constructs a new adornment for the view specified by <paramref name="parent"/>.</summary>
  22. /// <param name="parent"></param>
  23. public Adornment (View parent)
  24. {
  25. Application.GrabbingMouse += Application_GrabbingMouse;
  26. Application.UnGrabbingMouse += Application_UnGrabbingMouse;
  27. CanFocus = true;
  28. Parent = parent;
  29. }
  30. /// <summary>The Parent of this Adornment (the View this Adornment surrounds).</summary>
  31. /// <remarks>
  32. /// Adornments are distinguished from typical View classes in that they are not sub-views, but have a parent/child
  33. /// relationship with their containing View.
  34. /// </remarks>
  35. public View Parent { get; set; }
  36. #region Thickness
  37. private Thickness _thickness = Thickness.Empty;
  38. /// <summary>Defines the rectangle that the <see cref="Adornment"/> will use to draw its content.</summary>
  39. public Thickness Thickness
  40. {
  41. get => _thickness;
  42. set
  43. {
  44. Thickness prev = _thickness;
  45. _thickness = value;
  46. if (prev != _thickness)
  47. {
  48. if (Parent?.IsInitialized == false)
  49. {
  50. // When initialized Parent.LayoutSubViews will cause a LayoutAdornments
  51. Parent?.LayoutAdornments ();
  52. }
  53. else
  54. {
  55. Parent?.SetNeedsLayout ();
  56. Parent?.LayoutSubviews ();
  57. }
  58. OnThicknessChanged (prev);
  59. }
  60. }
  61. }
  62. /// <summary>Fired whenever the <see cref="Thickness"/> property changes.</summary>
  63. public event EventHandler<ThicknessEventArgs> ThicknessChanged;
  64. /// <summary>Called whenever the <see cref="Thickness"/> property changes.</summary>
  65. public void OnThicknessChanged (Thickness previousThickness)
  66. {
  67. ThicknessChanged?.Invoke (
  68. this,
  69. new () { Thickness = Thickness, PreviousThickness = previousThickness }
  70. );
  71. }
  72. #endregion Thickness
  73. #region View Overrides
  74. /// <summary>
  75. /// Adornments cannot be used as sub-views (see <see cref="Parent"/>); setting this property will throw
  76. /// <see cref="InvalidOperationException"/>.
  77. /// </summary>
  78. public override View SuperView
  79. {
  80. get => null;
  81. set => throw new NotImplementedException ();
  82. }
  83. //internal override Adornment CreateAdornment (Type adornmentType)
  84. //{
  85. // /* Do nothing - Adornments do not have Adornments */
  86. // return null;
  87. //}
  88. internal override void LayoutAdornments ()
  89. {
  90. /* Do nothing - Adornments do not have Adornments */
  91. }
  92. /// <summary>
  93. /// Gets the rectangle that describes the area of the Adornment. The Location is always (0,0).
  94. /// The size is the size of the <see cref="View.Frame"/>.
  95. /// </summary>
  96. public override Rectangle Bounds
  97. {
  98. get => Frame with { Location = Point.Empty };
  99. set => throw new InvalidOperationException ("It makes no sense to set Bounds of a Thickness.");
  100. }
  101. /// <inheritdoc/>
  102. public override Rectangle FrameToScreen ()
  103. {
  104. if (Parent is null)
  105. {
  106. return Frame;
  107. }
  108. // Adornments are *Children* of a View, not SubViews. Thus View.FrameToScreen will not work.
  109. // To get the screen-relative coordinates of an Adornment, we need get the parent's Frame
  110. // in screen coords, and add our Frame location to it.
  111. Rectangle parent = Parent.FrameToScreen ();
  112. return new (new (parent.X + Frame.X, parent.Y + Frame.Y), Frame.Size);
  113. }
  114. /// <inheritdoc/>
  115. public override Point ScreenToFrame (int x, int y) { return Parent.ScreenToFrame (x - Frame.X, y - Frame.Y); }
  116. /// <summary>Does nothing for Adornment</summary>
  117. /// <returns></returns>
  118. public override bool OnDrawAdornments () { return false; }
  119. /// <summary>Redraws the Adornments that comprise the <see cref="Adornment"/>.</summary>
  120. public override void OnDrawContent (Rectangle contentArea)
  121. {
  122. if (Thickness == Thickness.Empty)
  123. {
  124. return;
  125. }
  126. Rectangle screenBounds = BoundsToScreen (contentArea);
  127. Attribute normalAttr = GetNormalColor ();
  128. Driver.SetAttribute (normalAttr);
  129. // This just draws/clears the thickness, not the insides.
  130. Thickness.Draw (screenBounds, ToString ());
  131. if (!string.IsNullOrEmpty (TextFormatter.Text))
  132. {
  133. if (TextFormatter is { })
  134. {
  135. TextFormatter.Size = Frame.Size;
  136. TextFormatter.NeedsFormat = true;
  137. }
  138. }
  139. TextFormatter?.Draw (screenBounds, normalAttr, normalAttr, Rectangle.Empty);
  140. if (Subviews.Count > 0)
  141. {
  142. base.OnDrawContent (contentArea);
  143. }
  144. ClearLayoutNeeded ();
  145. ClearNeedsDisplay ();
  146. }
  147. /// <summary>Does nothing for Adornment</summary>
  148. /// <returns></returns>
  149. public override bool OnRenderLineCanvas () { return false; }
  150. /// <summary>
  151. /// Adornments only render to their <see cref="Parent"/>'s or Parent's SuperView's LineCanvas, so setting this
  152. /// property throws an <see cref="InvalidOperationException"/>.
  153. /// </summary>
  154. public override bool SuperViewRendersLineCanvas
  155. {
  156. get => false; // throw new NotImplementedException ();
  157. set => throw new NotImplementedException ();
  158. }
  159. #endregion View Overrides
  160. #region Mouse Support
  161. /// <summary>
  162. /// Indicates whether the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness.
  163. /// </summary>
  164. /// <remarks>
  165. /// The <paramref name="x"/> and <paramref name="x"/> are relative to the PARENT's SuperView.
  166. /// </remarks>
  167. /// <param name="x"></param>
  168. /// <param name="y"></param>
  169. /// <returns><see langword="true"/> if the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. </returns>
  170. public override bool Contains (int x, int y)
  171. {
  172. Rectangle frame = Frame;
  173. frame.Offset (Parent.Frame.Location);
  174. return Thickness.Contains (frame, x, y);
  175. }
  176. private Point? _dragPosition;
  177. private Point _startGrabPoint;
  178. /// <inheritdoc/>
  179. protected internal override bool OnMouseEnter (MouseEvent mouseEvent)
  180. {
  181. // Invert Normal
  182. if (Diagnostics.HasFlag (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null)
  183. {
  184. var cs = new ColorScheme (ColorScheme)
  185. {
  186. Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground)
  187. };
  188. ColorScheme = cs;
  189. }
  190. return base.OnMouseEnter (mouseEvent);
  191. }
  192. /// <summary>Called when a mouse event occurs within the Adornment.</summary>
  193. /// <remarks>
  194. /// <para>
  195. /// The coordinates are relative to <see cref="View.Bounds"/>.
  196. /// </para>
  197. /// <para>
  198. /// A mouse click on the Adornment will cause the Parent to focus.
  199. /// </para>
  200. /// <para>
  201. /// A mouse drag on the Adornment will cause the Parent to move.
  202. /// </para>
  203. /// </remarks>
  204. /// <param name="mouseEvent"></param>
  205. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  206. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  207. {
  208. var args = new MouseEventEventArgs (mouseEvent);
  209. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
  210. {
  211. if (Parent.CanFocus && !Parent.HasFocus)
  212. {
  213. Parent.SetFocus ();
  214. Parent.SetNeedsDisplay ();
  215. }
  216. return OnMouseClick (args);
  217. }
  218. if (!Parent.CanFocus || !Parent.Arrangement.HasFlag (ViewArrangement.Movable))
  219. {
  220. return true;
  221. }
  222. // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/3312
  223. if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  224. {
  225. Parent.SetFocus ();
  226. Application.BringOverlappedTopToFront ();
  227. // Only start grabbing if the user clicks in the Thickness area
  228. // Adornment.Contains takes Parent SuperView=relative coords.
  229. if (Contains (mouseEvent.X + Parent.Frame.X + Frame.X, mouseEvent.Y+ Parent.Frame.Y + Frame.Y))
  230. {
  231. // Set the start grab point to the Frame coords
  232. _startGrabPoint = new (mouseEvent.X + Frame.X, mouseEvent.Y + Frame.Y);
  233. _dragPosition = new (mouseEvent.X, mouseEvent.Y);
  234. Application.GrabMouse (this);
  235. }
  236. return true;
  237. }
  238. if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  239. {
  240. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  241. {
  242. if (Parent.SuperView is null)
  243. {
  244. // Redraw the entire app window.
  245. Application.Top.SetNeedsDisplay ();
  246. }
  247. else
  248. {
  249. Parent.SuperView.SetNeedsDisplay ();
  250. }
  251. _dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  252. Point parentLoc = Parent.SuperView?.ScreenToBounds (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y) ?? mouseEvent.ScreenPosition;
  253. GetLocationEnsuringFullVisibility (
  254. Parent,
  255. parentLoc.X - _startGrabPoint.X,
  256. parentLoc.Y - _startGrabPoint.Y,
  257. out int nx,
  258. out int ny,
  259. out _
  260. );
  261. Parent.X = nx;
  262. Parent.Y = ny;
  263. return true;
  264. }
  265. }
  266. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue)
  267. {
  268. _dragPosition = null;
  269. Application.UngrabMouse ();
  270. }
  271. return false;
  272. }
  273. /// <inheritdoc/>
  274. protected internal override bool OnMouseLeave (MouseEvent mouseEvent)
  275. {
  276. // Invert Normal
  277. if (Diagnostics.HasFlag (ViewDiagnosticFlags.MouseEnter) && ColorScheme != null)
  278. {
  279. var cs = new ColorScheme (ColorScheme)
  280. {
  281. Normal = new (ColorScheme.Normal.Background, ColorScheme.Normal.Foreground)
  282. };
  283. ColorScheme = cs;
  284. }
  285. return base.OnMouseLeave (mouseEvent);
  286. }
  287. /// <inheritdoc/>
  288. protected override void Dispose (bool disposing)
  289. {
  290. Application.GrabbingMouse -= Application_GrabbingMouse;
  291. Application.UnGrabbingMouse -= Application_UnGrabbingMouse;
  292. _dragPosition = null;
  293. base.Dispose (disposing);
  294. }
  295. private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e)
  296. {
  297. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  298. {
  299. e.Cancel = true;
  300. }
  301. }
  302. private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e)
  303. {
  304. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  305. {
  306. e.Cancel = true;
  307. }
  308. }
  309. #endregion Mouse Support
  310. }