Browse Source

[Avalonia.Native] Use relative placement for detecting drag and drop locations. (#20280)

* Use relative position of view for drag and drop

* Add Drag And Drop Test

* Add AutomationProperties
pull/16076/merge
Tim Miller 2 days ago
committed by GitHub
parent
commit
367d8b293b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      native/Avalonia.Native/src/OSX/AvnView.mm
  2. 1
      samples/IntegrationTestApp/MainWindow.axaml.cs
  3. 62
      samples/IntegrationTestApp/Pages/DragDropPage.axaml
  4. 103
      samples/IntegrationTestApp/Pages/DragDropPage.axaml.cs
  5. 117
      tests/Avalonia.IntegrationTests.Appium/DragDropTests.cs

7
native/Avalonia.Native/src/OSX/AvnView.mm

@ -854,9 +854,10 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
{
auto localPoint = [self convertPoint:[info draggingLocation] toView:self];
auto avnPoint = ToAvnPoint(localPoint);
auto point = [self translateLocalPoint:avnPoint];
NSPoint eventLocation = [info draggingLocation];
auto viewLocation = [self convertPoint:NSMakePoint(0, 0) toView:nil];
auto localPoint = NSMakePoint(eventLocation.x - viewLocation.x, viewLocation.y - eventLocation.y);
auto point = ToAvnPoint(localPoint);
auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]];
NSDragOperation nsop = [info draggingSourceOperationMask];

1
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -82,6 +82,7 @@ namespace IntegrationTestApp
new("ComboBox", () => new ComboBoxPage()),
new("ContextMenu", () => new ContextMenuPage()),
new("DesktopPage", () => new DesktopPage()),
new("DragDrop", () => new DragDropPage()),
new("Embedding", () => new EmbeddingPage()),
new("Gestures", () => new GesturesPage()),
new("ListBox", () => new ListBoxPage()),

62
samples/IntegrationTestApp/Pages/DragDropPage.axaml

@ -0,0 +1,62 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.Pages.DragDropPage">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="4">
<Button Name="ResetDragDrop" Click="ResetDragDrop_Click" Margin="0,0,8,0">Reset</Button>
<TextBlock VerticalAlignment="Center">Drop Position: </TextBlock>
<TextBlock Name="DropPosition" VerticalAlignment="Center" Margin="4,0"/>
<TextBlock VerticalAlignment="Center" Margin="8,0,0,0">Status: </TextBlock>
<TextBlock Name="DragDropStatus" VerticalAlignment="Center" Margin="4,0"/>
</StackPanel>
<!-- Use a Grid with row definitions to create offset from window origin -->
<!-- The top spacer ensures the drag/drop controls are NOT at (0,0) -->
<Grid RowDefinitions="100,*">
<!-- Spacer row to offset controls from window origin -->
<Border Grid.Row="0" Background="LightGray">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
Text="Spacer (100px) - Controls below are offset from window origin"/>
</Border>
<!-- Drag and Drop test area -->
<Grid Grid.Row="1" ColumnDefinitions="*,*" Margin="20">
<!-- Drag Source -->
<Border Name="DragSource"
Grid.Column="0"
Background="CornflowerBlue"
Margin="10"
CornerRadius="8"
AutomationProperties.AccessibilityView="Content"
PointerPressed="DragSource_PointerPressed">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="Drag Source"
Foreground="White"
FontWeight="Bold"/>
</Border>
<!-- Drop Target -->
<Border Name="DropTarget"
Grid.Column="1"
Background="LightGreen"
Margin="10"
CornerRadius="8"
AutomationProperties.AccessibilityView="Content"
DragDrop.AllowDrop="True">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="Drop Target"
HorizontalAlignment="Center"
FontWeight="Bold"/>
<TextBlock Name="DropTargetText"
HorizontalAlignment="Center"
Text="Drop items here"/>
</StackPanel>
</Border>
</Grid>
</Grid>
</DockPanel>
</UserControl>

103
samples/IntegrationTestApp/Pages/DragDropPage.axaml.cs

@ -0,0 +1,103 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
namespace IntegrationTestApp.Pages;
public partial class DragDropPage : UserControl
{
public DragDropPage()
{
InitializeComponent();
// Set up drag-drop event handlers
AddHandler(DragDrop.DragOverEvent, DropTarget_DragOver);
AddHandler(DragDrop.DropEvent, DropTarget_Drop);
}
private async void DragSource_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var dragData = new DataTransfer();
dragData.Add(DataTransferItem.CreateText("TestDragData"));
DragDropStatus.Text = "Dragging...";
var result = await DragDrop.DoDragDropAsync(e, dragData, DragDropEffects.Copy | DragDropEffects.Move);
DragDropStatus.Text = result switch
{
DragDropEffects.Copy => "Copied",
DragDropEffects.Move => "Moved",
DragDropEffects.None => "Cancelled",
_ => $"Result: {result}"
};
}
}
private void DropTarget_DragOver(object? sender, DragEventArgs e)
{
// Only handle events for the drop target
if (e.Source != DropTarget && !IsChildOf(e.Source as Visual, DropTarget))
return;
e.DragEffects = DragDropEffects.Copy;
// Get the position relative to the drop target
var position = e.GetPosition(DropTarget);
DropPosition.Text = $"DragOver: ({position.X:F0}, {position.Y:F0})";
}
private void DropTarget_Drop(object? sender, DragEventArgs e)
{
// Only handle events for the drop target
if (e.Source != DropTarget && !IsChildOf(e.Source as Visual, DropTarget))
return;
// Get the position relative to the drop target
var position = e.GetPosition(DropTarget);
DropPosition.Text = $"Drop: ({position.X:F0}, {position.Y:F0})";
// Check if the position is within reasonable bounds of the drop target
var bounds = DropTarget.Bounds;
var isWithinBounds = position.X >= 0 && position.X <= bounds.Width &&
position.Y >= 0 && position.Y <= bounds.Height;
var text = e.DataTransfer.TryGetText();
if (text != null)
{
DropTargetText.Text = isWithinBounds
? $"Dropped: {text} at ({position.X:F0}, {position.Y:F0})"
: $"ERROR: Position out of bounds! ({position.X:F0}, {position.Y:F0})";
DragDropStatus.Text = isWithinBounds ? "Drop OK" : "Drop position ERROR";
}
e.DragEffects = DragDropEffects.Copy;
}
private static bool IsChildOf(Visual? child, Visual? parent)
{
if (child == null || parent == null)
return false;
var current = child.Parent as Visual;
while (current != null)
{
if (current == parent)
return true;
current = current.Parent as Visual;
}
return false;
}
private void ResetDragDrop_Click(object? sender, RoutedEventArgs e)
{
DropPosition.Text = string.Empty;
DragDropStatus.Text = string.Empty;
DropTargetText.Text = "Drop items here";
}
}

117
tests/Avalonia.IntegrationTests.Appium/DragDropTests.cs

@ -0,0 +1,117 @@
using System;
using System.Threading;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Interactions;
using Xunit;
namespace Avalonia.IntegrationTests.Appium;
[Collection("Default")]
public class DragDropTests : TestBase
{
public DragDropTests(DefaultAppFixture fixture)
: base(fixture, "DragDrop")
{
var reset = Session.FindElementByAccessibilityId("ResetDragDrop");
reset.Click();
}
[PlatformFact(TestPlatforms.MacOS)]
public void DragDrop_Coordinates_Correct_When_Controls_Offset_From_Origin()
{
// This test verifies the fix for drag-drop coordinate calculation when
// controls are positioned away from the window origin.
// Issue: In embedded views or when controls have margin/offset from origin,
// the drag-drop coordinates were incorrectly calculated relative to the
// window rather than the view.
var dragSource = Session.FindElementByAccessibilityId("DragSource");
var dropTarget = Session.FindElementByAccessibilityId("DropTarget");
var dropPosition = Session.FindElementByAccessibilityId("DropPosition");
var dragDropStatus = Session.FindElementByAccessibilityId("DragDropStatus");
// Perform drag from source to target
new Actions(Session)
.MoveToElement(dragSource)
.ClickAndHold()
.MoveToElement(dropTarget)
.Release()
.Perform();
Thread.Sleep(500); // Allow UI to update
// Verify the drop was successful
var status = dragDropStatus.Text;
Assert.True(status == "Drop OK" || status == "Copied",
$"Expected drop to succeed, but status was: {status}");
// Verify the drop position is within the target bounds
// If the coordinate calculation bug exists, the position would be
// offset by the spacer/margin and would show negative coordinates
// or coordinates outside the target bounds
var positionText = dropPosition.Text;
Assert.StartsWith("Drop:", positionText, StringComparison.Ordinal);
// The DropTargetText should not contain "ERROR"
var dropTargetText = Session.FindElementByAccessibilityId("DropTargetText");
Assert.DoesNotContain("ERROR", dropTargetText.Text);
}
[PlatformFact(TestPlatforms.MacOS)]
public void DragDrop_Position_Updates_During_DragOver()
{
// Verifies that position is correctly reported during drag-over events
var dragSource = Session.FindElementByAccessibilityId("DragSource");
var dropTarget = Session.FindElementByAccessibilityId("DropTarget");
var dropPosition = Session.FindElementByAccessibilityId("DropPosition");
// Start drag and move over target, then release
var device = new PointerInputDevice(PointerKind.Mouse);
var builder = new ActionBuilder();
// Move to drag source and start drag
builder.AddAction(device.CreatePointerMove(dragSource, 0, 0, TimeSpan.FromMilliseconds(100)));
builder.AddAction(device.CreatePointerDown(MouseButton.Left));
// Move to drop target (this triggers DragOver events)
builder.AddAction(device.CreatePointerMove(dropTarget, 0, 0, TimeSpan.FromMilliseconds(200)));
// Pause to allow DragOver events to be processed
builder.AddAction(device.CreatePause(TimeSpan.FromMilliseconds(200)));
// Release at current position (completes the drag)
builder.AddAction(device.CreatePointerUp(MouseButton.Left));
Session.PerformActions(builder.ToActionSequenceList());
Thread.Sleep(200);
// Check that position was recorded (either DragOver or Drop)
var positionText = dropPosition.Text;
// Position should have been updated during drag-over or drop
Assert.True(positionText.Contains("DragOver:") || positionText.Contains("Drop:"),
$"Expected position to be updated during drag, but got: {positionText}");
}
[Fact]
public void DragDrop_Can_Be_Cancelled()
{
var dragSource = Session.FindElementByAccessibilityId("DragSource");
var dragDropStatus = Session.FindElementByAccessibilityId("DragDropStatus");
// Start drag but release outside any drop target
new Actions(Session)
.MoveToElement(dragSource)
.ClickAndHold()
.MoveByOffset(-200, 0) // Move away from drop target
.Release()
.Perform();
Thread.Sleep(500);
// Status should indicate cancelled
var status = dragDropStatus.Text;
Assert.Equal("Cancelled", status);
}
}
Loading…
Cancel
Save