fix(dock): centralize transactional tear-off logic and fix build break

This commit is contained in:
2026-03-28 16:15:27 +09:00
parent 08e4d3311a
commit c4c0b5cd87
3 changed files with 70 additions and 49 deletions

View File

@@ -53,6 +53,50 @@ 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> /// <summary>
/// Creates, registers, and shows a new DockWindow for a torn-off tab. /// Creates, registers, and shows a new DockWindow for a torn-off tab.
/// </summary> /// </summary>

View File

@@ -1,6 +1,7 @@
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using Ghost.Core;
using Ghost.Editor.Core.Controls.Internal.Docking; using Ghost.Editor.Core.Controls.Internal.Docking;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -308,43 +309,29 @@ public sealed partial class DockLayout : Control
return; return;
} }
int originalIndex = _sourceNode.Items.IndexOf(_draggedItem);
object? originalSelection = _sourceNode.SelectedItem; object? originalSelection = _sourceNode.SelectedItem;
if (originalIndex == -1) App.TryTearOffTab(_sourceNode.Items, _draggedItem, () =>
{
ClearDragOperationState();
return;
}
try
{
// Remove from current tree
if (_sourceNode.Items.Remove(_draggedItem))
{ {
try try
{ {
// Raise event to let the host handle window creation // Raise event to let the host handle window creation
TabTornOff.Invoke(this, new TabTornOffEventArgs(_draggedItem)); TabTornOff.Invoke(this, new TabTornOffEventArgs(_draggedItem));
// Only cleanup if the tear-off was successful (didn't throw) // Only cleanup if the tear-off was successful
DockMutationEngine.CleanupEmptyNodes(_sourceNode); DockMutationEngine.CleanupEmptyNodes(_sourceNode);
} }
catch (Exception ex) catch (Exception ex)
{ {
// Rollback: Re-insert the item at original position if the tear-off handler fails // If the event handler fails, we still need to cleanup or restore selection
_sourceNode.Items.Insert(originalIndex, _draggedItem);
_sourceNode.SelectedItem = originalSelection; _sourceNode.SelectedItem = originalSelection;
Logger.LogError(ex); throw; // Re-throw to trigger rollback in TryTearOffTab
} }
} });
}
finally
{
ClearDragOperationState(); ClearDragOperationState();
} }
} }
}
private object? _draggedItem; private object? _draggedItem;
private DockPanelNode? _sourceNode; private DockPanelNode? _sourceNode;

View File

@@ -43,28 +43,18 @@ 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; object? originalSelection = sender.SelectedItem;
try App.TryTearOffTab(sender.TabItems, args.Item, () =>
{ {
sender.TabItems.Remove(args.Item); // If we need to do anything else on success
});
try // 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)
{ {
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; sender.SelectedItem = originalSelection;
Logger.LogError(ex);
}
}
catch (Exception ex)
{
Logger.LogError(ex);
} }
} }
} }