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
{
get;

View File

@@ -299,36 +299,50 @@ public sealed partial class DockLayout : Control
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);
object? originalSelection = _sourceNode.SelectedItem;
if (originalIndex == -1)
{
ClearDragOperationState();
return;
}
// Remove from current tree
if (_sourceNode.Items.Remove(_draggedItem))
try
{
try
// Remove from current tree
if (_sourceNode.Items.Remove(_draggedItem))
{
// Raise event to let the host handle window creation
TabTornOff.Invoke(this, new TabTornOffEventArgs(_draggedItem));
// Only cleanup if the tear-off was successful (didn't throw)
DockMutationEngine.CleanupEmptyNodes(_sourceNode);
}
catch (Exception ex)
{
// Rollback: Re-insert the item at original position if the tear-off handler fails
_sourceNode.Items.Insert(originalIndex, _draggedItem);
_sourceNode.SelectedItem = _draggedItem;
Logger.LogError(ex);
try
{
// Raise event to let the host handle window creation
TabTornOff.Invoke(this, new TabTornOffEventArgs(_draggedItem));
// Only cleanup if the tear-off was successful (didn't throw)
DockMutationEngine.CleanupEmptyNodes(_sourceNode);
}
catch (Exception ex)
{
// Rollback: Re-insert the item at original position if the tear-off handler fails
_sourceNode.Items.Insert(originalIndex, _draggedItem);
_sourceNode.SelectedItem = originalSelection;
Logger.LogError(ex);
}
}
}
ClearDragOperationState();
finally
{
ClearDragOperationState();
}
}
}

View File

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

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services;
@@ -42,11 +43,29 @@ internal sealed partial class EngineEditorWindow : WindowEx
// For static tabs in EngineEditorWindow, we remove the item from TabItems
if (sender.TabItems.Contains(args.Item))
{
sender.TabItems.Remove(args.Item);
var newWindow = new DockWindow(args.Item);
App.AddSecondaryWindow(newWindow);
newWindow.Activate();
int originalIndex = sender.TabItems.IndexOf(args.Item);
object? originalSelection = sender.SelectedItem;
try
{
sender.TabItems.Remove(args.Item);
try
{
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);
}
}
}