fix(docking): improve type safety, document retention, and container cleanup

This commit is contained in:
2026-03-28 22:29:14 +09:00
parent 45d810e01c
commit 55eb240de6
5 changed files with 178 additions and 44 deletions

View File

@@ -3,6 +3,9 @@ using System.Collections.ObjectModel;
namespace Ghost.Editor.View.Controls.Docking;
/// <summary>
/// Base class for containers that can hold other dock modules.
/// </summary>
public abstract class DockContainer : DockModule
{
private readonly ObservableCollection<DockModule> _children = new();
@@ -20,6 +23,11 @@ public abstract class DockContainer : DockModule
}
public virtual void AddChild(DockModule module)
{
InsertChild(_children.Count, module);
}
public virtual void InsertChild(int index, DockModule module)
{
ArgumentNullException.ThrowIfNull(module);
@@ -42,7 +50,8 @@ public abstract class DockContainer : DockModule
module.Owner?.RemoveChild(module);
module.Owner = this;
_children.Add(module);
module.Root = Root;
_children.Insert(index, module);
}
public virtual void RemoveChild(DockModule module)
@@ -52,6 +61,16 @@ public abstract class DockContainer : DockModule
if (_children.Remove(module))
{
module.Owner = null;
module.Root = null;
CheckCleanup();
}
}
protected virtual void CheckCleanup()
{
if (_children.Count == 0)
{
Owner?.RemoveChild(this);
}
}
@@ -60,9 +79,18 @@ public abstract class DockContainer : DockModule
foreach (var child in _children)
{
child.Owner = null;
child.Root = null;
}
_children.Clear();
}
protected override void OnRootChanged()
{
foreach (var child in _children)
{
child.Root = Root;
}
}
protected virtual void OnChildrenUpdated() { }
}

View File

@@ -4,6 +4,9 @@ using Microsoft.UI.Xaml.Data;
namespace Ghost.Editor.View.Controls.Docking;
/// <summary>
/// A container that displays its children (documents) as tabs.
/// </summary>
[TemplatePart(Name = PART_TAB_VIEW, Type = typeof(TabView))]
public partial class DockGroup : DockContainer
{
@@ -27,9 +30,31 @@ public partial class DockGroup : DockContainer
base.AddChild(module);
}
public override void InsertChild(int index, DockModule module)
{
ArgumentNullException.ThrowIfNull(module);
if (module is not DockDocument)
{
throw new ArgumentException($"{nameof(DockGroup)} only accepts {nameof(DockDocument)} children.", nameof(module));
}
base.InsertChild(index, module);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_tabView != null)
{
_tabView.TabDragStarting -= OnTabDragStarting;
_tabView.TabDroppedOutside -= OnTabDroppedOutside;
_tabView.DragOver -= OnDragOver;
_tabView.Drop -= OnDrop;
_tabView.DragLeave -= OnDragLeave;
}
_tabView = GetTemplateChild(PART_TAB_VIEW) as TabView;
if (_tabView != null)
@@ -49,7 +74,6 @@ public partial class DockGroup : DockContainer
if (args.Tab.Tag is DockDocument doc)
{
args.Data.Properties.Add("DockDocument", doc);
doc.Detach();
}
}
@@ -65,7 +89,7 @@ public partial class DockGroup : DockContainer
{
if (e.DataView.Properties.ContainsKey("DockDocument"))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
Root?.ShowHighlight(this, e.GetPosition(this));
}
}

View File

@@ -2,14 +2,32 @@ using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.View.Controls.Docking;
/// <summary>
/// Base class for all dockable modules in the docking system.
/// </summary>
public abstract class DockModule : Control
{
public DockContainer? Owner { get; internal set; }
private DockingLayout? _root;
/// <summary>
/// Gets or sets the root docking layout this module belongs to.
/// </summary>
public DockingLayout? Root { get; internal set; }
public virtual DockingLayout? Root
{
get => _root;
internal set
{
if (_root != value)
{
_root = value;
OnRootChanged();
}
}
}
protected virtual void OnRootChanged() { }
public void Detach()
{

View File

@@ -41,6 +41,27 @@ public class DockPanel : DockContainer
UpdateLayoutStructure();
}
protected override void CheckCleanup()
{
if (Children.Count == 0)
{
base.CheckCleanup();
}
else if (Children.Count == 1)
{
var child = Children[0];
var owner = Owner;
if (owner != null)
{
int index = owner.Children.IndexOf(this);
owner.RemoveChild(this);
child.Detach();
owner.InsertChild(index, child);
}
}
}
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((DockPanel)d).UpdateLayoutStructure();
@@ -68,7 +89,7 @@ public class DockPanel : DockContainer
if (i < Children.Count - 1)
{
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS };
var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter { ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Columns, Width = SPLITTER_THICKNESS };
Grid.SetColumn(splitter, i * 2 + 1);
_grid.Children.Add(splitter);
}
@@ -86,7 +107,7 @@ public class DockPanel : DockContainer
if (i < Children.Count - 1)
{
_grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var splitter = new GridSplitter { ResizeDirection = GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS };
var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter { ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Rows, Height = SPLITTER_THICKNESS };
Grid.SetRow(splitter, i * 2 + 1);
_grid.Children.Add(splitter);
}

View File

@@ -28,9 +28,7 @@ public class DockingLayout : Control
set => SetValue(RootPanelProperty, value);
}
// Used in Task 5 for drag and drop highlight
private Canvas? _overlayCanvas;
// Used in Task 5 for drag and drop highlight
private DockRegionHighlight? _highlight;
public DockingLayout()
@@ -74,11 +72,6 @@ public class DockingLayout : Control
/// <param name="targetGroup">The target group to add the document to. If null, a suitable group will be found or created.</param>
public void AddDocument(DockDocument document, DockTarget target, DockGroup? targetGroup = null)
{
if (target != DockTarget.Center)
{
throw new NotImplementedException("Target docking will be implemented in Task 5");
}
if (targetGroup != null && targetGroup.Root != this)
{
throw new ArgumentException("targetGroup does not belong to this DockingLayout");
@@ -100,7 +93,77 @@ public class DockingLayout : Control
}
}
targetGroup.AddChild(document);
if (target == DockTarget.Center)
{
targetGroup.AddChild(document);
}
else
{
SplitGroup(targetGroup, document, target);
}
}
private void SplitGroup(DockGroup targetGroup, DockDocument doc, DockTarget target)
{
var parentPanel = targetGroup.Owner as DockPanel;
var newGroup = new DockGroup();
newGroup.AddChild(doc);
var orientation = (target == DockTarget.Left || target == DockTarget.Right) ? Orientation.Horizontal : Orientation.Vertical;
if (parentPanel == null)
{
// targetGroup is the root
var newPanel = new DockPanel { Orientation = orientation };
RootPanel = newPanel;
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
}
else
{
int index = parentPanel.Children.IndexOf(targetGroup);
if (parentPanel.Orientation == orientation)
{
// Same orientation, just insert
if (target == DockTarget.Left || target == DockTarget.Top)
{
parentPanel.InsertChild(index, newGroup);
}
else
{
parentPanel.InsertChild(index + 1, newGroup);
}
}
else
{
// Different orientation, need a new sub-panel
targetGroup.Detach();
var newPanel = new DockPanel { Orientation = orientation };
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
parentPanel.InsertChild(index, newPanel);
}
}
}
private static DockGroup? FindFirstDockGroup(DockContainer container)
@@ -125,7 +188,7 @@ public class DockingLayout : Control
return null;
}
internal void ShowHighlight(DockGroup targetGroup, Windows.Foundation.Point position)
internal void ShowHighlight(DockGroup targetGroup, global::Windows.Foundation.Point position)
{
if (_highlight == null || _overlayCanvas == null) return;
@@ -147,10 +210,10 @@ public class DockingLayout : Control
}
var transform = targetGroup.TransformToVisual(_overlayCanvas);
var point = transform.TransformPoint(new Windows.Foundation.Point(x, y));
var point = transform.TransformPoint(new global::Windows.Foundation.Point(x, y));
Canvas.SetLeft(_highlight, point.X);
Canvas.SetTop(_highlight, point.Y);
Microsoft.UI.Xaml.Controls.Canvas.SetLeft(_highlight, point.X);
Microsoft.UI.Xaml.Controls.Canvas.SetTop(_highlight, point.Y);
_highlight.Width = width;
_highlight.Height = height;
}
@@ -160,45 +223,24 @@ public class DockingLayout : Control
if (_highlight != null) _highlight.Visibility = Visibility.Collapsed;
}
internal void HandleDrop(DockDocument doc, DockGroup targetGroup, Windows.Foundation.Point position)
internal void HandleDrop(DockDocument doc, DockGroup targetGroup, global::Windows.Foundation.Point position)
{
HideHighlight();
var target = CalculateDockTarget(targetGroup, position);
doc.Detach();
if (target == DockTarget.Center)
{
targetGroup.AddChild(doc);
}
else
{
// Split logic: create new DockPanel, move targetGroup and doc into it
var parentPanel = targetGroup.Owner as DockPanel;
if (parentPanel != null)
{
int index = parentPanel.Children.IndexOf(targetGroup);
targetGroup.Detach();
var newPanel = new DockPanel { Orientation = (target == DockTarget.Left || target == DockTarget.Right) ? Orientation.Horizontal : Orientation.Vertical };
var newGroup = new DockGroup();
newGroup.AddChild(doc);
if (target == DockTarget.Left || target == DockTarget.Top)
{
newPanel.AddChild(newGroup);
newPanel.AddChild(targetGroup);
}
else
{
newPanel.AddChild(targetGroup);
newPanel.AddChild(newGroup);
}
parentPanel.Children.Insert(index, newPanel);
}
SplitGroup(targetGroup, doc, target);
}
}
private DockTarget CalculateDockTarget(DockGroup group, Windows.Foundation.Point position)
private DockTarget CalculateDockTarget(DockGroup group, global::Windows.Foundation.Point position)
{
double w = group.ActualWidth;
double h = group.ActualHeight;
@@ -214,6 +256,7 @@ public class DockingLayout : Control
internal void CreateFloatingWindow(DockDocument doc)
{
doc.Detach();
// To be implemented in Task 6
}
}