From 08e4d3311a52387cddd3614e6745b44539f6060e Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 16:10:25 +0900 Subject: [PATCH] fix(dock): centralize tear-off logic and ensure transactional integrity --- src/Editor/Ghost.Editor/App.xaml.cs | 10 ++++ .../Ghost.Editor/View/Controls/DockLayout.cs | 50 ++++++++++++------- .../View/Windows/DockWindow.xaml.cs | 4 +- .../View/Windows/EngineEditorWindow.xaml.cs | 29 +++++++++-- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/Editor/Ghost.Editor/App.xaml.cs b/src/Editor/Ghost.Editor/App.xaml.cs index ca6f91a..cbc36df 100644 --- a/src/Editor/Ghost.Editor/App.xaml.cs +++ b/src/Editor/Ghost.Editor/App.xaml.cs @@ -53,6 +53,16 @@ public partial class App : Application } } + /// + /// Creates, registers, and shows a new DockWindow for a torn-off tab. + /// + internal static void CreateAndShowDockWindow(object tabContent) + { + var newWindow = new Ghost.Editor.View.Windows.DockWindow(tabContent); + AddSecondaryWindow(newWindow); + newWindow.Activate(); + } + internal IHost Host { get; diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index 9cbf98e..794968e 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -299,36 +299,50 @@ public sealed partial class DockLayout : Control private void TabView_TabDroppedOutside(Microsoft.UI.Xaml.Controls.TabView sender, Microsoft.UI.Xaml.Controls.TabViewTabDroppedOutsideEventArgs args) { - if (_sourceNode != null && _draggedItem != null && TabTornOff != null) + if (_sourceNode != null && _draggedItem != null) { + if (TabTornOff == null) + { + Logger.LogWarning("Tab dropped outside but no TabTornOff subscribers found."); + ClearDragOperationState(); + return; + } + int originalIndex = _sourceNode.Items.IndexOf(_draggedItem); + object? originalSelection = _sourceNode.SelectedItem; + if (originalIndex == -1) { ClearDragOperationState(); return; } - // Remove from current tree - if (_sourceNode.Items.Remove(_draggedItem)) + try { - try + // Remove from current tree + if (_sourceNode.Items.Remove(_draggedItem)) { - // 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 = _draggedItem; - Logger.LogError(ex); + 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); + } } } - - ClearDragOperationState(); + finally + { + ClearDragOperationState(); + } } } diff --git a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs index 8bb1e79..3b887d7 100644 --- a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs @@ -22,8 +22,6 @@ internal sealed partial class DockWindow : WindowEx private void OnTabTornOff(object? sender, TabTornOffEventArgs e) { - var newWindow = new DockWindow(e.TabContent); - App.AddSecondaryWindow(newWindow); - newWindow.Activate(); + App.CreateAndShowDockWindow(e.TabContent); } } diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index 6646cee..00b4162 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -1,3 +1,4 @@ +using Ghost.Core; using Ghost.Editor.Core; using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Services; @@ -42,11 +43,29 @@ internal sealed partial class EngineEditorWindow : WindowEx // For static tabs in EngineEditorWindow, we remove the item from TabItems if (sender.TabItems.Contains(args.Item)) { - sender.TabItems.Remove(args.Item); - - var newWindow = new DockWindow(args.Item); - App.AddSecondaryWindow(newWindow); - newWindow.Activate(); + int originalIndex = sender.TabItems.IndexOf(args.Item); + object? originalSelection = sender.SelectedItem; + + try + { + 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) + { + Logger.LogError(ex); + } } }