Fix docking layout
This commit is contained in:
@@ -16,10 +16,18 @@ 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;
|
||||||
args.Data.Properties.Add(DRAG_PROPERTY_DOCK_TAB, payload); // Identify our drag
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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();
|
|
||||||
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();
|
ClearDragOperationState();
|
||||||
|
|
||||||
|
if (dropPosition == DockPosition.None ||
|
||||||
|
(payload.SourceNode == targetNode && dropPosition == DockPosition.Center) ||
|
||||||
|
Root == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
|
||||||
|
// Defer the visual tree mutation to the next tick
|
||||||
|
DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
if (DockMutationEngine.TryApplyDropMutation(Root, targetNode, payload.SourceNode, payload.Item, dropPosition))
|
||||||
|
{
|
||||||
|
DockMutationEngine.CleanupEmptyNodes(payload.SourceNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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}).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user