SplitContainer.cs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. using System;
  2. using Terminal.Gui.Graphs;
  3. using static Terminal.Gui.Dim;
  4. using static Terminal.Gui.Pos;
  5. namespace Terminal.Gui {
  6. public class SplitContainer : View {
  7. private LineView splitterLine;
  8. private bool panel1Collapsed;
  9. private bool panel2Collapsed;
  10. private Pos splitterDistance = Pos.Percent (50);
  11. private Orientation orientation = Orientation.Vertical;
  12. private int panel1MinSize;
  13. /// <summary>
  14. /// Creates a new instance of the SplitContainer class.
  15. /// </summary>
  16. public SplitContainer ()
  17. {
  18. // Default to a border of 1 so that View looks nice
  19. Border = new Border ();
  20. splitterLine = new SplitContainerLineView (this);
  21. this.Add (splitterLine);
  22. this.Add (Panel1);
  23. this.Add (Panel2);
  24. Setup ();
  25. CanFocus = false;
  26. }
  27. /// <summary>
  28. /// The left or top panel of the <see cref="SplitContainer"/>
  29. /// (depending on <see cref="Orientation"/>). Add panel contents
  30. /// to this <see cref="View"/> using <see cref="View.Add(View)"/>.
  31. /// </summary>
  32. public View Panel1 { get; } = new View ();
  33. /// <summary>
  34. /// The minimum size <see cref="Panel1"/> can be when adjusting
  35. /// <see cref="SplitterDistance"/>.
  36. /// </summary>
  37. public int Panel1MinSize {
  38. get { return panel1MinSize; }
  39. set {
  40. panel1MinSize = value;
  41. Setup();
  42. }
  43. }
  44. /// <summary>
  45. /// This determines if <see cref="Panel1"/> is collapsed.
  46. /// </summary>
  47. public bool Panel1Collapsed {
  48. get { return panel1Collapsed; }
  49. set {
  50. panel1Collapsed = value;
  51. if (value && panel2Collapsed) {
  52. panel2Collapsed = false;
  53. }
  54. Setup ();
  55. }
  56. }
  57. /// <summary>
  58. /// The right or bottom panel of the <see cref="SplitContainer"/>
  59. /// (depending on <see cref="Orientation"/>). Add panel contents
  60. /// to this <see cref="View"/> using <see cref="View.Add(View)"/>
  61. /// </summary>
  62. public View Panel2 { get; } = new View ();
  63. /// <summary>
  64. /// The minimum size <see cref="Panel2"/> can be when adjusting
  65. /// <see cref="SplitterDistance"/>.
  66. /// </summary>
  67. public int Panel2MinSize { get; set; }
  68. /// <summary>
  69. /// This determines if <see cref="Panel2"/> is collapsed.
  70. /// </summary>
  71. public bool Panel2Collapsed {
  72. get { return panel2Collapsed; }
  73. set {
  74. panel2Collapsed = value;
  75. if (value && panel1Collapsed) {
  76. panel1Collapsed = false;
  77. }
  78. Setup ();
  79. }
  80. }
  81. /// <summary>
  82. /// Orientation of the dividing line (Horizontal or Vertical).
  83. /// </summary>
  84. public Orientation Orientation {
  85. get { return orientation; }
  86. set {
  87. orientation = value;
  88. Setup ();
  89. }
  90. }
  91. /// <summary>
  92. /// Distance Horizontally or Vertically to the splitter line when
  93. /// neither panel is collapsed.
  94. /// </summary>
  95. public Pos SplitterDistance {
  96. get { return splitterDistance; }
  97. set {
  98. splitterDistance = value;
  99. Setup ();
  100. }
  101. }
  102. public override bool OnEnter (View view)
  103. {
  104. Driver.SetCursorVisibility (CursorVisibility.Invisible);
  105. return base.OnEnter (view);
  106. }
  107. private void Setup ()
  108. {
  109. splitterLine.Orientation = Orientation;
  110. if (panel1Collapsed || panel2Collapsed) {
  111. SetupForCollapsedPanel ();
  112. } else {
  113. SetupForNormal ();
  114. }
  115. }
  116. private void SetupForNormal ()
  117. {
  118. // Ensure all our component views are here
  119. // (e.g. if we are transitioning from a collapsed state)
  120. if (!this.Subviews.Contains (splitterLine)) {
  121. this.Add (splitterLine);
  122. }
  123. if (!this.Subviews.Contains (Panel1)) {
  124. this.Add (Panel1);
  125. }
  126. if (!this.Subviews.Contains (Panel2)) {
  127. this.Add (Panel2);
  128. }
  129. switch (Orientation) {
  130. case Orientation.Horizontal:
  131. splitterLine.X = 0;
  132. splitterLine.Y = splitterDistance;
  133. splitterLine.Width = Dim.Fill ();
  134. splitterLine.Height = 1;
  135. splitterLine.LineRune = Driver.HLine;
  136. this.Panel1.X = 0;
  137. this.Panel1.Y = 0;
  138. this.Panel1.Width = Dim.Fill ();
  139. this.Panel1.Height = new DimFunc (() =>
  140. splitterDistance.Anchor (Bounds.Height) - 1);
  141. this.Panel2.Y = Pos.Bottom (splitterLine) + 1;
  142. this.Panel2.X = 0;
  143. this.Panel2.Width = Dim.Fill ();
  144. this.Panel2.Height = Dim.Fill ();
  145. break;
  146. case Orientation.Vertical:
  147. splitterLine.X = splitterDistance;
  148. splitterLine.Y = 0;
  149. splitterLine.Width = 1;
  150. splitterLine.Height = Dim.Fill ();
  151. splitterLine.LineRune = Driver.VLine;
  152. this.Panel1.X = 0;
  153. this.Panel1.Y = 0;
  154. this.Panel1.Height = Dim.Fill ();
  155. this.Panel1.Width = new DimFunc (() =>
  156. splitterDistance.Anchor (Bounds.Width) - 1);
  157. this.Panel2.X = Pos.Right (splitterLine) + 1;
  158. this.Panel2.Y = 0;
  159. this.Panel2.Width = Dim.Fill ();
  160. this.Panel2.Height = Dim.Fill ();
  161. break;
  162. default: throw new ArgumentOutOfRangeException (nameof (orientation));
  163. };
  164. }
  165. private void SetupForCollapsedPanel ()
  166. {
  167. View toRemove = panel1Collapsed ? Panel1 : Panel2;
  168. View toFullSize = panel1Collapsed ? Panel2 : Panel1;
  169. if (this.Subviews.Contains (splitterLine)) {
  170. this.Subviews.Remove (splitterLine);
  171. }
  172. if (this.Subviews.Contains (toRemove)) {
  173. this.Subviews.Remove (toRemove);
  174. }
  175. if (!this.Subviews.Contains (toFullSize)) {
  176. this.Add (toFullSize);
  177. }
  178. toFullSize.X = 0;
  179. toFullSize.Y = 0;
  180. toFullSize.Width = Dim.Fill ();
  181. toFullSize.Height = Dim.Fill ();
  182. }
  183. private class SplitContainerLineView : LineView
  184. {
  185. private SplitContainer parent;
  186. Point? dragPosition;
  187. Pos dragOrignalPos;
  188. // TODO: Make focusable and allow moving with keyboard
  189. public SplitContainerLineView(SplitContainer parent)
  190. {
  191. CanFocus = true;
  192. this.parent = parent;
  193. }
  194. ///<inheritdoc/>
  195. public override bool MouseEvent (MouseEvent mouseEvent)
  196. {
  197. if (!CanFocus) {
  198. return true;
  199. }
  200. // Start a drag
  201. if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) {
  202. SetFocus ();
  203. Application.EnsuresTopOnFront ();
  204. if (mouseEvent.Flags == MouseFlags.Button1Pressed) {
  205. dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  206. dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X;
  207. Application.GrabMouse (this);
  208. }
  209. return true;
  210. } else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  211. {
  212. if (dragPosition.HasValue) {
  213. // how far has user dragged from original location?
  214. if(Orientation == Orientation.Horizontal)
  215. {
  216. int dy = mouseEvent.Y - dragPosition.Value.Y;
  217. parent.SplitterDistance = Offset(Y , dy);
  218. }
  219. else
  220. {
  221. int dx = mouseEvent.X - dragPosition.Value.X;
  222. parent.SplitterDistance = Offset(X , dx);
  223. }
  224. parent.SetNeedsDisplay ();
  225. return true;
  226. }
  227. }
  228. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
  229. Application.UngrabMouse ();
  230. Driver.UncookMouse ();
  231. FinalisePosition ();
  232. dragPosition = null;
  233. }
  234. return false;
  235. }
  236. private Pos Offset (Pos pos, int delta)
  237. {
  238. var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ?
  239. parent.Bounds.Width : parent.Bounds.Height);
  240. return posAbsolute + delta;
  241. }
  242. private void FinalisePosition ()
  243. {
  244. // if before dragging we were a proportional position
  245. // then preserve that when the mouse is released so that
  246. // resizing continues to work as intended
  247. if(dragOrignalPos is PosFactor) {
  248. if(Orientation == Orientation.Horizontal) {
  249. Y = ToPosFactor (Y, parent.Bounds.Height);
  250. } else {
  251. X = ToPosFactor (X, parent.Bounds.Width);
  252. }
  253. }
  254. }
  255. private Pos ToPosFactor (Pos y, int parentLength)
  256. {
  257. int position = y.Anchor (parentLength);
  258. return new PosFactor (position / (float)parentLength);
  259. }
  260. }
  261. }
  262. }