Fix docking layout

This commit is contained in:
2026-03-28 20:45:23 +09:00
parent 5845e7e9fb
commit 668e66937b
3 changed files with 42 additions and 112 deletions

View File

@@ -16,12 +16,20 @@ public sealed partial class DockLayout
private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args) private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args)
{ {
if (args.Item != null && sender.Tag is DockPanelNode sourceNode) if (sender.Tag is DockPanelNode sourceNode)
{ {
var payload = new DockDragPayload(args.Item, sourceNode); object? dragItem = null;
if (args.Item != null && sourceNode.Items.Contains(args.Item)) dragItem = args.Item;
else if (args.Tab != null && sourceNode.Items.Contains(args.Tab)) dragItem = args.Tab;
else dragItem = args.Item ?? args.Tab;
if (dragItem != null)
{
var payload = new DockDragPayload(dragItem, sourceNode);
args.Data.Properties.Add(DRAG_PROPERTY_DOCK_TAB, payload); // Identify our drag args.Data.Properties.Add(DRAG_PROPERTY_DOCK_TAB, payload); // Identify our drag
} }
} }
}
private void TabView_DragOver(object sender, DragEventArgs e) private void TabView_DragOver(object sender, DragEventArgs e)
{ {
@@ -30,6 +38,7 @@ public sealed partial class DockLayout
sender is FrameworkElement targetElement) sender is FrameworkElement targetElement)
{ {
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move; e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
e.Handled = true;
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);
@@ -113,41 +122,41 @@ public sealed partial class DockLayout
return; return;
} }
if (_currentDropPosition == DockPosition.None) var dropPosition = _currentDropPosition;
{
ClearDragOperationState(); ClearDragOperationState();
if (dropPosition == DockPosition.None ||
(payload.SourceNode == targetNode && dropPosition == DockPosition.Center) ||
Root == null)
{
return; return;
} }
if (payload.SourceNode == targetNode && _currentDropPosition == DockPosition.Center) e.Handled = true;
{
ClearDragOperationState();
return; // Reordering within same tab is handled natively by TabView
}
if (Root == null) // Defer the visual tree mutation to the next tick
DispatcherQueue.TryEnqueue(() =>
{ {
ClearDragOperationState(); if (DockMutationEngine.TryApplyDropMutation(Root, targetNode, payload.SourceNode, payload.Item, dropPosition))
return;
}
// 1. Execute mutation
if (DockMutationEngine.TryApplyDropMutation(Root, targetNode, payload.SourceNode, payload.Item, _currentDropPosition))
{ {
DockMutationEngine.CleanupEmptyNodes(payload.SourceNode); DockMutationEngine.CleanupEmptyNodes(payload.SourceNode);
} }
});
ClearDragOperationState();
} }
private void TabView_TabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args) private void TabView_TabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args)
{ {
try try
{ {
if (sender.Tag is DockPanelNode sourceNode && args.Item != null) if (sender.Tag is DockPanelNode sourceNode)
{ {
object? dragItem = null;
if (args.Item != null && sourceNode.Items.Contains(args.Item)) dragItem = args.Item;
else if (args.Tab != null && sourceNode.Items.Contains(args.Tab)) dragItem = args.Tab;
else dragItem = args.Item ?? args.Tab;
// Validate that the item actually belongs to this source node before attempting tear-off // Validate that the item actually belongs to this source node before attempting tear-off
if (sourceNode.Items.Contains(args.Item)) if (dragItem != null && sourceNode.Items.Contains(dragItem))
{ {
var handler = TabTornOff; var handler = TabTornOff;
if (handler == null) if (handler == null)
@@ -156,7 +165,7 @@ public sealed partial class DockLayout
return; return;
} }
var result = TabTearOffService.TryTearOffTab(sourceNode.Items, args.Item, (tab) => var result = TabTearOffService.TryTearOffTab(sourceNode.Items, dragItem, (tab) =>
{ {
// Raise event to let the host handle window creation // Raise event to let the host handle window creation
handler.Invoke(this, new TabTornOffEventArgs(tab, sourceNode)); handler.Invoke(this, new TabTornOffEventArgs(tab, sourceNode));
@@ -173,7 +182,7 @@ public sealed partial class DockLayout
} }
else else
{ {
string itemInfo = args.Item is FrameworkElement fe ? fe.GetType().Name : args.Item.ToString() ?? "unknown"; 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})."); Logger.LogWarning($"TabDroppedOutside: Item '{itemInfo}' not found in source node (Items count: {sourceNode.Items.Count}).");
} }
} }

View File

@@ -43,10 +43,10 @@ internal sealed partial class EngineEditorWindow : WindowEx
InitializeDockLayout(); InitializeDockLayout();
this.Unloaded += OnUnloaded; this.Closed += OnUnloaded;
} }
private void OnUnloaded(object sender, RoutedEventArgs e) private void OnUnloaded(object sender, WindowEventArgs e)
{ {
PART_DockLayout.TabTornOff -= OnTabTornOff; PART_DockLayout.TabTornOff -= OnTabTornOff;
} }

View File

@@ -1,79 +0,0 @@
using Ghost.Core;
using Ghost.Editor.Core.Controls.Internal.Docking;
using Microsoft.UI.Xaml;
using System.Collections;
namespace Ghost.Editor.View.Windows;
/// <summary>
/// Service for handling tab tear-off operations across the editor.
/// </summary>
internal static class TabTearOffService
{
/// <summary>
/// Attempts to tear off a tab into a new window.
/// </summary>
/// <param name="sourceItems">The collection to remove the item from.</param>
/// <param name="tabItem">The item to tear off.</param>
/// <param name="selectionContainer">Optional container to restore selection to on failure.</param>
/// <returns>A result indicating success or failure.</returns>
public static Result TryTearOffTab(IList sourceItems, object tabItem, object? selectionContainer = null)
{
int originalIndex = sourceItems.IndexOf(tabItem);
if (originalIndex == -1)
{
return Result.Failure("Item not found in source collection.");
}
object? originalSelection = GetSelection(selectionContainer);
try
{
sourceItems.Remove(tabItem);
try
{
// We no longer create the window here to decouple the service from the app shell.
// The caller is responsible for window creation (e.g. via an event handler).
return Result.Success();
}
catch (Exception ex)
{
Logger.LogError(ex);
// Rollback collection and selection
try
{
sourceItems.Insert(originalIndex, tabItem);
RestoreSelection(selectionContainer, originalSelection);
}
catch (Exception rollbackEx)
{
Logger.LogError(rollbackEx);
return Result.Failure($"Failed to tear off tab and rollback failed: {ex.Message}. Rollback error: {rollbackEx.Message}");
}
return Result.Failure($"Failed to tear off tab: {ex.Message}");
}
}
catch (Exception ex)
{
Logger.LogError(ex);
return Result.Failure($"Failed to remove item from source: {ex.Message}");
}
}
private static object? GetSelection(object? container)
{
if (container is DockPanelNode panel) return panel.SelectedItem;
if (container is Microsoft.UI.Xaml.Controls.TabView tabView) return tabView.SelectedItem;
return null;
}
private static void RestoreSelection(object? container, object? selection)
{
if (selection == null) return;
if (container is DockPanelNode panel) panel.SelectedItem = selection;
else if (container is Microsoft.UI.Xaml.Controls.TabView tabView) tabView.SelectedItem = selection;
}
}