fix(dock): enforce tree invariants, sync selection, and fix AOT warnings
This commit is contained in:
@@ -7,12 +7,45 @@ namespace Ghost.Editor.Core.Controls.Internal.Docking;
|
|||||||
public partial class DockGroupNode : DockNode
|
public partial class DockGroupNode : DockNode
|
||||||
{
|
{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private Orientation _orientation = Orientation.Horizontal;
|
public partial Orientation Orientation { get; set; }
|
||||||
|
|
||||||
|
public DockGroupNode()
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableCollection<DockNode> Children { get; } = new();
|
public ObservableCollection<DockNode> Children { get; } = new();
|
||||||
|
|
||||||
public void AddChild(DockNode node)
|
public void AddChild(DockNode node)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(node);
|
||||||
|
|
||||||
|
if (node == this)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot add a node to itself.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Children.Contains(node))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cycles
|
||||||
|
var current = this.Parent;
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (current == node)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot add an ancestor as a child (cycle detected).");
|
||||||
|
}
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.Parent != null && node.Parent != this)
|
||||||
|
{
|
||||||
|
node.Parent.RemoveChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
node.Parent = this;
|
node.Parent = this;
|
||||||
Children.Add(node);
|
Children.Add(node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ namespace Ghost.Editor.Core.Controls.Internal.Docking;
|
|||||||
public abstract partial class DockNode : ObservableObject
|
public abstract partial class DockNode : ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private DockGroupNode? _parent;
|
public partial DockGroupNode? Parent { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,64 @@ public partial class DockPanelNode : DockNode
|
|||||||
public ObservableCollection<object> Items { get; } = new();
|
public ObservableCollection<object> Items { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private int _selectedIndex = -1;
|
public partial int SelectedIndex { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private object? _selectedItem;
|
public partial object? SelectedItem { get; set; }
|
||||||
|
|
||||||
|
public DockPanelNode()
|
||||||
|
{
|
||||||
|
SelectedIndex = -1;
|
||||||
|
Items.CollectionChanged += OnItemsCollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Items.Count == 0)
|
||||||
|
{
|
||||||
|
SelectedIndex = -1;
|
||||||
|
SelectedItem = null;
|
||||||
|
}
|
||||||
|
else if (SelectedIndex >= Items.Count)
|
||||||
|
{
|
||||||
|
SelectedIndex = Items.Count - 1;
|
||||||
|
}
|
||||||
|
else if (SelectedIndex == -1 && Items.Count > 0)
|
||||||
|
{
|
||||||
|
SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedIndexChanged(int value)
|
||||||
|
{
|
||||||
|
if (value >= 0 && value < Items.Count)
|
||||||
|
{
|
||||||
|
SelectedItem = Items[value];
|
||||||
|
}
|
||||||
|
else if (value == -1)
|
||||||
|
{
|
||||||
|
SelectedItem = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Clamp or reset if out of bounds
|
||||||
|
SelectedIndex = Items.Count > 0 ? 0 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedItemChanged(object? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
SelectedIndex = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int index = Items.IndexOf(value);
|
||||||
|
if (index != -1)
|
||||||
|
{
|
||||||
|
SelectedIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/Test/Ghost.UnitTest/DockingModelTest.cs
Normal file
123
src/Test/Ghost.UnitTest/DockingModelTest.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using Ghost.Editor.Core.Controls.Internal.Docking;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace Ghost.UnitTest;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class DockingModelTest
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddChild_SetsParent()
|
||||||
|
{
|
||||||
|
var group = new DockGroupNode();
|
||||||
|
var child = new DockPanelNode();
|
||||||
|
|
||||||
|
group.AddChild(child);
|
||||||
|
|
||||||
|
Assert.AreEqual(group, child.Parent);
|
||||||
|
Assert.IsTrue(group.Children.Contains(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddChild_RemovesFromOldParent()
|
||||||
|
{
|
||||||
|
var group1 = new DockGroupNode();
|
||||||
|
var group2 = new DockGroupNode();
|
||||||
|
var child = new DockPanelNode();
|
||||||
|
|
||||||
|
group1.AddChild(child);
|
||||||
|
group2.AddChild(child);
|
||||||
|
|
||||||
|
Assert.AreEqual(group2, child.Parent);
|
||||||
|
Assert.IsFalse(group1.Children.Contains(child));
|
||||||
|
Assert.IsTrue(group2.Children.Contains(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestAddChild_PreventsCycle()
|
||||||
|
{
|
||||||
|
var group1 = new DockGroupNode();
|
||||||
|
var group2 = new DockGroupNode();
|
||||||
|
|
||||||
|
group1.AddChild(group2);
|
||||||
|
bool thrown = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
group2.AddChild(group1);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
thrown = true;
|
||||||
|
}
|
||||||
|
Assert.IsTrue(thrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRemoveChild_ClearsParent()
|
||||||
|
{
|
||||||
|
var group = new DockGroupNode();
|
||||||
|
var child = new DockPanelNode();
|
||||||
|
|
||||||
|
group.AddChild(child);
|
||||||
|
group.RemoveChild(child);
|
||||||
|
|
||||||
|
Assert.IsNull(child.Parent);
|
||||||
|
Assert.IsFalse(group.Children.Contains(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestPanel_SelectionSync_IndexToItem()
|
||||||
|
{
|
||||||
|
var panel = new DockPanelNode();
|
||||||
|
var item1 = new object();
|
||||||
|
var item2 = new object();
|
||||||
|
|
||||||
|
panel.Items.Add(item1);
|
||||||
|
panel.Items.Add(item2);
|
||||||
|
|
||||||
|
panel.SelectedIndex = 1;
|
||||||
|
Assert.AreEqual(item2, panel.SelectedItem);
|
||||||
|
|
||||||
|
panel.SelectedIndex = 0;
|
||||||
|
Assert.AreEqual(item1, panel.SelectedItem);
|
||||||
|
|
||||||
|
panel.SelectedIndex = -1;
|
||||||
|
Assert.IsNull(panel.SelectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestPanel_SelectionSync_ItemToIndex()
|
||||||
|
{
|
||||||
|
var panel = new DockPanelNode();
|
||||||
|
var item1 = new object();
|
||||||
|
var item2 = new object();
|
||||||
|
|
||||||
|
panel.Items.Add(item1);
|
||||||
|
panel.Items.Add(item2);
|
||||||
|
|
||||||
|
panel.SelectedItem = item2;
|
||||||
|
Assert.AreEqual(1, panel.SelectedIndex);
|
||||||
|
|
||||||
|
panel.SelectedItem = item1;
|
||||||
|
Assert.AreEqual(0, panel.SelectedIndex);
|
||||||
|
|
||||||
|
panel.SelectedItem = null;
|
||||||
|
Assert.AreEqual(-1, panel.SelectedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestPanel_CollectionChanged_UpdatesSelection()
|
||||||
|
{
|
||||||
|
var panel = new DockPanelNode();
|
||||||
|
var item1 = new object();
|
||||||
|
|
||||||
|
panel.Items.Add(item1);
|
||||||
|
Assert.AreEqual(0, panel.SelectedIndex);
|
||||||
|
Assert.AreEqual(item1, panel.SelectedItem);
|
||||||
|
|
||||||
|
panel.Items.Remove(item1);
|
||||||
|
Assert.AreEqual(-1, panel.SelectedIndex);
|
||||||
|
Assert.IsNull(panel.SelectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user