SplitContainer.cs 14 KB

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