using System; using Terminal.Gui.Graphs; using static Terminal.Gui.Dim; using static Terminal.Gui.Pos; namespace Terminal.Gui { public class SplitContainer : View { private LineView splitterLine; private bool panel1Collapsed; private bool panel2Collapsed; private Pos splitterDistance = Pos.Percent (50); private Orientation orientation = Orientation.Vertical; private int panel1MinSize; /// /// Creates a new instance of the SplitContainer class. /// public SplitContainer () { // Default to a border of 1 so that View looks nice Border = new Border (); splitterLine = new SplitContainerLineView (this); this.Add (splitterLine); this.Add (Panel1); this.Add (Panel2); Setup (); CanFocus = false; } /// /// The left or top panel of the /// (depending on ). Add panel contents /// to this using . /// public View Panel1 { get; } = new View (); /// /// The minimum size can be when adjusting /// . /// public int Panel1MinSize { get { return panel1MinSize; } set { panel1MinSize = value; Setup(); } } /// /// This determines if is collapsed. /// public bool Panel1Collapsed { get { return panel1Collapsed; } set { panel1Collapsed = value; if (value && panel2Collapsed) { panel2Collapsed = false; } Setup (); } } /// /// The right or bottom panel of the /// (depending on ). Add panel contents /// to this using /// public View Panel2 { get; } = new View (); /// /// The minimum size can be when adjusting /// . /// public int Panel2MinSize { get; set; } /// /// This determines if is collapsed. /// public bool Panel2Collapsed { get { return panel2Collapsed; } set { panel2Collapsed = value; if (value && panel1Collapsed) { panel1Collapsed = false; } Setup (); } } /// /// Orientation of the dividing line (Horizontal or Vertical). /// public Orientation Orientation { get { return orientation; } set { orientation = value; Setup (); } } /// /// Distance Horizontally or Vertically to the splitter line when /// neither panel is collapsed. /// public Pos SplitterDistance { get { return splitterDistance; } set { splitterDistance = value; Setup (); } } public override bool OnEnter (View view) { Driver.SetCursorVisibility (CursorVisibility.Invisible); return base.OnEnter (view); } private void Setup () { splitterLine.Orientation = Orientation; if (panel1Collapsed || panel2Collapsed) { SetupForCollapsedPanel (); } else { SetupForNormal (); } } private void SetupForNormal () { // Ensure all our component views are here // (e.g. if we are transitioning from a collapsed state) if (!this.Subviews.Contains (splitterLine)) { this.Add (splitterLine); } if (!this.Subviews.Contains (Panel1)) { this.Add (Panel1); } if (!this.Subviews.Contains (Panel2)) { this.Add (Panel2); } switch (Orientation) { case Orientation.Horizontal: splitterLine.X = 0; splitterLine.Y = splitterDistance; splitterLine.Width = Dim.Fill (); splitterLine.Height = 1; splitterLine.LineRune = Driver.HLine; this.Panel1.X = 0; this.Panel1.Y = 0; this.Panel1.Width = Dim.Fill (); this.Panel1.Height = new DimFunc (() => splitterDistance.Anchor (Bounds.Height) - 1); this.Panel2.Y = Pos.Bottom (splitterLine) + 1; this.Panel2.X = 0; this.Panel2.Width = Dim.Fill (); this.Panel2.Height = Dim.Fill (); break; case Orientation.Vertical: splitterLine.X = splitterDistance; splitterLine.Y = 0; splitterLine.Width = 1; splitterLine.Height = Dim.Fill (); splitterLine.LineRune = Driver.VLine; this.Panel1.X = 0; this.Panel1.Y = 0; this.Panel1.Height = Dim.Fill (); this.Panel1.Width = new DimFunc (() => splitterDistance.Anchor (Bounds.Width) - 1); this.Panel2.X = Pos.Right (splitterLine) + 1; this.Panel2.Y = 0; this.Panel2.Width = Dim.Fill (); this.Panel2.Height = Dim.Fill (); break; default: throw new ArgumentOutOfRangeException (nameof (orientation)); }; } private void SetupForCollapsedPanel () { View toRemove = panel1Collapsed ? Panel1 : Panel2; View toFullSize = panel1Collapsed ? Panel2 : Panel1; if (this.Subviews.Contains (splitterLine)) { this.Subviews.Remove (splitterLine); } if (this.Subviews.Contains (toRemove)) { this.Subviews.Remove (toRemove); } if (!this.Subviews.Contains (toFullSize)) { this.Add (toFullSize); } toFullSize.X = 0; toFullSize.Y = 0; toFullSize.Width = Dim.Fill (); toFullSize.Height = Dim.Fill (); } private class SplitContainerLineView : LineView { private SplitContainer parent; Point? dragPosition; Pos dragOrignalPos; // TODO: Make focusable and allow moving with keyboard public SplitContainerLineView(SplitContainer parent) { CanFocus = true; this.parent = parent; base.AddCommand (Command.Right, () => { if (Orientation == Orientation.Vertical) { parent.SplitterDistance = Offset (X, 1); return true; } return false; }); base.AddCommand (Command.Left, () => { if (Orientation == Orientation.Vertical) { parent.SplitterDistance = Offset (X, -1); return true; } return false; }); base.AddCommand (Command.LineUp, () => { if (Orientation == Orientation.Horizontal) { parent.SplitterDistance = Offset (Y, -1); return true; } return false; }); base.AddCommand (Command.LineDown, () => { if (Orientation == Orientation.Horizontal) { parent.SplitterDistance = Offset (Y, 1); return true; } return false; }); AddKeyBinding (Key.CursorRight, Command.Right); AddKeyBinding (Key.CursorLeft, Command.Left); AddKeyBinding (Key.CursorUp, Command.LineUp); AddKeyBinding (Key.CursorDown, Command.LineDown); } /// public override bool ProcessKey (KeyEvent kb) { var result = InvokeKeybindings (kb); if (result != null) return (bool)result; return base.ProcessKey (kb); } /// public override bool MouseEvent (MouseEvent mouseEvent) { if (!CanFocus) { return true; } // Start a drag if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) { SetFocus (); Application.EnsuresTopOnFront (); if (mouseEvent.Flags == MouseFlags.Button1Pressed) { dragPosition = new Point (mouseEvent.X, mouseEvent.Y); dragOrignalPos = Orientation == Orientation.Horizontal ? Y : X; Application.GrabMouse (this); } return true; } else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { if (dragPosition.HasValue) { // how far has user dragged from original location? if(Orientation == Orientation.Horizontal) { int dy = mouseEvent.Y - dragPosition.Value.Y; parent.SplitterDistance = Offset(Y , dy); } else { int dx = mouseEvent.X - dragPosition.Value.X; parent.SplitterDistance = Offset(X , dx); } parent.SetNeedsDisplay (); return true; } } if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) { Application.UngrabMouse (); Driver.UncookMouse (); FinalisePosition (); dragPosition = null; } return false; } private Pos Offset (Pos pos, int delta) { var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ? parent.Bounds.Width : parent.Bounds.Height); return posAbsolute + delta; } private void FinalisePosition () { // if before dragging we were a proportional position // then preserve that when the mouse is released so that // resizing continues to work as intended if(dragOrignalPos is PosFactor) { if(Orientation == Orientation.Horizontal) { Y = ToPosFactor (Y, parent.Bounds.Height); } else { X = ToPosFactor (X, parent.Bounds.Width); } } } private Pos ToPosFactor (Pos y, int parentLength) { int position = y.Anchor (parentLength); return new PosFactor (position / (float)parentLength); } } } }