From c4c0b5cd8770a727f0ee9e1d938843b67d2b9779 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 16:15:27 +0900 Subject: [PATCH] fix(dock): centralize transactional tear-off logic and fix build break --- src/Editor/Ghost.Editor/App.xaml.cs | 44 +++++++++++++++++ .../Ghost.Editor/View/Controls/DockLayout.cs | 47 +++++++------------ .../View/Windows/EngineEditorWindow.xaml.cs | 28 ++++------- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/Editor/Ghost.Editor/App.xaml.cs b/src/Editor/Ghost.Editor/App.xaml.cs index cbc36df..760f966 100644 --- a/src/Editor/Ghost.Editor/App.xaml.cs +++ b/src/Editor/Ghost.Editor/App.xaml.cs @@ -53,6 +53,50 @@ public partial class App : Application } } + /// + /// Attempts to tear off a tab into a new window. + /// + /// The collection to remove the item from. + /// The item to tear off. + /// Optional callback after successful tear-off. + /// A result indicating success or failure. + 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}"); + } + } + /// /// Creates, registers, and shows a new DockWindow for a torn-off tab. /// diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index 794968e..4760afe 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; +using Ghost.Core; using Ghost.Editor.Core.Controls.Internal.Docking; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -308,41 +309,27 @@ public sealed partial class DockLayout : Control return; } - int originalIndex = _sourceNode.Items.IndexOf(_draggedItem); 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 - TabTornOff.Invoke(this, new TabTornOffEventArgs(_draggedItem)); - - // Only cleanup if the tear-off was successful (didn't throw) - DockMutationEngine.CleanupEmptyNodes(_sourceNode); - } - catch (Exception ex) - { - // Rollback: Re-insert the item at original position if the tear-off handler fails - _sourceNode.Items.Insert(originalIndex, _draggedItem); - _sourceNode.SelectedItem = originalSelection; - Logger.LogError(ex); - } + // 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); } - } - finally - { - ClearDragOperationState(); - } + 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 + } + }); + + ClearDragOperationState(); } } diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index 00b4162..9e69aa6 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -43,28 +43,18 @@ internal sealed partial class EngineEditorWindow : WindowEx // For static tabs in EngineEditorWindow, we remove the item from TabItems if (sender.TabItems.Contains(args.Item)) { - int originalIndex = sender.TabItems.IndexOf(args.Item); object? originalSelection = sender.SelectedItem; - - try + + App.TryTearOffTab(sender.TabItems, args.Item, () => { - sender.TabItems.Remove(args.Item); - - try - { - 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; - Logger.LogError(ex); - } - } - catch (Exception ex) + // 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) { - Logger.LogError(ex); + sender.SelectedItem = originalSelection; } } }