fix(docking): improve type safety, document retention, and container cleanup
This commit is contained in:
@@ -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() { }
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user