fix(dock): centralize tear-off transaction in TabTearOffService and fix build breaks

This commit is contained in:
2026-03-28 16:21:48 +09:00
parent c4c0b5cd87
commit 10bc76a654
4 changed files with 79 additions and 77 deletions

View File

@@ -53,50 +53,6 @@ public partial class App : Application
}
}
/// <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="onSuccess">Optional callback after successful tear-off.</param>
/// <returns>A result indicating success or failure.</returns>
internal static Result TryTearOffTab(System.Collections.IList sourceItems, object tabItem, Action? onSuccess = null)
{
int originalIndex = sourceItems.IndexOf(tabItem);
if (originalIndex == -1)
{
return Result.Failure("Item not found in source collection.");
}
object? originalSelection = null;
// Try to capture selection if the source is a DockPanelNode or similar
// For now, we'll just handle the collection mutation.
try
{
sourceItems.Remove(tabItem);
try
{
CreateAndShowDockWindow(tabItem);
onSuccess?.Invoke();
return Result.Success();
}
catch (Exception ex)
{
// Rollback
sourceItems.Insert(originalIndex, tabItem);
Logger.LogError(ex);
return Result.Failure($"Failed to create tear-off window: {ex.Message}");
}
}
catch (Exception ex)
{
Logger.LogError(ex);
return Result.Failure($"Failed to remove item from source: {ex.Message}");
}
}
/// <summary>
/// Creates, registers, and shows a new DockWindow for a torn-off tab.
/// </summary>

View File

@@ -302,32 +302,16 @@ public sealed partial class DockLayout : Control
{
if (_sourceNode != null && _draggedItem != null)
{
if (TabTornOff == null)
var result = TabTearOffService.TryTearOffTab(_sourceNode.Items, _draggedItem, _sourceNode);
if (result.IsSuccess)
{
Logger.LogWarning("Tab dropped outside but no TabTornOff subscribers found.");
ClearDragOperationState();
return;
DockMutationEngine.CleanupEmptyNodes(_sourceNode);
}
object? originalSelection = _sourceNode.SelectedItem;
App.TryTearOffTab(_sourceNode.Items, _draggedItem, () =>
else
{
try
{
// Raise event to let the host handle window creation
TabTornOff.Invoke(this, new TabTornOffEventArgs(_draggedItem));
// Only cleanup if the tear-off was successful
DockMutationEngine.CleanupEmptyNodes(_sourceNode);
}
catch (Exception ex)
{
// If the event handler fails, we still need to cleanup or restore selection
_sourceNode.SelectedItem = originalSelection;
throw; // Re-throw to trigger rollback in TryTearOffTab
}
});
Logger.LogWarning($"Tab tear-off failed: {result.Error}");
}
ClearDragOperationState();
}

View File

@@ -43,18 +43,11 @@ internal sealed partial class EngineEditorWindow : WindowEx
// For static tabs in EngineEditorWindow, we remove the item from TabItems
if (sender.TabItems.Contains(args.Item))
{
object? originalSelection = sender.SelectedItem;
var result = TabTearOffService.TryTearOffTab(sender.TabItems, args.Item, sender);
App.TryTearOffTab(sender.TabItems, args.Item, () =>
if (!result.IsSuccess)
{
// If we need to do anything else on success
});
// Note: If TryTearOffTab fails, it will have already re-inserted the item.
// We might want to restore selection if it was the selected item.
if (sender.TabItems.Contains(args.Item) && sender.SelectedItem == null)
{
sender.SelectedItem = originalSelection;
Logger.LogWarning($"Tab tear-off failed: {result.Error}");
}
}
}

View File

@@ -0,0 +1,69 @@
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
{
App.CreateAndShowDockWindow(tabItem);
return Result.Success();
}
catch (Exception ex)
{
// Rollback collection
sourceItems.Insert(originalIndex, tabItem);
RestoreSelection(selectionContainer, originalSelection);
Logger.LogError(ex);
return Result.Failure($"Failed to create tear-off window: {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;
}
}