feat(dock): implement tree mutation on drop and empty node cleanup
This commit is contained in:
@@ -38,9 +38,27 @@ public partial class DockGroupNode : DockNode
|
|||||||
/// <exception cref="ArgumentNullException">Thrown if node is null.</exception>
|
/// <exception cref="ArgumentNullException">Thrown if node is null.</exception>
|
||||||
/// <exception cref="InvalidOperationException">Thrown if adding the node would create a cycle or if adding self.</exception>
|
/// <exception cref="InvalidOperationException">Thrown if adding the node would create a cycle or if adding self.</exception>
|
||||||
public void AddChild(DockNode node)
|
public void AddChild(DockNode node)
|
||||||
|
{
|
||||||
|
InsertChild(_children.Count, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inserts a child node at the specified index, enforcing tree invariants.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The zero-based index at which node should be inserted.</param>
|
||||||
|
/// <param name="node">The node to insert.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown if node is null.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if index is less than 0 or greater than Children.Count.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if adding the node would create a cycle or if adding self.</exception>
|
||||||
|
public void InsertChild(int index, DockNode node)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(node);
|
ArgumentNullException.ThrowIfNull(node);
|
||||||
|
|
||||||
|
if (index < 0 || index > _children.Count)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
|
||||||
if (node == this)
|
if (node == this)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Cannot add a node to itself.");
|
throw new InvalidOperationException("Cannot add a node to itself.");
|
||||||
@@ -48,7 +66,10 @@ public partial class DockGroupNode : DockNode
|
|||||||
|
|
||||||
if (_children.Contains(node))
|
if (_children.Contains(node))
|
||||||
{
|
{
|
||||||
return;
|
int oldIndex = _children.IndexOf(node);
|
||||||
|
if (oldIndex == index || oldIndex == index - 1) return;
|
||||||
|
_children.RemoveAt(oldIndex);
|
||||||
|
if (index > oldIndex) index--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for cycles
|
// Check for cycles
|
||||||
@@ -68,7 +89,7 @@ public partial class DockGroupNode : DockNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
node.Parent = this;
|
node.Parent = this;
|
||||||
_children.Add(node);
|
_children.Insert(index, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -380,8 +380,120 @@ public sealed partial class DockLayout : Control
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void TabView_Drop(object sender, DragEventArgs e)
|
private void TabView_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
if (_draggedItem == null || _sourceNode == null || !(sender is FrameworkElement targetElement) || !(targetElement.Tag is DockPanelNode targetNode))
|
||||||
{
|
{
|
||||||
ClearDragOperationState();
|
ClearDragOperationState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sourceNode == targetNode && _currentDropPosition == DockPosition.Center)
|
||||||
|
{
|
||||||
|
ClearDragOperationState();
|
||||||
|
return; // Reordering within same tab is handled natively by TabView
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Remove from source
|
||||||
|
_sourceNode.Items.Remove(_draggedItem);
|
||||||
|
var sourceNodeCopy = _sourceNode; // Keep reference for cleanup
|
||||||
|
CleanupEmptyNodes(sourceNodeCopy);
|
||||||
|
|
||||||
|
// 2. Add to target
|
||||||
|
if (_currentDropPosition == DockPosition.Center)
|
||||||
|
{
|
||||||
|
targetNode.Items.Add(_draggedItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Split scenario
|
||||||
|
var parentGroup = targetNode.Parent;
|
||||||
|
if (parentGroup != null)
|
||||||
|
{
|
||||||
|
int targetIndex = parentGroup.Children.IndexOf(targetNode);
|
||||||
|
bool isHorizontalSplit = _currentDropPosition == DockPosition.Left || _currentDropPosition == DockPosition.Right;
|
||||||
|
bool isAfter = _currentDropPosition == DockPosition.Right || _currentDropPosition == DockPosition.Bottom;
|
||||||
|
|
||||||
|
if ((isHorizontalSplit && parentGroup.Orientation == Orientation.Horizontal) ||
|
||||||
|
(!isHorizontalSplit && parentGroup.Orientation == Orientation.Vertical))
|
||||||
|
{
|
||||||
|
// Same orientation, just insert next to it
|
||||||
|
var newNode = new DockPanelNode();
|
||||||
|
newNode.Items.Add(_draggedItem);
|
||||||
|
parentGroup.InsertChild(isAfter ? targetIndex + 1 : targetIndex, newNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Different orientation, need to replace targetNode with a new group
|
||||||
|
parentGroup.RemoveChild(targetNode);
|
||||||
|
|
||||||
|
var newGroup = new DockGroupNode
|
||||||
|
{
|
||||||
|
Orientation = isHorizontalSplit ? Orientation.Horizontal : Orientation.Vertical
|
||||||
|
};
|
||||||
|
|
||||||
|
var newNode = new DockPanelNode();
|
||||||
|
newNode.Items.Add(_draggedItem);
|
||||||
|
|
||||||
|
if (isAfter)
|
||||||
|
{
|
||||||
|
newGroup.AddChild(targetNode);
|
||||||
|
newGroup.AddChild(newNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newGroup.AddChild(newNode);
|
||||||
|
newGroup.AddChild(targetNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
parentGroup.InsertChild(targetIndex, newGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearDragOperationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupEmptyNodes(DockPanelNode panelNode)
|
||||||
|
{
|
||||||
|
if (panelNode.Items.Count > 0) return;
|
||||||
|
|
||||||
|
var parentGroup = panelNode.Parent;
|
||||||
|
if (parentGroup != null)
|
||||||
|
{
|
||||||
|
parentGroup.RemoveChild(panelNode);
|
||||||
|
|
||||||
|
// If group only has 1 child left, collapse it
|
||||||
|
if (parentGroup.Children.Count == 1)
|
||||||
|
{
|
||||||
|
var onlyChild = parentGroup.Children[0];
|
||||||
|
var grandParent = parentGroup.Parent;
|
||||||
|
|
||||||
|
if (grandParent != null)
|
||||||
|
{
|
||||||
|
int groupIndex = grandParent.Children.IndexOf(parentGroup);
|
||||||
|
grandParent.RemoveChild(parentGroup);
|
||||||
|
grandParent.InsertChild(groupIndex, onlyChild);
|
||||||
|
}
|
||||||
|
else if (parentGroup == Root)
|
||||||
|
{
|
||||||
|
// If root is collapsing, the only child becomes the new root
|
||||||
|
parentGroup.RemoveChild(onlyChild);
|
||||||
|
if (onlyChild is DockGroupNode newRootGroup)
|
||||||
|
{
|
||||||
|
Root = newRootGroup;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Wrap panel in a new group to keep Root as a GroupNode
|
||||||
|
var wrapperGroup = new DockGroupNode();
|
||||||
|
wrapperGroup.AddChild(onlyChild);
|
||||||
|
Root = wrapperGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnApplyTemplate()
|
protected override void OnApplyTemplate()
|
||||||
|
|||||||
Reference in New Issue
Block a user