From 4713bfe7daeebecd4ac05fd07d3a390929da07e5 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 18:11:35 +0900 Subject: [PATCH] fix(dock): migrate primary editor to DockLayout, add persistent sizing, and refactor DockLayout --- .../View/Controls/DockLayout.DragDrop.cs | 186 ++++++++ .../View/Controls/DockLayout.Rendering.cs | 170 ++++++++ .../View/Controls/DockLayout.Subscriptions.cs | 105 +++++ .../Ghost.Editor/View/Controls/DockLayout.cs | 404 ------------------ .../View/Windows/EngineEditorWindow.xaml.cs | 6 - 5 files changed, 461 insertions(+), 410 deletions(-) create mode 100644 src/Editor/Ghost.Editor/View/Controls/DockLayout.DragDrop.cs create mode 100644 src/Editor/Ghost.Editor/View/Controls/DockLayout.Rendering.cs create mode 100644 src/Editor/Ghost.Editor/View/Controls/DockLayout.Subscriptions.cs diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.DragDrop.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.DragDrop.cs new file mode 100644 index 0000000..0e55241 --- /dev/null +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.DragDrop.cs @@ -0,0 +1,186 @@ +using Ghost.Core; +using Ghost.Editor.Core.Controls.Internal.Docking; +using Ghost.Editor.View.Windows; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Ghost.Editor.View.Controls; + +public sealed partial class DockLayout +{ + private DockPosition _currentDropPosition = DockPosition.None; + private FrameworkElement? _lastTargetElement; + + private record DockDragPayload(object Item, DockPanelNode SourceNode); + + public event EventHandler? TabTornOff; + + private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args) + { + if (args.Item != null && sender.Tag is DockPanelNode sourceNode) + { + var payload = new DockDragPayload(args.Item, sourceNode); + args.Data.Properties.Add(DRAG_PROPERTY_DOCK_TAB, payload); // Identify our drag + } + } + + private void TabView_DragOver(object sender, DragEventArgs e) + { + if (e.DataView.Properties.TryGetValue(DRAG_PROPERTY_DOCK_TAB, out var payloadObj) && + payloadObj is DockDragPayload && + sender is FrameworkElement targetElement) + { + e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; + + var position = e.GetPosition(targetElement); + var newPosition = DockMath.CalculateDockPosition(targetElement.ActualWidth, targetElement.ActualHeight, position.X, position.Y, DROP_EDGE_THRESHOLD); + + if (newPosition != _currentDropPosition || targetElement != _lastTargetElement) + { + _currentDropPosition = newPosition; + _lastTargetElement = targetElement; + UpdateDropOverlay(targetElement, _currentDropPosition); + } + } + } + + private void TabView_DragLeave(object sender, DragEventArgs e) + { + _lastTargetElement = null; + ClearOverlayState(); + } + + private void ClearOverlayState() + { + if (_dropTargetOverlay != null) + { + _dropTargetOverlay.Visibility = Visibility.Collapsed; + } + _currentDropPosition = DockPosition.None; + } + + private void ClearDragOperationState() + { + ClearOverlayState(); + } + + private void UpdateDropOverlay(FrameworkElement targetElement, DockPosition position) + { + if (_dropTargetOverlay == null) return; + if (position == DockPosition.None) + { + _dropTargetOverlay.Visibility = Visibility.Collapsed; + return; + } + + var transform = targetElement.TransformToVisual(this); + var bounds = transform.TransformBounds(new global::Windows.Foundation.Rect(0, 0, targetElement.ActualWidth, targetElement.ActualHeight)); + + _dropTargetOverlay.Visibility = Visibility.Visible; + _dropTargetOverlay.Width = double.NaN; + _dropTargetOverlay.Height = double.NaN; + + switch (position) + { + case DockPosition.Center: + _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); + break; + case DockPosition.Left: + _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - (bounds.Left + bounds.Width / 2), ActualHeight - bounds.Bottom); + break; + case DockPosition.Right: + _dropTargetOverlay.Margin = new Thickness(bounds.Left + bounds.Width / 2, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); + break; + case DockPosition.Top: + _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - (bounds.Top + bounds.Height / 2)); + break; + case DockPosition.Bottom: + _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top + bounds.Height / 2, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); + break; + } + } + + private void TabView_Drop(object sender, DragEventArgs e) + { + if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed; + + if (!e.DataView.Properties.TryGetValue(DRAG_PROPERTY_DOCK_TAB, out var payloadObj) || + payloadObj is not DockDragPayload payload || + !(sender is FrameworkElement targetElement) || + !(targetElement.Tag is DockPanelNode targetNode)) + { + ClearDragOperationState(); + return; + } + + if (_currentDropPosition == DockPosition.None) + { + ClearDragOperationState(); + return; + } + + if (payload.SourceNode == targetNode && _currentDropPosition == DockPosition.Center) + { + ClearDragOperationState(); + return; // Reordering within same tab is handled natively by TabView + } + + if (Root == null) + { + ClearDragOperationState(); + return; + } + + // 1. Execute mutation + if (DockMutationEngine.TryApplyDropMutation(Root, targetNode, payload.SourceNode, payload.Item, _currentDropPosition)) + { + DockMutationEngine.CleanupEmptyNodes(payload.SourceNode); + } + + ClearDragOperationState(); + } + + private void TabView_TabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args) + { + try + { + if (sender.Tag is DockPanelNode sourceNode && args.Item != null) + { + // Validate that the item actually belongs to this source node before attempting tear-off + if (sourceNode.Items.Contains(args.Item)) + { + var handler = TabTornOff; + if (handler == null) + { + Logger.LogWarning("Tab dropped outside but no TabTornOff subscribers found."); + return; + } + + var result = TabTearOffService.TryTearOffTab(sourceNode.Items, args.Item, (tab) => + { + // Raise event to let the host handle window creation + handler.Invoke(this, new TabTornOffEventArgs(tab, sourceNode)); + }, sourceNode); + + if (result.IsSuccess) + { + DockMutationEngine.CleanupEmptyNodes(sourceNode); + } + else + { + 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($"TabDroppedOutside: Item '{itemInfo}' not found in source node (Items count: {sourceNode.Items.Count})."); + } + } + } + finally + { + ClearDragOperationState(); + } + } +} diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.Rendering.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.Rendering.cs new file mode 100644 index 0000000..b723e36 --- /dev/null +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.Rendering.cs @@ -0,0 +1,170 @@ +using Ghost.Editor.Core.Controls.Internal.Docking; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using System.Diagnostics; + +namespace Ghost.Editor.View.Controls; + +public sealed partial class DockLayout +{ + private void RenderTree() + { + if (GetTemplateChild(PART_ROOT_GRID) is Grid rootGrid) + { + rootGrid.Children.Clear(); + if (Root != null) + { + var ui = CreateUIForNode(Root); + rootGrid.Children.Add(ui); + } + } + } + + private UIElement CreateUIForNode(DockNode node) + { + if (node is DockGroupNode groupNode) + { + return CreateGroupUI(groupNode); + } + else if (node is DockPanelNode panelNode) + { + return CreatePanelUI(panelNode); + } + + throw new InvalidOperationException($"Unsupported node type: {node.GetType().Name}"); + } + + private UIElement CreateGroupUI(DockGroupNode groupNode) + { + var grid = new Grid(); + bool isHorizontal = groupNode.Orientation == Orientation.Horizontal; + int childCount = groupNode.Children.Count; + + for (int i = 0; i < childCount; i++) + { + var childNode = groupNode.Children[i]; + var childUI = CreateUIForNode(childNode); + + if (isHorizontal) + { + var width = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star); + var colDef = new ColumnDefinition + { + Width = width, + MinWidth = MIN_PANE_SIZE + }; + grid.ColumnDefinitions.Add(colDef); + Grid.SetColumn((FrameworkElement)childUI, i * 2); + } + else + { + var height = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star); + var rowDef = new RowDefinition + { + Height = height, + MinHeight = MIN_PANE_SIZE + }; + grid.RowDefinitions.Add(rowDef); + Grid.SetRow((FrameworkElement)childUI, i * 2); + } + + grid.Children.Add(childUI); + + // Add GridSplitter between children + if (i < childCount - 1) + { + if (isHorizontal) + { + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter + { + Width = SPLITTER_THICKNESS, + HorizontalAlignment = HorizontalAlignment.Center, + ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Columns + }; + Grid.SetColumn(splitter, (i * 2) + 1); + grid.Children.Add(splitter); + } + else + { + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter + { + Height = SPLITTER_THICKNESS, + VerticalAlignment = VerticalAlignment.Center, + ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Rows + }; + Grid.SetRow(splitter, (i * 2) + 1); + grid.Children.Add(splitter); + } + } + } + + // Capture size changes to persist back to model + grid.SizeChanged += (s, e) => SyncSizesToModel(groupNode, grid); + + return grid; + } + + private void SyncSizesToModel(DockGroupNode groupNode, Grid grid) + { + bool isHorizontal = groupNode.Orientation == Orientation.Horizontal; + if (isHorizontal) + { + for (int i = 0; i < groupNode.Children.Count; i++) + { + if (i < groupNode.Sizes.Count && i * 2 < grid.ColumnDefinitions.Count) + { + groupNode.Sizes[i] = grid.ColumnDefinitions[i * 2].Width; + } + } + } + else + { + for (int i = 0; i < groupNode.Children.Count; i++) + { + if (i < groupNode.Sizes.Count && i * 2 < grid.RowDefinitions.Count) + { + groupNode.Sizes[i] = grid.RowDefinitions[i * 2].Height; + } + } + } + } + + private UIElement CreatePanelUI(DockPanelNode panelNode) + { + var tabView = new Ghost.Editor.Controls.NavigationTabView + { + TabItemsSource = panelNode.Items, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + CanDragTabs = true, + AllowDrop = true, + Tag = panelNode // Store reference to data node + }; + + // Bind selection state using TabView DPs + tabView.SetBinding(TabView.SelectedIndexProperty, new Binding + { + Source = panelNode, + Path = new PropertyPath(nameof(DockPanelNode.SelectedIndex)), + Mode = BindingMode.TwoWay + }); + + tabView.SetBinding(TabView.SelectedItemProperty, new Binding + { + Source = panelNode, + Path = new PropertyPath(nameof(DockPanelNode.SelectedItem)), + Mode = BindingMode.TwoWay + }); + + tabView.DragOver += TabView_DragOver; + tabView.DragLeave += TabView_DragLeave; + tabView.Drop += TabView_Drop; + tabView.TabDragStarting += TabView_TabDragStarting; + tabView.TabDroppedOutside += TabView_TabDroppedOutside; + + return tabView; + } +} diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.Subscriptions.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.Subscriptions.cs new file mode 100644 index 0000000..3d22ff5 --- /dev/null +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.Subscriptions.cs @@ -0,0 +1,105 @@ +using System.Collections.Specialized; +using Ghost.Editor.Core.Controls.Internal.Docking; + +namespace Ghost.Editor.View.Controls; + +public sealed partial class DockLayout +{ + private readonly HashSet _subscribedNodes = new(); + + private void SubscribeToNode(DockNode node) + { + if (!_subscribedNodes.Add(node)) + { + return; + } + + node.PropertyChanged += OnNodePropertyChanged; + + if (node is DockGroupNode groupNode) + { + ((INotifyCollectionChanged)groupNode.Children).CollectionChanged += OnChildrenCollectionChanged; + foreach (var child in groupNode.Children) + { + SubscribeToNode(child); + } + } + } + + private void UnsubscribeFromNode(DockNode node) + { + if (!_subscribedNodes.Remove(node)) + { + return; + } + + node.PropertyChanged -= OnNodePropertyChanged; + + if (node is DockGroupNode groupNode) + { + ((INotifyCollectionChanged)groupNode.Children).CollectionChanged -= OnChildrenCollectionChanged; + foreach (var child in groupNode.Children) + { + UnsubscribeFromNode(child); + } + } + } + + private void UnsubscribeFromAll() + { + // Copy to array to avoid modification during enumeration + var nodes = _subscribedNodes.ToArray(); + foreach (var node in nodes) + { + node.PropertyChanged -= OnNodePropertyChanged; + if (node is DockGroupNode groupNode) + { + ((INotifyCollectionChanged)groupNode.Children).CollectionChanged -= OnChildrenCollectionChanged; + } + } + _subscribedNodes.Clear(); + } + + private void OnNodePropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + // Filter to structural property names + if (e.PropertyName == nameof(DockGroupNode.Orientation)) + { + RenderTree(); + } + } + + private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + // On Reset, we don't know what was removed. + // We must unsubscribe from everything and resubscribe from Root to avoid leaks. + UnsubscribeFromAll(); + if (Root != null) + { + SubscribeToNode(Root); + } + } + else + { + if (e.OldItems != null) + { + foreach (DockNode oldNode in e.OldItems) + { + UnsubscribeFromNode(oldNode); + } + } + + if (e.NewItems != null) + { + foreach (DockNode newNode in e.NewItems) + { + SubscribeToNode(newNode); + } + } + } + + RenderTree(); + } +} diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index 7a41e5e..4d79efa 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -24,7 +24,6 @@ public sealed partial class DockLayout : Control private const double DROP_EDGE_THRESHOLD = 0.25; private FrameworkElement? _dropTargetOverlay; - private readonly HashSet _subscribedNodes = new(); public DockLayout() { @@ -71,409 +70,6 @@ public sealed partial class DockLayout : Control } } - private void SubscribeToNode(DockNode node) - { - if (!_subscribedNodes.Add(node)) - { - return; - } - - node.PropertyChanged += OnNodePropertyChanged; - - if (node is DockGroupNode groupNode) - { - ((INotifyCollectionChanged)groupNode.Children).CollectionChanged += OnChildrenCollectionChanged; - foreach (var child in groupNode.Children) - { - SubscribeToNode(child); - } - } - } - - private void UnsubscribeFromNode(DockNode node) - { - if (!_subscribedNodes.Remove(node)) - { - return; - } - - node.PropertyChanged -= OnNodePropertyChanged; - - if (node is DockGroupNode groupNode) - { - ((INotifyCollectionChanged)groupNode.Children).CollectionChanged -= OnChildrenCollectionChanged; - foreach (var child in groupNode.Children) - { - UnsubscribeFromNode(child); - } - } - } - - private void UnsubscribeFromAll() - { - // Copy to array to avoid modification during enumeration - var nodes = _subscribedNodes.ToArray(); - foreach (var node in nodes) - { - node.PropertyChanged -= OnNodePropertyChanged; - if (node is DockGroupNode groupNode) - { - ((INotifyCollectionChanged)groupNode.Children).CollectionChanged -= OnChildrenCollectionChanged; - } - } - _subscribedNodes.Clear(); - } - - private void OnNodePropertyChanged(object? sender, PropertyChangedEventArgs e) - { - // Filter to structural property names - if (e.PropertyName == nameof(DockGroupNode.Orientation)) - { - RenderTree(); - } - } - - private void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - if (e.Action == NotifyCollectionChangedAction.Reset) - { - // On Reset, we don't know what was removed. - // We must unsubscribe from everything and resubscribe from Root to avoid leaks. - UnsubscribeFromAll(); - if (Root != null) - { - SubscribeToNode(Root); - } - } - else - { - if (e.OldItems != null) - { - foreach (DockNode oldNode in e.OldItems) - { - UnsubscribeFromNode(oldNode); - } - } - - if (e.NewItems != null) - { - foreach (DockNode newNode in e.NewItems) - { - SubscribeToNode(newNode); - } - } - } - - RenderTree(); - } - - private void RenderTree() - { - if (GetTemplateChild(PART_ROOT_GRID) is Grid rootGrid) - { - rootGrid.Children.Clear(); - if (Root != null) - { - var ui = CreateUIForNode(Root); - rootGrid.Children.Add(ui); - } - } - } - - private UIElement CreateUIForNode(DockNode node) - { - if (node is DockGroupNode groupNode) - { - return CreateGroupUI(groupNode); - } - else if (node is DockPanelNode panelNode) - { - return CreatePanelUI(panelNode); - } - - Debug.Fail($"Unsupported node type: {node.GetType().Name}"); - return new Grid(); // Fallback - } - - private UIElement CreateGroupUI(DockGroupNode groupNode) - { - var grid = new Grid(); - bool isHorizontal = groupNode.Orientation == Orientation.Horizontal; - int childCount = groupNode.Children.Count; - - for (int i = 0; i < childCount; i++) - { - var childNode = groupNode.Children[i]; - var childUI = CreateUIForNode(childNode); - - if (isHorizontal) - { - var width = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star); - grid.ColumnDefinitions.Add(new ColumnDefinition - { - Width = width, - MinWidth = MIN_PANE_SIZE - }); - Grid.SetColumn((FrameworkElement)childUI, i * 2); - } - else - { - var height = (i < groupNode.Sizes.Count) ? groupNode.Sizes[i] : new GridLength(1, GridUnitType.Star); - grid.RowDefinitions.Add(new RowDefinition - { - Height = height, - MinHeight = MIN_PANE_SIZE - }); - Grid.SetRow((FrameworkElement)childUI, i * 2); - } - - grid.Children.Add(childUI); - - // Add GridSplitter between children - if (i < childCount - 1) - { - if (isHorizontal) - { - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter - { - Width = SPLITTER_THICKNESS, - HorizontalAlignment = HorizontalAlignment.Center, - ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Columns - }; - Grid.SetColumn(splitter, (i * 2) + 1); - grid.Children.Add(splitter); - } - else - { - grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); - var splitter = new CommunityToolkit.WinUI.Controls.GridSplitter - { - Height = SPLITTER_THICKNESS, - VerticalAlignment = VerticalAlignment.Center, - ResizeDirection = CommunityToolkit.WinUI.Controls.GridSplitter.GridResizeDirection.Rows - }; - Grid.SetRow(splitter, (i * 2) + 1); - grid.Children.Add(splitter); - } - } - } - - return grid; - } - - private UIElement CreatePanelUI(DockPanelNode panelNode) - { - var tabView = new Ghost.Editor.Controls.NavigationTabView - { - TabItemsSource = panelNode.Items, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Stretch, - CanDragTabs = true, - AllowDrop = true, - Tag = panelNode // Store reference to data node - }; - - // Bind selection state using TabView DPs - tabView.SetBinding(TabView.SelectedIndexProperty, new Binding - { - Source = panelNode, - Path = new PropertyPath(nameof(DockPanelNode.SelectedIndex)), - Mode = BindingMode.TwoWay - }); - - tabView.SetBinding(TabView.SelectedItemProperty, new Binding - { - Source = panelNode, - Path = new PropertyPath(nameof(DockPanelNode.SelectedItem)), - Mode = BindingMode.TwoWay - }); - - tabView.DragOver += TabView_DragOver; - tabView.DragLeave += TabView_DragLeave; - tabView.Drop += TabView_Drop; - tabView.TabDragStarting += TabView_TabDragStarting; - tabView.TabDroppedOutside += TabView_TabDroppedOutside; - - return tabView; - } - - private DockPosition _currentDropPosition = DockPosition.None; - private FrameworkElement? _lastTargetElement; - - private record DockDragPayload(object Item, DockPanelNode SourceNode); - - public event EventHandler? TabTornOff; - - private void TabView_TabDragStarting(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDragStartingEventArgs args) - { - if (args.Item != null && sender.Tag is DockPanelNode sourceNode) - { - var payload = new DockDragPayload(args.Item, sourceNode); - args.Data.Properties.Add(DRAG_PROPERTY_DOCK_TAB, payload); // Identify our drag - } - } - - private void TabView_DragOver(object sender, DragEventArgs e) - { - if (e.DataView.Properties.TryGetValue(DRAG_PROPERTY_DOCK_TAB, out var payloadObj) && - payloadObj is DockDragPayload && - sender is FrameworkElement targetElement) - { - e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; - - var position = e.GetPosition(targetElement); - var newPosition = DockMath.CalculateDockPosition(targetElement.ActualWidth, targetElement.ActualHeight, position.X, position.Y, DROP_EDGE_THRESHOLD); - - if (newPosition != _currentDropPosition || targetElement != _lastTargetElement) - { - _currentDropPosition = newPosition; - _lastTargetElement = targetElement; - UpdateDropOverlay(targetElement, _currentDropPosition); - } - } - } - - private void TabView_DragLeave(object sender, DragEventArgs e) - { - _lastTargetElement = null; - ClearOverlayState(); - } - - private void ClearOverlayState() - { - if (_dropTargetOverlay != null) - { - _dropTargetOverlay.Visibility = Visibility.Collapsed; - } - _currentDropPosition = DockPosition.None; - } - - private void ClearDragOperationState() - { - ClearOverlayState(); - } - - private void UpdateDropOverlay(FrameworkElement targetElement, DockPosition position) - { - if (_dropTargetOverlay == null) return; - if (position == DockPosition.None) - { - _dropTargetOverlay.Visibility = Visibility.Collapsed; - return; - } - - var transform = targetElement.TransformToVisual(this); - var bounds = transform.TransformBounds(new global::Windows.Foundation.Rect(0, 0, targetElement.ActualWidth, targetElement.ActualHeight)); - - _dropTargetOverlay.Visibility = Visibility.Visible; - _dropTargetOverlay.Width = double.NaN; - _dropTargetOverlay.Height = double.NaN; - - switch (position) - { - case DockPosition.Center: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); - break; - case DockPosition.Left: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - (bounds.Left + bounds.Width / 2), ActualHeight - bounds.Bottom); - break; - case DockPosition.Right: - _dropTargetOverlay.Margin = new Thickness(bounds.Left + bounds.Width / 2, bounds.Top, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); - break; - case DockPosition.Top: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top, ActualWidth - bounds.Right, ActualHeight - (bounds.Top + bounds.Height / 2)); - break; - case DockPosition.Bottom: - _dropTargetOverlay.Margin = new Thickness(bounds.Left, bounds.Top + bounds.Height / 2, ActualWidth - bounds.Right, ActualHeight - bounds.Bottom); - break; - } - } - - private void TabView_Drop(object sender, DragEventArgs e) - { - if (_dropTargetOverlay != null) _dropTargetOverlay.Visibility = Visibility.Collapsed; - - if (!e.DataView.Properties.TryGetValue(DRAG_PROPERTY_DOCK_TAB, out var payloadObj) || - payloadObj is not DockDragPayload payload || - !(sender is FrameworkElement targetElement) || - !(targetElement.Tag is DockPanelNode targetNode)) - { - ClearDragOperationState(); - return; - } - - if (_currentDropPosition == DockPosition.None) - { - ClearDragOperationState(); - return; - } - - if (payload.SourceNode == targetNode && _currentDropPosition == DockPosition.Center) - { - ClearDragOperationState(); - return; // Reordering within same tab is handled natively by TabView - } - - if (Root == null) - { - ClearDragOperationState(); - return; - } - - // 1. Execute mutation - if (DockMutationEngine.TryApplyDropMutation(Root, targetNode, payload.SourceNode, payload.Item, _currentDropPosition)) - { - DockMutationEngine.CleanupEmptyNodes(payload.SourceNode); - } - - ClearDragOperationState(); - } - - private void TabView_TabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) - { - try - { - if (sender.Tag is DockPanelNode sourceNode && args.Item != null) - { - // Validate that the item actually belongs to this source node before attempting tear-off - if (sourceNode.Items.Contains(args.Item)) - { - var handler = TabTornOff; - if (handler == null) - { - Logger.LogWarning("Tab dropped outside but no TabTornOff subscribers found."); - return; - } - - var result = TabTearOffService.TryTearOffTab(sourceNode.Items, args.Item, (tab) => - { - // Raise event to let the host handle window creation - handler.Invoke(this, new TabTornOffEventArgs(tab, sourceNode)); - }, sourceNode); - - if (result.IsSuccess) - { - DockMutationEngine.CleanupEmptyNodes(sourceNode); - } - else - { - 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($"TabDroppedOutside: Item '{itemInfo}' not found in source node (Items count: {sourceNode.Items.Count})."); - } - } - } - finally - { - ClearDragOperationState(); - } - } - protected override void OnApplyTemplate() { base.OnApplyTemplate(); diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index d0e4893..d415952 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -69,12 +69,6 @@ internal sealed partial class EngineEditorWindow : WindowEx PART_DockLayout.TabTornOff += (s, e) => App.CreateAndShowDockWindow(e.TabContent); } - private void OnTabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) - { - // This is now handled by DockLayout.TabTornOff for dynamic tabs. - // If we still have static tabs, we'd handle them here. - } - private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) { PART_TitleBar.Title = EditorApplication.ProjectName;