fix(dock): restore transactional integrity and fix build breaks
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
using Ghost.Core;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.Collections;
|
||||
|
||||
namespace Ghost.Editor.Core.Controls.Internal.Docking;
|
||||
|
||||
/// <summary>
|
||||
/// Service for handling tab tear-off operations across the editor.
|
||||
/// </summary>
|
||||
internal static class TabTearOffService
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="sourceItems">The collection to remove the item from.</param>
|
||||
/// <param name="tabItem">The item to tear off.</param>
|
||||
/// <param name="createCallback">The callback to create the new host (e.g. window) for the tab.</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, Action<object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Ghost.Editor.Core.Controls.Internal.Docking;
|
||||
|
||||
namespace Ghost.Editor.View.Controls;
|
||||
|
||||
/// <summary>
|
||||
@@ -13,9 +15,9 @@ public sealed class TabTornOffEventArgs : EventArgs
|
||||
/// <summary>
|
||||
/// Gets the source node the tab is being torn off from.
|
||||
/// </summary>
|
||||
public object SourceNode { get; }
|
||||
public DockPanelNode SourceNode { get; }
|
||||
|
||||
public TabTornOffEventArgs(object tabContent, object sourceNode)
|
||||
public TabTornOffEventArgs(object tabContent, DockPanelNode sourceNode)
|
||||
{
|
||||
TabContent = tabContent;
|
||||
SourceNode = sourceNode;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user