using System;
using Terminal.Gui.Graphs;
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 Pos panel1MinSize = 0;
private Pos panel2MinSize = 0;
///
/// Creates a new instance of the SplitContainer class.
///
public SplitContainer ()
{
splitterLine = new SplitContainerLineView (this);
this.Add (Panel1);
this.Add (splitterLine);
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 Pos 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 Pos Panel2MinSize {
get {
return panel2MinSize;
}
set {
panel2MinSize = value;
Setup ();
}
}
///
/// 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);
}
public override void Redraw (Rect bounds)
{
Driver.SetAttribute (ColorScheme.Normal);
Clear ();
base.Redraw (bounds);
}
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);
}
splitterDistance = BoundByMinimumSizes (splitterDistance);
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 Dim.DimFunc (() =>
splitterDistance.Anchor (Bounds.Height));
this.Panel2.Y = Pos.Bottom (splitterLine);
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 Dim.DimFunc (() =>
splitterDistance.Anchor (Bounds.Width));
this.Panel2.X = Pos.Right (splitterLine);
this.Panel2.Y = 0;
this.Panel2.Width = Dim.Fill ();
this.Panel2.Height = Dim.Fill ();
break;
default: throw new ArgumentOutOfRangeException (nameof (orientation));
};
this.LayoutSubviews ();
}
///
/// Considers as a candidate for
/// then either returns (if valid) or returns adjusted if invalid with respect to
/// or .
///
///
///
private Pos BoundByMinimumSizes (Pos pos)
{
// if we are not yet initialized then we don't know
// how big we are and therefore cannot sensibly calculate
// how big the panels will be with a given SplitterDistance
if (!IsInitialized) {
return pos;
}
var availableSpace = Orientation == Orientation.Horizontal ? this.Bounds.Height : this.Bounds.Width;
var idealPosition = pos.Anchor (availableSpace);
var panel1MinSizeAbs = panel1MinSize.Anchor (availableSpace);
var panel2MinSizeAbs = panel2MinSize.Anchor (availableSpace);
// bad position because not enough space for panel1
if (idealPosition < panel1MinSizeAbs) {
// TODO: we should preserve Absolute/Percent status here not just force it to absolute
return (Pos)Math.Min (panel1MinSizeAbs, availableSpace);
}
// bad position because not enough space for panel2
if(availableSpace - idealPosition <= panel2MinSizeAbs) {
// TODO: we should preserve Absolute/Percent status here not just force it to absolute
// +1 is to allow space for the splitter
return (Pos)Math.Max (availableSpace - (panel2MinSizeAbs+1), 0);
}
// this splitter position is fine, there is enough space for everyone
return pos;
}
private void SetupForCollapsedPanel ()
{
View toRemove = panel1Collapsed ? Panel1 : Panel2;
View toFullSize = panel1Collapsed ? Panel2 : Panel1;
if (this.Subviews.Contains (splitterLine)) {
this.Remove(splitterLine);
}
if (this.Subviews.Contains (toRemove)) {
this.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;
Point? moveRuneRenderLocation;
// TODO: Make focusable and allow moving with keyboard
public SplitContainerLineView (SplitContainer parent)
{
CanFocus = true;
TabStop = true;
this.parent = parent;
base.AddCommand (Command.Right, () => {
return MoveSplitter (1, 0);
});
base.AddCommand (Command.Left, () => {
return MoveSplitter (-1, 0);
});
base.AddCommand (Command.LineUp, () => {
return MoveSplitter (0, -1);
});
base.AddCommand (Command.LineDown, () => {
return MoveSplitter (0, 1);
});
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)
{
if (!CanFocus || !HasFocus) {
return base.ProcessKey (kb);
}
var result = InvokeKeybindings (kb);
if (result != null)
return (bool)result;
return base.ProcessKey (kb);
}
public override void PositionCursor ()
{
base.PositionCursor ();
Move (this.Bounds.Width / 2, this.Bounds.Height / 2);
}
public override bool OnEnter (View view)
{
Driver.SetCursorVisibility (CursorVisibility.Default);
PositionCursor ();
return base.OnEnter (view);
}
public override void Redraw (Rect bounds)
{
base.Redraw (bounds);
if (CanFocus && HasFocus) {
var location = moveRuneRenderLocation ??
new Point (Bounds.Width / 2, Bounds.Height / 2);
AddRune (location.X, location.Y, Driver.Diamond);
}
}
///
public override bool MouseEvent (MouseEvent mouseEvent)
{
if (!CanFocus) {
return true;
}
if (!dragPosition.HasValue && (mouseEvent.Flags == MouseFlags.Button1Pressed)) {
// Start a Drag
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 (
dragPosition.HasValue &&
(mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
// Continue Drag
// how far has user dragged from original location?
if (Orientation == Orientation.Horizontal) {
int dy = mouseEvent.Y - dragPosition.Value.Y;
parent.SplitterDistance = Offset (Y, dy);
moveRuneRenderLocation = new Point (mouseEvent.X, 0);
} else {
int dx = mouseEvent.X - dragPosition.Value.X;
parent.SplitterDistance = Offset (X, dx);
moveRuneRenderLocation = new Point (0, mouseEvent.Y);
}
parent.SetNeedsDisplay ();
return true;
}
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
// End Drag
Application.UngrabMouse ();
Driver.UncookMouse ();
FinalisePosition (
dragOrignalPos,
Orientation == Orientation.Horizontal ? Y : X);
dragPosition = null;
moveRuneRenderLocation = null;
}
return false;
}
private bool MoveSplitter (int distanceX, int distanceY)
{
if (Orientation == Orientation.Vertical) {
// Cannot move in this direction
if (distanceX == 0) {
return false;
}
var oldX = X;
FinalisePosition (oldX, (Pos)Offset (X, distanceX));
return true;
} else {
// Cannot move in this direction
if (distanceY == 0) {
return false;
}
var oldY = Y;
FinalisePosition (oldY, (Pos)Offset (Y, distanceY));
return true;
}
}
private Pos Offset (Pos pos, int delta)
{
var posAbsolute = pos.Anchor (Orientation == Orientation.Horizontal ?
parent.Bounds.Height : parent.Bounds.Width);
return posAbsolute + delta;
}
///
///
/// Moves to
/// preserving format
/// (absolute / relative) that had.
///
/// This ensures that if splitter location was e.g. 50% before and you move it
/// to absolute 5 then you end up with 10% (assuming a parent had 50 width).
///
///
///
private void FinalisePosition (Pos oldValue, Pos newValue)
{
if (oldValue is Pos.PosFactor) {
if (Orientation == Orientation.Horizontal) {
parent.SplitterDistance = ConvertToPosFactor (newValue, parent.Bounds.Height);
} else {
parent.SplitterDistance = ConvertToPosFactor (newValue, parent.Bounds.Width);
}
} else {
parent.SplitterDistance = newValue;
}
}
///
///
/// Determines the absolute position of and
/// returns a that describes the percentage of that.
///
/// Effectively turning any into a
/// (as if created with )
///
/// The to convert to
/// The Height/Width that lies within
///
private Pos ConvertToPosFactor (Pos p, int parentLength)
{
int position = p.Anchor (parentLength);
return new Pos.PosFactor (position / (float)parentLength);
}
}
}
}