fix(dock): centralize tear-off logic and ensure transactional integrity

This commit is contained in:
2026-03-28 16:10:25 +09:00
parent 299bcf520c
commit 08e4d3311a
4 changed files with 67 additions and 26 deletions

View File

@@ -53,6 +53,16 @@ public partial class App : Application
} }
} }
/// <summary>
/// Creates, registers, and shows a new DockWindow for a torn-off tab.
/// </summary>
internal static void CreateAndShowDockWindow(object tabContent)
{
var newWindow = new Ghost.Editor.View.Windows.DockWindow(tabContent);
AddSecondaryWindow(newWindow);
newWindow.Activate();
}
internal IHost Host internal IHost Host
{ {
get; get;

View File

@@ -299,15 +299,26 @@ public sealed partial class DockLayout : Control
private void TabView_TabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) private void TabView_TabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args)
{ {
if (_sourceNode != null && _draggedItem != null && TabTornOff != null) if (_sourceNode != null && _draggedItem != null)
{ {
if (TabTornOff == null)
{
Logger.LogWarning("Tab dropped outside but no TabTornOff subscribers found.");
ClearDragOperationState();
return;
}
int originalIndex = _sourceNode.Items.IndexOf(_draggedItem); int originalIndex = _sourceNode.Items.IndexOf(_draggedItem);
object? originalSelection = _sourceNode.SelectedItem;
if (originalIndex == -1) if (originalIndex == -1)
{ {
ClearDragOperationState(); ClearDragOperationState();
return; return;
} }
try
{
// Remove from current tree // Remove from current tree
if (_sourceNode.Items.Remove(_draggedItem)) if (_sourceNode.Items.Remove(_draggedItem))
{ {
@@ -323,14 +334,17 @@ public sealed partial class DockLayout : Control
{ {
// Rollback: Re-insert the item at original position if the tear-off handler fails // Rollback: Re-insert the item at original position if the tear-off handler fails
_sourceNode.Items.Insert(originalIndex, _draggedItem); _sourceNode.Items.Insert(originalIndex, _draggedItem);
_sourceNode.SelectedItem = _draggedItem; _sourceNode.SelectedItem = originalSelection;
Logger.LogError(ex); Logger.LogError(ex);
} }
} }
}
finally
{
ClearDragOperationState(); ClearDragOperationState();
} }
} }
}
private object? _draggedItem; private object? _draggedItem;
private DockPanelNode? _sourceNode; private DockPanelNode? _sourceNode;

View File

@@ -22,8 +22,6 @@ internal sealed partial class DockWindow : WindowEx
private void OnTabTornOff(object? sender, TabTornOffEventArgs e) private void OnTabTornOff(object? sender, TabTornOffEventArgs e)
{ {
var newWindow = new DockWindow(e.TabContent); App.CreateAndShowDockWindow(e.TabContent);
App.AddSecondaryWindow(newWindow);
newWindow.Activate();
} }
} }

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
@@ -41,12 +42,30 @@ internal sealed partial class EngineEditorWindow : WindowEx
{ {
// For static tabs in EngineEditorWindow, we remove the item from TabItems // For static tabs in EngineEditorWindow, we remove the item from TabItems
if (sender.TabItems.Contains(args.Item)) if (sender.TabItems.Contains(args.Item))
{
int originalIndex = sender.TabItems.IndexOf(args.Item);
object? originalSelection = sender.SelectedItem;
try
{ {
sender.TabItems.Remove(args.Item); sender.TabItems.Remove(args.Item);
var newWindow = new DockWindow(args.Item); try
App.AddSecondaryWindow(newWindow); {
newWindow.Activate(); App.CreateAndShowDockWindow(args.Item);
}
catch (Exception ex)
{
// Rollback: Re-insert the item at original position if the tear-off fails
sender.TabItems.Insert(originalIndex, args.Item);
sender.SelectedItem = originalSelection;
Logger.LogError(ex);
}
}
catch (Exception ex)
{
Logger.LogError(ex);
}
} }
} }