diff --git a/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/TabTearOffService.cs b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/TabTearOffService.cs
new file mode 100644
index 0000000..ce3b5de
--- /dev/null
+++ b/src/Editor/Ghost.Editor.Core/Controls/Internal/Docking/TabTearOffService.cs
@@ -0,0 +1,79 @@
+using Ghost.Core;
+using Microsoft.UI.Xaml.Controls;
+using System.Collections;
+
+namespace Ghost.Editor.Core.Controls.Internal.Docking;
+
+///
+/// Service for handling tab tear-off operations across the editor.
+///
+internal static class TabTearOffService
+{
+ ///
+ /// Attempts to tear off a tab by removing it from its source and executing a creation callback.
+ /// If the creation callback fails, the tab is restored to its original position and selection state.
+ ///
+ /// The collection to remove the item from.
+ /// The item to tear off.
+ /// The callback to create the new host (e.g. window) for the tab.
+ /// Optional container to restore selection to on failure.
+ /// A result indicating success or failure.
+ public static Result TryTearOffTab(IList sourceItems, object tabItem, Action createCallback, 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
+ {
+ createCallback(tabItem);
+ 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 create tear-off host and rollback failed: {ex.Message}. Rollback error: {rollbackEx.Message}");
+ }
+
+ return Result.Failure($"Failed to create tear-off host: {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 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 TabView tabView) tabView.SelectedItem = selection;
+ }
+}
diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs
index 43b6c7b..83bbaf0 100644
--- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs
+++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs
@@ -3,7 +3,6 @@ using System.ComponentModel;
using System.Diagnostics;
using Ghost.Core;
using Ghost.Editor.Core.Controls.Internal.Docking;
-using Ghost.Editor.View.Windows;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
@@ -435,13 +434,14 @@ public sealed partial class DockLayout : Control
// Validate that the item actually belongs to this source node before attempting tear-off
if (sourceNode.Items.Contains(args.Item))
{
- var result = TabTearOffService.TryTearOffTab(sourceNode.Items, args.Item, sourceNode);
+ var result = TabTearOffService.TryTearOffTab(sourceNode.Items, args.Item, (tab) =>
+ {
+ // Raise event to let the host handle window creation
+ TabTornOff?.Invoke(this, new TabTornOffEventArgs(tab, sourceNode));
+ }, sourceNode);
if (result.IsSuccess)
{
- // Raise event to let the host handle window creation
- TabTornOff?.Invoke(this, new TabTornOffEventArgs(args.Item, sourceNode));
-
DockMutationEngine.CleanupEmptyNodes(sourceNode);
}
else
diff --git a/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs b/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs
index 6d4f128..e08a2e1 100644
--- a/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs
+++ b/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs
@@ -1,3 +1,5 @@
+using Ghost.Editor.Core.Controls.Internal.Docking;
+
namespace Ghost.Editor.View.Controls;
///
@@ -13,9 +15,9 @@ public sealed class TabTornOffEventArgs : EventArgs
///
/// Gets the source node the tab is being torn off from.
///
- public object SourceNode { get; }
+ public DockPanelNode SourceNode { get; }
- public TabTornOffEventArgs(object tabContent, object sourceNode)
+ public TabTornOffEventArgs(object tabContent, DockPanelNode sourceNode)
{
TabContent = tabContent;
SourceNode = sourceNode;
diff --git a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs
index f86ed8a..d2c36d9 100644
--- a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs
+++ b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs
@@ -1,3 +1,4 @@
+using Ghost.Core;
using Ghost.Editor.Core.Controls.Internal.Docking;
using Ghost.Editor.View.Controls;
using WinUIEx;
@@ -22,8 +23,15 @@ internal sealed partial class DockWindow : WindowEx
private void OnTabTornOff(object? sender, TabTornOffEventArgs e)
{
- App.CreateAndShowDockWindow(e.TabContent);
+ try
+ {
+ App.CreateAndShowDockWindow(e.TabContent);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex);
+ // The service handles rollback if this was called from TryTearOffTab
+ throw;
+ }
}
}
-
-}
diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs
index 29f7d65..77fb606 100644
--- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs
+++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs
@@ -43,13 +43,12 @@ internal sealed partial class EngineEditorWindow : WindowEx
// For static tabs in EngineEditorWindow, we remove the item from TabItems
if (sender.TabItems is System.Collections.IList list && list.Contains(args.Item))
{
- var result = TabTearOffService.TryTearOffTab(list, args.Item, sender);
-
- if (result.IsSuccess)
+ var result = TabTearOffService.TryTearOffTab(list, args.Item, (tab) =>
{
- App.CreateAndShowDockWindow(args.Item);
- }
- else
+ App.CreateAndShowDockWindow(tab);
+ }, sender);
+
+ if (!result.IsSuccess)
{
Logger.LogWarning($"Tab tear-off failed: {result.Message}");
}