feat(dock): implement tree mutation on drop and empty node cleanup

This commit is contained in:
2026-03-28 14:34:35 +09:00
parent c2cfd18273
commit 1cd0971b4d
2 changed files with 135 additions and 2 deletions

View File

@@ -38,9 +38,27 @@ public partial class DockGroupNode : DockNode
/// <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>
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);
if (index < 0 || index > _children.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
if (node == this)
{
throw new InvalidOperationException("Cannot add a node to itself.");
@@ -48,7 +66,10 @@ public partial class DockGroupNode : DockNode
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
@@ -68,7 +89,7 @@ public partial class DockGroupNode : DockNode
}
node.Parent = this;
_children.Add(node);
_children.Insert(index, node);
}
/// <summary>

View File

@@ -380,8 +380,120 @@ public sealed partial class DockLayout : Control
}
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();
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()