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;
|
namespace Ghost.Editor.View.Controls.Docking;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for containers that can hold other dock modules.
|
||||||
|
/// </summary>
|
||||||
public abstract class DockContainer : DockModule
|
public abstract class DockContainer : DockModule
|
||||||
{
|
{
|
||||||
private readonly ObservableCollection<DockModule> _children = new();
|
private readonly ObservableCollection<DockModule> _children = new();
|
||||||
@@ -20,6 +23,11 @@ public abstract class DockContainer : DockModule
|
|||||||
}
|
}
|
||||||
|
|
||||||
public virtual void AddChild(DockModule module)
|
public virtual void AddChild(DockModule module)
|
||||||
|
{
|
||||||
|
InsertChild(_children.Count, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void InsertChild(int index, DockModule module)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(module);
|
ArgumentNullException.ThrowIfNull(module);
|
||||||
|
|
||||||
@@ -42,7 +50,8 @@ public abstract class DockContainer : DockModule
|
|||||||
|
|
||||||
module.Owner?.RemoveChild(module);
|
module.Owner?.RemoveChild(module);
|
||||||
module.Owner = this;
|
module.Owner = this;
|
||||||
_children.Add(module);
|
module.Root = Root;
|
||||||
|
_children.Insert(index, module);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void RemoveChild(DockModule module)
|
public virtual void RemoveChild(DockModule module)
|
||||||
@@ -52,6 +61,16 @@ public abstract class DockContainer : DockModule
|
|||||||
if (_children.Remove(module))
|
if (_children.Remove(module))
|
||||||
{
|
{
|
||||||
module.Owner = null;
|
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)
|
foreach (var child in _children)
|
||||||
{
|
{
|
||||||
child.Owner = null;
|
child.Owner = null;
|
||||||
|
child.Root = null;
|
||||||
}
|
}
|
||||||
_children.Clear();
|
_children.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnRootChanged()
|
||||||
|
{
|
||||||
|
foreach (var child in _children)
|
||||||
|
{
|
||||||
|
child.Root = Root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected virtual void OnChildrenUpdated() { }
|
protected virtual void OnChildrenUpdated() { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ using Microsoft.UI.Xaml.Data;
|
|||||||
|
|
||||||
namespace Ghost.Editor.View.Controls.Docking;
|
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))]
|
[TemplatePart(Name = PART_TAB_VIEW, Type = typeof(TabView))]
|
||||||
public partial class DockGroup : DockContainer
|
public partial class DockGroup : DockContainer
|
||||||
{
|
{
|
||||||
@@ -27,9 +30,31 @@ public partial class DockGroup : DockContainer
|
|||||||
base.AddChild(module);
|
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()
|
protected override void OnApplyTemplate()
|
||||||
{
|
{
|
||||||
base.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;
|
_tabView = GetTemplateChild(PART_TAB_VIEW) as TabView;
|
||||||
|
|
||||||
if (_tabView != null)
|
if (_tabView != null)
|
||||||
@@ -49,7 +74,6 @@ public partial class DockGroup : DockContainer
|
|||||||
if (args.Tab.Tag is DockDocument doc)
|
if (args.Tab.Tag is DockDocument doc)
|
||||||
{
|
{
|
||||||
args.Data.Properties.Add("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"))
|
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));
|
Root?.ShowHighlight(this, e.GetPosition(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,32 @@ using Microsoft.UI.Xaml.Controls;
|
|||||||
|
|
||||||
namespace Ghost.Editor.View.Controls.Docking;
|
namespace Ghost.Editor.View.Controls.Docking;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for all dockable modules in the docking system.
|
||||||
|
/// </summary>
|
||||||
public abstract class DockModule : Control
|
public abstract class DockModule : Control
|
||||||
{
|
{
|
||||||
public DockContainer? Owner { get; internal set; }
|
public DockContainer? Owner { get; internal set; }
|
||||||
|
|
||||||
|
private DockingLayout? _root;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the root docking layout this module belongs to.
|
/// Gets or sets the root docking layout this module belongs to.
|
||||||
/// </summary>
|
/// </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()
|
public void Detach()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,27 @@ public class DockPanel : DockContainer
|
|||||||
UpdateLayoutStructure();
|
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)
|
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
((DockPanel)d).UpdateLayoutStructure();
|
((DockPanel)d).UpdateLayoutStructure();
|
||||||
@@ -68,7 +89,7 @@ public class DockPanel : DockContainer
|
|||||||
if (i < Children.Count - 1)
|
if (i < Children.Count - 1)
|
||||||
{
|
{
|
||||||
_grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
_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.SetColumn(splitter, i * 2 + 1);
|
||||||
_grid.Children.Add(splitter);
|
_grid.Children.Add(splitter);
|
||||||
}
|
}
|
||||||
@@ -86,7 +107,7 @@ public class DockPanel : DockContainer
|
|||||||
if (i < Children.Count - 1)
|
if (i < Children.Count - 1)
|
||||||
{
|
{
|
||||||
_grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
_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.SetRow(splitter, i * 2 + 1);
|
||||||
_grid.Children.Add(splitter);
|
_grid.Children.Add(splitter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ public class DockingLayout : Control
|
|||||||
set => SetValue(RootPanelProperty, value);
|
set => SetValue(RootPanelProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used in Task 5 for drag and drop highlight
|
|
||||||
private Canvas? _overlayCanvas;
|
private Canvas? _overlayCanvas;
|
||||||
// Used in Task 5 for drag and drop highlight
|
|
||||||
private DockRegionHighlight? _highlight;
|
private DockRegionHighlight? _highlight;
|
||||||
|
|
||||||
public DockingLayout()
|
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>
|
/// <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)
|
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)
|
if (targetGroup != null && targetGroup.Root != this)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("targetGroup does not belong to this DockingLayout");
|
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)
|
private static DockGroup? FindFirstDockGroup(DockContainer container)
|
||||||
@@ -125,7 +188,7 @@ public class DockingLayout : Control
|
|||||||
return null;
|
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;
|
if (_highlight == null || _overlayCanvas == null) return;
|
||||||
|
|
||||||
@@ -147,10 +210,10 @@ public class DockingLayout : Control
|
|||||||
}
|
}
|
||||||
|
|
||||||
var transform = targetGroup.TransformToVisual(_overlayCanvas);
|
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);
|
Microsoft.UI.Xaml.Controls.Canvas.SetLeft(_highlight, point.X);
|
||||||
Canvas.SetTop(_highlight, point.Y);
|
Microsoft.UI.Xaml.Controls.Canvas.SetTop(_highlight, point.Y);
|
||||||
_highlight.Width = width;
|
_highlight.Width = width;
|
||||||
_highlight.Height = height;
|
_highlight.Height = height;
|
||||||
}
|
}
|
||||||
@@ -160,45 +223,24 @@ public class DockingLayout : Control
|
|||||||
if (_highlight != null) _highlight.Visibility = Visibility.Collapsed;
|
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();
|
HideHighlight();
|
||||||
var target = CalculateDockTarget(targetGroup, position);
|
var target = CalculateDockTarget(targetGroup, position);
|
||||||
|
|
||||||
|
doc.Detach();
|
||||||
|
|
||||||
if (target == DockTarget.Center)
|
if (target == DockTarget.Center)
|
||||||
{
|
{
|
||||||
targetGroup.AddChild(doc);
|
targetGroup.AddChild(doc);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Split logic: create new DockPanel, move targetGroup and doc into it
|
SplitGroup(targetGroup, doc, target);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DockTarget CalculateDockTarget(DockGroup group, Windows.Foundation.Point position)
|
private DockTarget CalculateDockTarget(DockGroup group, global::Windows.Foundation.Point position)
|
||||||
{
|
{
|
||||||
double w = group.ActualWidth;
|
double w = group.ActualWidth;
|
||||||
double h = group.ActualHeight;
|
double h = group.ActualHeight;
|
||||||
@@ -214,6 +256,7 @@ public class DockingLayout : Control
|
|||||||
|
|
||||||
internal void CreateFloatingWindow(DockDocument doc)
|
internal void CreateFloatingWindow(DockDocument doc)
|
||||||
{
|
{
|
||||||
|
doc.Detach();
|
||||||
// To be implemented in Task 6
|
// To be implemented in Task 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user