fix(dock): migrate primary editor to DockLayout and add persistent sizing
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Controls.Internal.Docking;
|
namespace Ghost.Editor.Core.Controls.Internal.Docking;
|
||||||
@@ -22,15 +24,48 @@ public partial class DockGroupNode : DockNode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyObservableCollection<DockNode> Children { get; }
|
public ReadOnlyObservableCollection<DockNode> Children { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of sizes for the children.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<Microsoft.UI.Xaml.GridLength> Sizes { get; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DockGroupNode"/> class.
|
/// Initializes a new instance of the <see cref="DockGroupNode"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DockGroupNode()
|
public DockGroupNode()
|
||||||
{
|
{
|
||||||
Children = new ReadOnlyObservableCollection<DockNode>(_children);
|
Children = new ReadOnlyObservableCollection<DockNode>(_children);
|
||||||
|
_children.CollectionChanged += OnChildrenChanged;
|
||||||
Orientation = Orientation.Horizontal;
|
Orientation = Orientation.Horizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// Maintain Sizes collection to match Children
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < e.NewItems.Count; i++)
|
||||||
|
{
|
||||||
|
Sizes.Insert(e.NewStartingIndex + i, new Microsoft.UI.Xaml.GridLength(1, Microsoft.UI.Xaml.GridUnitType.Star));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < e.OldItems.Count; i++)
|
||||||
|
{
|
||||||
|
Sizes.RemoveAt(e.OldStartingIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||||
|
{
|
||||||
|
Sizes.Clear();
|
||||||
|
foreach (var _ in _children)
|
||||||
|
{
|
||||||
|
Sizes.Add(new Microsoft.UI.Xaml.GridLength(1, Microsoft.UI.Xaml.GridUnitType.Star));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a child node to this group, enforcing tree invariants.
|
/// Adds a child node to this group, enforcing tree invariants.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -208,18 +208,20 @@ public sealed partial class DockLayout : Control
|
|||||||
|
|
||||||
if (isHorizontal)
|
if (isHorizontal)
|
||||||
{
|
{
|
||||||
|
var width = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star);
|
||||||
grid.ColumnDefinitions.Add(new ColumnDefinition
|
grid.ColumnDefinitions.Add(new ColumnDefinition
|
||||||
{
|
{
|
||||||
Width = new GridLength(1, GridUnitType.Star),
|
Width = width,
|
||||||
MinWidth = MIN_PANE_SIZE
|
MinWidth = MIN_PANE_SIZE
|
||||||
});
|
});
|
||||||
Grid.SetColumn((FrameworkElement)childUI, i * 2);
|
Grid.SetColumn((FrameworkElement)childUI, i * 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var height = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star);
|
||||||
grid.RowDefinitions.Add(new RowDefinition
|
grid.RowDefinitions.Add(new RowDefinition
|
||||||
{
|
{
|
||||||
Height = new GridLength(1, GridUnitType.Star),
|
Height = height,
|
||||||
MinHeight = MIN_PANE_SIZE
|
MinHeight = MIN_PANE_SIZE
|
||||||
});
|
});
|
||||||
Grid.SetRow((FrameworkElement)childUI, i * 2);
|
Grid.SetRow((FrameworkElement)childUI, i * 2);
|
||||||
@@ -297,6 +299,7 @@ public sealed partial class DockLayout : Control
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DockPosition _currentDropPosition = DockPosition.None;
|
private DockPosition _currentDropPosition = DockPosition.None;
|
||||||
|
private FrameworkElement? _lastTargetElement;
|
||||||
|
|
||||||
private record DockDragPayload(object Item, DockPanelNode SourceNode);
|
private record DockDragPayload(object Item, DockPanelNode SourceNode);
|
||||||
|
|
||||||
@@ -322,9 +325,10 @@ public sealed partial class DockLayout : Control
|
|||||||
var position = e.GetPosition(targetElement);
|
var position = e.GetPosition(targetElement);
|
||||||
var newPosition = DockMath.CalculateDockPosition(targetElement.ActualWidth, targetElement.ActualHeight, position.X, position.Y, DROP_EDGE_THRESHOLD);
|
var newPosition = DockMath.CalculateDockPosition(targetElement.ActualWidth, targetElement.ActualHeight, position.X, position.Y, DROP_EDGE_THRESHOLD);
|
||||||
|
|
||||||
if (newPosition != _currentDropPosition)
|
if (newPosition != _currentDropPosition || targetElement != _lastTargetElement)
|
||||||
{
|
{
|
||||||
_currentDropPosition = newPosition;
|
_currentDropPosition = newPosition;
|
||||||
|
_lastTargetElement = targetElement;
|
||||||
UpdateDropOverlay(targetElement, _currentDropPosition);
|
UpdateDropOverlay(targetElement, _currentDropPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,6 +336,7 @@ public sealed partial class DockLayout : Control
|
|||||||
|
|
||||||
private void TabView_DragLeave(object sender, DragEventArgs e)
|
private void TabView_DragLeave(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
|
_lastTargetElement = null;
|
||||||
ClearOverlayState();
|
ClearOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ internal sealed partial class DockWindow : WindowEx
|
|||||||
|
|
||||||
PART_DockLayout.Root = rootGroup;
|
PART_DockLayout.Root = rootGroup;
|
||||||
PART_DockLayout.TabTornOff += OnTabTornOff;
|
PART_DockLayout.TabTornOff += OnTabTornOff;
|
||||||
|
|
||||||
|
((System.Collections.Specialized.INotifyCollectionChanged)rootGroup.Children).CollectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (rootGroup.Children.Count == 0)
|
||||||
|
{
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTabTornOff(object? sender, TabTornOffEventArgs e)
|
private void OnTabTornOff(object? sender, TabTornOffEventArgs e)
|
||||||
|
|||||||
@@ -66,85 +66,7 @@
|
|||||||
|
|
||||||
<!-- Editor -->
|
<!-- Editor -->
|
||||||
<Grid Grid.Row="2">
|
<Grid Grid.Row="2">
|
||||||
<Grid.RowDefinitions>
|
<controls:DockLayout x:Name="PART_DockLayout" />
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid Grid.Row="0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<ghost:NavigationTabView
|
|
||||||
x:Name="PART_HierarchyTabView"
|
|
||||||
Grid.Column="0"
|
|
||||||
Width="350"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
TabDroppedOutside="OnTabDroppedOutside">
|
|
||||||
<ghost:NavigationTabView.TabItems>
|
|
||||||
<TabViewItem Header="Hierarchy">
|
|
||||||
<TabViewItem.IconSource>
|
|
||||||
<FontIconSource Glyph="" />
|
|
||||||
</TabViewItem.IconSource>
|
|
||||||
<controls:Hierarchy />
|
|
||||||
</TabViewItem>
|
|
||||||
</ghost:NavigationTabView.TabItems>
|
|
||||||
</ghost:NavigationTabView>
|
|
||||||
|
|
||||||
<ghost:NavigationTabView
|
|
||||||
x:Name="PART_SceneTabView"
|
|
||||||
Grid.Column="1"
|
|
||||||
TabDroppedOutside="OnTabDroppedOutside">
|
|
||||||
<ghost:NavigationTabView.TabItems>
|
|
||||||
<ee:ScenePage Header="Scene">
|
|
||||||
<ee:ScenePage.IconSource>
|
|
||||||
<FontIconSource Glyph="" />
|
|
||||||
</ee:ScenePage.IconSource>
|
|
||||||
</ee:ScenePage>
|
|
||||||
</ghost:NavigationTabView.TabItems>
|
|
||||||
</ghost:NavigationTabView>
|
|
||||||
|
|
||||||
<ghost:NavigationTabView
|
|
||||||
x:Name="PART_InspectorTabView"
|
|
||||||
Grid.Column="2"
|
|
||||||
Width="350"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
TabDroppedOutside="OnTabDroppedOutside">
|
|
||||||
<ghost:NavigationTabView.TabItems>
|
|
||||||
<ee:InspectorPage Header="Inspector">
|
|
||||||
<ee:InspectorPage.IconSource>
|
|
||||||
<FontIconSource Glyph="" />
|
|
||||||
</ee:InspectorPage.IconSource>
|
|
||||||
</ee:InspectorPage>
|
|
||||||
</ghost:NavigationTabView.TabItems>
|
|
||||||
</ghost:NavigationTabView>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ghost:NavigationTabView
|
|
||||||
x:Name="PART_BottomTabView"
|
|
||||||
Grid.Row="1"
|
|
||||||
Height="350"
|
|
||||||
TabDroppedOutside="OnTabDroppedOutside">
|
|
||||||
<ghost:NavigationTabView.TabItems>
|
|
||||||
<TabViewItem Header="Project">
|
|
||||||
<TabViewItem.IconSource>
|
|
||||||
<FontIconSource Glyph="" />
|
|
||||||
</TabViewItem.IconSource>
|
|
||||||
<controls:ProjectBrowser />
|
|
||||||
</TabViewItem>
|
|
||||||
<TabViewItem Header="Console">
|
|
||||||
<TabViewItem.IconSource>
|
|
||||||
<FontIconSource Glyph="" />
|
|
||||||
</TabViewItem.IconSource>
|
|
||||||
<ee:ConsolePage />
|
|
||||||
</TabViewItem>
|
|
||||||
</ghost:NavigationTabView.TabItems>
|
|
||||||
</ghost:NavigationTabView>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
|
|||||||
@@ -38,31 +38,41 @@ internal sealed partial class EngineEditorWindow : WindowEx
|
|||||||
|
|
||||||
SetTitleBar(PART_TitleBar);
|
SetTitleBar(PART_TitleBar);
|
||||||
this.CenterOnScreen();
|
this.CenterOnScreen();
|
||||||
|
|
||||||
|
InitializeDockLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeDockLayout()
|
||||||
|
{
|
||||||
|
var root = new DockGroupNode { Orientation = Orientation.Horizontal };
|
||||||
|
|
||||||
|
var leftGroup = new DockGroupNode { Orientation = Orientation.Vertical };
|
||||||
|
var hierarchyPanel = new DockPanelNode();
|
||||||
|
hierarchyPanel.Items.Add(new TabViewItem { Header = "Hierarchy", Content = new Hierarchy() });
|
||||||
|
leftGroup.AddChild(hierarchyPanel);
|
||||||
|
|
||||||
|
var centerGroup = new DockGroupNode { Orientation = Orientation.Vertical };
|
||||||
|
var scenePanel = new DockPanelNode();
|
||||||
|
scenePanel.Items.Add(new ScenePage { Header = "Scene" });
|
||||||
|
centerGroup.AddChild(scenePanel);
|
||||||
|
|
||||||
|
var rightGroup = new DockGroupNode { Orientation = Orientation.Vertical };
|
||||||
|
var inspectorPanel = new DockPanelNode();
|
||||||
|
inspectorPanel.Items.Add(new InspectorPage { Header = "Inspector" });
|
||||||
|
rightGroup.AddChild(inspectorPanel);
|
||||||
|
|
||||||
|
root.AddChild(leftGroup);
|
||||||
|
root.AddChild(centerGroup);
|
||||||
|
root.AddChild(rightGroup);
|
||||||
|
|
||||||
|
PART_DockLayout.Root = root;
|
||||||
|
PART_DockLayout.TabTornOff += (s, e) => App.CreateAndShowDockWindow(e.TabContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args)
|
private void OnTabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args)
|
||||||
{
|
{
|
||||||
// For static tabs in EngineEditorWindow, we remove the item from TabItems
|
// This is now handled by DockLayout.TabTornOff for dynamic tabs.
|
||||||
if (sender.TabItems is System.Collections.IList list)
|
// If we still have static tabs, we'd handle them here.
|
||||||
{
|
|
||||||
if (list.Contains(args.Item))
|
|
||||||
{
|
|
||||||
var result = TabTearOffService.TryTearOffTab(list, args.Item, (tab) =>
|
|
||||||
{
|
|
||||||
App.CreateAndShowDockWindow(tab);
|
|
||||||
}, sender);
|
|
||||||
|
|
||||||
if (!result.IsSuccess)
|
|
||||||
{
|
|
||||||
Logger.LogWarning($"Tab tear-off failed: {result.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string itemInfo = args.Item is FrameworkElement fe ? fe.GetType().Name : args.Item?.ToString() ?? "unknown";
|
|
||||||
Logger.LogWarning($"OnTabDroppedOutside: Item '{itemInfo}' not found in source TabView (Items count: {list.Count}).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||||
|
|||||||
Reference in New Issue
Block a user