diff --git a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs index ef94e7f..e785b44 100644 --- a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs +++ b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs @@ -47,6 +47,10 @@ public partial class DockGroupNode : DockNode /// /// The zero-based index at which node should be inserted. /// The node to insert. + /// + /// If the node is already a child of this group, it will be moved to the specified index. + /// The index represents the desired final position in the collection. + /// /// Thrown if node is null. /// Thrown if index is less than 0 or greater than Children.Count. /// Thrown if adding the node would create a cycle or if adding self. @@ -68,15 +72,8 @@ public partial class DockGroupNode : DockNode { int oldIndex = _children.IndexOf(node); if (oldIndex == index) return; - - // If we're moving an item forward, the target index will shift after removal - int targetIndex = index; - if (targetIndex > oldIndex) targetIndex--; - - if (oldIndex == targetIndex) return; - _children.RemoveAt(oldIndex); - _children.Insert(targetIndex, node); + _children.Move(oldIndex, index); return; } diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index 7f55ac5..6f794e7 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -430,86 +430,99 @@ public sealed partial class DockLayout : Control return; } - // Remove from source first (if it's the same node, we'll re-add it to the new structure) + // If targetNode is the only child of Root, we wrap it. + if (Root.Children.Count == 1 && ReferenceEquals(Root.Children[0], targetNode)) + { + // Remove from source first + if (!_sourceNode.Items.Remove(_draggedItem)) + { + ClearDragOperationState(); + return; + } + + var newGroup = new DockGroupNode + { + Orientation = isHorizontalSplit ? Orientation.Horizontal : Orientation.Vertical + }; + + var newNode = new DockPanelNode(); + newNode.Items.Add(_draggedItem); + + Root.RemoveChild(targetNode); + Root.AddChild(newGroup); + + if (isAfter) + { + newGroup.AddChild(targetNode); + newGroup.AddChild(newNode); + } + else + { + newGroup.AddChild(newNode); + newGroup.AddChild(targetNode); + } + + CleanupEmptyNodes(_sourceNode); + } + else + { + ClearDragOperationState(); + return; + } + } + else + { + int targetIndex = parentGroup.Children.IndexOf(targetNode); + if (targetIndex < 0) + { + ClearDragOperationState(); + return; + } + + // Remove from source if (!_sourceNode.Items.Remove(_draggedItem)) { ClearDragOperationState(); return; } - var newRoot = new DockGroupNode + if ((isHorizontalSplit && parentGroup.Orientation == Orientation.Horizontal) || + (!isHorizontalSplit && parentGroup.Orientation == Orientation.Vertical)) { - Orientation = isHorizontalSplit ? Orientation.Horizontal : Orientation.Vertical - }; - - var newNode = new DockPanelNode(); - newNode.Items.Add(_draggedItem); - - // If targetNode was the only child of Root, we replace it in Root. - // If targetNode WAS the Root (impossible by type, but let's be safe with the model), we replace Root. - // Actually, Root is always a DockGroupNode. So targetNode must be a child of Root if parent is null? - // No, if parent is null it's either detached or it IS the root. - // But targetNode is DockPanelNode, and Root is DockGroupNode. - - // If targetNode is detached, we can't split it. - // Let's assume it's a child of a group that we don't have a reference to? No, Parent should be set. - // The only case where Parent is null for a DockPanelNode in a valid tree is if it's NOT in the tree. - // Wait, if Root has only one child, that child's parent SHOULD be Root. - - ClearDragOperationState(); - return; // Abort for now, parent should not be null in a valid tree for a Panel. - } - - int targetIndex = parentGroup.Children.IndexOf(targetNode); - if (targetIndex < 0) - { - ClearDragOperationState(); - return; - } - - // Remove from source - if (!_sourceNode.Items.Remove(_draggedItem)) - { - ClearDragOperationState(); - return; - } - - 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); + // Same orientation, just insert next to it + var newNode = new DockPanelNode(); + newNode.Items.Add(_draggedItem); + parentGroup.InsertChild(isAfter ? targetIndex + 1 : targetIndex, newNode); } else { - newGroup.AddChild(newNode); - newGroup.AddChild(targetNode); + // 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); } - parentGroup.InsertChild(targetIndex, newGroup); + CleanupEmptyNodes(_sourceNode); } - - CleanupEmptyNodes(_sourceNode); } ClearDragOperationState(); diff --git a/src/Test/Ghost.UnitTest/DockingModelTest.cs b/src/Test/Ghost.UnitTest/DockingModelTest.cs index bc70ab8..1560a55 100644 --- a/src/Test/Ghost.UnitTest/DockingModelTest.cs +++ b/src/Test/Ghost.UnitTest/DockingModelTest.cs @@ -171,17 +171,23 @@ public class DockingModelTest group.AddChild(child2); group.AddChild(child3); - // Move child1 to end - group.InsertChild(3, child1); + // Move child1 to end (index 2) + group.InsertChild(2, child1); Assert.AreEqual(child2, group.Children[0]); Assert.AreEqual(child3, group.Children[1]); Assert.AreEqual(child1, group.Children[2]); - // Move child3 to start + // Move child3 to start (index 0) group.InsertChild(0, child3); Assert.AreEqual(child3, group.Children[0]); Assert.AreEqual(child2, group.Children[1]); Assert.AreEqual(child1, group.Children[2]); + + // Move child2 forward by one (index 1 -> 2) + group.InsertChild(2, child2); + Assert.AreEqual(child3, group.Children[0]); + Assert.AreEqual(child1, group.Children[1]); + Assert.AreEqual(child2, group.Children[2]); } [TestMethod] @@ -198,4 +204,20 @@ public class DockingModelTest Assert.AreEqual(child1, group.Children[0]); Assert.AreEqual(child2, group.Children[1]); } + + [TestMethod] + public void TestPanel_SetInvalidSelectedItem_ResetsSelection() + { + var panel = new DockPanelNode(); + var item1 = new object(); + var item2 = new object(); + + panel.Items.Add(item1); + panel.SelectedItem = item1; + + panel.SelectedItem = item2; // Not in collection + + Assert.IsNull(panel.SelectedItem); + Assert.AreEqual(-1, panel.SelectedIndex); + } }