SplitContainer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  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));
  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 = ParentHasBorder () ? Driver.LeftTee : (Rune?)null;
  270. EndingAnchor = ParentHasBorder () ? Driver.RightTee : (Rune?)null;
  271. } else {
  272. StartingAnchor = ParentHasBorder () ? Driver.TopTee : (Rune?)null;
  273. EndingAnchor = ParentHasBorder () ? Driver.BottomTee : (Rune?)null;
  274. }
  275. };
  276. }
  277. private bool ParentHasBorder ()
  278. {
  279. return parent.Border != null && parent.Border.BorderStyle != BorderStyle.None;
  280. }
  281. public override bool ProcessKey (KeyEvent kb)
  282. {
  283. if (!CanFocus || !HasFocus) {
  284. return base.ProcessKey (kb);
  285. }
  286. var result = InvokeKeybindings (kb);
  287. if (result != null)
  288. return (bool)result;
  289. return base.ProcessKey (kb);
  290. }
  291. public override void PositionCursor ()
  292. {
  293. base.PositionCursor ();
  294. var location = moveRuneRenderLocation ??
  295. new Point (Bounds.Width / 2, Bounds.Height / 2);
  296. Move (location.X, location.Y);
  297. }
  298. public override bool OnEnter (View view)
  299. {
  300. Driver.SetCursorVisibility (CursorVisibility.Default);
  301. PositionCursor ();
  302. return base.OnEnter (view);
  303. }
  304. public override void Redraw (Rect bounds)
  305. {
  306. base.Redraw (bounds);
  307. if (CanFocus && HasFocus) {
  308. var location = moveRuneRenderLocation ??
  309. new Point (Bounds.Width / 2, Bounds.Height / 2);
  310. AddRune (location.X, location.Y, Driver.Diamond);
  311. }
  312. }
  313. public override bool MouseEvent (MouseEvent mouseEvent)
  314. {
  315. if (!CanFocus) {
  316. return true;
  317. }
  318. if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) {
  319. // Start a Drag
  320. SetFocus ();
  321. Application.EnsuresTopOnFront ();
  322. if (mouseEvent.Flags == MouseFlags.Button1Pressed) {
  323. dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  324. dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X;
  325. Application.GrabMouse (this);
  326. if (Orientation == Orientation.Horizontal) {
  327. } else {
  328. moveRuneRenderLocation = new Point (0, Math.Max (1, Math.Min (Bounds.Height - 2, mouseEvent.Y)));
  329. }
  330. }
  331. return true;
  332. } else if (
  333. dragPosition.HasValue &&
  334. (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
  335. // Continue Drag
  336. // how far has user dragged from original location?
  337. if (Orientation == Orientation.Horizontal) {
  338. int dy = mouseEvent.Y - dragPosition.Value.Y;
  339. parent.SplitterDistance = Offset (Y, dy);
  340. moveRuneRenderLocation = new Point (mouseEvent.X, 0);
  341. } else {
  342. int dx = mouseEvent.X - dragPosition.Value.X;
  343. parent.SplitterDistance = Offset (X, dx);
  344. moveRuneRenderLocation = new Point (0, Math.Max (1, Math.Min (Bounds.Height - 2, mouseEvent.Y)));
  345. }
  346. parent.SetNeedsDisplay ();
  347. return true;
  348. }
  349. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
  350. // End Drag
  351. Application.UngrabMouse ();
  352. Driver.UncookMouse ();
  353. FinalisePosition (
  354. dragOrignalPos,
  355. Orientation == Orientation.Horizontal ? Y : X);
  356. dragPosition = null;
  357. //moveRuneRenderLocation = null;
  358. }
  359. return false;
  360. }
  361. private bool MoveSplitter (int distanceX, int distanceY)
  362. {
  363. if (Orientation == Orientation.Vertical) {
  364. // Cannot move in this direction
  365. if (distanceX == 0) {
  366. return false;
  367. }
  368. var oldX = X;
  369. FinalisePosition (oldX, (Pos)Offset (X, distanceX));
  370. return true;
  371. } else {
  372. // Cannot move in this direction
  373. if (distanceY == 0) {
  374. return false;
  375. }
  376. var oldY = Y;
  377. FinalisePosition (oldY, (Pos)Offset (Y, distanceY));
  378. return true;
  379. }
  380. }
  381. private Pos Offset (Pos pos, int delta)
  382. {
  383. var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ?
  384. parent.Bounds.Height : parent.Bounds.Width);
  385. return posAbsolute + delta;
  386. }
  387. /// <summary>
  388. /// <para>
  389. /// Moves <see cref="parent"/> <see cref="SplitContainer.SplitterDistance"/> to
  390. /// <see cref="Pos"/> <paramref name="newValue"/> preserving <see cref="Pos"/> format
  391. /// (absolute / relative) that <paramref name="oldValue"/> had.
  392. /// </para>
  393. /// <remarks>This ensures that if splitter location was e.g. 50% before and you move it
  394. /// to absolute 5 then you end up with 10% (assuming a parent had 50 width). </remarks>
  395. /// </summary>
  396. /// <param name="oldValue"></param>
  397. /// <param name="newValue"></param>
  398. private void FinalisePosition (Pos oldValue, Pos newValue)
  399. {
  400. if (oldValue is Pos.PosFactor) {
  401. if (Orientation == Orientation.Horizontal) {
  402. parent.SplitterDistance = ConvertToPosFactor (newValue, parent.Bounds.Height);
  403. } else {
  404. parent.SplitterDistance = ConvertToPosFactor (newValue, parent.Bounds.Width);
  405. }
  406. } else {
  407. parent.SplitterDistance = newValue;
  408. }
  409. }
  410. /// <summary>
  411. /// <para>
  412. /// Determines the absolute position of <paramref name="p"/> and
  413. /// returns a <see cref="Pos.PosFactor"/> that describes the percentage of that.
  414. /// </para>
  415. /// <para>Effectively turning any <see cref="Pos"/> into a <see cref="Pos.PosFactor"/>
  416. /// (as if created with <see cref="Pos.Percent(float)"/>)</para>
  417. /// </summary>
  418. /// <param name="p">The <see cref="Pos"/> to convert to <see cref="Pos.Percent(float)"/></param>
  419. /// <param name="parentLength">The Height/Width that <paramref name="p"/> lies within</param>
  420. /// <returns></returns>
  421. private Pos ConvertToPosFactor (Pos p, int parentLength)
  422. {
  423. // calculate position in the 'middle' of the cell at p distance along parentLength
  424. float position = p.Anchor (parentLength) + 0.5f;
  425. return new Pos.PosFactor (position / parentLength);
  426. }
  427. }
  428. }
  429. /// <summary>
  430. /// Provides data for <see cref="SplitContainer"/> events.
  431. /// </summary>
  432. public class SplitterEventArgs : EventArgs {
  433. /// <summary>
  434. /// Creates a new instance of the <see cref="SplitterEventArgs"/> class.
  435. /// </summary>
  436. /// <param name="splitContainer"></param>
  437. /// <param name="splitterDistance"></param>
  438. public SplitterEventArgs (SplitContainer splitContainer, Pos splitterDistance)
  439. {
  440. SplitterDistance = splitterDistance;
  441. SplitContainer = splitContainer;
  442. }
  443. /// <summary>
  444. /// New position of the <see cref="SplitContainer.SplitterDistance"/>
  445. /// </summary>
  446. public Pos SplitterDistance { get; }
  447. /// <summary>
  448. /// Container (sender) of the event.
  449. /// </summary>
  450. public SplitContainer SplitContainer { get; }
  451. }
  452. /// <summary>
  453. /// Represents a method that will handle splitter events.
  454. /// </summary>
  455. public delegate void SplitterEventHandler (object sender, SplitterEventArgs e);
  456. }