SplitContainer.cs 14 KB

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