From 55eb240de6d57e4e6b4acfd3fa7273699d4410ad Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 22:29:14 +0900 Subject: [PATCH] fix(docking): improve type safety, document retention, and container cleanup --- .../View/Controls/Docking/DockContainer.cs | 30 ++++- .../View/Controls/Docking/DockGroup.cs | 28 ++++- .../View/Controls/Docking/DockModule.cs | 20 ++- .../View/Controls/Docking/DockPanel.cs | 25 +++- .../View/Controls/Docking/DockingLayout.cs | 119 ++++++++++++------ 5 files changed, 178 insertions(+), 44 deletions(-) diff --git a/src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs b/src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs index 9dabd59..2329ad3 100644 --- a/src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs +++ b/src/Editor/Ghost.Editor/View/Controls/Docking/DockContainer.cs @@ -3,6 +3,9 @@ using System.Collections.ObjectModel; namespace Ghost.Editor.View.Controls.Docking; +/// +/// Base class for containers that can hold other dock modules. +/// public abstract class DockContainer : DockModule { private readonly ObservableCollection _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() { } } diff --git a/src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs b/src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs index 0d24f6d..90fd4da 100644 --- a/src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs +++ b/src/Editor/Ghost.Editor/View/Controls/Docking/DockGroup.cs @@ -4,6 +4,9 @@ using Microsoft.UI.Xaml.Data; namespace Ghost.Editor.View.Controls.Docking; +/// +/// A container that displays its children (documents) as tabs. +/// [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)); } } diff --git a/src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs b/src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs index 38bc044..f0a6631 100644 --- a/src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs +++ b/src/Editor/Ghost.Editor/View/Controls/Docking/DockModule.cs @@ -2,14 +2,32 @@ using Microsoft.UI.Xaml.Controls; namespace Ghost.Editor.View.Controls.Docking; +/// +/// Base class for all dockable modules in the docking system. +/// public abstract class DockModule : Control { public DockContainer? Owner { get; internal set; } + private DockingLayout? _root; + /// /// Gets or sets the root docking layout this module belongs to. /// - 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() { diff --git a/src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs b/src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs index 7420275..dd573a6 100644 --- a/src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs +++ b/src/Editor/Ghost.Editor/View/Controls/Docking/DockPanel.cs @@ -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); } diff --git a/src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs b/src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs index c6115b5..4e1f2e1 100644 --- a/src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/Docking/DockingLayout.cs @@ -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 /// The target group to add the document to. If null, a suitable group will be found or created. 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 } }