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 02ea774..ed0ebc8 100644 --- a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs +++ b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockGroupNode.cs @@ -7,12 +7,45 @@ namespace Ghost.Editor.Core.Controls.Internal.Docking; public partial class DockGroupNode : DockNode { [ObservableProperty] - private Orientation _orientation = Orientation.Horizontal; + public partial Orientation Orientation { get; set; } + + public DockGroupNode() + { + Orientation = Orientation.Horizontal; + } public ObservableCollection Children { get; } = new(); 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; Children.Add(node); } diff --git a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs index ba0aa3a..795e754 100644 --- a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs +++ b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockNode.cs @@ -5,5 +5,5 @@ namespace Ghost.Editor.Core.Controls.Internal.Docking; public abstract partial class DockNode : ObservableObject { [ObservableProperty] - private DockGroupNode? _parent; + public partial DockGroupNode? Parent { get; set; } } diff --git a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs index 6d3f471..f7906da 100644 --- a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs +++ b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/DockPanelNode.cs @@ -8,8 +8,64 @@ public partial class DockPanelNode : DockNode public ObservableCollection Items { get; } = new(); [ObservableProperty] - private int _selectedIndex = -1; + public partial int SelectedIndex { get; set; } [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; + } + } + } } diff --git a/src/Test/Ghost.UnitTest/DockingModelTest.cs b/src/Test/Ghost.UnitTest/DockingModelTest.cs new file mode 100644 index 0000000..2bd2f04 --- /dev/null +++ b/src/Test/Ghost.UnitTest/DockingModelTest.cs @@ -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); + } +}