SplitContainer.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. using System;
  2. using Terminal.Gui.Graphs;
  3. namespace Terminal.Gui {
  4. public class SplitContainer : View {
  5. private LineView splitterLine;
  6. private bool panel1Collapsed;
  7. private bool panel2Collapsed;
  8. private Pos splitterDistance = Pos.Percent (50);
  9. private Orientation orientation = Orientation.Vertical;
  10. private Pos panel1MinSize = 0;
  11. private Pos panel2MinSize = 0;
  12. /// <summary>
  13. /// Creates a new instance of the SplitContainer class.
  14. /// </summary>
  15. public SplitContainer ()
  16. {
  17. splitterLine = new SplitContainerLineView (this);
  18. this.Add (Panel1);
  19. this.Add (splitterLine);
  20. this.Add (Panel2);
  21. Setup ();
  22. CanFocus = false;
  23. }
  24. /// <summary>
  25. /// The left or top panel of the <see cref="SplitContainer"/>
  26. /// (depending on <see cref="Orientation"/>). Add panel contents
  27. /// to this <see cref="View"/> using <see cref="View.Add(View)"/>.
  28. /// </summary>
  29. public View Panel1 { get; } = new View ();
  30. /// <summary>
  31. /// The minimum size <see cref="Panel1"/> can be when adjusting
  32. /// <see cref="SplitterDistance"/>.
  33. /// </summary>
  34. public Pos Panel1MinSize {
  35. get { return panel1MinSize; }
  36. set {
  37. panel1MinSize = value;
  38. Setup ();
  39. }
  40. }
  41. /// <summary>
  42. /// This determines if <see cref="Panel1"/> is collapsed.
  43. /// </summary>
  44. public bool Panel1Collapsed {
  45. get { return panel1Collapsed; }
  46. set {
  47. panel1Collapsed = value;
  48. if (value && panel2Collapsed) {
  49. panel2Collapsed = false;
  50. }
  51. Setup ();
  52. }
  53. }
  54. /// <summary>
  55. /// The right or bottom panel of the <see cref="SplitContainer"/>
  56. /// (depending on <see cref="Orientation"/>). Add panel contents
  57. /// to this <see cref="View"/> using <see cref="View.Add(View)"/>
  58. /// </summary>
  59. public View Panel2 { get; } = new View ();
  60. /// <summary>
  61. /// The minimum size <see cref="Panel2"/> can be when adjusting
  62. /// <see cref="SplitterDistance"/>.
  63. /// </summary>
  64. public Pos Panel2MinSize {
  65. get {
  66. return panel2MinSize;
  67. }
  68. set {
  69. panel2MinSize = value;
  70. Setup ();
  71. }
  72. }
  73. /// <summary>
  74. /// This determines if <see cref="Panel2"/> is collapsed.
  75. /// </summary>
  76. public bool Panel2Collapsed {
  77. get { return panel2Collapsed; }
  78. set {
  79. panel2Collapsed = value;
  80. if (value && panel1Collapsed) {
  81. panel1Collapsed = false;
  82. }
  83. Setup ();
  84. }
  85. }
  86. /// <summary>
  87. /// Orientation of the dividing line (Horizontal or Vertical).
  88. /// </summary>
  89. public Orientation Orientation {
  90. get { return orientation; }
  91. set {
  92. orientation = value;
  93. Setup ();
  94. }
  95. }
  96. /// <summary>
  97. /// Distance Horizontally or Vertically to the splitter line when
  98. /// neither panel is collapsed.
  99. /// </summary>
  100. public Pos SplitterDistance {
  101. get { return splitterDistance; }
  102. set {
  103. splitterDistance = value;
  104. Setup ();
  105. }
  106. }
  107. public override bool OnEnter (View view)
  108. {
  109. Driver.SetCursorVisibility (CursorVisibility.Invisible);
  110. return base.OnEnter (view);
  111. }
  112. public override void Redraw (Rect bounds)
  113. {
  114. Driver.SetAttribute (ColorScheme.Normal);
  115. Clear ();
  116. base.Redraw (bounds);
  117. }
  118. private void Setup ()
  119. {
  120. splitterLine.Orientation = Orientation;
  121. if (panel1Collapsed || panel2Collapsed) {
  122. SetupForCollapsedPanel ();
  123. } else {
  124. SetupForNormal ();
  125. }
  126. }
  127. private void SetupForNormal ()
  128. {
  129. // Ensure all our component views are here
  130. // (e.g. if we are transitioning from a collapsed state)
  131. if (!this.Subviews.Contains (splitterLine)) {
  132. this.Add (splitterLine);
  133. }
  134. if (!this.Subviews.Contains (Panel1)) {
  135. this.Add (Panel1);
  136. }
  137. if (!this.Subviews.Contains (Panel2)) {
  138. this.Add (Panel2);
  139. }
  140. splitterDistance = BoundByMinimumSizes (splitterDistance);
  141. switch (Orientation) {
  142. case Orientation.Horizontal:
  143. splitterLine.X = 0;
  144. splitterLine.Y = splitterDistance;
  145. splitterLine.Width = Dim.Fill ();
  146. splitterLine.Height = 1;
  147. splitterLine.LineRune = Driver.HLine;
  148. this.Panel1.X = 0;
  149. this.Panel1.Y = 0;
  150. this.Panel1.Width = Dim.Fill ();
  151. this.Panel1.Height = new Dim.DimFunc (() =>
  152. splitterDistance.Anchor (Bounds.Height));
  153. this.Panel2.Y = Pos.Bottom (splitterLine);
  154. this.Panel2.X = 0;
  155. this.Panel2.Width = Dim.Fill ();
  156. this.Panel2.Height = Dim.Fill ();
  157. break;
  158. case Orientation.Vertical:
  159. splitterLine.X = splitterDistance;
  160. splitterLine.Y = 0;
  161. splitterLine.Width = 1;
  162. splitterLine.Height = Dim.Fill ();
  163. splitterLine.LineRune = Driver.VLine;
  164. this.Panel1.X = 0;
  165. this.Panel1.Y = 0;
  166. this.Panel1.Height = Dim.Fill ();
  167. this.Panel1.Width = new Dim.DimFunc (() =>
  168. splitterDistance.Anchor (Bounds.Width));
  169. this.Panel2.X = Pos.Right (splitterLine);
  170. this.Panel2.Y = 0;
  171. this.Panel2.Width = Dim.Fill ();
  172. this.Panel2.Height = Dim.Fill ();
  173. break;
  174. default: throw new ArgumentOutOfRangeException (nameof (orientation));
  175. };
  176. this.LayoutSubviews ();
  177. }
  178. /// <summary>
  179. /// Considers <paramref name="pos"/> as a candidate for <see cref="splitterDistance"/>
  180. /// then either returns (if valid) or returns adjusted if invalid with respect to
  181. /// <see cref="Panel1MinSize"/> or <see cref="Panel2MinSize"/>.
  182. /// </summary>
  183. /// <param name="pos"></param>
  184. /// <returns></returns>
  185. private Pos BoundByMinimumSizes (Pos pos)
  186. {
  187. // if we are not yet initialized then we don't know
  188. // how big we are and therefore cannot sensibly calculate
  189. // how big the panels will be with a given SplitterDistance
  190. if (!IsInitialized) {
  191. return pos;
  192. }
  193. var availableSpace = Orientation == Orientation.Horizontal ? this.Bounds.Height : this.Bounds.Width;
  194. var idealPosition = pos.Anchor (availableSpace);
  195. var panel1MinSizeAbs = panel1MinSize.Anchor (availableSpace);
  196. var panel2MinSizeAbs = panel2MinSize.Anchor (availableSpace);
  197. // bad position because not enough space for panel1
  198. if (idealPosition < panel1MinSizeAbs) {
  199. // TODO: we should preserve Absolute/Percent status here not just force it to absolute
  200. return (Pos)Math.Min (panel1MinSizeAbs, availableSpace);
  201. }
  202. // bad position because not enough space for panel2
  203. if(availableSpace - idealPosition <= panel2MinSizeAbs) {
  204. // TODO: we should preserve Absolute/Percent status here not just force it to absolute
  205. // +1 is to allow space for the splitter
  206. return (Pos)Math.Max (availableSpace - (panel2MinSizeAbs+1), 0);
  207. }
  208. // this splitter position is fine, there is enough space for everyone
  209. return pos;
  210. }
  211. private void SetupForCollapsedPanel ()
  212. {
  213. View toRemove = panel1Collapsed ? Panel1 : Panel2;
  214. View toFullSize = panel1Collapsed ? Panel2 : Panel1;
  215. if (this.Subviews.Contains (splitterLine)) {
  216. this.Remove(splitterLine);
  217. }
  218. if (this.Subviews.Contains (toRemove)) {
  219. this.Remove (toRemove);
  220. }
  221. if (!this.Subviews.Contains (toFullSize)) {
  222. this.Add (toFullSize);
  223. }
  224. toFullSize.X = 0;
  225. toFullSize.Y = 0;
  226. toFullSize.Width = Dim.Fill ();
  227. toFullSize.Height = Dim.Fill ();
  228. }
  229. private class SplitContainerLineView : LineView {
  230. private SplitContainer parent;
  231. Point? dragPosition;
  232. Pos dragOrignalPos;
  233. Point? moveRuneRenderLocation;
  234. // TODO: Make focusable and allow moving with keyboard
  235. public SplitContainerLineView (SplitContainer parent)
  236. {
  237. CanFocus = true;
  238. TabStop = true;
  239. this.parent = parent;
  240. base.AddCommand (Command.Right, () => {
  241. return MoveSplitter (1, 0);
  242. });
  243. base.AddCommand (Command.Left, () => {
  244. return MoveSplitter (-1, 0);
  245. });
  246. base.AddCommand (Command.LineUp, () => {
  247. return MoveSplitter (0, -1);
  248. });
  249. base.AddCommand (Command.LineDown, () => {
  250. return MoveSplitter (0, 1);
  251. });
  252. AddKeyBinding (Key.CursorRight, Command.Right);
  253. AddKeyBinding (Key.CursorLeft, Command.Left);
  254. AddKeyBinding (Key.CursorUp, Command.LineUp);
  255. AddKeyBinding (Key.CursorDown, Command.LineDown);
  256. }
  257. ///<inheritdoc/>
  258. public override bool ProcessKey (KeyEvent kb)
  259. {
  260. if (!CanFocus || !HasFocus) {
  261. return base.ProcessKey (kb);
  262. }
  263. var result = InvokeKeybindings (kb);
  264. if (result != null)
  265. return (bool)result;
  266. return base.ProcessKey (kb);
  267. }
  268. public override void PositionCursor ()
  269. {
  270. base.PositionCursor ();
  271. Move (this.Bounds.Width / 2, this.Bounds.Height / 2);
  272. }
  273. public override bool OnEnter (View view)
  274. {
  275. Driver.SetCursorVisibility (CursorVisibility.Default);
  276. PositionCursor ();
  277. return base.OnEnter (view);
  278. }
  279. public override void Redraw (Rect bounds)
  280. {
  281. base.Redraw (bounds);
  282. if (CanFocus && HasFocus) {
  283. var location = moveRuneRenderLocation ??
  284. new Point (Bounds.Width / 2, Bounds.Height / 2);
  285. AddRune (location.X, location.Y, Driver.Diamond);
  286. }
  287. }
  288. ///<inheritdoc/>
  289. public override bool MouseEvent (MouseEvent mouseEvent)
  290. {
  291. if (!CanFocus) {
  292. return true;
  293. }
  294. if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) {
  295. // Start a Drag
  296. SetFocus ();
  297. Application.EnsuresTopOnFront ();
  298. if (mouseEvent.Flags == MouseFlags.Button1Pressed) {
  299. dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  300. dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X;
  301. Application.GrabMouse (this);
  302. }
  303. return true;
  304. } else if (
  305. dragPosition.HasValue &&
  306. (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
  307. // Continue Drag
  308. // how far has user dragged from original location?
  309. if (Orientation == Orientation.Horizontal) {
  310. int dy = mouseEvent.Y - dragPosition.Value.Y;
  311. parent.SplitterDistance = Offset (Y, dy);
  312. moveRuneRenderLocation = new Point (mouseEvent.X, 0);
  313. } else {
  314. int dx = mouseEvent.X - dragPosition.Value.X;
  315. parent.SplitterDistance = Offset (X, dx);
  316. moveRuneRenderLocation = new Point (0, mouseEvent.Y);
  317. }
  318. parent.SetNeedsDisplay ();
  319. return true;
  320. }
  321. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
  322. // End Drag
  323. Application.UngrabMouse ();
  324. Driver.UncookMouse ();
  325. FinalisePosition (
  326. dragOrignalPos,
  327. Orientation == Orientation.Horizontal ? Y : X);
  328. dragPosition = null;
  329. moveRuneRenderLocation = null;
  330. }
  331. return false;
  332. }
  333. private bool MoveSplitter (int distanceX, int distanceY)
  334. {
  335. if (Orientation == Orientation.Vertical) {
  336. // Cannot move in this direction
  337. if (distanceX == 0) {
  338. return false;
  339. }
  340. var oldX = X;
  341. FinalisePosition (oldX, (Pos)Offset (X, distanceX));
  342. return true;
  343. } else {
  344. // Cannot move in this direction
  345. if (distanceY == 0) {
  346. return false;
  347. }
  348. var oldY = Y;
  349. FinalisePosition (oldY, (Pos)Offset (Y, distanceY));
  350. return true;
  351. }
  352. }
  353. private Pos Offset (Pos pos, int delta)
  354. {
  355. var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ?
  356. parent.Bounds.Height : parent.Bounds.Width);
  357. return posAbsolute + delta;
  358. }
  359. /// <summary>
  360. /// <para>
  361. /// Moves <see cref="parent"/> <see cref="SplitContainer.SplitterDistance"/> to
  362. /// <see cref="Pos"/> <paramref name="newValue"/> preserving <see cref="Pos"/> format
  363. /// (absolute / relative) that <paramref name="oldValue"/> had.
  364. /// </para>
  365. /// <remarks>This ensures that if splitter location was e.g. 50% before and you move it
  366. /// to absolute 5 then you end up with 10% (assuming a parent had 50 width). </remarks>
  367. /// </summary>
  368. /// <param name="oldValue"></param>
  369. /// <param name="newValue"></param>
  370. private void FinalisePosition (Pos oldValue, Pos newValue)
  371. {
  372. if (oldValue is Pos.PosFactor) {
  373. if (Orientation == Orientation.Horizontal) {
  374. parent.SplitterDistance = ConvertToPosFactor (newValue, parent.Bounds.Height);
  375. } else {
  376. parent.SplitterDistance = ConvertToPosFactor (newValue, parent.Bounds.Width);
  377. }
  378. } else {
  379. parent.SplitterDistance = newValue;
  380. }
  381. }
  382. /// <summary>
  383. /// <para>
  384. /// Determines the absolute position of <paramref name="p"/> and
  385. /// returns a <see cref="Pos.PosFactor"/> that describes the percentage of that.
  386. /// </para>
  387. /// <para>Effectively turning any <see cref="Pos"/> into a <see cref="Pos.PosFactor"/>
  388. /// (as if created with <see cref="Pos.Percent(float)"/>)</para>
  389. /// </summary>
  390. /// <param name="p">The <see cref="Pos"/> to convert to <see cref="Pos.Percent(float)"/></param>
  391. /// <param name="parentLength">The Height/Width that <paramref name="p"/> lies within</param>
  392. /// <returns></returns>
  393. private Pos ConvertToPosFactor (Pos p, int parentLength)
  394. {
  395. // calculate position in the 'middle' of the cell at p distance along parentLength
  396. float position = p.Anchor (parentLength) + 0.5f;
  397. return new Pos.PosFactor (position / parentLength);
  398. }
  399. }
  400. }
  401. }