Adornment.cs 11 KB

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