using System.Windows.Controls;
namespace CopaData.FileInspector.GUI.TilingPanels.Models;
///
/// This class represents a branch in the tree structure, having two children,
/// which can be either another - in the case of the tree branching onwards -
/// or a , if we're at the end of a branch (a so-called leaf node).
/// A panel contains information about how its children and
/// are separated by exposing an and split ratio.
///
///
/// The entire attaching and detaching logic is handled from static methods in this class for brevity.
/// Those methods are and .
///
public class TilingPanel : TilingNode
{
///
private Orientation _splitOrientation = Orientation.Horizontal;
///
/// The orientation of the split, so either vertical or horizontal.
///
public Orientation SplitOrientation
{
get => _splitOrientation;
set => SetField(ref _splitOrientation, value);
}
///
private double _splitRatio = 0.5;
///
/// The width or height ratio of the split, with 0.5 being the exact middle.
///
public double SplitRatio
{
get => _splitRatio;
set => SetField(ref _splitRatio, Math.Min(1, Math.Max(0, value)));
}
///
private double _cutoffLength = 256;
///
/// TODO: Move this into the view/control, it really got nothing in common with the model i think.
/// The width or height of alpha in pixels, will be initialized with 50% of the panel's available width.
///
public double CutoffLength
{
get => _cutoffLength;
set => SetField(ref _cutoffLength, Math.Min(1, Math.Max(0, value)));
}
///
private TilingNode _alpha;
///
/// 'Alpha is always set' is an enforced paradigm, so it will always point to an existing node.
///
public TilingNode Alpha
{
get => _alpha;
private set => SetField(ref _alpha, value);
}
///
private TilingNode? _beta;
///
/// The beta node may be null, for example if a panel is not yet split.
///
public TilingNode? Beta
{
get => _beta;
private set => SetField(ref _beta, value);
}
///
/// Whether this panel is split in two, so whether beta is not null.
///
public bool IsSplit => Beta != null;
public TilingPanel(TilingNode alpha, TilingNode? beta = null)
: base()
{
_alpha = alpha;
_beta = beta;
}
///
/// Traverses down the entire tree from until
/// is encountered, returning its direct parent panel.
///
///
/// This may not be the quickest approach, but we're looking at UI tiling panels with perhaps 20 children at most.
///
public static TilingPanel? GetParentPanel(TilingPanel rootPanel, TilingNode node)
{
if (rootPanel.Alpha == node || rootPanel.Beta == node)
{
return rootPanel;
}
TilingPanel? parent = null;
if (rootPanel.Alpha is TilingPanel alphaPanel)
{
parent = GetParentPanel(alphaPanel, node);
}
if (parent == null && rootPanel.Beta is TilingPanel betaPanel)
{
parent = GetParentPanel(betaPanel, node);
}
return parent;
}
///
///
/// Attaches to 's parent as a new .
/// The parent will be looked up by traversing the tree from .
/// The following ordered rules apply when attaching:
///
///
/// 1. If 's parent has an open beta slot, will become the beta node, wrappped in a new host.
/// 2. If one of the parent's child nodes is an , will be inserted
/// 3. If none of the above apply, the parent's beta node will be split into a new tiling panel.
///
///
///
/// All of these actions will automatically split the parent panel.
/// If had a parent assigned previously, that parent will be detached from the branch first.
/// If more frames are present in 's parent, only that frame will be removed from it,
/// and a new is created, containing only that frame.
/// Note that all passed elements have to be children to the same tree.
/// In order to attach a to a foreign tree, it first and insert it to the foreign host.
///
/// The root to start branch traversal from.
/// The host to be split.
///
/// The frame to be attached inside a new host to 's parent.
/// If set to null, an will be created in its place.
///
/// The direction at which to drop relative to .
/// The that now contains and .
public static TilingPanel Attach(TilingPanel rootPanel,
TilingHost targetHost,
TilingFrame? newFrame,
TilingDirection direction)
{
// This method looks way more complicated than it actually is,
// all I wanted to achieve is one centralized place where branching and child assignments happen,
// as tree structures are notoriously hard to navigate and handle when putting references everywhere.
// It really boils down to a simple three-step process:
// 1. Get or create correct host to attach,
// 2. Find out where to place the new host (Alpha or Beta, depending on tiling direction),
// 3. Create branch to be attached, or attach to free slot of parent.
// Todo: This note is in place for the reviewer to have a bit more context. Remove when done.
TilingPanel? parent = GetParentPanel(rootPanel, targetHost);
if (parent == null)
{
throw new NullReferenceException($"Detached target: Could not find a parent for {targetHost} in any of {rootPanel}'s child nodes.");
}
TilingHost? newHost;
if (newFrame == null)
{
// Substitute for the split.
newHost = new EmptyHost();
}
else
{
if (newFrame.Parent?.Frames.Count < 2)
{
// Detach newFrame's host from its parent.
Detach(rootPanel, newFrame.Parent);
}
newHost = new TilingHost([ newFrame ]);
}
// I tried formulating an explanation for this step but I simply can't.
// "Left or Top go alpha, Bottom or Right go beta."
bool newHostIsAlpha = direction is TilingDirection.Left or TilingDirection.Top;
// Check whether there's a free beta slot in the parent to use
if (parent.Beta == null || parent.Beta is EmptyHost emptyHost)
{
if (newHostIsAlpha)
{
// Open beta slot but newHost wants to be in alpha slot, simply swap and attach.
parent.Beta = parent.Alpha;
parent.Alpha = newHost;
}
else
{
// Open beta slot means we just put newHost there, no branching needed.
parent.Beta = newHost;
}
return parent;
}
// Create a new branch to hold both target- and newHost.
TilingPanel branch;
if (direction is TilingDirection.Left or TilingDirection.Top)
{
branch = new TilingPanel(newHost, targetHost);
}
else
{
branch = new TilingPanel(targetHost, newHost);
}
// Assign new branch to correct chíld node.
if (parent.Alpha == targetHost)
{
parent.Alpha = branch;
}
else if (parent.Beta == targetHost)
{
parent.Beta = branch;
}
return branch;
}
///
/// Detaches from its parent node, which will be looked up by traversing the .
/// If the child happened to be the alpha value, the beta value will be moved into the alpha slot to ensure alpha always having a value.
/// If the child's parent node ends up having no children left, which would violate the 'alpha is always set' rule,
/// that node itself will also be detached in order to prevent trailing zombie nodes.
///
public static void Detach(TilingPanel rootPanel, TilingNode child)
{
TilingPanel? parent = GetParentPanel(rootPanel, child);
if (parent == null)
{
return; // Already an orphan.
}
if (parent.Alpha == child)
{
if (parent.Beta != null)
{
// Move beta over to alpha if beta has a value.
parent.Alpha = parent.Beta;
parent.Beta = null;
}
else
{
// Both branches have detached, which means we'll detach the parent itself
// in order to prevent empty, trailing panels with no children.
Detach(rootPanel, child);
}
}
else
{
// Child was the beta node, so we simply null it.
parent.Beta = null;
}
}
}