From 304df0a3816b2e0bfeb5eddc4e44076d2a954ff7 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 28 Mar 2026 16:01:42 +0900 Subject: [PATCH] fix(dock): complete tear-off flow and add rollback on failure --- .../Ghost.Editor/View/Controls/DockLayout.cs | 19 +++++++++++-------- .../View/Controls/TabTornOffEventArgs.cs | 17 +++++++++++++++++ .../View/Windows/DockWindow.xaml.cs | 9 +++++++++ .../View/Windows/EngineEditorWindow.xaml.cs | 12 ++++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs diff --git a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs index a349551..d045357 100644 --- a/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs +++ b/src/Editor/Ghost.Editor/View/Controls/DockLayout.cs @@ -306,20 +306,23 @@ public sealed partial class DockLayout : Control { DockMutationEngine.CleanupEmptyNodes(_sourceNode); - // Raise event to let the host handle window creation - TabTornOff?.Invoke(this, new TabTornOffEventArgs(_draggedItem)); + try + { + // Raise event to let the host handle window creation + TabTornOff?.Invoke(this, new TabTornOffEventArgs(_draggedItem)); + } + catch (Exception ex) + { + // Rollback: Re-insert the item if the tear-off handler fails + _sourceNode.Items.Add(_draggedItem); + Logger.LogError($"Failed to tear off tab: {ex.Message}"); + } } ClearDragOperationState(); } } - public class TabTornOffEventArgs : EventArgs - { - public object TabContent { get; } - public TabTornOffEventArgs(object tabContent) => TabContent = tabContent; - } - private object? _draggedItem; private DockPanelNode? _sourceNode; private DockPosition _currentDropPosition = DockPosition.None; diff --git a/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs b/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs new file mode 100644 index 0000000..0515ea5 --- /dev/null +++ b/src/Editor/Ghost.Editor/View/Controls/TabTornOffEventArgs.cs @@ -0,0 +1,17 @@ +namespace Ghost.Editor.View.Controls; + +/// +/// Event arguments for the TabTornOff event. +/// +public sealed class TabTornOffEventArgs : EventArgs +{ + /// + /// Gets the content of the tab being torn off. + /// + public object TabContent { get; } + + public TabTornOffEventArgs(object tabContent) + { + TabContent = tabContent; + } +} diff --git a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs index 2a89b92..8bb1e79 100644 --- a/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/DockWindow.xaml.cs @@ -1,4 +1,5 @@ using Ghost.Editor.Core.Controls.Internal.Docking; +using Ghost.Editor.View.Controls; using WinUIEx; namespace Ghost.Editor.View.Windows; @@ -16,5 +17,13 @@ internal sealed partial class DockWindow : WindowEx rootGroup.AddChild(panel); PART_DockLayout.Root = rootGroup; + PART_DockLayout.TabTornOff += OnTabTornOff; + } + + private void OnTabTornOff(object? sender, TabTornOffEventArgs e) + { + var newWindow = new DockWindow(e.TabContent); + App.AddSecondaryWindow(newWindow); + newWindow.Activate(); } } diff --git a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index a0f8668..cb1b0c3 100644 --- a/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/src/Editor/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -1,6 +1,7 @@ using Ghost.Editor.Core; using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Services; +using Ghost.Editor.View.Controls; using Ghost.Editor.ViewModels.Windows; using Windows.ApplicationModel; using WinUIEx; @@ -34,6 +35,17 @@ internal sealed partial class EngineEditorWindow : WindowEx SetTitleBar(PART_TitleBar); this.CenterOnScreen(); + + // Note: DockLayout is not directly in the XAML but created by RenderTree. + // However, we can subscribe to the event if we find it or if it's exposed. + // Since DockLayout is a TemplatePart or child of a Grid, we might need to wait for it. + } + + private void OnTabTornOff(object? sender, TabTornOffEventArgs e) + { + var newWindow = new DockWindow(e.TabContent); + App.AddSecondaryWindow(newWindow); + newWindow.Activate(); } private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)