SplitContainer.cs 14 KB

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