SplitView.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using Terminal.Gui.Graphs;
  6. namespace Terminal.Gui {
  7. /// <summary>
  8. /// A <see cref="View"/> consisting of a moveable bar that divides
  9. /// the display area into 2 resizeable views.
  10. /// </summary>
  11. public class SplitView : View
  12. {
  13. private SplitContainerLineView splitterLine;
  14. SplitView parentSplitView;
  15. /// TODO: Might be able to make Border virtual and override here
  16. /// To make this more API friendly
  17. /// <summary>
  18. /// Use this field instead of Border to create an integrated
  19. /// Border in which lines connect with subviews and splitters
  20. /// seamlessly
  21. /// </summary>
  22. public BorderStyle IntegratedBorder {get;set;}
  23. /// <summary>
  24. /// The <see cref="View"/> showing in the left hand pane of a
  25. /// <see cref="Orientation.Vertical"/> or top of an
  26. /// <see cref="Orientation.Horizontal"/> pane. May be another
  27. /// <see cref="SplitView"/> if further splitter subdivisions are
  28. /// desired (e.g. to create a resizeable grid.
  29. /// </summary>
  30. public View View1 { get; private set; }
  31. public int View1MinSize { get; set; } = 1;
  32. public ustring View1Title { get; set; } = string.Empty;
  33. /// <summary>
  34. /// The <see cref="View"/> showing in the right hand pane of a
  35. /// <see cref="Orientation.Vertical"/> or bottom of an
  36. /// <see cref="Orientation.Horizontal"/> pane. May be another
  37. /// <see cref="SplitView"/> if further splitter subdivisions are
  38. /// desired (e.g. to create a resizeable grid.
  39. /// </summary>
  40. public View View2 { get; private set; }
  41. public int View2MinSize { get; set; } = 1;
  42. public ustring View2Title { get; set; } = string.Empty;
  43. private Pos splitterDistance = Pos.Percent (50);
  44. private Orientation orientation = Orientation.Vertical;
  45. /// <summary>
  46. /// Creates a new instance of the SplitContainer class.
  47. /// </summary>
  48. public SplitView ()
  49. {
  50. splitterLine = new SplitContainerLineView (this);
  51. View1 = new View () { Width = Dim.Fill (), Height = Dim.Fill() };
  52. View2 = new View () { Width = Dim.Fill (), Height = Dim.Fill () };
  53. this.Add (View1);
  54. this.Add (splitterLine);
  55. this.Add (View2);
  56. CanFocus = true;
  57. }
  58. /// <summary>
  59. /// Invoked when the <see cref="SplitterDistance"/> is changed
  60. /// </summary>
  61. public event SplitterEventHandler SplitterMoved;
  62. /// <summary>
  63. /// Raises the <see cref="SplitterMoved"/> event
  64. /// </summary>
  65. protected virtual void OnSplitterMoved ()
  66. {
  67. SplitterMoved?.Invoke (this, new SplitterEventArgs (this, splitterDistance));
  68. }
  69. /// <summary>
  70. /// Orientation of the dividing line (Horizontal or Vertical).
  71. /// </summary>
  72. public Orientation Orientation {
  73. get { return orientation; }
  74. set {
  75. orientation = value;
  76. LayoutSubviews ();
  77. }
  78. }
  79. public override void LayoutSubviews ()
  80. {
  81. var contentArea = Bounds;
  82. if(HasBorder())
  83. {
  84. // TODO: Bound with Max/Min
  85. contentArea = new Rect(
  86. contentArea.X + 1,
  87. contentArea.Y + 1,
  88. Math.Max (0, contentArea.Width - 2),
  89. Math.Max (0, contentArea.Height - 2));
  90. }
  91. else if(HasAnyTitles() && IsRootSplitContainer())
  92. {
  93. // TODO: Bound with Max/Min
  94. contentArea = new Rect(
  95. contentArea.X,
  96. contentArea.Y + 1,
  97. contentArea.Width,
  98. Math.Max(0,contentArea.Height - 1));
  99. }
  100. Setup (contentArea);
  101. base.LayoutSubviews ();
  102. }
  103. /// <summary>
  104. /// <para>Distance Horizontally or Vertically to the splitter line when
  105. /// neither view is collapsed.
  106. /// </para>
  107. /// <para>Only absolute values (e.g. 10) and percent values (i.e. <see cref="Pos.Percent(float)"/>)
  108. /// are supported for this property.</para>
  109. /// </summary>
  110. public Pos SplitterDistance {
  111. get { return splitterDistance; }
  112. set {
  113. if (!(value is Pos.PosAbsolute) && !(value is Pos.PosFactor)) {
  114. throw new ArgumentException ($"Only Percent and Absolute values are supported for {nameof (SplitterDistance)} property. Passed value was {value.GetType ().Name}");
  115. }
  116. splitterDistance = value;
  117. GetRootSplitContainer ().LayoutSubviews ();
  118. OnSplitterMoved ();
  119. }
  120. }
  121. /// <inheritdoc/>
  122. public override bool OnEnter (View view)
  123. {
  124. Driver.SetCursorVisibility (CursorVisibility.Invisible);
  125. return base.OnEnter (view);
  126. }
  127. /// <inheritdoc/>
  128. public override void Redraw (Rect bounds)
  129. {
  130. var childTitles = new List<ChildSplitterLine> ();
  131. Driver.SetAttribute (ColorScheme.Normal);
  132. Clear ();
  133. base.Redraw (bounds);
  134. var lc = new LineCanvas(Application.Driver);
  135. var allLines = GetAllChildSplitContainerLineViewRecursively (this);
  136. if (IsRootSplitContainer())
  137. {
  138. if(HasBorder ()) {
  139. lc.AddLine (new Point (0, 0), bounds.Width - 1, Orientation.Horizontal, IntegratedBorder);
  140. lc.AddLine (new Point (0, 0), bounds.Height - 1, Orientation.Vertical, IntegratedBorder);
  141. lc.AddLine (new Point (bounds.Width - 1, bounds.Height - 1), -bounds.Width + 1, Orientation.Horizontal, IntegratedBorder);
  142. lc.AddLine (new Point (bounds.Width - 1, bounds.Height - 1), -bounds.Height + 1, Orientation.Vertical, IntegratedBorder);
  143. }
  144. foreach (var line in allLines.Where(l=>l.Visible))
  145. {
  146. bool isRoot = line == splitterLine;
  147. line.ViewToScreen(0,0,out var x1,out var y1);
  148. var origin = ScreenToView(x1,y1);
  149. var length = line.Orientation == Orientation.Horizontal ?
  150. line.Frame.Width - 1 :
  151. line.Frame.Height - 1;
  152. if(!isRoot) {
  153. if(line.Orientation == Orientation.Horizontal) {
  154. origin.X -= 1;
  155. } else {
  156. origin.Y -= 1;
  157. }
  158. length += 2;
  159. childTitles.Add (
  160. new ChildSplitterLine(line));
  161. }
  162. lc.AddLine(origin,length,line.Orientation,IntegratedBorder);
  163. }
  164. }
  165. Driver.SetAttribute (ColorScheme.Normal);
  166. lc.Draw(this,bounds);
  167. // Redraw the lines so that focus/drag symbol renders
  168. foreach(var line in allLines) {
  169. line.DrawSplitterSymbol ();
  170. }
  171. foreach(var child in childTitles) {
  172. child.DrawTitles ();
  173. }
  174. // Draw Titles over Border
  175. var screen = ViewToScreen (new Rect(0,0,bounds.Width,1));
  176. if (View1.Visible && View1Title.Length > 0) {
  177. Driver.SetAttribute (View1.HasFocus ? ColorScheme.HotNormal : ColorScheme.Normal);
  178. Driver.DrawWindowTitle (new Rect (screen.X, screen.Y, View1.Frame.Width, 0), View1Title, 0, 0, 0, 0);
  179. }
  180. if (splitterLine.Visible) {
  181. screen = ViewToScreen (splitterLine.Frame);
  182. } else {
  183. screen.X--;
  184. //screen.Y--;
  185. }
  186. if (Orientation == Orientation.Horizontal) {
  187. if (View2.Visible && View2Title?.Length > 0) {
  188. Driver.SetAttribute (View2.HasFocus ? ColorScheme.HotNormal : ColorScheme.Normal);
  189. Driver.DrawWindowTitle (new Rect (screen.X, screen.Y, View2.Bounds.Width, 1), View2Title, 0, 0, 0, 0);
  190. }
  191. } else {
  192. if (View2.Visible && View2Title?.Length > 0) {
  193. Driver.SetAttribute (View2.HasFocus ? ColorScheme.HotNormal : ColorScheme.Normal);
  194. Driver.DrawWindowTitle (new Rect (screen.X, screen.Y, View2.Bounds.Width, 1), View2Title, 0, 0, 0, 0);
  195. }
  196. }
  197. }
  198. /// <summary>
  199. /// Converts <see cref="View1"/> from a regular <see cref="View"/>
  200. /// container to a new nested <see cref="SplitView"/>. If <see cref="View1"/>
  201. /// is already a <see cref="SplitView"/> then returns false.
  202. /// </summary>
  203. /// <remarks>After successful splitting, the returned container's <see cref="View1"/>
  204. /// will contain the original content and <see cref="View1Title"/> (if any) while
  205. /// <see cref="View2"/> will be empty and available for adding to.
  206. /// for adding to.</remarks>
  207. /// <param name="result">The new <see cref="SplitView"/> now showing in
  208. /// <see cref="View1"/> or the existing one if it was already been converted before.</param>
  209. /// <returns><see langword="true"/> if a <see cref="View"/> was converted to a new nested
  210. /// <see cref="SplitView"/>. <see langword="false"/> if it was already a nested
  211. /// <see cref="SplitView"/></returns>
  212. public bool TrySplitView1(out SplitView result)
  213. {
  214. // when splitting a view into 2 sub views we will need to migrate
  215. // the title too
  216. var title = View1Title;
  217. bool returnValue = TrySplit (
  218. this.View1,
  219. (newSplitContainer) => {
  220. this.View1 = newSplitContainer;
  221. // Move title to new container
  222. View1Title = string.Empty;
  223. newSplitContainer.View1Title = title;
  224. },
  225. out result);
  226. return returnValue;
  227. }
  228. /// <summary>
  229. /// Converts <see cref="View2"/> from a regular <see cref="View"/>
  230. /// container to a new nested <see cref="SplitView"/>. If <see cref="View2"/>
  231. /// is already a <see cref="SplitView"/> then returns false.
  232. /// </summary>
  233. /// <remarks>After successful splitting, the returned container's <see cref="View1"/>
  234. /// will contain the original content and <see cref="View2Title"/> (if any) while
  235. /// <see cref="View2"/> will be empty and available for adding to.
  236. /// for adding to.</remarks>
  237. /// <param name="result">The new <see cref="SplitView"/> now showing in
  238. /// <see cref="View2"/> or the existing one if it was already been converted before.</param>
  239. /// <returns><see langword="true"/> if a <see cref="View"/> was converted to a new nested
  240. /// <see cref="SplitView"/>. <see langword="false"/> if it was already a nested
  241. /// <see cref="SplitView"/></returns>
  242. public bool TrySplitView2 (out SplitView result)
  243. {
  244. // when splitting a view into 2 sub views we will need to migrate
  245. // the title too
  246. var title = View2Title;
  247. bool returnValue = TrySplit (
  248. this.View2,
  249. (newSplitContainer) => {
  250. this.View2 = newSplitContainer;
  251. // Move title to new container
  252. View2Title = string.Empty;
  253. // Content always goes into View1 of the new container
  254. // so that is where the title goes too
  255. newSplitContainer.View1Title = title;
  256. },
  257. out result);
  258. return returnValue;
  259. }
  260. private bool TrySplit(
  261. View toMove,
  262. Action<SplitView> newSplitContainerSetter,
  263. out SplitView result)
  264. {
  265. if (toMove is SplitView existing) {
  266. result = existing;
  267. return false;
  268. }
  269. var newContainer = new SplitView {
  270. Width = Dim.Fill (),
  271. Height = Dim.Fill (),
  272. parentSplitView = this,
  273. };
  274. // Take everything out of the View we are moving
  275. var childViews = toMove.Subviews.ToArray();
  276. toMove.RemoveAll ();
  277. // Remove the view itself and replace it with the new SplitContainer
  278. Remove (toMove);
  279. Add (newContainer);
  280. newSplitContainerSetter(newContainer);
  281. // Add the original content into the first view of the new container
  282. foreach(var childView in childViews) {
  283. newContainer.View1.Add (childView);
  284. }
  285. result = newContainer;
  286. return true;
  287. }
  288. private List<SplitContainerLineView> GetAllChildSplitContainerLineViewRecursively (View v)
  289. {
  290. var lines = new List<SplitContainerLineView>();
  291. foreach(var sub in v.Subviews)
  292. {
  293. if(sub is SplitContainerLineView s)
  294. {
  295. if(s.Parent.GetRootSplitContainer() == this) {
  296. lines.Add (s);
  297. }
  298. }
  299. else {
  300. lines.AddRange(GetAllChildSplitContainerLineViewRecursively(sub));
  301. }
  302. }
  303. return lines;
  304. }
  305. private bool IsRootSplitContainer ()
  306. {
  307. // TODO: don't want to layout subviews since the parent recursively lays them all out
  308. return parentSplitView == null;
  309. }
  310. private SplitView GetRootSplitContainer ()
  311. {
  312. SplitView root = this;
  313. while (root.parentSplitView != null) {
  314. root = root.parentSplitView;
  315. }
  316. return root;
  317. }
  318. private void Setup (Rect bounds)
  319. {
  320. splitterLine.Orientation = Orientation;
  321. // splitterLine.Text = View2.Title;
  322. // TODO: Recursion
  323. if (!View1.Visible || !View2.Visible) {
  324. View toFullSize = !View1.Visible ? View2 : View1;
  325. splitterLine.Visible = false;
  326. toFullSize.X = bounds.X;
  327. toFullSize.Y = bounds.Y;
  328. toFullSize.Width = bounds.Width;
  329. toFullSize.Height = bounds.Height;
  330. } else {
  331. splitterLine.Visible = true;
  332. splitterDistance = BoundByMinimumSizes (splitterDistance);
  333. View1.X = bounds.X;
  334. View1.Y = bounds.Y;
  335. switch (Orientation) {
  336. case Orientation.Horizontal:
  337. splitterLine.X = 0;
  338. splitterLine.Y = splitterDistance;
  339. splitterLine.Width = Dim.Fill ();
  340. splitterLine.Height = 1;
  341. splitterLine.LineRune = Driver.HLine;
  342. View1.Width = Dim.Fill (HasBorder()? 1:0);
  343. View1.Height = new Dim.DimFunc (() =>
  344. splitterDistance.Anchor (bounds.Height));
  345. View2.Y = Pos.Bottom (splitterLine);
  346. View2.X = bounds.X;
  347. View2.Width = bounds.Width;
  348. View2.Height = Dim.Fill(HasBorder () ? 1 : 0);
  349. break;
  350. case Orientation.Vertical:
  351. splitterLine.X = splitterDistance;
  352. splitterLine.Y = 0;
  353. splitterLine.Width = 1;
  354. splitterLine.Height = Dim.Fill ();
  355. splitterLine.LineRune = Driver.VLine;
  356. View1.Height = Dim.Fill();
  357. View1.Width = new Dim.DimFunc (() =>
  358. splitterDistance.Anchor (bounds.Width));
  359. View2.X = Pos.Right (splitterLine);
  360. View2.Y = bounds.Y;
  361. View2.Height = bounds.Height;
  362. View2.Width = Dim.Fill(HasBorder()? 1:0);
  363. break;
  364. default: throw new ArgumentOutOfRangeException (nameof (orientation));
  365. };
  366. }
  367. }
  368. /// <summary>
  369. /// Considers <paramref name="pos"/> as a candidate for <see cref="splitterDistance"/>
  370. /// then either returns (if valid) or returns adjusted if invalid with respect to the
  371. /// <see cref="SplitterView.MinSize"/> of the views.
  372. /// </summary>
  373. /// <param name="pos"></param>
  374. /// <returns></returns>
  375. private Pos BoundByMinimumSizes (Pos pos)
  376. {
  377. // if we are not yet initialized then we don't know
  378. // how big we are and therefore cannot sensibly calculate
  379. // how big the views will be with a given SplitterDistance
  380. if (!IsInitialized) {
  381. return pos;
  382. }
  383. var view1MinSize = View1MinSize;
  384. var view2MinSize = View2MinSize;
  385. // if there is a border then there is less space
  386. // for the views so we need to make size restrictions
  387. // tighter.
  388. if(HasBorder()) {
  389. view1MinSize++;
  390. view2MinSize++;
  391. }
  392. var availableSpace = Orientation == Orientation.Horizontal ? this.Bounds.Height : this.Bounds.Width;
  393. // we probably haven't finished layout even if IsInitialized is true :(
  394. if(availableSpace <= 0) {
  395. return pos;
  396. }
  397. var idealPosition = pos.Anchor (availableSpace);
  398. // bad position because not enough space for View1
  399. if (idealPosition < view1MinSize) {
  400. // TODO: we should preserve Absolute/Percent status here not just force it to absolute
  401. return (Pos)Math.Min (view1MinSize, availableSpace);
  402. }
  403. // bad position because not enough space for View2
  404. if (availableSpace - idealPosition <= view2MinSize) {
  405. // TODO: we should preserve Absolute/Percent status here not just force it to absolute
  406. // +1 is to allow space for the splitter
  407. return (Pos)Math.Max (availableSpace - (view2MinSize + 1), 0);
  408. }
  409. // this splitter position is fine, there is enough space for everyone
  410. return pos;
  411. }
  412. private class SplitContainerLineView : LineView {
  413. public SplitView Parent { get; private set; }
  414. Point? dragPosition;
  415. Pos dragOrignalPos;
  416. public Point? moveRuneRenderLocation;
  417. public SplitContainerLineView (SplitView parent)
  418. {
  419. CanFocus = true;
  420. TabStop = true;
  421. this.Parent = parent;
  422. base.AddCommand (Command.Right, () => {
  423. return MoveSplitter (1, 0);
  424. });
  425. base.AddCommand (Command.Left, () => {
  426. return MoveSplitter (-1, 0);
  427. });
  428. base.AddCommand (Command.LineUp, () => {
  429. return MoveSplitter (0, -1);
  430. });
  431. base.AddCommand (Command.LineDown, () => {
  432. return MoveSplitter (0, 1);
  433. });
  434. AddKeyBinding (Key.CursorRight, Command.Right);
  435. AddKeyBinding (Key.CursorLeft, Command.Left);
  436. AddKeyBinding (Key.CursorUp, Command.LineUp);
  437. AddKeyBinding (Key.CursorDown, Command.LineDown);
  438. }
  439. public override bool ProcessKey (KeyEvent kb)
  440. {
  441. if (!CanFocus || !HasFocus) {
  442. return base.ProcessKey (kb);
  443. }
  444. var result = InvokeKeybindings (kb);
  445. if (result != null)
  446. return (bool)result;
  447. return base.ProcessKey (kb);
  448. }
  449. public override void PositionCursor ()
  450. {
  451. base.PositionCursor ();
  452. var location = moveRuneRenderLocation ??
  453. new Point (Bounds.Width / 2, Bounds.Height / 2);
  454. Move (location.X, location.Y);
  455. }
  456. public override bool OnEnter (View view)
  457. {
  458. Driver.SetCursorVisibility (CursorVisibility.Default);
  459. PositionCursor ();
  460. return base.OnEnter (view);
  461. }
  462. public override void Redraw (Rect bounds)
  463. {
  464. base.Redraw (bounds);
  465. DrawSplitterSymbol ();
  466. }
  467. public void DrawSplitterSymbol()
  468. {
  469. if (CanFocus && HasFocus) {
  470. var location = moveRuneRenderLocation ??
  471. new Point (Bounds.Width / 2, Bounds.Height / 2);
  472. AddRune (location.X, location.Y, Driver.Diamond);
  473. }
  474. }
  475. public override bool MouseEvent (MouseEvent mouseEvent)
  476. {
  477. if (!CanFocus) {
  478. return true;
  479. }
  480. if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) {
  481. // Start a Drag
  482. SetFocus ();
  483. Application.EnsuresTopOnFront ();
  484. if (mouseEvent.Flags == MouseFlags.Button1Pressed) {
  485. dragPosition = new Point (mouseEvent.X, mouseEvent.Y);
  486. dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X;
  487. Application.GrabMouse (this);
  488. if (Orientation == Orientation.Horizontal) {
  489. } else {
  490. moveRuneRenderLocation = new Point (0, Math.Max (1, Math.Min (Bounds.Height - 2, mouseEvent.Y)));
  491. }
  492. }
  493. return true;
  494. } else if (
  495. dragPosition.HasValue &&
  496. (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
  497. // Continue Drag
  498. // how far has user dragged from original location?
  499. if (Orientation == Orientation.Horizontal) {
  500. int dy = mouseEvent.Y - dragPosition.Value.Y;
  501. Parent.SplitterDistance = Offset (Y, dy);
  502. moveRuneRenderLocation = new Point (mouseEvent.X, 0);
  503. } else {
  504. int dx = mouseEvent.X - dragPosition.Value.X;
  505. Parent.SplitterDistance = Offset (X, dx);
  506. moveRuneRenderLocation = new Point (0, Math.Max (1, Math.Min (Bounds.Height - 2, mouseEvent.Y)));
  507. }
  508. Parent.SetNeedsDisplay ();
  509. return true;
  510. }
  511. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
  512. // End Drag
  513. Application.UngrabMouse ();
  514. Driver.UncookMouse ();
  515. FinalisePosition (
  516. dragOrignalPos,
  517. Orientation == Orientation.Horizontal ? Y : X);
  518. dragPosition = null;
  519. moveRuneRenderLocation = null;
  520. }
  521. return false;
  522. }
  523. private bool MoveSplitter (int distanceX, int distanceY)
  524. {
  525. if (Orientation == Orientation.Vertical) {
  526. // Cannot move in this direction
  527. if (distanceX == 0) {
  528. return false;
  529. }
  530. var oldX = X;
  531. FinalisePosition (oldX, (Pos)Offset (X, distanceX));
  532. return true;
  533. } else {
  534. // Cannot move in this direction
  535. if (distanceY == 0) {
  536. return false;
  537. }
  538. var oldY = Y;
  539. FinalisePosition (oldY, (Pos)Offset (Y, distanceY));
  540. return true;
  541. }
  542. }
  543. private Pos Offset (Pos pos, int delta)
  544. {
  545. var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ?
  546. Parent.Bounds.Height : Parent.Bounds.Width);
  547. return posAbsolute + delta;
  548. }
  549. /// <summary>
  550. /// <para>
  551. /// Moves <see cref="parent"/> <see cref="SplitView.SplitterDistance"/> to
  552. /// <see cref="Pos"/> <paramref name="newValue"/> preserving <see cref="Pos"/> format
  553. /// (absolute / relative) that <paramref name="oldValue"/> had.
  554. /// </para>
  555. /// <remarks>This ensures that if splitter location was e.g. 50% before and you move it
  556. /// to absolute 5 then you end up with 10% (assuming a parent had 50 width). </remarks>
  557. /// </summary>
  558. /// <param name="oldValue"></param>
  559. /// <param name="newValue"></param>
  560. private void FinalisePosition (Pos oldValue, Pos newValue)
  561. {
  562. if (oldValue is Pos.PosFactor) {
  563. if (Orientation == Orientation.Horizontal) {
  564. Parent.SplitterDistance = ConvertToPosFactor (newValue, Parent.Bounds.Height);
  565. } else {
  566. Parent.SplitterDistance = ConvertToPosFactor (newValue, Parent.Bounds.Width);
  567. }
  568. } else {
  569. Parent.SplitterDistance = newValue;
  570. }
  571. }
  572. /// <summary>
  573. /// <para>
  574. /// Determines the absolute position of <paramref name="p"/> and
  575. /// returns a <see cref="Pos.PosFactor"/> that describes the percentage of that.
  576. /// </para>
  577. /// <para>Effectively turning any <see cref="Pos"/> into a <see cref="Pos.PosFactor"/>
  578. /// (as if created with <see cref="Pos.Percent(float)"/>)</para>
  579. /// </summary>
  580. /// <param name="p">The <see cref="Pos"/> to convert to <see cref="Pos.Percent(float)"/></param>
  581. /// <param name="parentLength">The Height/Width that <paramref name="p"/> lies within</param>
  582. /// <returns></returns>
  583. private Pos ConvertToPosFactor (Pos p, int parentLength)
  584. {
  585. // calculate position in the 'middle' of the cell at p distance along parentLength
  586. float position = p.Anchor (parentLength) + 0.5f;
  587. return new Pos.PosFactor (position / parentLength);
  588. }
  589. }
  590. private bool HasBorder ()
  591. {
  592. return IntegratedBorder != BorderStyle.None;
  593. }
  594. private bool HasAnyTitles()
  595. {
  596. return View1Title.Length > 0 || View2Title.Length > 0;
  597. }
  598. private class ChildSplitterLine {
  599. readonly SplitContainerLineView currentLine;
  600. internal ChildSplitterLine (SplitContainerLineView currentLine)
  601. {
  602. this.currentLine = currentLine;
  603. }
  604. internal void DrawTitles ()
  605. {
  606. if(currentLine.Orientation == Orientation.Horizontal)
  607. {
  608. var screenRect = currentLine.ViewToScreen (
  609. new Rect(0,0,currentLine.Frame.Width,currentLine.Frame.Height));
  610. Driver.DrawWindowTitle (screenRect, currentLine.Parent.View2Title, 0, 0, 0, 0);
  611. }
  612. }
  613. }
  614. }
  615. /// <summary>
  616. /// Provides data for <see cref="SplitContainer"/> events.
  617. /// </summary>
  618. public class SplitterEventArgs : EventArgs {
  619. /// <summary>
  620. /// Creates a new instance of the <see cref="SplitterEventArgs"/> class.
  621. /// </summary>
  622. /// <param name="splitContainer"></param>
  623. /// <param name="splitterDistance"></param>
  624. public SplitterEventArgs (SplitView splitContainer, Pos splitterDistance)
  625. {
  626. SplitterDistance = splitterDistance;
  627. SplitContainer = splitContainer;
  628. }
  629. /// <summary>
  630. /// New position of the <see cref="SplitView.SplitterDistance"/>
  631. /// </summary>
  632. public Pos SplitterDistance { get; }
  633. /// <summary>
  634. /// Container (sender) of the event.
  635. /// </summary>
  636. public SplitView SplitContainer { get; }
  637. }
  638. /// <summary>
  639. /// Represents a method that will handle splitter events.
  640. /// </summary>
  641. public delegate void SplitterEventHandler (object sender, SplitterEventArgs e);
  642. }