Adornment.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. namespace Terminal.Gui;
  2. // TODO: Missing 3D effect - 3D effects will be drawn by a mechanism separate from Adornments
  3. // TODO: If a Adornment has focus, navigation keys (e.g Command.NextView) should cycle through SubViews of the Adornments
  4. // QUESTION: How does a user navigate out of an Adornment to another Adornment, or back into the Parent's SubViews?
  5. /// <summary>
  6. /// Adornments are a special form of <see cref="View"/> that appear outside the <see cref="View.Bounds"/>:
  7. /// <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/>. They are defined using the
  8. /// <see cref="Thickness"/> class, which specifies the thickness of the sides of a rectangle.
  9. /// </summary>
  10. /// <remarsk>
  11. /// <para>
  12. /// There is no prevision for creating additional subclasses of Adornment. It is not abstract to enable unit
  13. /// testing.
  14. /// </para>
  15. /// <para>Each of <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/> can be customized.</para>
  16. /// </remarsk>
  17. public class Adornment : View
  18. {
  19. private Point? _dragPosition;
  20. private Point _startGrabPoint;
  21. private Thickness _thickness = Thickness.Empty;
  22. /// <inheritdoc/>
  23. public Adornment ()
  24. {
  25. /* Do nothing; A parameter-less constructor is required to support all views unit tests. */
  26. }
  27. /// <summary>Constructs a new adornment for the view specified by <paramref name="parent"/>.</summary>
  28. /// <param name="parent"></param>
  29. public Adornment (View parent)
  30. {
  31. Application.GrabbingMouse += Application_GrabbingMouse;
  32. Application.UnGrabbingMouse += Application_UnGrabbingMouse;
  33. Parent = parent;
  34. }
  35. /// <summary>Gets the rectangle that describes the inner area of the Adornment. The Location is always (0,0).</summary>
  36. public override Rectangle Bounds
  37. {
  38. get => new (Point.Empty, Thickness?.GetInside (new (Point.Empty, Frame.Size)).Size ?? Frame.Size);
  39. // QUESTION: So why even have a setter then?
  40. set => throw new InvalidOperationException ("It makes no sense to set Bounds of a Thickness.");
  41. }
  42. /// <summary>The Parent of this Adornment (the View this Adornment surrounds).</summary>
  43. /// <remarks>
  44. /// Adornments are distinguished from typical View classes in that they are not sub-views, but have a parent/child
  45. /// relationship with their containing View.
  46. /// </remarks>
  47. public View Parent { get; set; }
  48. /// <summary>
  49. /// Adornments cannot be used as sub-views (see <see cref="Parent"/>); this method always throws an
  50. /// <see cref="InvalidOperationException"/>. TODO: Are we sure?
  51. /// </summary>
  52. public override View SuperView
  53. {
  54. get => null;
  55. set => throw new NotImplementedException ();
  56. }
  57. /// <summary>
  58. /// Adornments only render to their <see cref="Parent"/>'s or Parent's SuperView's LineCanvas, so setting this
  59. /// property throws an <see cref="InvalidOperationException"/>.
  60. /// </summary>
  61. public override bool SuperViewRendersLineCanvas
  62. {
  63. get => false; // throw new NotImplementedException ();
  64. set => throw new NotImplementedException ();
  65. }
  66. /// <summary>Defines the rectangle that the <see cref="Adornment"/> will use to draw its content.</summary>
  67. public Thickness Thickness
  68. {
  69. get => _thickness;
  70. set
  71. {
  72. Thickness prev = _thickness;
  73. _thickness = value;
  74. if (prev != _thickness)
  75. {
  76. Parent?.LayoutAdornments ();
  77. OnThicknessChanged (prev);
  78. }
  79. }
  80. }
  81. /// <inheritdoc/>
  82. public override void BoundsToScreen (int col, int row, out int rcol, out int rrow, bool clipped = true)
  83. {
  84. // Adornments are *Children* of a View, not SubViews. Thus View.BoundsToScreen will not work.
  85. // To get the screen-relative coordinates of a Adornment, we need to know who
  86. // the Parent is
  87. Rectangle parentFrame = Parent?.Frame ?? Frame;
  88. rrow = row + parentFrame.Y;
  89. rcol = col + parentFrame.X;
  90. // We now have rcol/rrow in coordinates relative to our View's SuperView. If our View's SuperView has
  91. // a SuperView, keep going...
  92. Parent?.SuperView?.BoundsToScreen (rcol, rrow, out rcol, out rrow, clipped);
  93. }
  94. /// <inheritdoc/>
  95. public override Rectangle FrameToScreen ()
  96. {
  97. if (Parent is null)
  98. {
  99. return Frame;
  100. }
  101. // Adornments are *Children* of a View, not SubViews. Thus View.FrameToScreen will not work.
  102. // To get the screen-relative coordinates of a Adornment, we need to know who
  103. // the Parent is
  104. Rectangle parent = Parent.FrameToScreen ();
  105. // We now have coordinates relative to our View. If our View's SuperView has
  106. // a SuperView, keep going...
  107. return new (new (parent.X + Frame.X, parent.Y + Frame.Y), Frame.Size);
  108. }
  109. /// <summary>Does nothing for Adornment</summary>
  110. /// <returns></returns>
  111. public override bool OnDrawAdornments () { return false; }
  112. /// <summary>Redraws the Adornments that comprise the <see cref="Adornment"/>.</summary>
  113. public override void OnDrawContent (Rectangle contentArea)
  114. {
  115. if (Thickness == Thickness.Empty)
  116. {
  117. return;
  118. }
  119. Rectangle screenBounds = BoundsToScreen (Frame);
  120. Attribute normalAttr = GetNormalColor ();
  121. // This just draws/clears the thickness, not the insides.
  122. Driver.SetAttribute (normalAttr);
  123. Thickness.Draw (screenBounds, ToString ());
  124. if (!string.IsNullOrEmpty (TextFormatter.Text))
  125. {
  126. if (TextFormatter is { })
  127. {
  128. TextFormatter.Size = Frame.Size;
  129. TextFormatter.NeedsFormat = true;
  130. }
  131. }
  132. TextFormatter?.Draw (screenBounds, normalAttr, normalAttr, Rectangle.Empty);
  133. //base.OnDrawContent (contentArea);
  134. }
  135. /// <summary>Does nothing for Adornment</summary>
  136. /// <returns></returns>
  137. public override bool OnRenderLineCanvas () { return false; }
  138. /// <summary>Called whenever the <see cref="Thickness"/> property changes.</summary>
  139. public virtual void OnThicknessChanged (Thickness previousThickness)
  140. {
  141. ThicknessChanged?.Invoke (
  142. this,
  143. new () { Thickness = Thickness, PreviousThickness = previousThickness }
  144. );
  145. }
  146. /// <summary>Fired whenever the <see cref="Thickness"/> property changes.</summary>
  147. public event EventHandler<ThicknessEventArgs> ThicknessChanged;
  148. /// <summary>Called when a mouse event occurs within the Adornment.</summary>
  149. /// <remarks>
  150. /// <para>
  151. /// The coordinates are relative to <see cref="View.Bounds"/>.
  152. /// </para>
  153. /// <para>
  154. /// A mouse click on the Adornment will cause the Parent to focus.
  155. /// </para>
  156. /// <para>
  157. /// A mouse drag on the Adornment will cause the Parent to move.
  158. /// </para>
  159. /// </remarks>
  160. /// <param name="mouseEvent"></param>
  161. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  162. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  163. {
  164. var args = new MouseEventEventArgs (mouseEvent);
  165. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
  166. {
  167. if (Parent.CanFocus && !Parent.HasFocus)
  168. {
  169. Parent.SetFocus ();
  170. Parent.SetNeedsDisplay ();
  171. }
  172. return OnMouseClick (args);
  173. }
  174. // TODO: Checking for Toplevel is a hack until #2537 is fixed
  175. if (!Parent.CanFocus || !Parent.Arrangement.HasFlag(ViewArrangement.Movable))
  176. {
  177. return true;
  178. }
  179. int nx, ny;
  180. // BUGBUG: This is true even when the mouse started dragging outside of the Adornment, which is not correct.
  181. if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  182. {
  183. Parent.SetFocus ();
  184. Application.BringOverlappedTopToFront ();
  185. // Only start grabbing if the user clicks in the Thickness area
  186. if (Thickness.Contains (Frame, mouseEvent.X, mouseEvent.Y) && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  187. {
  188. _startGrabPoint = new (mouseEvent.X, mouseEvent.Y);
  189. _dragPosition = new (mouseEvent.X, mouseEvent.Y);
  190. Application.GrabMouse (this);
  191. }
  192. return true;
  193. }
  194. if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  195. {
  196. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  197. {
  198. if (Parent.SuperView is null)
  199. {
  200. // Redraw the entire app window.
  201. Application.Top.SetNeedsDisplay ();
  202. }
  203. else
  204. {
  205. Parent.SuperView.SetNeedsDisplay ();
  206. }
  207. _dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  208. Point parentLoc = Parent.SuperView?.ScreenToBounds (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y) ?? mouseEvent.ScreenPosition;
  209. GetLocationThatFits (
  210. Parent,
  211. parentLoc.X - _startGrabPoint.X,
  212. parentLoc.Y - _startGrabPoint.Y,
  213. out nx,
  214. out ny,
  215. out _,
  216. out _
  217. );
  218. Parent.X = nx;
  219. Parent.Y = ny;
  220. return true;
  221. }
  222. }
  223. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue)
  224. {
  225. _dragPosition = null;
  226. Application.UngrabMouse ();
  227. }
  228. return false;
  229. }
  230. /// <inheritdoc/>
  231. protected override void Dispose (bool disposing)
  232. {
  233. Application.GrabbingMouse -= Application_GrabbingMouse;
  234. Application.UnGrabbingMouse -= Application_UnGrabbingMouse;
  235. _dragPosition = null;
  236. base.Dispose (disposing);
  237. }
  238. internal override Adornment CreateAdornment (Type adornmentType)
  239. {
  240. /* Do nothing - Adornments do not have Adornments */
  241. return null;
  242. }
  243. internal override void LayoutAdornments ()
  244. {
  245. /* Do nothing - Adornments do not have Adornments */
  246. }
  247. private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e)
  248. {
  249. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  250. {
  251. e.Cancel = true;
  252. }
  253. }
  254. private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e)
  255. {
  256. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  257. {
  258. e.Cancel = true;
  259. }
  260. }
  261. }