fix(dock): complete tear-off flow and add rollback on failure

This commit is contained in:
2026-03-28 16:01:42 +09:00
parent 8c136709ff
commit 304df0a381
4 changed files with 49 additions and 8 deletions

View File

@@ -306,20 +306,23 @@ public sealed partial class DockLayout : Control
{ {
DockMutationEngine.CleanupEmptyNodes(_sourceNode); DockMutationEngine.CleanupEmptyNodes(_sourceNode);
// Raise event to let the host handle window creation try
TabTornOff?.Invoke(this, new TabTornOffEventArgs(_draggedItem)); {
// 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(); ClearDragOperationState();
} }
} }
public class TabTornOffEventArgs : EventArgs
{
public object TabContent { get; }
public TabTornOffEventArgs(object tabContent) => TabContent = tabContent;
}
private object? _draggedItem; private object? _draggedItem;
private DockPanelNode? _sourceNode; private DockPanelNode? _sourceNode;
private DockPosition _currentDropPosition = DockPosition.None; private DockPosition _currentDropPosition = DockPosition.None;

View File

@@ -0,0 +1,17 @@
namespace Ghost.Editor.View.Controls;
/// <summary>
/// Event arguments for the TabTornOff event.
/// </summary>
public sealed class TabTornOffEventArgs : EventArgs
{
/// <summary>
/// Gets the content of the tab being torn off.
/// </summary>
public object TabContent { get; }
public TabTornOffEventArgs(object tabContent)
{
TabContent = tabContent;
}
}

View File

@@ -1,4 +1,5 @@
using Ghost.Editor.Core.Controls.Internal.Docking; using Ghost.Editor.Core.Controls.Internal.Docking;
using Ghost.Editor.View.Controls;
using WinUIEx; using WinUIEx;
namespace Ghost.Editor.View.Windows; namespace Ghost.Editor.View.Windows;
@@ -16,5 +17,13 @@ internal sealed partial class DockWindow : WindowEx
rootGroup.AddChild(panel); rootGroup.AddChild(panel);
PART_DockLayout.Root = rootGroup; 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();
} }
} }

View File

@@ -1,6 +1,7 @@
using Ghost.Editor.Core; using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts; using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Services; using Ghost.Editor.Core.Services;
using Ghost.Editor.View.Controls;
using Ghost.Editor.ViewModels.Windows; using Ghost.Editor.ViewModels.Windows;
using Windows.ApplicationModel; using Windows.ApplicationModel;
using WinUIEx; using WinUIEx;
@@ -34,6 +35,17 @@ internal sealed partial class EngineEditorWindow : WindowEx
SetTitleBar(PART_TitleBar); SetTitleBar(PART_TitleBar);
this.CenterOnScreen(); 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) private void MainGrid_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)