SplitContainer.cs 16 KB

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