260 lines
9.7 KiB
C#
260 lines
9.7 KiB
C#
using System.Windows.Controls;
|
|
|
|
namespace CopaData.FileInspector.GUI.TilingPanels.Models;
|
|
|
|
/// <summary>
|
|
/// This class represents a branch in the tree structure, having two children,<br/>
|
|
/// which can be either another <see cref="TilingPanel"/> - in the case of the tree branching onwards -<br/>
|
|
/// or a <see cref="TilingHost"/>, if we're at the end of a branch (a so-called leaf node).<br/>
|
|
/// A panel contains information about how its children <see cref="Alpha"/> and <see cref="Beta"/>
|
|
/// are separated by exposing an <see cref="Orientation"/> and split ratio.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The entire attaching and detaching logic is handled from static methods in this class for brevity.<br/>
|
|
/// Those methods are <see cref="Attach(TilingPanel, TilingHost, TilingHost, TilingDirection)"/> and <see cref="Detach(TilingPanel, TilingNode)"/>.
|
|
/// </remarks>
|
|
public class TilingPanel : TilingNode
|
|
{
|
|
/// <inheritdoc cref="SplitOrientation" />
|
|
private Orientation _splitOrientation = Orientation.Horizontal;
|
|
/// <summary>
|
|
/// The orientation of the split, so either vertical or horizontal.
|
|
/// </summary>
|
|
public Orientation SplitOrientation
|
|
{
|
|
get => _splitOrientation;
|
|
set => SetField(ref _splitOrientation, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="SplitRatio" />
|
|
private double _splitRatio = 0.5;
|
|
/// <summary>
|
|
/// The width or height ratio of the split, with 0.5 being the exact middle.
|
|
/// </summary>
|
|
public double SplitRatio
|
|
{
|
|
get => _splitRatio;
|
|
set => SetField(ref _splitRatio, Math.Min(1, Math.Max(0, value)));
|
|
}
|
|
|
|
/// <inheritdoc cref="CutoffLength" />
|
|
private double _cutoffLength = 256;
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public double CutoffLength
|
|
{
|
|
get => _cutoffLength;
|
|
set => SetField(ref _cutoffLength, Math.Min(1, Math.Max(0, value)));
|
|
}
|
|
|
|
/// <inheritdoc cref="Alpha" />
|
|
private TilingNode _alpha;
|
|
/// <summary>
|
|
/// 'Alpha is always set' is an enforced paradigm, so it will always point to an existing node.
|
|
/// </summary>
|
|
public TilingNode Alpha
|
|
{
|
|
get => _alpha;
|
|
private set => SetField(ref _alpha, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="Beta" />
|
|
private TilingNode? _beta;
|
|
/// <summary>
|
|
/// The beta node may be null, for example if a panel is not yet split.
|
|
/// </summary>
|
|
public TilingNode? Beta
|
|
{
|
|
get => _beta;
|
|
private set => SetField(ref _beta, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether this panel is split in two, so whether beta is not null.
|
|
/// </summary>
|
|
public bool IsSplit => Beta != null;
|
|
|
|
public TilingPanel(TilingNode alpha, TilingNode? beta = null)
|
|
: base()
|
|
{
|
|
_alpha = alpha;
|
|
_beta = beta;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Traverses down the entire tree from <paramref name="rootPanel"/> until
|
|
/// <paramref name="node"/> is encountered, returning its direct parent panel.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This may not be the quickest approach, but we're looking at UI tiling panels with perhaps 20 children at most.
|
|
/// </remarks>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// <para>
|
|
/// Attaches <paramref name="newFrame"/> to <paramref name="targetHost"/>'s parent as a new <see cref="TilingHost"/>.<br/>
|
|
/// The parent will be looked up by traversing the tree from <paramref name="rootPanel"/>.<br/>
|
|
/// The following ordered rules apply when attaching:
|
|
/// </para>
|
|
/// <para>
|
|
/// 1. If <paramref name="targetHost"/>'s parent has an open beta slot, <paramref name="newFrame"/> will become the beta node, wrappped in a new host.<br/>
|
|
/// 2. If one of the <paramref name="targetHost"/> parent's child nodes is an <see cref="EmptyHost"/>, <paramref name="newFrame"/> will be inserted<br/>
|
|
/// 3. If none of the above apply, the <paramref name="targetHost"/> parent's beta node will be split into a new tiling panel.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// All of these actions will automatically split the parent panel.<br/>
|
|
/// If <paramref name="newFrame"/> had a parent assigned previously, that parent will be detached from the branch first.<br/>
|
|
/// If more frames are present in <paramref name="newFrame"/>'s parent, only that frame will be removed from it,<br/>
|
|
/// and a new <see cref="TilingHost"/> is created, containing only that frame.<br/>
|
|
/// Note that all passed elements have to be children to the same <see cref="TilingNode"/> tree.
|
|
/// In order to attach a <see cref="TilingFrame"/> to a foreign tree, <see cref="Detach"/> it first and insert it to the foreign host.
|
|
/// </remarks>
|
|
/// <param name="rootPanel">The root to start branch traversal from.</param>
|
|
/// <param name="targetHost">The host to be split.</param>
|
|
/// <param name="newFrame">
|
|
/// The frame to be attached inside a new host to <paramref name="targetHost"/>'s parent.
|
|
/// If set to null, an <see cref="EmptyHost"/> will be created in its place.
|
|
/// </param>
|
|
/// <param name="direction">The direction at which to drop <paramref name="newHost"/> relative to <paramref name="targetHost"/>.</param>
|
|
/// <returns>The <see cref="TilingPanel"/> that now contains <paramref name="newHost"/> and <paramref name="targetHost"/>.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detaches <paramref name="child"/> from its parent node, which will be looked up by traversing the <paramref name="rootPanel"/>.<br/>
|
|
/// 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.<br/>
|
|
/// If the child's parent node ends up having no children left, which would violate the 'alpha is always set' rule,<br/>
|
|
/// that node itself will also be detached in order to prevent trailing zombie nodes.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|