From 4d67eaf5d7054374fb4f99fee5824320d99c0268 Mon Sep 17 00:00:00 2001 From: Dan Walmsley <4672627+danwalmsley@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:12:31 +0000 Subject: [PATCH 1/2] Restore float fallback for incompatible dock groups --- .../Internal/DockControlState.cs | 72 +++++++++------ src/Dock.Avalonia/Internal/HostWindowState.cs | 30 ++++-- .../Internal/ManagedHostWindowState.cs | 28 +++++- .../DockControlStateTests.cs | 92 +++++++++++++++++++ 4 files changed, 183 insertions(+), 39 deletions(-) diff --git a/src/Dock.Avalonia/Internal/DockControlState.cs b/src/Dock.Avalonia/Internal/DockControlState.cs index 91aa6635a..f1264140f 100644 --- a/src/Dock.Avalonia/Internal/DockControlState.cs +++ b/src/Dock.Avalonia/Internal/DockControlState.cs @@ -221,7 +221,7 @@ private DockOperationResolution ResolveDockOperation( isValid); } - private void Drop( + private bool Drop( Point point, DragAction dragAction, Control dropControl, @@ -231,21 +231,21 @@ private void Drop( { if (selectedOperation == DockOperation.None) { - return; + return false; } RemoveAdorners(); if (_context.DragControl is null || DropControl is null) { - return; + return false; } if (useGlobalOperation) { if (DropControl is not { } dropCtrl) { - return; + return false; } if (_context.DragControl.DataContext is IDockable sourceDockable @@ -267,10 +267,11 @@ private void Drop( var screenPixel = new PixelPoint((int)Math.Round(screenPoint.X), (int)Math.Round(screenPoint.Y)); var activePoint = active.PointToClient(screenPixel); Float(activePoint, active, sourceDockable, factory, _context.DragOffset); + return true; } - } - return; - } + } + return false; + } // TODO: The validation fails in floating window as ActiveDockable is a tool dock. // if (!ValidateGlobalTarget(sourceDockable, targetDock)) @@ -285,17 +286,18 @@ private void Drop( sourceRoot, targetRoot, DockSettings.GlobalDockingProportion); - } - } - else - { - if (_context.DragControl.DataContext is IDockable sourceDockable) - { - var target = DropControl.DataContext as IDockable; - if (target is null) - { - return; - } + return true; + } + } + else + { + if (_context.DragControl.DataContext is IDockable sourceDockable) + { + var target = DropControl.DataContext as IDockable; + if (target is null) + { + return false; + } if (!ValidateLocalTarget(sourceDockable, target)) { @@ -304,21 +306,30 @@ private void Drop( { var activeDockControl = _context.DragControl.FindAncestorOfType(); var factory = activeDockControl?.Layout?.Factory ?? DropControl.FindAncestorOfType()?.Layout?.Factory; - if (activeDockControl is { } active && factory is { }) + if (activeDockControl is { } active && factory is { }) { var screenPoint = DockHelpers.GetScreenPoint(relativeTo, point); var screenPixel = new PixelPoint((int)Math.Round(screenPoint.X), (int)Math.Round(screenPoint.Y)); var activePoint = active.PointToClient(screenPixel); Float(activePoint, active, sourceDockable, factory, _context.DragOffset); + return true; } - } - return; - } + } + return false; + } - Execute(point, selectedOperation, dragAction, relativeTo, sourceDockable, target); - } - } - } + if (selectedOperation == DockOperation.Window) + { + return false; + } + + Execute(point, selectedOperation, dragAction, relativeTo, sourceDockable, target); + return true; + } + } + + return false; + } private void Leave() { @@ -547,8 +558,13 @@ public void Process(Point point, Vector delta, EventType eventType, DragAction d selectedOperation = resolution.SelectedOperation; } - Drop(_context.TargetPoint, dragAction, dropControl, _context.TargetDockControl, useGlobalOperation, selectedOperation); - executed = true; + executed = Drop( + _context.TargetPoint, + dragAction, + dropControl, + _context.TargetDockControl, + useGlobalOperation, + selectedOperation); LogDragState($"Drop executed on '{dropControl.GetType().Name}' with action '{dragAction}'."); } else diff --git a/src/Dock.Avalonia/Internal/HostWindowState.cs b/src/Dock.Avalonia/Internal/HostWindowState.cs index 10c39637f..5e591be9d 100644 --- a/src/Dock.Avalonia/Internal/HostWindowState.cs +++ b/src/Dock.Avalonia/Internal/HostWindowState.cs @@ -101,7 +101,7 @@ private void Over(Point point, DragAction dragAction, Control dropControl, Visua LocalAdornerHelper.SetGlobalDockActive(globalOperation != DockOperation.None); } - private void Drop(Point point, DragAction dragAction, Control dropControl, Visual relativeTo) + private bool Drop(Point point, DragAction dragAction, Control dropControl, Visual relativeTo) { var localOperation = DockOperation.Window; var globalOperation = DockOperation.None; @@ -120,7 +120,7 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua if (DropControl is null) { - return; + return false; } var layout = _hostWindow.Window?.Layout; @@ -129,7 +129,7 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua { if (DropControl is not { } dropCtrl) { - return; + return false; } if (layout?.ActiveDockable is { } sourceDockable @@ -137,10 +137,11 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua { if (!ValidateGlobalTarget(sourceDockable, targetDock)) { - return; + return false; } Execute(point, globalOperation, dragAction, relativeTo, sourceDockable, targetDock); + return true; } } else @@ -151,9 +152,12 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua if (localOperation != DockOperation.Window) { Execute(point, localOperation, dragAction, relativeTo, sourceDockable, targetDockable); + return true; } } } + + return false; } private void Leave() @@ -327,6 +331,8 @@ public void Process(PixelPoint point, EventType eventType) { if (_context.DoDragDrop) { + var executed = false; + if (_context.TargetDockControl is { } && DropControl is { }) { var isDropEnabled = true; @@ -338,9 +344,21 @@ public void Process(PixelPoint point, EventType eventType) if (isDropEnabled) { - Drop(_context.TargetPoint, _context.DragAction, DropControl, _context.TargetDockControl); + executed = Drop(_context.TargetPoint, _context.DragAction, DropControl, _context.TargetDockControl); } - } + } + + if (!executed + && _hostWindow.DataContext is IDockable dockable + && DockCapabilityResolver.IsEnabled( + dockable, + DockCapability.Float, + DockCapabilityResolver.ResolveOperationDock(dockable)) + && _hostWindow.Window?.Factory is { } factory) + { + dockable.SetPointerScreenPosition(point.X, point.Y); + factory.FloatDockable(dockable); + } } Leave(); diff --git a/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs b/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs index 6ca524bc4..dcbedabf2 100644 --- a/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs +++ b/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs @@ -91,6 +91,8 @@ public void Process(PixelPoint screenPoint, EventType eventType) { if (_context.DoDragDrop) { + var executed = false; + if (_context.TargetDockControl is { } && DropControl is { }) { var isDropEnabled = true; @@ -101,9 +103,21 @@ public void Process(PixelPoint screenPoint, EventType eventType) if (isDropEnabled) { - Drop(_context.TargetPoint, _context.DragAction, DropControl, _context.TargetDockControl); + executed = Drop(_context.TargetPoint, _context.DragAction, DropControl, _context.TargetDockControl); } } + + if (!executed + && _context.DragDockable is { } dockable + && DockCapabilityResolver.IsEnabled( + dockable, + DockCapability.Float, + DockCapabilityResolver.ResolveOperationDock(dockable)) + && _hostWindow.Window?.Factory is { } factory) + { + dockable.SetPointerScreenPosition(screenPoint.X, screenPoint.Y); + factory.FloatDockable(dockable); + } } _dragPreviewHelper.Hide(); @@ -318,7 +332,7 @@ private void Over(Point point, DragAction dragAction, Control dropControl, Visua LocalAdornerHelper.SetGlobalDockActive(globalOperation != DockOperation.None); } - private void Drop(Point point, DragAction dragAction, Control dropControl, Visual relativeTo) + private bool Drop(Point point, DragAction dragAction, Control dropControl, Visual relativeTo) { var localOperation = DockOperation.Window; var globalOperation = DockOperation.None; @@ -337,7 +351,7 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua if (DropControl is null) { - return; + return false; } var layout = _hostWindow.Window?.Layout; @@ -346,7 +360,7 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua { if (DropControl is not { } dropCtrl) { - return; + return false; } if (layout?.ActiveDockable is { } sourceDockable @@ -354,10 +368,11 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua { if (!ValidateGlobalTarget(sourceDockable, targetDock)) { - return; + return false; } Execute(point, globalOperation, dragAction, relativeTo, sourceDockable, targetDock); + return true; } } else @@ -368,9 +383,12 @@ private void Drop(Point point, DragAction dragAction, Control dropControl, Visua if (localOperation != DockOperation.Window) { Execute(point, localOperation, dragAction, relativeTo, sourceDockable, targetDockable); + return true; } } } + + return false; } private void Leave() diff --git a/tests/Dock.Avalonia.HeadlessTests/DockControlStateTests.cs b/tests/Dock.Avalonia.HeadlessTests/DockControlStateTests.cs index 48683930a..9855974b4 100644 --- a/tests/Dock.Avalonia.HeadlessTests/DockControlStateTests.cs +++ b/tests/Dock.Avalonia.HeadlessTests/DockControlStateTests.cs @@ -9,12 +9,26 @@ using Dock.Model.Avalonia.Controls; using Dock.Model.Controls; using Dock.Model.Core; +using Dock.Settings; +using System.Reflection; using Xunit; namespace Dock.Avalonia.HeadlessTests; public class DockControlStateTests { + private sealed class RecordingFactory : Factory + { + public int FloatCount { get; private set; } + public IDockable? LastFloatedDockable { get; private set; } + + public override void FloatDockable(IDockable dockable) + { + FloatCount++; + LastFloatedDockable = dockable; + } + } + private static DockControlState CreateState(DockManager manager) { return new DockControlState(manager, new DefaultDragOffsetCalculator()); @@ -51,6 +65,84 @@ public void Process_Released_Ends_Drag() Assert.False(dock.IsDraggingDock); } + [AvaloniaFact] + public void Process_Released_Floats_WhenResolvedDropTargetHasDifferentDockGroup() + { + var factory = new RecordingFactory(); + var root = factory.CreateRootDock(); + root.Factory = factory; + root.VisibleDockables = factory.CreateList(); + + var sourceDock = factory.CreateDocumentDock(); + sourceDock.VisibleDockables = factory.CreateList(); + factory.AddDockable(root, sourceDock); + + var targetDock = factory.CreateDocumentDock(); + targetDock.VisibleDockables = factory.CreateList(); + factory.AddDockable(root, targetDock); + + var sourceDocument = factory.CreateDocument(); + sourceDocument.DockGroup = "Documents"; + factory.AddDockable(sourceDock, sourceDocument); + + var targetDocument = factory.CreateDocument(); + targetDocument.DockGroup = "Widgets"; + factory.AddDockable(targetDock, targetDocument); + + var dockControl = new DockControl + { + Layout = root + }; + var window = new Window + { + Width = 300, + Height = 200, + Content = dockControl + }; + + try + { + window.Show(); + window.UpdateLayout(); + dockControl.ApplyTemplate(); + + var state = CreateState(new DockManager(new DockService())); + var dragControl = new Control + { + DataContext = sourceDocument + }; + var dropControl = new Border + { + DataContext = targetDocument + }; + dropControl.SetValue(DockProperties.IsDropEnabledProperty, true); + + var contextField = typeof(DockControlState) + .GetField("_context", BindingFlags.Instance | BindingFlags.NonPublic)!; + var context = contextField.GetValue(state)!; + context.GetType().GetProperty("DragControl")!.SetValue(context, dragControl); + context.GetType().GetProperty("DoDragDrop")!.SetValue(context, true); + context.GetType().GetProperty("TargetPoint")!.SetValue(context, new Point(5, 5)); + context.GetType().GetProperty("TargetDockControl")!.SetValue(context, dockControl); + context.GetType().GetProperty("ResolvedOperation")!.SetValue(context, DockOperation.Fill); + context.GetType().GetProperty("UseGlobalOperation")!.SetValue(context, false); + context.GetType().GetProperty("HasResolvedOperation")!.SetValue(context, true); + + var dropControlProperty = typeof(DockManagerState) + .GetProperty("DropControl", BindingFlags.Instance | BindingFlags.NonPublic)!; + dropControlProperty.SetValue(state, dropControl); + + state.Process(new Point(5, 5), default, EventType.Released, DragAction.Move, dockControl, new List { dockControl }); + + Assert.Equal(1, factory.FloatCount); + Assert.Same(sourceDocument, factory.LastFloatedDockable); + } + finally + { + window.Close(); + } + } + [AvaloniaFact] public void GlobalDockOperationSelector_UsesGlobal_WhenNoLocalAdorner() { From cf684a63c4eea99121d72ac08460d064b59740d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley <4672627+danwalmsley@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:53:04 +0000 Subject: [PATCH 2/2] Avoid floating managed docs on failed drops --- .../Internal/ManagedHostWindowState.cs | 1 + .../ManagedWindowParityTests.cs | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs b/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs index dcbedabf2..5e2aaf9ee 100644 --- a/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs +++ b/src/Dock.Avalonia/Internal/ManagedHostWindowState.cs @@ -109,6 +109,7 @@ public void Process(PixelPoint screenPoint, EventType eventType) if (!executed && _context.DragDockable is { } dockable + && dockable is not ManagedDockWindowDocument && DockCapabilityResolver.IsEnabled( dockable, DockCapability.Float, diff --git a/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs b/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs index b90843727..2ebc31e35 100644 --- a/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs +++ b/tests/Dock.Avalonia.HeadlessTests/ManagedWindowParityTests.cs @@ -30,6 +30,17 @@ namespace Dock.Avalonia.HeadlessTests; public class ManagedWindowParityTests { + private sealed class RecordingManagedFactory : Factory + { + public int FloatCount { get; private set; } + + public override void FloatDockable(IDockable dockable) + { + FloatCount++; + base.FloatDockable(dockable); + } + } + private static (ManagedHostWindow Host, DockWindow Window, IRootDock Root) CreateManagedWindow(Factory factory) { return CreateManagedWindow(factory, new DockWindow()); @@ -401,6 +412,37 @@ public void ManagedHostWindowDrag_CaptureLost_Does_Not_Activate_DragPreview() } } + [AvaloniaFact] + public void ManagedHostWindowDrag_InvalidDrop_DoesNotFloatManagedDocument() + { + var factory = new RecordingManagedFactory(); + var (host, window, _) = CreateManagedWindow(factory); + var dock = ManagedWindowRegistry.GetOrCreateDock(factory); + var managedDocument = dock.VisibleDockables!.OfType() + .Single(document => ReferenceEquals(document.Window, window)); + + var state = new ManagedHostWindowState(new DockManager(new DockService()), host); + var contextField = typeof(ManagedHostWindowState) + .GetField("_context", BindingFlags.Instance | BindingFlags.NonPublic)!; + var context = contextField.GetValue(state)!; + context.GetType().GetProperty("DoDragDrop")!.SetValue(context, true); + context.GetType().GetProperty("DragDockable")!.SetValue(context, managedDocument); + context.GetType().GetProperty("TargetDockControl")!.SetValue(context, new DockControl()); + context.GetType().GetProperty("TargetPoint")!.SetValue(context, new Point(5, 5)); + + var dropControl = new Border(); + dropControl.SetValue(DockProperties.IsDropEnabledProperty, true); + + var dropControlProperty = typeof(DockManagerState) + .GetProperty("DropControl", BindingFlags.Instance | BindingFlags.NonPublic)!; + dropControlProperty.SetValue(state, dropControl); + + state.Process(new PixelPoint(5, 5), EventType.Released); + + Assert.Equal(0, factory.FloatCount); + Assert.Contains(managedDocument, dock.VisibleDockables!); + } + [AvaloniaFact] public void DragPreviewHelper_Uses_Managed_Layer_Overlay() {