fix(dock): robust selection sync, internal parent setter, and XML docs

This commit is contained in:
2026-03-28 12:42:42 +09:00
parent 4052ffb854
commit 3ea4260405
4 changed files with 153 additions and 15 deletions

View File

@@ -4,19 +4,38 @@ using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Core.Controls.Internal.Docking;
/// <summary>
/// A docking node that contains multiple children and arranges them in a specific orientation.
/// </summary>
public partial class DockGroupNode : DockNode
{
/// <summary>
/// Gets or sets the layout orientation of the children.
/// </summary>
[ObservableProperty]
public partial Orientation Orientation { get; set; }
/// <summary>
/// Gets the collection of child nodes.
/// </summary>
public ObservableCollection<DockNode> Children { get; } = new();
/// <summary>
/// Initializes a new instance of the <see cref="DockGroupNode"/> class.
/// </summary>
public DockGroupNode()
{
Orientation = Orientation.Horizontal;
}
public ObservableCollection<DockNode> Children { get; } = new();
/// <summary>
/// Adds a child node to this group, enforcing tree invariants.
/// </summary>
/// <param name="node">The node to add.</param>
/// <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)
{
ArgumentNullException.ThrowIfNull(node);
@@ -50,6 +69,10 @@ public partial class DockGroupNode : DockNode
Children.Add(node);
}
/// <summary>
/// Removes a child node from this group.
/// </summary>
/// <param name="node">The node to remove.</param>
public void RemoveChild(DockNode node)
{
if (Children.Remove(node))

View File

@@ -2,8 +2,14 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace Ghost.Editor.Core.Controls.Internal.Docking;
/// <summary>
/// Base class for all nodes in the docking layout tree.
/// </summary>
public abstract partial class DockNode : ObservableObject
{
/// <summary>
/// Gets the parent group of this node.
/// </summary>
[ObservableProperty]
public partial DockGroupNode? Parent { get; set; }
public partial DockGroupNode? Parent { get; internal set; }
}

View File

@@ -3,16 +3,31 @@ using CommunityToolkit.Mvvm.ComponentModel;
namespace Ghost.Editor.Core.Controls.Internal.Docking;
/// <summary>
/// A docking node that contains a collection of items (tabs) and manages selection.
/// </summary>
public partial class DockPanelNode : DockNode
{
/// <summary>
/// Gets the collection of items (tabs) in this panel.
/// </summary>
public ObservableCollection<object> Items { get; } = new();
/// <summary>
/// Gets or sets the index of the currently selected item.
/// </summary>
[ObservableProperty]
public partial int SelectedIndex { get; set; }
/// <summary>
/// Gets or sets the currently selected item.
/// </summary>
[ObservableProperty]
public partial object? SelectedItem { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="DockPanelNode"/> class.
/// </summary>
public DockPanelNode()
{
SelectedIndex = -1;
@@ -21,18 +36,38 @@ public partial class DockPanelNode : DockNode
private void OnItemsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Reconcile selection on every collection change
if (Items.Count == 0)
{
SelectedIndex = -1;
SelectedItem = null;
return;
}
else if (SelectedIndex >= Items.Count)
if (SelectedItem != null)
{
SelectedIndex = Items.Count - 1;
int index = Items.IndexOf(SelectedItem);
if (index != -1)
{
// Item still exists, update index if it changed
if (SelectedIndex != index)
{
SelectedIndex = index;
}
return;
}
}
else if (SelectedIndex == -1 && Items.Count > 0)
// SelectedItem is null or no longer in collection
if (SelectedIndex >= 0 && SelectedIndex < Items.Count)
{
SelectedIndex = 0;
// Keep current index if valid, update item
SelectedItem = Items[SelectedIndex];
}
else
{
// Fallback to first item or -1
SelectedIndex = Items.Count > 0 ? 0 : -1;
}
}
@@ -40,11 +75,18 @@ public partial class DockPanelNode : DockNode
{
if (value >= 0 && value < Items.Count)
{
SelectedItem = Items[value];
object newItem = Items[value];
if (SelectedItem != newItem)
{
SelectedItem = newItem;
}
}
else if (value == -1)
{
SelectedItem = null;
if (SelectedItem != null)
{
SelectedItem = null;
}
}
else
{
@@ -57,14 +99,26 @@ public partial class DockPanelNode : DockNode
{
if (value == null)
{
SelectedIndex = -1;
if (SelectedIndex != -1)
{
SelectedIndex = -1;
}
}
else
{
int index = Items.IndexOf(value);
if (index != -1)
{
SelectedIndex = index;
if (SelectedIndex != index)
{
SelectedIndex = index;
}
}
else
{
// Item not in collection - reject selection
SelectedItem = null;
SelectedIndex = -1;
}
}
}