Adornment.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. CanFocus = true;
  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. rcol = 0;
  86. rrow = 0;
  87. // Adornments are *Children* of a View, not SubViews. Thus View.BoundsToScreen will not work.
  88. // To get the screen-relative coordinates of a Adornment, we need to know who
  89. // the Parent is
  90. Rectangle parentFrame = Parent?.Frame ?? Frame;
  91. rrow = row + parentFrame.Y;
  92. rcol = col + parentFrame.X;
  93. // We now have rcol/rrow in coordinates relative to our Parent View's SuperView. If our Parent View's SuperView has
  94. // a SuperView, keep going...
  95. Parent?.SuperView?.BoundsToScreen (rcol, rrow, out rcol, out rrow, clipped);
  96. }
  97. /// <inheritdoc/>
  98. public override Rectangle FrameToScreen ()
  99. {
  100. if (Parent is null)
  101. {
  102. return Frame;
  103. }
  104. // Adornments are *Children* of a View, not SubViews. Thus View.FrameToScreen will not work.
  105. // To get the screen-relative coordinates of a Adornment, we need to know who
  106. // the Parent is
  107. Rectangle parent = Parent.FrameToScreen ();
  108. // We now have coordinates relative to our View. If our View's SuperView has
  109. // a SuperView, keep going...
  110. return new (new (parent.X + Frame.X, parent.Y + Frame.Y), Frame.Size);
  111. }
  112. /// <inheritdoc/>
  113. public override Point ScreenToFrame (int x, int y)
  114. {
  115. return Parent.ScreenToFrame (x - Frame.X, y - Frame.Y);
  116. }
  117. ///// <inheritdoc/>
  118. //public override void SetNeedsDisplay (Rectangle region)
  119. //{
  120. // SetSubViewNeedsDisplay ();
  121. // foreach (View subView in Subviews)
  122. // {
  123. // subView.SetNeedsDisplay ();
  124. // }
  125. //}
  126. /// <inheritdoc/>
  127. //protected override void ClearNeedsDisplay ()
  128. //{
  129. // base.ClearNeedsDisplay ();
  130. // foreach (View subView in Subviews)
  131. // {
  132. // subView.NeedsDisplay = false;
  133. // }
  134. //}
  135. /// <summary>Does nothing for Adornment</summary>
  136. /// <returns></returns>
  137. public override bool OnDrawAdornments () { return false; }
  138. /// <summary>Redraws the Adornments that comprise the <see cref="Adornment"/>.</summary>
  139. public override void OnDrawContent (Rectangle contentArea)
  140. {
  141. if (Thickness == Thickness.Empty)
  142. {
  143. return;
  144. }
  145. if (Parent is Label)
  146. {
  147. }
  148. Rectangle screenBounds = BoundsToScreen (Frame);
  149. Attribute normalAttr = GetNormalColor ();
  150. // This just draws/clears the thickness, not the insides.
  151. Driver.SetAttribute (normalAttr);
  152. Thickness.Draw (screenBounds, ToString ());
  153. if (!string.IsNullOrEmpty (TextFormatter.Text))
  154. {
  155. if (TextFormatter is { })
  156. {
  157. TextFormatter.Size = Frame.Size;
  158. TextFormatter.NeedsFormat = true;
  159. }
  160. }
  161. TextFormatter?.Draw (screenBounds, normalAttr, normalAttr, Rectangle.Empty);
  162. if (Subviews.Count > 0)
  163. {
  164. base.OnDrawContent (contentArea);
  165. }
  166. ClearLayoutNeeded ();
  167. ClearNeedsDisplay ();
  168. }
  169. /// <summary>Does nothing for Adornment</summary>
  170. /// <returns></returns>
  171. public override bool OnRenderLineCanvas () { return false; }
  172. /// <summary>Called whenever the <see cref="Thickness"/> property changes.</summary>
  173. public virtual void OnThicknessChanged (Thickness previousThickness)
  174. {
  175. ThicknessChanged?.Invoke (
  176. this,
  177. new () { Thickness = Thickness, PreviousThickness = previousThickness }
  178. );
  179. }
  180. /// <summary>Fired whenever the <see cref="Thickness"/> property changes.</summary>
  181. public event EventHandler<ThicknessEventArgs> ThicknessChanged;
  182. /// <summary>Called when a mouse event occurs within the Adornment.</summary>
  183. /// <remarks>
  184. /// <para>
  185. /// The coordinates are relative to <see cref="View.Bounds"/>.
  186. /// </para>
  187. /// <para>
  188. /// A mouse click on the Adornment will cause the Parent to focus.
  189. /// </para>
  190. /// <para>
  191. /// A mouse drag on the Adornment will cause the Parent to move.
  192. /// </para>
  193. /// </remarks>
  194. /// <param name="mouseEvent"></param>
  195. /// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
  196. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  197. {
  198. var args = new MouseEventEventArgs (mouseEvent);
  199. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked))
  200. {
  201. if (Parent.CanFocus && !Parent.HasFocus)
  202. {
  203. Parent.SetFocus ();
  204. Parent.SetNeedsDisplay ();
  205. }
  206. return OnMouseClick (args);
  207. }
  208. // TODO: Checking for Toplevel is a hack until #2537 is fixed
  209. if (!Parent.CanFocus || !Parent.Arrangement.HasFlag(ViewArrangement.Movable))
  210. {
  211. return true;
  212. }
  213. int nx, ny;
  214. // BUGBUG: This is true even when the mouse started dragging outside of the Adornment, which is not correct.
  215. if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  216. {
  217. Parent.SetFocus ();
  218. Application.BringOverlappedTopToFront ();
  219. // Only start grabbing if the user clicks in the Thickness area
  220. if (Thickness.Contains (Frame, mouseEvent.X, mouseEvent.Y) && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  221. {
  222. _startGrabPoint = new (mouseEvent.X, mouseEvent.Y);
  223. _dragPosition = new (mouseEvent.X, mouseEvent.Y);
  224. Application.GrabMouse (this);
  225. }
  226. return true;
  227. }
  228. if (mouseEvent.Flags is (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  229. {
  230. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  231. {
  232. if (Parent.SuperView is null)
  233. {
  234. // Redraw the entire app window.
  235. Application.Top.SetNeedsDisplay ();
  236. }
  237. else
  238. {
  239. Parent.SuperView.SetNeedsDisplay ();
  240. }
  241. _dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  242. Point parentLoc = Parent.SuperView?.ScreenToBounds (mouseEvent.ScreenPosition.X, mouseEvent.ScreenPosition.Y) ?? mouseEvent.ScreenPosition;
  243. GetLocationThatFits (
  244. Parent,
  245. parentLoc.X - _startGrabPoint.X,
  246. parentLoc.Y - _startGrabPoint.Y,
  247. out nx,
  248. out ny,
  249. out _,
  250. out _
  251. );
  252. Parent.X = nx;
  253. Parent.Y = ny;
  254. return true;
  255. }
  256. }
  257. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && _dragPosition.HasValue)
  258. {
  259. _dragPosition = null;
  260. Application.UngrabMouse ();
  261. }
  262. return false;
  263. }
  264. /// <inheritdoc/>
  265. protected override void Dispose (bool disposing)
  266. {
  267. Application.GrabbingMouse -= Application_GrabbingMouse;
  268. Application.UnGrabbingMouse -= Application_UnGrabbingMouse;
  269. _dragPosition = null;
  270. base.Dispose (disposing);
  271. }
  272. internal override Adornment CreateAdornment (Type adornmentType)
  273. {
  274. /* Do nothing - Adornments do not have Adornments */
  275. return null;
  276. }
  277. internal override void LayoutAdornments ()
  278. {
  279. /* Do nothing - Adornments do not have Adornments */
  280. }
  281. private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e)
  282. {
  283. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  284. {
  285. e.Cancel = true;
  286. }
  287. }
  288. private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e)
  289. {
  290. if (Application.MouseGrabView == this && _dragPosition.HasValue)
  291. {
  292. e.Cancel = true;
  293. }
  294. }
  295. }