fix(dock): refactor mutation logic and fix AddChild regression

This commit is contained in:
2026-03-28 14:54:40 +09:00
parent 231756006e
commit e69e071ce2
3 changed files with 99 additions and 120 deletions

View File

@@ -39,6 +39,7 @@ public partial class DockGroupNode : DockNode
/// <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)
{ {
if (_children.Contains(node)) return;
InsertChild(_children.Count, node); InsertChild(_children.Count, node);
} }

View File

@@ -401,44 +401,42 @@ public sealed partial class DockLayout : Control
return; // Reordering within same tab is handled natively by TabView return; // Reordering within same tab is handled natively by TabView
} }
// 1. Prepare mutation plan // 1. Execute mutation
bool isHorizontalSplit = _currentDropPosition == DockPosition.Left || _currentDropPosition == DockPosition.Right; if (TryApplyDropMutation(targetNode, _sourceNode, _draggedItem, _currentDropPosition))
bool isAfter = _currentDropPosition == DockPosition.Right || _currentDropPosition == DockPosition.Bottom;
// 2. Execute mutation
if (_currentDropPosition == DockPosition.Center)
{ {
if (!_sourceNode.Items.Remove(_draggedItem))
{
ClearDragOperationState();
return;
}
targetNode.Items.Add(_draggedItem);
CleanupEmptyNodes(_sourceNode); CleanupEmptyNodes(_sourceNode);
} }
else
ClearDragOperationState();
}
/// <summary>
/// Applies the tree mutation for a drop operation.
/// </summary>
/// <returns>True if the mutation was applied; otherwise, false.</returns>
private bool TryApplyDropMutation(DockPanelNode targetNode, DockPanelNode sourceNode, object item, DockPosition position)
{ {
if (position == DockPosition.Center)
{
if (!sourceNode.Items.Remove(item)) return false;
targetNode.Items.Add(item);
return true;
}
// Split scenario // Split scenario
bool isHorizontalSplit = position == DockPosition.Left || position == DockPosition.Right;
bool isAfter = position == DockPosition.Right || position == DockPosition.Bottom;
var parentGroup = targetNode.Parent; var parentGroup = targetNode.Parent;
if (parentGroup == null) if (parentGroup == null)
{ {
// Target is root or only child of root. // Target is root or only child of root.
// We must ensure we can wrap it. // In a valid tree, a Panel must have a parent Group (Root is always a Group).
if (Root == null) if (Root == null) return false;
{
ClearDragOperationState();
return;
}
// If targetNode is the only child of Root, we wrap it.
if (Root.Children.Count == 1 && ReferenceEquals(Root.Children[0], targetNode)) if (Root.Children.Count == 1 && ReferenceEquals(Root.Children[0], targetNode))
{ {
// Remove from source first if (!sourceNode.Items.Remove(item)) return false;
if (!_sourceNode.Items.Remove(_draggedItem))
{
ClearDragOperationState();
return;
}
var newGroup = new DockGroupNode var newGroup = new DockGroupNode
{ {
@@ -446,7 +444,7 @@ public sealed partial class DockLayout : Control
}; };
var newNode = new DockPanelNode(); var newNode = new DockPanelNode();
newNode.Items.Add(_draggedItem); newNode.Items.Add(item);
Root.RemoveChild(targetNode); Root.RemoveChild(targetNode);
Root.AddChild(newGroup); Root.AddChild(newGroup);
@@ -461,42 +459,25 @@ public sealed partial class DockLayout : Control
newGroup.AddChild(newNode); newGroup.AddChild(newNode);
newGroup.AddChild(targetNode); newGroup.AddChild(targetNode);
} }
return true;
}
return false;
}
CleanupEmptyNodes(_sourceNode);
}
else
{
ClearDragOperationState();
return;
}
}
else
{
int targetIndex = parentGroup.Children.IndexOf(targetNode); int targetIndex = parentGroup.Children.IndexOf(targetNode);
if (targetIndex < 0) if (targetIndex < 0) return false;
{
ClearDragOperationState();
return;
}
// Remove from source if (!sourceNode.Items.Remove(item)) return false;
if (!_sourceNode.Items.Remove(_draggedItem))
{
ClearDragOperationState();
return;
}
if ((isHorizontalSplit && parentGroup.Orientation == Orientation.Horizontal) || if ((isHorizontalSplit && parentGroup.Orientation == Orientation.Horizontal) ||
(!isHorizontalSplit && parentGroup.Orientation == Orientation.Vertical)) (!isHorizontalSplit && parentGroup.Orientation == Orientation.Vertical))
{ {
// Same orientation, just insert next to it
var newNode = new DockPanelNode(); var newNode = new DockPanelNode();
newNode.Items.Add(_draggedItem); newNode.Items.Add(item);
parentGroup.InsertChild(isAfter ? targetIndex + 1 : targetIndex, newNode); parentGroup.InsertChild(isAfter ? targetIndex + 1 : targetIndex, newNode);
} }
else else
{ {
// Different orientation, need to replace targetNode with a new group
parentGroup.RemoveChild(targetNode); parentGroup.RemoveChild(targetNode);
var newGroup = new DockGroupNode var newGroup = new DockGroupNode
@@ -505,7 +486,7 @@ public sealed partial class DockLayout : Control
}; };
var newNode = new DockPanelNode(); var newNode = new DockPanelNode();
newNode.Items.Add(_draggedItem); newNode.Items.Add(item);
if (isAfter) if (isAfter)
{ {
@@ -521,11 +502,7 @@ public sealed partial class DockLayout : Control
parentGroup.InsertChild(targetIndex, newGroup); parentGroup.InsertChild(targetIndex, newGroup);
} }
CleanupEmptyNodes(_sourceNode); return true;
}
}
ClearDragOperationState();
} }
private void CleanupEmptyNodes(DockNode node) private void CleanupEmptyNodes(DockNode node)

View File

@@ -107,6 +107,7 @@ public class DockingMutationTest
{ {
var onlyChild = group1.Children[0]; var onlyChild = group1.Children[0];
var parent = group1.Parent; var parent = group1.Parent;
Assert.IsNotNull(parent);
int index = parent.Children.IndexOf(group1); int index = parent.Children.IndexOf(group1);
parent.RemoveChild(group1); parent.RemoveChild(group1);