SplitContainer.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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 0 but not null so that user can
  19. // more easily change size (without null references)
  20. Border = new Border ();
  21. splitterLine = new SplitContainerLineView (this);
  22. this.Add (Panel1);
  23. this.Add (splitterLine);
  24. this.Add (Panel2);
  25. Setup ();
  26. CanFocus = false;
  27. }
  28. /// <summary>
  29. /// The left or top panel of the <see cref="SplitContainer"/>
  30. /// (depending on <see cref="Orientation"/>). Add panel contents
  31. /// to this <see cref="View"/> using <see cref="View.Add(View)"/>.
  32. /// </summary>
  33. public View Panel1 { get; } = new View ();
  34. /// <summary>
  35. /// The minimum size <see cref="Panel1"/> can be when adjusting
  36. /// <see cref="SplitterDistance"/>.
  37. /// </summary>
  38. public int Panel1MinSize {
  39. get { return panel1MinSize; }
  40. set {
  41. panel1MinSize = value;
  42. Setup ();
  43. }
  44. }
  45. /// <summary>
  46. /// This determines if <see cref="Panel1"/> is collapsed.
  47. /// </summary>
  48. public bool Panel1Collapsed {
  49. get { return panel1Collapsed; }
  50. set {
  51. panel1Collapsed = value;
  52. if (value && panel2Collapsed) {
  53. panel2Collapsed = false;
  54. }
  55. Setup ();
  56. }
  57. }
  58. /// <summary>
  59. /// The right or bottom panel of the <see cref="SplitContainer"/>
  60. /// (depending on <see cref="Orientation"/>). Add panel contents
  61. /// to this <see cref="View"/> using <see cref="View.Add(View)"/>
  62. /// </summary>
  63. public View Panel2 { get; } = new View ();
  64. /// <summary>
  65. /// The minimum size <see cref="Panel2"/> can be when adjusting
  66. /// <see cref="SplitterDistance"/>.
  67. /// </summary>
  68. public int Panel2MinSize { get; set; }
  69. /// <summary>
  70. /// This determines if <see cref="Panel2"/> is collapsed.
  71. /// </summary>
  72. public bool Panel2Collapsed {
  73. get { return panel2Collapsed; }
  74. set {
  75. panel2Collapsed = value;
  76. if (value && panel1Collapsed) {
  77. panel1Collapsed = false;
  78. }
  79. Setup ();
  80. }
  81. }
  82. /// <summary>
  83. /// Orientation of the dividing line (Horizontal or Vertical).
  84. /// </summary>
  85. public Orientation Orientation {
  86. get { return orientation; }
  87. set {
  88. orientation = value;
  89. Setup ();
  90. }
  91. }
  92. /// <summary>
  93. /// Distance Horizontally or Vertically to the splitter line when
  94. /// neither panel is collapsed.
  95. /// </summary>
  96. public Pos SplitterDistance {
  97. get { return splitterDistance; }
  98. set {
  99. splitterDistance = value;
  100. Setup ();
  101. }
  102. }
  103. public override bool OnEnter (View view)
  104. {
  105. Driver.SetCursorVisibility (CursorVisibility.Invisible);
  106. return base.OnEnter (view);
  107. }
  108. private void Setup ()
  109. {
  110. splitterLine.Orientation = Orientation;
  111. if (panel1Collapsed || panel2Collapsed) {
  112. SetupForCollapsedPanel ();
  113. } else {
  114. SetupForNormal ();
  115. }
  116. }
  117. private void SetupForNormal ()
  118. {
  119. // Ensure all our component views are here
  120. // (e.g. if we are transitioning from a collapsed state)
  121. if (!this.Subviews.Contains (splitterLine)) {
  122. this.Add (splitterLine);
  123. }
  124. if (!this.Subviews.Contains (Panel1)) {
  125. this.Add (Panel1);
  126. }
  127. if (!this.Subviews.Contains (Panel2)) {
  128. this.Add (Panel2);
  129. }
  130. switch (Orientation) {
  131. case Orientation.Horizontal:
  132. splitterLine.X = 0;
  133. splitterLine.Y = splitterDistance;
  134. splitterLine.Width = Dim.Fill ();
  135. splitterLine.Height = 1;
  136. splitterLine.LineRune = Driver.HLine;
  137. this.Panel1.X = 0;
  138. this.Panel1.Y = 0;
  139. this.Panel1.Width = Dim.Fill ();
  140. this.Panel1.Height = new DimFunc (() =>
  141. splitterDistance.Anchor (Bounds.Height));
  142. this.Panel2.Y = Pos.Bottom (splitterLine);
  143. this.Panel2.X = 0;
  144. this.Panel2.Width = Dim.Fill ();
  145. this.Panel2.Height = Dim.Fill ();
  146. break;
  147. case Orientation.Vertical:
  148. splitterLine.X = splitterDistance;
  149. splitterLine.Y = 0;
  150. splitterLine.Width = 1;
  151. splitterLine.Height = Dim.Fill ();
  152. splitterLine.LineRune = Driver.VLine;
  153. this.Panel1.X = 0;
  154. this.Panel1.Y = 0;
  155. this.Panel1.Height = Dim.Fill ();
  156. this.Panel1.Width = new DimFunc (() =>
  157. splitterDistance.Anchor (Bounds.Width));
  158. this.Panel2.X = Pos.Right (splitterLine);
  159. this.Panel2.Y = 0;
  160. this.Panel2.Width = Dim.Fill ();
  161. this.Panel2.Height = Dim.Fill ();
  162. break;
  163. default: throw new ArgumentOutOfRangeException (nameof (orientation));
  164. };
  165. this.LayoutSubviews ();
  166. }
  167. private void SetupForCollapsedPanel ()
  168. {
  169. View toRemove = panel1Collapsed ? Panel1 : Panel2;
  170. View toFullSize = panel1Collapsed ? Panel2 : Panel1;
  171. if (this.Subviews.Contains (splitterLine)) {
  172. this.Subviews.Remove (splitterLine);
  173. }
  174. if (this.Subviews.Contains (toRemove)) {
  175. this.Subviews.Remove (toRemove);
  176. }
  177. if (!this.Subviews.Contains (toFullSize)) {
  178. this.Add (toFullSize);
  179. }
  180. toFullSize.X = 0;
  181. toFullSize.Y = 0;
  182. toFullSize.Width = Dim.Fill ();
  183. toFullSize.Height = Dim.Fill ();
  184. }
  185. private class SplitContainerLineView : LineView {
  186. private SplitContainer parent;
  187. Point? dragPosition;
  188. Pos dragOrignalPos;
  189. Point? moveRuneRenderLocation;
  190. // TODO: Make focusable and allow moving with keyboard
  191. public SplitContainerLineView (SplitContainer parent)
  192. {
  193. CanFocus = true;
  194. TabStop = true;
  195. this.parent = parent;
  196. base.AddCommand (Command.Right, () => {
  197. if (Orientation == Orientation.Vertical) {
  198. parent.SplitterDistance = Offset (X, 1);
  199. return true;
  200. }
  201. return false;
  202. });
  203. base.AddCommand (Command.Left, () => {
  204. if (Orientation == Orientation.Vertical) {
  205. parent.SplitterDistance = Offset (X, -1);
  206. return true;
  207. }
  208. return false;
  209. });
  210. base.AddCommand (Command.LineUp, () => {
  211. if (Orientation == Orientation.Horizontal) {
  212. parent.SplitterDistance = Offset (Y, -1);
  213. return true;
  214. }
  215. return false;
  216. });
  217. base.AddCommand (Command.LineDown, () => {
  218. if (Orientation == Orientation.Horizontal) {
  219. parent.SplitterDistance = Offset (Y, 1);
  220. return true;
  221. }
  222. return false;
  223. });
  224. AddKeyBinding (Key.CursorRight, Command.Right);
  225. AddKeyBinding (Key.CursorLeft, Command.Left);
  226. AddKeyBinding (Key.CursorUp, Command.LineUp);
  227. AddKeyBinding (Key.CursorDown, Command.LineDown);
  228. }
  229. ///<inheritdoc/>
  230. public override bool ProcessKey (KeyEvent kb)
  231. {
  232. var result = InvokeKeybindings (kb);
  233. if (result != null)
  234. return (bool)result;
  235. return base.ProcessKey (kb);
  236. }
  237. public override void PositionCursor ()
  238. {
  239. base.PositionCursor ();
  240. Move (this.Bounds.Width / 2, this.Bounds.Height / 2);
  241. }
  242. public override bool OnEnter (View view)
  243. {
  244. Driver.SetCursorVisibility (CursorVisibility.Default);
  245. PositionCursor ();
  246. return base.OnEnter (view);
  247. }
  248. public override void Redraw (Rect bounds)
  249. {
  250. base.Redraw (bounds);
  251. if (CanFocus && HasFocus) {
  252. var location = moveRuneRenderLocation ??
  253. new Point (Bounds.Width / 2, Bounds.Height / 2);
  254. AddRune (location.X,location.Y, Driver.Diamond);
  255. }
  256. }
  257. ///<inheritdoc/>
  258. public override bool MouseEvent (MouseEvent mouseEvent)
  259. {
  260. if (!CanFocus) {
  261. return true;
  262. }
  263. if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) {
  264. // Start a Drag
  265. SetFocus ();
  266. Application.EnsuresTopOnFront ();
  267. if (mouseEvent.Flags == MouseFlags.Button1Pressed) {
  268. dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  269. dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X;
  270. Application.GrabMouse (this);
  271. }
  272. return true;
  273. } else if (
  274. dragPosition.HasValue &&
  275. (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
  276. // Continue Drag
  277. // how far has user dragged from original location?
  278. if (Orientation == Orientation.Horizontal) {
  279. int dy = mouseEvent.Y - dragPosition.Value.Y;
  280. parent.SplitterDistance = Offset (Y, dy);
  281. moveRuneRenderLocation = new Point (mouseEvent.X, 0);
  282. } else {
  283. int dx = mouseEvent.X - dragPosition.Value.X;
  284. parent.SplitterDistance = Offset (X, dx);
  285. moveRuneRenderLocation = new Point (0, mouseEvent.Y);
  286. }
  287. parent.SetNeedsDisplay ();
  288. return true;
  289. }
  290. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
  291. // End Drag
  292. Application.UngrabMouse ();
  293. Driver.UncookMouse ();
  294. FinalisePosition ();
  295. dragPosition = null;
  296. moveRuneRenderLocation = null;
  297. }
  298. return false;
  299. }
  300. private Pos Offset (Pos pos, int delta)
  301. {
  302. var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ?
  303. parent.Bounds.Width : parent.Bounds.Height);
  304. return posAbsolute + delta;
  305. }
  306. private void FinalisePosition ()
  307. {
  308. // if before dragging we were a proportional position
  309. // then preserve that when the mouse is released so that
  310. // resizing continues to work as intended
  311. if (dragOrignalPos is PosFactor) {
  312. if (Orientation == Orientation.Horizontal) {
  313. parent.splitterDistance = ToPosFactor (Y, parent.Bounds.Height);
  314. } else {
  315. parent.splitterDistance = ToPosFactor (X, parent.Bounds.Width);
  316. }
  317. }
  318. }
  319. private Pos ToPosFactor (Pos y, int parentLength)
  320. {
  321. int position = y.Anchor (parentLength);
  322. return new PosFactor (position / (float)parentLength);
  323. }
  324. }
  325. }
  326. }