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 System.Diagnostics;
|
||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.Controls.Internal.Docking;
|
using Ghost.Editor.Core.Controls.Internal.Docking;
|
||||||
using Ghost.Editor.View.Windows;
|
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Data;
|
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
|
// Validate that the item actually belongs to this source node before attempting tear-off
|
||||||
if (sourceNode.Items.Contains(args.Item))
|
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)
|
if (result.IsSuccess)
|
||||||
{
|
{
|
||||||
// Raise event to let the host handle window creation
|
|
||||||
TabTornOff?.Invoke(this, new TabTornOffEventArgs(args.Item, sourceNode));
|
|
||||||
|
|
||||||
DockMutationEngine.CleanupEmptyNodes(sourceNode);
|
DockMutationEngine.CleanupEmptyNodes(sourceNode);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Ghost.Editor.Core.Controls.Internal.Docking;
|
||||||
|
|
||||||
namespace Ghost.Editor.View.Controls;
|
namespace Ghost.Editor.View.Controls;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -13,9 +15,9 @@ public sealed class TabTornOffEventArgs : EventArgs
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the source node the tab is being torn off from.
|
/// Gets the source node the tab is being torn off from.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object SourceNode { get; }
|
public DockPanelNode SourceNode { get; }
|
||||||
|
|
||||||
public TabTornOffEventArgs(object tabContent, object sourceNode)
|
public TabTornOffEventArgs(object tabContent, DockPanelNode sourceNode)
|
||||||
{
|
{
|
||||||
TabContent = tabContent;
|
TabContent = tabContent;
|
||||||
SourceNode = sourceNode;
|
SourceNode = sourceNode;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.Controls.Internal.Docking;
|
using Ghost.Editor.Core.Controls.Internal.Docking;
|
||||||
using Ghost.Editor.View.Controls;
|
using Ghost.Editor.View.Controls;
|
||||||
using WinUIEx;
|
using WinUIEx;
|
||||||
@@ -21,9 +22,16 @@ internal sealed partial class DockWindow : WindowEx
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnTabTornOff(object? sender, TabTornOffEventArgs e)
|
private void OnTabTornOff(object? sender, TabTornOffEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
App.CreateAndShowDockWindow(e.TabContent);
|
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
|
// For static tabs in EngineEditorWindow, we remove the item from TabItems
|
||||||
if (sender.TabItems is System.Collections.IList list && list.Contains(args.Item))
|
if (sender.TabItems is System.Collections.IList list && list.Contains(args.Item))
|
||||||
{
|
{
|
||||||
var result = TabTearOffService.TryTearOffTab(list, args.Item, sender);
|
var result = TabTearOffService.TryTearOffTab(list, args.Item, (tab) =>
|
||||||
|
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
{
|
||||||
App.CreateAndShowDockWindow(args.Item);
|
App.CreateAndShowDockWindow(tab);
|
||||||
}
|
}, sender);
|
||||||
else
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
{
|
{
|
||||||
Logger.LogWarning($"Tab tear-off failed: {result.Message}");
|
Logger.LogWarning($"Tab tear-off failed: {result.Message}");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user