diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs
index b151cabf43..a2048005a4 100644
--- a/samples/ControlCatalog.Desktop/Program.cs
+++ b/samples/ControlCatalog.Desktop/Program.cs
@@ -10,6 +10,7 @@ namespace ControlCatalog
{
internal class Program
{
+ [STAThread]
static void Main(string[] args)
{
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 346535d39d..b45a93455e 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -9,8 +9,10 @@ namespace ControlCatalog.NetCore
{
static class Program
{
+
static void Main(string[] args)
{
+ Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA);
if (args.Contains("--wait-for-attach"))
{
Console.WriteLine("Attach debugger and use 'Set next statement'");
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 63b0f4a961..b8a8479a49 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -2,7 +2,6 @@
netstandard2.0
-
%(Filename)
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index a0e0df450b..1107d34b3e 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -15,6 +15,7 @@
+
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.cs b/samples/ControlCatalog/Pages/DragAndDropPage.cs
new file mode 100644
index 0000000000..172331204f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.cs
@@ -0,0 +1,71 @@
+using Avalonia.Controls;
+using Avalonia.Input.DragDrop;
+using Avalonia.Markup.Xaml;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace ControlCatalog.Pages
+{
+ public class DragAndDropPage : UserControl
+ {
+ private TextBlock _DropState;
+ private TextBlock _DragState;
+ private Border _DragMe;
+ private int DragCount = 0;
+
+ public DragAndDropPage()
+ {
+ this.InitializeComponent();
+
+ _DragMe.PointerPressed += DoDrag;
+
+ AddHandler(DragDrop.DropEvent, Drop);
+ AddHandler(DragDrop.DragOverEvent, DragOver);
+ }
+
+ private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
+ {
+ DataObject dragData = new DataObject();
+ dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
+
+ var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy);
+ switch(result)
+ {
+ case DragDropEffects.Copy:
+ _DragState.Text = "The text was copied"; break;
+ case DragDropEffects.Link:
+ _DragState.Text = "The text was linked"; break;
+ case DragDropEffects.None:
+ _DragState.Text = "The drag operation was canceled"; break;
+ }
+ }
+
+ private void DragOver(object sender, DragEventArgs e)
+ {
+ // Only allow Copy or Link as Drop Operations.
+ e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
+
+ // Only allow if the dragged data contains text or filenames.
+ if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames))
+ e.DragEffects = DragDropEffects.None;
+ }
+
+ private void Drop(object sender, DragEventArgs e)
+ {
+ if (e.Data.Contains(DataFormats.Text))
+ _DropState.Text = e.Data.GetText();
+ else if (e.Data.Contains(DataFormats.FileNames))
+ _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _DropState = this.Find("DropState");
+ _DragState = this.Find("DragState");
+ _DragMe = this.Find("DragMe");
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
new file mode 100644
index 0000000000..af679d2f9a
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
@@ -0,0 +1,19 @@
+
+
+ Drag+Drop
+ Example of Drag+Drop capabilities
+
+
+
+ Drag Me
+
+
+ Drop some text or files here
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index 06c1a8b4cc..3dc07f81e6 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -12,6 +12,10 @@ using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Threading;
using System.Reactive.Concurrency;
+using Avalonia.Input.DragDrop.Raw;
+using Avalonia.Controls.Platform;
+using Avalonia.Platform;
+using Avalonia.Input.DragDrop;
namespace Avalonia
{
@@ -234,7 +238,9 @@ namespace Avalonia
.Bind().ToConstant(_styler)
.Bind().ToSingleton()
.Bind().ToConstant(this)
- .Bind().ToConstant(AvaloniaScheduler.Instance);
+ .Bind().ToConstant(AvaloniaScheduler.Instance)
+ .Bind().ToConstant(DragDropDevice.Instance)
+ .Bind().ToTransient();
}
}
}
diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
new file mode 100644
index 0000000000..1e97c7bf96
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
@@ -0,0 +1,212 @@
+using System;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Input.DragDrop;
+using Avalonia.Input.DragDrop.Raw;
+using Avalonia.Input.Platform;
+using Avalonia.Input.Raw;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Platform
+{
+ class InProcessDragSource : IPlatformDragSource
+ {
+ private const InputModifiers MOUSE_INPUTMODIFIERS = InputModifiers.LeftMouseButton|InputModifiers.MiddleMouseButton|InputModifiers.RightMouseButton;
+ private readonly IDragDropDevice _dragDrop;
+ private readonly IInputManager _inputManager;
+ private readonly Subject _result = new Subject();
+
+ private DragDropEffects _allowedEffects;
+ private IDataObject _draggedData;
+ private IInputElement _lastRoot;
+ private Point _lastPosition;
+ private StandardCursorType _lastCursorType;
+ private object _originalCursor;
+ private InputModifiers? _initialInputModifiers;
+
+ public InProcessDragSource()
+ {
+ _inputManager = AvaloniaLocator.Current.GetService();
+ _dragDrop = AvaloniaLocator.Current.GetService();
+ }
+
+ public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ if (_draggedData == null)
+ {
+ _draggedData = data;
+ _lastRoot = null;
+ _lastPosition = default(Point);
+ _allowedEffects = allowedEffects;
+
+ using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents))
+ {
+ using (_inputManager.PreProcess.OfType().Subscribe(ProcessKeyEvents))
+ {
+ var effect = await _result.FirstAsync();
+ return effect;
+ }
+ }
+ }
+ return DragDropEffects.None;
+ }
+
+
+ private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputElement root, Point pt, InputModifiers modifiers)
+ {
+ _lastPosition = pt;
+
+ RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects);
+ var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
+ tl.PlatformImpl.Input(rawEvent);
+
+ var effect = GetPreferredEffect(rawEvent.Effects & _allowedEffects, modifiers);
+ UpdateCursor(root, effect);
+ return effect;
+ }
+
+ private DragDropEffects GetPreferredEffect(DragDropEffects effect, InputModifiers modifiers)
+ {
+ if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None)
+ return effect; // No need to check for the modifiers.
+ if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(InputModifiers.Alt))
+ return DragDropEffects.Link;
+ if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(InputModifiers.Control))
+ return DragDropEffects.Copy;
+ return DragDropEffects.Move;
+ }
+
+ private StandardCursorType GetCursorForDropEffect(DragDropEffects effects)
+ {
+ if (effects.HasFlag(DragDropEffects.Copy))
+ return StandardCursorType.DragCopy;
+ if (effects.HasFlag(DragDropEffects.Move))
+ return StandardCursorType.DragMove;
+ if (effects.HasFlag(DragDropEffects.Link))
+ return StandardCursorType.DragLink;
+ return StandardCursorType.No;
+ }
+
+ private void UpdateCursor(IInputElement root, DragDropEffects effect)
+ {
+ if (_lastRoot != root)
+ {
+ if (_lastRoot is InputElement ieLast)
+ {
+ if (_originalCursor == AvaloniaProperty.UnsetValue)
+ ieLast.ClearValue(InputElement.CursorProperty);
+ else
+ ieLast.Cursor = _originalCursor as Cursor;
+ }
+
+ if (root is InputElement ieNew)
+ {
+ if (!ieNew.IsSet(InputElement.CursorProperty))
+ _originalCursor = AvaloniaProperty.UnsetValue;
+ else
+ _originalCursor = root.Cursor;
+ }
+ else
+ _originalCursor = null;
+
+ _lastCursorType = StandardCursorType.Arrow;
+ _lastRoot = root;
+ }
+
+ if (root is InputElement ie)
+ {
+ var ct = GetCursorForDropEffect(effect);
+ if (ct != _lastCursorType)
+ {
+ _lastCursorType = ct;
+ ie.Cursor = new Cursor(ct);
+ }
+ }
+ }
+
+ private void CancelDragging()
+ {
+ if (_lastRoot != null)
+ RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, InputModifiers.None);
+ UpdateCursor(null, DragDropEffects.None);
+ _result.OnNext(DragDropEffects.None);
+ }
+
+ private void ProcessKeyEvents(RawKeyEventArgs e)
+ {
+ if (e.Type == RawKeyEventType.KeyDown && e.Key == Key.Escape)
+ {
+ if (_lastRoot != null)
+ RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, e.Modifiers);
+ UpdateCursor(null, DragDropEffects.None);
+ _result.OnNext(DragDropEffects.None);
+ e.Handled = true;
+ }
+ else if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt)
+ RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers);
+ }
+
+ private void ProcessMouseEvents(RawMouseEventArgs e)
+ {
+ if (!_initialInputModifiers.HasValue)
+ _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS;
+
+
+ void CheckDraggingAccepted(InputModifiers changedMouseButton)
+ {
+ if (_initialInputModifiers.Value.HasFlag(changedMouseButton))
+ {
+ var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers);
+ UpdateCursor(null, DragDropEffects.None);
+ _result.OnNext(result);
+ }
+ else
+ CancelDragging();
+ e.Handled = true;
+ }
+
+ switch (e.Type)
+ {
+ case RawMouseEventType.LeftButtonDown:
+ case RawMouseEventType.RightButtonDown:
+ case RawMouseEventType.MiddleButtonDown:
+ case RawMouseEventType.NonClientLeftButtonDown:
+ CancelDragging();
+ e.Handled = true;
+ return;
+ case RawMouseEventType.LeaveWindow:
+ RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break;
+ case RawMouseEventType.LeftButtonUp:
+ CheckDraggingAccepted(InputModifiers.LeftMouseButton); break;
+ case RawMouseEventType.MiddleButtonUp:
+ CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break;
+ case RawMouseEventType.RightButtonUp:
+ CheckDraggingAccepted(InputModifiers.RightMouseButton); break;
+ case RawMouseEventType.Move:
+ var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS;
+ if (_initialInputModifiers.Value != mods)
+ {
+ CancelDragging();
+ e.Handled = true;
+ return;
+ }
+
+ if (e.Root != _lastRoot)
+ {
+ if (_lastRoot != null)
+ RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), e.InputModifiers);
+ RaiseEventAndUpdateCursor(RawDragEventType.DragEnter, e.Root, e.Position, e.InputModifiers);
+ }
+ else
+ RaiseEventAndUpdateCursor(RawDragEventType.DragOver, e.Root, e.Position, e.InputModifiers);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
index ae8c88f7e8..b0877e0fb7 100644
--- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs
@@ -11,6 +11,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.DragDrop")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]
diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs
index e3860e58e5..02a026c998 100644
--- a/src/Avalonia.Input/Cursors.cs
+++ b/src/Avalonia.Input/Cursors.cs
@@ -38,7 +38,10 @@ namespace Avalonia.Input
TopLeftCorner,
TopRightCorner,
BottomLeftCorner,
- BottomRightCorner
+ BottomRightCorner,
+ DragMove,
+ DragCopy,
+ DragLink,
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
// We might enable them later, preferably, by loading pixmax direclty from theme with fallback image
diff --git a/src/Avalonia.Input/DragDrop/DataFormats.cs b/src/Avalonia.Input/DragDrop/DataFormats.cs
new file mode 100644
index 0000000000..4115701478
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/DataFormats.cs
@@ -0,0 +1,15 @@
+namespace Avalonia.Input.DragDrop
+{
+ public static class DataFormats
+ {
+ ///
+ /// Dataformat for plaintext
+ ///
+ public static string Text = nameof(Text);
+
+ ///
+ /// Dataformat for one or more filenames
+ ///
+ public static string FileNames = nameof(FileNames);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/DataObject.cs b/src/Avalonia.Input/DragDrop/DataObject.cs
new file mode 100644
index 0000000000..0db1008a6c
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/DataObject.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Input.DragDrop
+{
+ public class DataObject : IDataObject
+ {
+ private readonly Dictionary _items = new Dictionary();
+
+ public bool Contains(string dataFormat)
+ {
+ return _items.ContainsKey(dataFormat);
+ }
+
+ public object Get(string dataFormat)
+ {
+ if (_items.ContainsKey(dataFormat))
+ return _items[dataFormat];
+ return null;
+ }
+
+ public IEnumerable GetDataFormats()
+ {
+ return _items.Keys;
+ }
+
+ public IEnumerable GetFileNames()
+ {
+ return Get(DataFormats.FileNames) as IEnumerable;
+ }
+
+ public string GetText()
+ {
+ return Get(DataFormats.Text) as string;
+ }
+
+ public void Set(string dataFormat, object value)
+ {
+ _items[dataFormat] = value;
+ }
+ }
+}
diff --git a/src/Avalonia.Input/DragDrop/DragDrop.cs b/src/Avalonia.Input/DragDrop/DragDrop.cs
new file mode 100644
index 0000000000..0cbbfb1dbe
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/DragDrop.cs
@@ -0,0 +1,54 @@
+using System.Threading.Tasks;
+using Avalonia.Interactivity;
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Input.DragDrop
+{
+ public static class DragDrop
+ {
+ ///
+ /// Event which is raised, when a drag-and-drop operation enters the element.
+ ///
+ public static RoutedEvent DragEnterEvent = RoutedEvent.Register("DragEnter", RoutingStrategies.Bubble, typeof(DragDrop));
+ ///
+ /// Event which is raised, when a drag-and-drop operation leaves the element.
+ ///
+ public static RoutedEvent DragLeaveEvent = RoutedEvent.Register("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop));
+ ///
+ /// Event which is raised, when a drag-and-drop operation is updated while over the element.
+ ///
+ public static RoutedEvent DragOverEvent = RoutedEvent.Register("DragOver", RoutingStrategies.Bubble, typeof(DragDrop));
+ ///
+ /// Event which is raised, when a drag-and-drop operation should complete over the element.
+ ///
+ public static RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop));
+
+ public static AvaloniaProperty AllowDropProperty = AvaloniaProperty.RegisterAttached("AllowDrop", typeof(DragDrop), inherits: true);
+
+ ///
+ /// Gets a value indicating whether the given element can be used as the target of a drag-and-drop operation.
+ ///
+ public static bool GetAllowDrop(Interactive interactive)
+ {
+ return interactive.GetValue(AllowDropProperty);
+ }
+
+ ///
+ /// Sets a value indicating whether the given interactive can be used as the target of a drag-and-drop operation.
+ ///
+ public static void SetAllowDrop(Interactive interactive, bool value)
+ {
+ interactive.SetValue(AllowDropProperty, value);
+ }
+
+ ///
+ /// Starts a dragging operation with the given and returns the applied drop effect from the target.
+ ///
+ ///
+ public static Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ {
+ var src = AvaloniaLocator.Current.GetService();
+ return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/DragDropDevice.cs b/src/Avalonia.Input/DragDrop/DragDropDevice.cs
new file mode 100644
index 0000000000..7ef6f212f8
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/DragDropDevice.cs
@@ -0,0 +1,112 @@
+using Avalonia.Interactivity;
+using Avalonia.VisualTree;
+using System.Linq;
+using Avalonia.Input.DragDrop.Raw;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Input.DragDrop
+{
+ public class DragDropDevice : IDragDropDevice
+ {
+ public static readonly DragDropDevice Instance = new DragDropDevice();
+
+ private Interactive _lastTarget = null;
+
+ private Interactive GetTarget(IInputElement root, Point local)
+ {
+ var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault();
+ if (target != null && DragDrop.GetAllowDrop(target))
+ return target;
+ return null;
+ }
+
+ private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data)
+ {
+ if (target == null)
+ return DragDropEffects.None;
+ var args = new DragEventArgs(routedEvent, data)
+ {
+ RoutedEvent = routedEvent,
+ DragEffects = operation
+ };
+ target.RaiseEvent(args);
+ return args.DragEffects;
+ }
+
+ private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+ {
+ _lastTarget = GetTarget(inputRoot, point);
+ return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data);
+ }
+
+ private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+ {
+ var target = GetTarget(inputRoot, point);
+
+ if (target == _lastTarget)
+ return RaiseDragEvent(target, DragDrop.DragOverEvent, effects, data);
+
+ try
+ {
+ if (_lastTarget != null)
+ _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent));
+ return RaiseDragEvent(target, DragDrop.DragEnterEvent, effects, data);
+ }
+ finally
+ {
+ _lastTarget = target;
+ }
+ }
+
+ private void DragLeave(IInputElement inputRoot)
+ {
+ if (_lastTarget == null)
+ return;
+ try
+ {
+ _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent));
+ }
+ finally
+ {
+ _lastTarget = null;
+ }
+ }
+
+ private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects)
+ {
+ try
+ {
+ return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, effects, data);
+ }
+ finally
+ {
+ _lastTarget = null;
+ }
+ }
+
+ public void ProcessRawEvent(RawInputEventArgs e)
+ {
+ if (!e.Handled && e is RawDragEvent margs)
+ ProcessRawEvent(margs);
+ }
+
+ private void ProcessRawEvent(RawDragEvent e)
+ {
+ switch (e.Type)
+ {
+ case RawDragEventType.DragEnter:
+ e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects);
+ break;
+ case RawDragEventType.DragOver:
+ e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects);
+ break;
+ case RawDragEventType.DragLeave:
+ DragLeave(e.InputRoot);
+ break;
+ case RawDragEventType.Drop:
+ e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects);
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/DragDropEffects.cs b/src/Avalonia.Input/DragDrop/DragDropEffects.cs
new file mode 100644
index 0000000000..7f093bc89c
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/DragDropEffects.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Avalonia.Input.DragDrop
+{
+ [Flags]
+ public enum DragDropEffects
+ {
+ None = 0,
+ Copy = 1,
+ Move = 2,
+ Link = 4,
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/DragEventArgs.cs b/src/Avalonia.Input/DragDrop/DragEventArgs.cs
new file mode 100644
index 0000000000..bff19c760c
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/DragEventArgs.cs
@@ -0,0 +1,18 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.DragDrop
+{
+ public class DragEventArgs : RoutedEventArgs
+ {
+ public DragDropEffects DragEffects { get; set; }
+
+ public IDataObject Data { get; private set; }
+
+ public DragEventArgs(RoutedEvent routedEvent, IDataObject data)
+ : base(routedEvent)
+ {
+ this.Data = data;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/IDataObject.cs b/src/Avalonia.Input/DragDrop/IDataObject.cs
new file mode 100644
index 0000000000..bf2d00f529
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/IDataObject.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Input.DragDrop
+{
+ ///
+ /// Interface to access information about the data of a drag-and-drop operation.
+ ///
+ public interface IDataObject
+ {
+ ///
+ /// Lists all formats which are present in the DataObject.
+ ///
+ ///
+ IEnumerable GetDataFormats();
+
+ ///
+ /// Checks wether a given DataFormat is present in this object
+ ///
+ ///
+ bool Contains(string dataFormat);
+
+ ///
+ /// Returns the dragged text if the DataObject contains any text.
+ ///
+ ///
+ string GetText();
+
+ ///
+ /// Returns a list of filenames if the DataObject contains filenames.
+ ///
+ ///
+ IEnumerable GetFileNames();
+
+ ///
+ /// Tries to get the data of the given DataFormat.
+ ///
+ object Get(string dataFormat);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/Raw/IDragDropDevice.cs b/src/Avalonia.Input/DragDrop/Raw/IDragDropDevice.cs
new file mode 100644
index 0000000000..2022e842ae
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/Raw/IDragDropDevice.cs
@@ -0,0 +1,8 @@
+using Avalonia.Input;
+
+namespace Avalonia.Input.DragDrop.Raw
+{
+ public interface IDragDropDevice : IInputDevice
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/Raw/RawDragEvent.cs b/src/Avalonia.Input/DragDrop/Raw/RawDragEvent.cs
new file mode 100644
index 0000000000..af18186d4b
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/Raw/RawDragEvent.cs
@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Input.DragDrop.Raw
+{
+ public class RawDragEvent : RawInputEventArgs
+ {
+ public IInputElement InputRoot { get; }
+ public Point Location { get; }
+ public IDataObject Data { get; }
+ public DragDropEffects Effects { get; set; }
+ public RawDragEventType Type { get; }
+
+ public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type,
+ IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects)
+ :base(inputDevice, 0)
+ {
+ Type = type;
+ InputRoot = inputRoot;
+ Location = location;
+ Data = data;
+ Effects = effects;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/DragDrop/Raw/RawDragEventType.cs b/src/Avalonia.Input/DragDrop/Raw/RawDragEventType.cs
new file mode 100644
index 0000000000..31450efb51
--- /dev/null
+++ b/src/Avalonia.Input/DragDrop/Raw/RawDragEventType.cs
@@ -0,0 +1,10 @@
+namespace Avalonia.Input.DragDrop.Raw
+{
+ public enum RawDragEventType
+ {
+ DragEnter,
+ DragOver,
+ DragLeave,
+ Drop
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Input/Platform/IPlatformDragSource.cs b/src/Avalonia.Input/Platform/IPlatformDragSource.cs
new file mode 100644
index 0000000000..44b4d30760
--- /dev/null
+++ b/src/Avalonia.Input/Platform/IPlatformDragSource.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Input.DragDrop;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input.Platform
+{
+ public interface IPlatformDragSource
+ {
+ Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects);
+ }
+}
diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs
index ac547b8bc2..d6a3c1f260 100644
--- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs
+++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs
@@ -32,7 +32,10 @@ namespace Avalonia.Gtk3
{StandardCursorType.TopLeftCorner, CursorType.TopLeftCorner},
{StandardCursorType.TopRightCorner, CursorType.TopRightCorner},
{StandardCursorType.BottomLeftCorner, CursorType.BottomLeftCorner},
- {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}
+ {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner},
+ {StandardCursorType.DragCopy, CursorType.CenterPtr},
+ {StandardCursorType.DragMove, CursorType.Fleur},
+ {StandardCursorType.DragLink, CursorType.Cross},
};
private static readonly Dictionary Cache =
diff --git a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj
index 5f6be91571..c31c131ea9 100644
--- a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj
+++ b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0
True
@@ -19,4 +19,4 @@
-
+
\ No newline at end of file
diff --git a/src/OSX/Avalonia.MonoMac/Cursor.cs b/src/OSX/Avalonia.MonoMac/Cursor.cs
index 10445e62e2..d9370e527b 100644
--- a/src/OSX/Avalonia.MonoMac/Cursor.cs
+++ b/src/OSX/Avalonia.MonoMac/Cursor.cs
@@ -51,6 +51,10 @@ namespace Avalonia.MonoMac
[StandardCursorType.TopSide] = NSCursor.ResizeUpCursor,
[StandardCursorType.UpArrow] = NSCursor.ResizeUpCursor,
[StandardCursorType.Wait] = NSCursor.ArrowCursor, //TODO
+ [StandardCursorType.DragMove] = NSCursor.DragCopyCursor, // TODO
+ [StandardCursorType.DragCopy] = NSCursor.DragCopyCursor,
+ [StandardCursorType.DragLink] = NSCursor.DragLinkCursor,
+
};
}
diff --git a/src/OSX/Avalonia.MonoMac/DragSource.cs b/src/OSX/Avalonia.MonoMac/DragSource.cs
new file mode 100644
index 0000000000..96d650ab3a
--- /dev/null
+++ b/src/OSX/Avalonia.MonoMac/DragSource.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Runtime.InteropServices;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input.DragDrop;
+using Avalonia.Input.Platform;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using MonoMac;
+using MonoMac.AppKit;
+using MonoMac.CoreGraphics;
+using MonoMac.Foundation;
+using MonoMac.OpenGL;
+
+namespace Avalonia.MonoMac
+{
+ public class DragSource : NSDraggingSource, IPlatformDragSource
+ {
+ private const string NSPasteboardTypeString = "public.utf8-plain-text";
+ private const string NSPasteboardTypeFileUrl = "public.file-url";
+
+ private readonly Subject _result = new Subject();
+ private readonly IInputManager _inputManager;
+ private DragDropEffects _allowedEffects;
+
+ public override bool IgnoreModifierKeysWhileDragging => false;
+
+ public DragSource()
+ {
+ _inputManager = AvaloniaLocator.Current.GetService();
+ }
+
+ private string DataFormatToUTI(string s)
+ {
+ if (s == DataFormats.FileNames)
+ return NSPasteboardTypeFileUrl;
+ if (s == DataFormats.Text)
+ return NSPasteboardTypeString;
+ return s;
+ }
+
+ private NSDraggingItem CreateDraggingItem(string format, object data)
+ {
+ var pasteboardItem = new NSPasteboardItem();
+ NSData nsData;
+ if (data is string s)
+ {
+ if (format == DataFormats.FileNames)
+ s = new Uri(s).AbsoluteUri; // Ensure file uris...
+ nsData = NSData.FromString(s);
+ }
+ else if (data is Stream strm)
+ nsData = NSData.FromStream(strm);
+ else if (data is byte[] bytes)
+ nsData = NSData.FromArray(bytes);
+ else
+ {
+ BinaryFormatter bf = new BinaryFormatter();
+ using (var ms = new MemoryStream())
+ {
+ bf.Serialize(ms, data);
+ ms.Position = 0;
+ nsData = NSData.FromStream(ms);
+ }
+ }
+ pasteboardItem.SetDataForType(nsData, DataFormatToUTI(format));
+
+ NSPasteboardWriting writing = new NSPasteboardWriting(pasteboardItem.Handle);
+
+ return new NSDraggingItem(writing);
+ }
+
+ public IEnumerable CreateDraggingItems(string format, object data)
+ {
+ if (format == DataFormats.FileNames && data is IEnumerable files)
+ {
+ foreach (var file in files)
+ yield return CreateDraggingItem(format, file);
+
+ yield break;
+ }
+
+ yield return CreateDraggingItem(format, data);
+ }
+
+
+ public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ {
+ // We need the TopLevelImpl + a mouse location so we just wait for the next event.
+ var mouseEv = await _inputManager.PreProcess.OfType().FirstAsync();
+ var view = ((mouseEv.Root as TopLevel)?.PlatformImpl as TopLevelImpl)?.View;
+ if (view == null)
+ return DragDropEffects.None;
+
+ // Prepare the source event:
+ var pt = view.TranslateLocalPoint(mouseEv.Position).ToMonoMacPoint();
+ var ev = NSEvent.MouseEvent(NSEventType.LeftMouseDown, pt, 0, 0, 0, null, 0, 0, 0);
+
+ _allowedEffects = allowedEffects;
+ var items = data.GetDataFormats().SelectMany(fmt => CreateDraggingItems(fmt, data.Get(fmt))).ToArray();
+ view.BeginDraggingSession(items ,ev, this);
+
+ return await _result;
+ }
+
+ public override NSDragOperation DraggingSourceOperationMaskForLocal(bool flag)
+ {
+ return DraggingInfo.ConvertDragOperation(_allowedEffects);
+ }
+
+ public override void DraggedImageEndedAtOperation(NSImage image, CGPoint screenPoint, NSDragOperation operation)
+ {
+ _result.OnNext(DraggingInfo.ConvertDragOperation(operation));
+ _result.OnCompleted();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs
new file mode 100644
index 0000000000..ca8a24ba82
--- /dev/null
+++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Input.DragDrop;
+using MonoMac.AppKit;
+using MonoMac.Foundation;
+
+namespace Avalonia.MonoMac
+{
+ class DraggingInfo : IDataObject
+ {
+ private readonly NSDraggingInfo _info;
+
+ public DraggingInfo(NSDraggingInfo info)
+ {
+ _info = info;
+ }
+
+ internal static NSDragOperation ConvertDragOperation(DragDropEffects d)
+ {
+ NSDragOperation result = NSDragOperation.None;
+ if (d.HasFlag(DragDropEffects.Copy))
+ result |= NSDragOperation.Copy;
+ if (d.HasFlag(DragDropEffects.Link))
+ result |= NSDragOperation.Link;
+ if (d.HasFlag(DragDropEffects.Move))
+ result |= NSDragOperation.Move;
+ return result;
+ }
+
+ internal static DragDropEffects ConvertDragOperation(NSDragOperation d)
+ {
+ DragDropEffects result = DragDropEffects.None;
+ if (d.HasFlag(NSDragOperation.Copy))
+ result |= DragDropEffects.Copy;
+ if (d.HasFlag(NSDragOperation.Link))
+ result |= DragDropEffects.Link;
+ if (d.HasFlag(NSDragOperation.Move))
+ result |= DragDropEffects.Move;
+ return result;
+ }
+
+ public Point Location => new Point(_info.DraggingLocation.X, _info.DraggingLocation.Y);
+
+ public IEnumerable GetDataFormats()
+ {
+ return _info.DraggingPasteboard.Types.Select(NSTypeToWellknownType);
+ }
+
+ private string NSTypeToWellknownType(string type)
+ {
+ if (type == NSPasteboard.NSStringType)
+ return DataFormats.Text;
+ if (type == NSPasteboard.NSFilenamesType)
+ return DataFormats.FileNames;
+ return type;
+ }
+
+ public string GetText()
+ {
+ return _info.DraggingPasteboard.GetStringForType(NSPasteboard.NSStringType);
+ }
+
+ public IEnumerable GetFileNames()
+ {
+ using(var fileNames = (NSArray)_info.DraggingPasteboard.GetPropertyListForType(NSPasteboard.NSFilenamesType))
+ {
+ if (fileNames != null)
+ return NSArray.StringArrayFromHandle(fileNames.Handle);
+ }
+
+ return Enumerable.Empty();
+ }
+
+ public bool Contains(string dataFormat)
+ {
+ return GetDataFormats().Any(f => f == dataFormat);
+ }
+
+ public object Get(string dataFormat)
+ {
+ if (dataFormat == DataFormats.Text)
+ return GetText();
+ if (dataFormat == DataFormats.FileNames)
+ return GetFileNames();
+
+ return _info.DraggingPasteboard.GetDataForType(dataFormat).ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
index 5907459459..ba45ad8403 100644
--- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
+++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
@@ -35,7 +35,8 @@ namespace Avalonia.MonoMac
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(s_renderLoop)
- .Bind().ToConstant(PlatformThreadingInterface.Instance);
+ .Bind().ToConstant(PlatformThreadingInterface.Instance)
+ /*.Bind().ToTransient()*/;
}
public static void Initialize()
diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
index a655bc1ec5..34d0949b57 100644
--- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using Avalonia.Input.DragDrop;
+using Avalonia.Input.DragDrop.Raw;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
@@ -18,6 +20,7 @@ namespace Avalonia.MonoMac
{
public TopLevelView View { get; }
private readonly IMouseDevice _mouse = AvaloniaLocator.Current.GetService();
+ private readonly IDragDropDevice _dragDevice = AvaloniaLocator.Current.GetService();
protected TopLevelImpl()
{
View = new TopLevelView(this);
@@ -53,6 +56,10 @@ namespace Avalonia.MonoMac
_tl = tl;
_mouse = AvaloniaLocator.Current.GetService();
_keyboard = AvaloniaLocator.Current.GetService();
+
+ RegisterForDraggedTypes(new string[] {
+ "public.data" // register for any kind of data.
+ });
}
protected override void Dispose(bool disposing)
@@ -149,6 +156,48 @@ namespace Avalonia.MonoMac
UpdateCursor();
}
+ private NSDragOperation SendRawDragEvent(NSDraggingInfo sender, RawDragEventType type)
+ {
+ Action input = _tl.Input;
+ IDragDropDevice dragDevice = _tl._dragDevice;
+ IInputRoot root = _tl?.InputRoot;
+ if (root == null || dragDevice == null || input == null)
+ return NSDragOperation.None;
+
+ var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask);
+ DraggingInfo info = new DraggingInfo(sender);
+ var pt = TranslateLocalPoint(info.Location);
+ var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp);
+ input(args);
+ return DraggingInfo.ConvertDragOperation(args.Effects);
+ }
+
+ public override NSDragOperation DraggingEntered(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.DragEnter);
+ }
+
+ public override NSDragOperation DraggingUpdated(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.DragOver);
+ }
+
+ public override void DraggingExited(NSDraggingInfo sender)
+ {
+ SendRawDragEvent(sender, RawDragEventType.DragLeave);
+ }
+
+ public override bool PrepareForDragOperation(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.DragOver) != NSDragOperation.None;
+ }
+
+ public override bool PerformDragOperation(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.Drop) != NSDragOperation.None;
+ }
+
+
public override void SetFrameSize(CGSize newSize)
{
lock (SyncRoot)
diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs
new file mode 100644
index 0000000000..a604f634f6
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using Avalonia.Input.DragDrop;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ static class ClipboardFormats
+ {
+ private const int MAX_FORMAT_NAME_LENGTH = 260;
+
+ class ClipboardFormat
+ {
+ public short Format { get; private set; }
+ public string Name { get; private set; }
+ public short[] Synthesized { get; private set; }
+
+ public ClipboardFormat(string name, short format, params short[] synthesized)
+ {
+ Format = format;
+ Name = name;
+ Synthesized = synthesized;
+ }
+ }
+
+ private static readonly List FormatList = new List()
+ {
+ new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT),
+ new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP),
+ };
+
+
+ private static string QueryFormatName(short format)
+ {
+ StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH);
+ if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0)
+ return sb.ToString();
+ return null;
+ }
+
+ public static string GetFormat(short format)
+ {
+ lock (FormatList)
+ {
+ var pd = FormatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0);
+ if (pd == null)
+ {
+ string name = QueryFormatName(format);
+ if (string.IsNullOrEmpty(name))
+ name = string.Format("Unknown_Format_{0}", format);
+ pd = new ClipboardFormat(name, format);
+ FormatList.Add(pd);
+ }
+ return pd.Name;
+ }
+ }
+
+ public static short GetFormat(string format)
+ {
+ lock (FormatList)
+ {
+ var pd = FormatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format));
+ if (pd == null)
+ {
+ int id = UnmanagedMethods.RegisterClipboardFormat(format);
+ if (id == 0)
+ throw new Win32Exception();
+ pd = new ClipboardFormat(format, (short)id);
+ FormatList.Add(pd);
+ }
+ return pd.Format;
+ }
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs
index 0d529d6b91..fa2fbe4810 100644
--- a/src/Windows/Avalonia.Win32/CursorFactory.cs
+++ b/src/Windows/Avalonia.Win32/CursorFactory.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Platform;
+using System.Runtime.InteropServices;
namespace Avalonia.Win32
{
@@ -20,6 +21,27 @@ namespace Avalonia.Win32
{
}
+ static CursorFactory()
+ {
+ LoadModuleCursor(StandardCursorType.DragMove, "ole32.dll", 2);
+ LoadModuleCursor(StandardCursorType.DragCopy, "ole32.dll", 3);
+ LoadModuleCursor(StandardCursorType.DragLink, "ole32.dll", 4);
+ }
+
+ private static void LoadModuleCursor(StandardCursorType cursorType, string module, int id)
+ {
+ IntPtr mh = UnmanagedMethods.GetModuleHandle(module);
+ if (mh != IntPtr.Zero)
+ {
+ IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id));
+ if (cursor != IntPtr.Zero)
+ {
+ PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType);
+ Cache.Add(cursorType, phCursor);
+ }
+ }
+ }
+
private static readonly Dictionary CursorTypeMapping = new Dictionary
{
@@ -47,6 +69,11 @@ namespace Avalonia.Win32
//Using SizeNorthEastSouthWest
{StandardCursorType.TopRightCorner, 32643},
{StandardCursorType.BottomLeftCorner, 32643},
+
+ // Fallback, should have been loaded from ole32.dll
+ {StandardCursorType.DragMove, 32516},
+ {StandardCursorType.DragCopy, 32516},
+ {StandardCursorType.DragLink, 32516},
};
private static readonly Dictionary Cache =
diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs
new file mode 100644
index 0000000000..2a990419e7
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/DataObject.cs
@@ -0,0 +1,361 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+using Avalonia.Input.DragDrop;
+using Avalonia.Win32.Interop;
+using IDataObject = Avalonia.Input.DragDrop.IDataObject;
+using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+
+namespace Avalonia.Win32
+{
+ class DataObject : IDataObject, IOleDataObject
+ {
+ // Compatibility with WinForms + WPF...
+ internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray();
+
+ class FormatEnumerator : IEnumFORMATETC
+ {
+ private FORMATETC[] _formats;
+ private int _current;
+
+ private FormatEnumerator(FORMATETC[] formats, int current)
+ {
+ _formats = formats;
+ _current = current;
+ }
+
+ public FormatEnumerator(IDataObject dataobj)
+ {
+ _formats = dataobj.GetDataFormats().Select(ConvertToFormatEtc).ToArray();
+ _current = 0;
+ }
+
+ private FORMATETC ConvertToFormatEtc(string aFormatName)
+ {
+ FORMATETC result = default(FORMATETC);
+ result.cfFormat = ClipboardFormats.GetFormat(aFormatName);
+ result.dwAspect = DVASPECT.DVASPECT_CONTENT;
+ result.ptd = IntPtr.Zero;
+ result.lindex = -1;
+ result.tymed = TYMED.TYMED_HGLOBAL;
+ return result;
+ }
+
+ public void Clone(out IEnumFORMATETC newEnum)
+ {
+ newEnum = new FormatEnumerator(_formats, _current);
+ }
+
+ public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched)
+ {
+ if (rgelt == null)
+ return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG);
+
+ int i = 0;
+ while (i < celt && _current < _formats.Length)
+ {
+ rgelt[i] = _formats[_current];
+ _current++;
+ i++;
+ }
+ if (pceltFetched != null)
+ pceltFetched[0] = i;
+
+ if (i != celt)
+ return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+
+ public int Reset()
+ {
+ _current = 0;
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+
+ public int Skip(int celt)
+ {
+ _current += Math.Min(celt, int.MaxValue - _current);
+ if (_current >= _formats.Length)
+ return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ }
+
+ private const int DV_E_TYMED = unchecked((int)0x80040069);
+ private const int DV_E_DVASPECT = unchecked((int)0x8004006B);
+ private const int DV_E_FORMATETC = unchecked((int)0x80040064);
+ private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003);
+ private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070);
+ private const int GMEM_ZEROINIT = 0x0040;
+ private const int GMEM_MOVEABLE = 0x0002;
+
+
+ IDataObject _wrapped;
+
+ public DataObject(IDataObject wrapped)
+ {
+ _wrapped = wrapped;
+ }
+
+ #region IDataObject
+ bool IDataObject.Contains(string dataFormat)
+ {
+ return _wrapped.Contains(dataFormat);
+ }
+
+ IEnumerable IDataObject.GetDataFormats()
+ {
+ return _wrapped.GetDataFormats();
+ }
+
+ IEnumerable IDataObject.GetFileNames()
+ {
+ return _wrapped.GetFileNames();
+ }
+
+ string IDataObject.GetText()
+ {
+ return _wrapped.GetText();
+ }
+
+ object IDataObject.Get(string dataFormat)
+ {
+ return _wrapped.Get(dataFormat);
+ }
+ #endregion
+
+ #region IOleDataObject
+
+ int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection);
+ connection = 0;
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+
+ void IOleDataObject.DUnadvise(int connection)
+ {
+ if (_wrapped is IOleDataObject ole)
+ ole.DUnadvise(connection);
+ Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED);
+ }
+
+ int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.EnumDAdvise(out enumAdvise);
+
+ enumAdvise = null;
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+
+ IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.EnumFormatEtc(direction);
+ if (direction == DATADIR.DATADIR_GET)
+ return new FormatEnumerator(_wrapped);
+ throw new NotSupportedException();
+ }
+
+ int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut);
+
+ formatOut = new FORMATETC();
+ formatOut.ptd = IntPtr.Zero;
+ return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL);
+ }
+
+ void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium)
+ {
+ if (_wrapped is IOleDataObject ole)
+ {
+ ole.GetData(ref format, out medium);
+ return;
+ }
+ if(!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL))
+ Marshal.ThrowExceptionForHR(DV_E_TYMED);
+
+ if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
+ Marshal.ThrowExceptionForHR(DV_E_DVASPECT);
+
+ string fmt = ClipboardFormats.GetFormat(format.cfFormat);
+ if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt))
+ Marshal.ThrowExceptionForHR(DV_E_FORMATETC);
+
+ medium = default(STGMEDIUM);
+ medium.tymed = TYMED.TYMED_HGLOBAL;
+ int result = WriteDataToHGlobal(fmt, ref medium.unionmember);
+ Marshal.ThrowExceptionForHR(result);
+ }
+
+ void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium)
+ {
+ if (_wrapped is IOleDataObject ole)
+ {
+ ole.GetDataHere(ref format, ref medium);
+ return;
+ }
+
+ if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlag(TYMED.TYMED_HGLOBAL))
+ Marshal.ThrowExceptionForHR(DV_E_TYMED);
+
+ if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
+ Marshal.ThrowExceptionForHR(DV_E_DVASPECT);
+
+ string fmt = ClipboardFormats.GetFormat(format.cfFormat);
+ if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt))
+ Marshal.ThrowExceptionForHR(DV_E_FORMATETC);
+
+ if (medium.unionmember == IntPtr.Zero)
+ Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL);
+
+ int result = WriteDataToHGlobal(fmt, ref medium.unionmember);
+ Marshal.ThrowExceptionForHR(result);
+ }
+
+ int IOleDataObject.QueryGetData(ref FORMATETC format)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.QueryGetData(ref format);
+ if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
+ return DV_E_DVASPECT;
+ if (!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL))
+ return DV_E_TYMED;
+
+ string dataFormat = ClipboardFormats.GetFormat(format.cfFormat);
+ if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat))
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ return DV_E_FORMATETC;
+ }
+
+ void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release)
+ {
+ if (_wrapped is IOleDataObject ole)
+ {
+ ole.SetData(ref formatIn, ref medium, release);
+ return;
+ }
+ Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL));
+ }
+
+ private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal)
+ {
+ object data = _wrapped.Get(dataFormat);
+ if (dataFormat == DataFormats.Text || data is string)
+ return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data));
+ if (dataFormat == DataFormats.FileNames && data is IEnumerable files)
+ return WriteFileListToHGlobal(ref hGlobal, files);
+ if (data is Stream stream)
+ {
+ byte[] buffer = new byte[stream.Length - stream.Position];
+ stream.Read(buffer, 0, buffer.Length);
+ return WriteBytesToHGlobal(ref hGlobal, buffer);
+ }
+ if (data is IEnumerable bytes)
+ {
+ var byteArr = bytes is byte[] ? (byte[])bytes : bytes.ToArray();
+ return WriteBytesToHGlobal(ref hGlobal, byteArr);
+ }
+ return WriteBytesToHGlobal(ref hGlobal, SerializeObject(data));
+ }
+
+ private byte[] SerializeObject(object data)
+ {
+ using (var ms = new MemoryStream())
+ {
+ ms.Write(SerializedObjectGUID, 0, SerializedObjectGUID.Length);
+ BinaryFormatter binaryFormatter = new BinaryFormatter();
+ binaryFormatter.Serialize(ms, data);
+ return ms.ToArray();
+ }
+ }
+
+ private int WriteBytesToHGlobal(ref IntPtr hGlobal, byte[] data)
+ {
+ int required = data.Length;
+ if (hGlobal == IntPtr.Zero)
+ hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required);
+
+ long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ if (required > available)
+ return STG_E_MEDIUMFULL;
+
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ Marshal.Copy(data, 0, ptr, data.Length);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files)
+ {
+ if (!files?.Any() ?? false)
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+
+ char[] filesStr = (string.Join("\0", files) + "\0\0").ToCharArray();
+ _DROPFILES df = new _DROPFILES();
+ df.pFiles = Marshal.SizeOf<_DROPFILES>();
+ df.fWide = true;
+
+ int required = (filesStr.Length * sizeof(char)) + Marshal.SizeOf<_DROPFILES>();
+ if (hGlobal == IntPtr.Zero)
+ hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required);
+
+ long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ if (required > available)
+ return STG_E_MEDIUMFULL;
+
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ Marshal.StructureToPtr(df, ptr, false);
+
+ Marshal.Copy(filesStr, 0, ptr + Marshal.SizeOf<_DROPFILES>(), filesStr.Length);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private int WriteStringToHGlobal(ref IntPtr hGlobal, string data)
+ {
+ int required = (data.Length + 1) * sizeof(char);
+ if (hGlobal == IntPtr.Zero)
+ hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, required);
+
+ long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ if (required > available)
+ return STG_E_MEDIUMFULL;
+
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ char[] chars = (data + '\0').ToCharArray();
+ Marshal.Copy(chars, 0, ptr, chars.Length);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs
new file mode 100644
index 0000000000..81dd790066
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/DragSource.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Input.DragDrop;
+using Avalonia.Input.Platform;
+using Avalonia.Threading;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ class DragSource : IPlatformDragSource
+ {
+ public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+
+ OleDragSource src = new OleDragSource();
+ DataObject dataObject = new DataObject(data);
+ int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects);
+
+ int[] finalEffect = new int[1];
+ UnmanagedMethods.DoDragDrop(dataObject, src, allowed, finalEffect);
+
+ return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect[0]));}
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index f13dd3272c..aa86ab0f8d 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
using System.Text;
// ReSharper disable InconsistentNaming
@@ -951,6 +952,32 @@ namespace Avalonia.Win32.Interop
[DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)]
public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count);
+ [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+ public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target);
+
+ [DllImport("ole32.dll", EntryPoint = "OleInitialize")]
+ public static extern HRESULT OleInitialize(IntPtr val);
+
+ [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+ internal static extern void ReleaseStgMedium(ref STGMEDIUM medium);
+
+ [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)]
+ public static extern int GetClipboardFormatName(int format, StringBuilder lpString, int cchMax);
+
+ [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)]
+ public static extern int RegisterClipboardFormat(string format);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)]
+ public static extern IntPtr GlobalSize(IntPtr hGlobal);
+
+ [DllImport("shell32.dll", BestFitMapping = false, CharSet = CharSet.Auto)]
+ public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch);
+
+ [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)]
+ public static extern void DoDragDrop(IDataObject dataObject, IDropSource dropSource, int allowedEffects, int[] finalEffect);
+
+
+
public enum MONITOR
{
MONITOR_DEFAULTTONULL = 0x00000000,
@@ -990,10 +1017,28 @@ namespace Avalonia.Win32.Interop
MDT_DEFAULT = MDT_EFFECTIVE_DPI
}
- public enum ClipboardFormat
+ public enum ClipboardFormat
{
+ ///
+ /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text.
+ ///
CF_TEXT = 1,
- CF_UNICODETEXT = 13
+ ///
+ /// A handle to a bitmap
+ ///
+ CF_BITMAP = 2,
+ ///
+ /// A memory object containing a BITMAPINFO structure followed by the bitmap bits.
+ ///
+ CF_DIB = 3,
+ ///
+ /// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data.
+ ///
+ CF_UNICODETEXT = 13,
+ ///
+ /// A handle to type HDROP that identifies a list of files.
+ ///
+ CF_HDROP = 15,
}
public struct MSG
@@ -1136,7 +1181,9 @@ namespace Avalonia.Win32.Interop
S_FALSE = 0x0001,
S_OK = 0x0000,
E_INVALIDARG = 0x80070057,
- E_OUTOFMEMORY = 0x8007000E
+ E_OUTOFMEMORY = 0x8007000E,
+ E_NOTIMPL = 0x80004001,
+ E_UNEXPECTED = 0x8000FFFF,
}
public enum Icons
@@ -1300,4 +1347,53 @@ namespace Avalonia.Win32.Interop
uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
}
+
+ [Flags]
+ internal enum DropEffect : int
+ {
+ None = 0,
+ Copy = 1,
+ Move = 2,
+ Link = 4,
+ Scroll = -2147483648,
+ }
+
+
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000122-0000-0000-C000-000000000046")]
+ internal interface IDropTarget
+ {
+ [PreserveSig]
+ UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect);
+ [PreserveSig]
+ UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect);
+ [PreserveSig]
+ UnmanagedMethods.HRESULT DragLeave();
+ [PreserveSig]
+ UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect);
+ }
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000121-0000-0000-C000-000000000046")]
+ internal interface IDropSource
+ {
+ [PreserveSig]
+ int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState);
+ [PreserveSig]
+ int GiveFeedback([MarshalAs(UnmanagedType.U4)] [In] int dwEffect);
+ }
+
+
+ [StructLayoutAttribute(LayoutKind.Sequential)]
+ internal struct _DROPFILES
+ {
+ public Int32 pFiles;
+ public Int32 X;
+ public Int32 Y;
+ public bool fNC;
+ public bool fWide;
+ }
}
diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs
new file mode 100644
index 0000000000..085c0f8ea9
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleContext.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ class OleContext
+ {
+ private static OleContext fCurrent;
+
+ internal static OleContext Current
+ {
+ get
+ {
+ if (!IsValidOleThread())
+ return null;
+
+ if (fCurrent == null)
+ fCurrent = new OleContext();
+ return fCurrent;
+ }
+ }
+
+
+ private OleContext()
+ {
+ if (UnmanagedMethods.OleInitialize(IntPtr.Zero) != UnmanagedMethods.HRESULT.S_OK)
+ throw new SystemException("Failed to initialize OLE");
+ }
+
+ private static bool IsValidOleThread()
+ {
+ return Dispatcher.UIThread.CheckAccess() &&
+ Thread.CurrentThread.GetApartmentState() == ApartmentState.STA;
+ }
+
+ internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target)
+ {
+ if (hwnd?.HandleDescriptor != "HWND" || target == null)
+ return false;
+
+ return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK;
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs
new file mode 100644
index 0000000000..ee1c3046eb
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleDataObject.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Text;
+using Avalonia.Input.DragDrop;
+using Avalonia.Win32.Interop;
+using IDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
+
+namespace Avalonia.Win32
+{
+ class OleDataObject : Avalonia.Input.DragDrop.IDataObject
+ {
+ private IDataObject _wrapped;
+
+ public OleDataObject(IDataObject wrapped)
+ {
+ _wrapped = wrapped;
+ }
+
+ public bool Contains(string dataFormat)
+ {
+ return GetDataFormatsCore().Any(df => StringComparer.OrdinalIgnoreCase.Equals(df, dataFormat));
+ }
+
+ public IEnumerable GetDataFormats()
+ {
+ return GetDataFormatsCore().Distinct();
+ }
+
+ public string GetText()
+ {
+ return GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT) as string;
+ }
+
+ public IEnumerable GetFileNames()
+ {
+ return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable;
+ }
+
+ public object Get(string dataFormat)
+ {
+ return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT);
+ }
+
+ private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect)
+ {
+ FORMATETC formatEtc = new FORMATETC();
+ formatEtc.cfFormat = ClipboardFormats.GetFormat(format);
+ formatEtc.dwAspect = aspect;
+ formatEtc.lindex = -1;
+ formatEtc.tymed = TYMED.TYMED_HGLOBAL;
+ if (_wrapped.QueryGetData(ref formatEtc) == 0)
+ {
+ _wrapped.GetData(ref formatEtc, out STGMEDIUM medium);
+ try
+ {
+ if (medium.unionmember != IntPtr.Zero && medium.tymed == TYMED.TYMED_HGLOBAL)
+ {
+ if (format == DataFormats.Text)
+ return ReadStringFromHGlobal(medium.unionmember);
+ if (format == DataFormats.FileNames)
+ return ReadFileNamesFromHGlobal(medium.unionmember);
+
+ byte[] data = ReadBytesFromHGlobal(medium.unionmember);
+
+ if (IsSerializedObject(data))
+ {
+ using (var ms = new MemoryStream(data))
+ {
+ ms.Position = DataObject.SerializedObjectGUID.Length;
+ BinaryFormatter binaryFormatter = new BinaryFormatter();
+ return binaryFormatter.Deserialize(ms);
+ }
+ }
+ return data;
+ }
+ }
+ finally
+ {
+ UnmanagedMethods.ReleaseStgMedium(ref medium);
+ }
+ }
+ return null;
+ }
+
+ private bool IsSerializedObject(byte[] data)
+ {
+ if (data.Length < DataObject.SerializedObjectGUID.Length)
+ return false;
+ for (int i = 0; i < DataObject.SerializedObjectGUID.Length; i++)
+ if (data[i] != DataObject.SerializedObjectGUID[i])
+ return false;
+ return true;
+ }
+
+ private static IEnumerable ReadFileNamesFromHGlobal(IntPtr hGlobal)
+ {
+ List files = new List();
+ int fileCount = UnmanagedMethods.DragQueryFile(hGlobal, -1, null, 0);
+ if (fileCount > 0)
+ {
+ for (int i = 0; i < fileCount; i++)
+ {
+ int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0);
+ StringBuilder sb = new StringBuilder(pathLen+1);
+
+ if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen)
+ {
+ files.Add(sb.ToString());
+ }
+ }
+ }
+ return files;
+ }
+
+ private static string ReadStringFromHGlobal(IntPtr hGlobal)
+ {
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ return Marshal.PtrToStringAuto(ptr);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private static byte[] ReadBytesFromHGlobal(IntPtr hGlobal)
+ {
+ IntPtr source = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ int size = (int)UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ byte[] data = new byte[size];
+ Marshal.Copy(source, data, 0, size);
+ return data;
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private IEnumerable GetDataFormatsCore()
+ {
+ var enumFormat = _wrapped.EnumFormatEtc(DATADIR.DATADIR_GET);
+ if (enumFormat != null)
+ {
+ enumFormat.Reset();
+ FORMATETC[] formats = new FORMATETC[1];
+ int[] fetched = { 1 };
+ while (fetched[0] > 0)
+ {
+ fetched[0] = 0;
+ if (enumFormat.Next(1, formats, fetched) == 0 && fetched[0] > 0)
+ {
+ if (formats[0].ptd != IntPtr.Zero)
+ Marshal.FreeCoTaskMem(formats[0].ptd);
+
+ yield return ClipboardFormats.GetFormat(formats[0].cfFormat);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs
new file mode 100644
index 0000000000..522014abc0
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleDragSource.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ class OleDragSource : IDropSource
+ {
+ private const int DRAGDROP_S_USEDEFAULTCURSORS = 0x00040102;
+ private const int DRAGDROP_S_DROP = 0x00040100;
+ private const int DRAGDROP_S_CANCEL = 0x00040101;
+
+ private const int KEYSTATE_LEFTMB = 1;
+ private const int KEYSTATE_MIDDLEMB = 16;
+ private const int KEYSTATE_RIGHTMB = 2;
+ private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB };
+
+ public int QueryContinueDrag(int fEscapePressed, int grfKeyState)
+ {
+ if (fEscapePressed != 0)
+ return DRAGDROP_S_CANCEL;
+
+ int pressedMouseButtons = MOUSE_BUTTONS.Where(mb => (grfKeyState & mb) == mb).Count();
+
+ if (pressedMouseButtons >= 2)
+ return DRAGDROP_S_CANCEL;
+ if (pressedMouseButtons == 0)
+ return DRAGDROP_S_DROP;
+
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+
+ public int GiveFeedback(int dwEffect)
+ {
+ return DRAGDROP_S_USEDEFAULTCURSORS;
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs
new file mode 100644
index 0000000000..e3791747b1
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs
@@ -0,0 +1,161 @@
+using Avalonia.Input.DragDrop;
+using Avalonia.Input.DragDrop.Raw;
+using Avalonia.Input;
+using Avalonia.Platform;
+using Avalonia.Win32.Interop;
+using IDataObject = Avalonia.Input.DragDrop.IDataObject;
+using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
+
+namespace Avalonia.Win32
+{
+ class OleDropTarget : IDropTarget
+ {
+ private readonly IInputElement _target;
+ private readonly ITopLevelImpl _tl;
+ private readonly IDragDropDevice _dragDevice;
+
+ private IDataObject _currentDrag = null;
+
+ public OleDropTarget(ITopLevelImpl tl, IInputElement target)
+ {
+ _dragDevice = AvaloniaLocator.Current.GetService();
+ _tl = tl;
+ _target = target;
+ }
+
+ public static DropEffect ConvertDropEffect(DragDropEffects operation)
+ {
+ DropEffect result = DropEffect.None;
+ if (operation.HasFlag(DragDropEffects.Copy))
+ result |= DropEffect.Copy;
+ if (operation.HasFlag(DragDropEffects.Move))
+ result |= DropEffect.Move;
+ if (operation.HasFlag(DragDropEffects.Link))
+ result |= DropEffect.Link;
+ return result;
+ }
+
+ public static DragDropEffects ConvertDropEffect(DropEffect effect)
+ {
+ DragDropEffects result = DragDropEffects.None;
+ if (effect.HasFlag(DropEffect.Copy))
+ result |= DragDropEffects.Copy;
+ if (effect.HasFlag(DropEffect.Move))
+ result |= DragDropEffects.Move;
+ if (effect.HasFlag(DropEffect.Link))
+ result |= DragDropEffects.Link;
+ return result;
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect)
+ {
+ var dispatch = _tl?.Input;
+ if (dispatch == null)
+ {
+ pdwEffect = DropEffect.None;
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+ _currentDrag = pDataObj as IDataObject;
+ if (_currentDrag == null)
+ _currentDrag = new OleDataObject(pDataObj);
+ var args = new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.DragEnter,
+ _target,
+ GetDragLocation(pt),
+ _currentDrag,
+ ConvertDropEffect(pdwEffect)
+ );
+ dispatch(args);
+ pdwEffect = ConvertDropEffect(args.Effects);
+
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect)
+ {
+ var dispatch = _tl?.Input;
+ if (dispatch == null)
+ {
+ pdwEffect = DropEffect.None;
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ var args = new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.DragOver,
+ _target,
+ GetDragLocation(pt),
+ _currentDrag,
+ ConvertDropEffect(pdwEffect)
+ );
+ dispatch(args);
+ pdwEffect = ConvertDropEffect(args.Effects);
+
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.DragLeave()
+ {
+ try
+ {
+ _tl?.Input(new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.DragLeave,
+ _target,
+ default(Point),
+ null,
+ DragDropEffects.None
+ ));
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+ finally
+ {
+ _currentDrag = null;
+ }
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.Drop(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect)
+ {
+ try
+ {
+ var dispatch = _tl?.Input;
+ if (dispatch == null)
+ {
+ pdwEffect = DropEffect.None;
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ _currentDrag = pDataObj as IDataObject;
+ if (_currentDrag == null)
+ _currentDrag= new OleDataObject(pDataObj);
+
+ var args = new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.Drop,
+ _target,
+ GetDragLocation(pt),
+ _currentDrag,
+ ConvertDropEffect(pdwEffect)
+ );
+ dispatch(args);
+ pdwEffect = ConvertDropEffect(args.Effects);
+
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+ finally
+ {
+ _currentDrag = null;
+ }
+ }
+
+ private Point GetDragLocation(long dragPoint)
+ {
+ int x = (int)dragPoint;
+ int y = (int)(dragPoint >> 32);
+
+ Point screenPt = new Point(x, y);
+ return _target.PointToClient(screenPt);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 4e1ba618a8..902abaf65b 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -86,6 +86,9 @@ namespace Avalonia.Win32
.Bind().ToConstant(s_instance)
.Bind().ToConstant(s_instance);
+ if (OleContext.Current != null)
+ AvaloniaLocator.CurrentMutable.Bind().ToSingleton();
+
UseDeferredRendering = deferredRendering;
_uiThread = UnmanagedMethods.GetCurrentThreadId();
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index a67362d59f..bb3c4cf6e6 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -34,6 +34,7 @@ namespace Avalonia.Win32
private double _scaling = 1;
private WindowState _showWindowState;
private FramebufferManager _framebuffer;
+ private OleDropTarget _dropTarget;
#if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag;
#endif
@@ -310,6 +311,7 @@ namespace Avalonia.Win32
public void SetInputRoot(IInputRoot inputRoot)
{
_owner = inputRoot;
+ CreateDropTarget();
}
public void SetTitle(string title)
@@ -699,6 +701,13 @@ namespace Avalonia.Win32
}
}
+ private void CreateDropTarget()
+ {
+ OleDropTarget odt = new OleDropTarget(this, _owner);
+ if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false)
+ _dropTarget = odt;
+ }
+
private Point DipFromLParam(IntPtr lParam)
{
return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling;