SplitContainer.cs 16 KB

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