SplitContainer.cs 11 KB

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