Skip to content

Commit

Permalink
feat(dragdrop): Hook into macOS native window drag/drop methods
Browse files Browse the repository at this point in the history
  • Loading branch information
robloo committed Oct 19, 2020
1 parent bfcfb79 commit 80381ca
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 1 deletion.
189 changes: 189 additions & 0 deletions src/Uno.UI/Controls/Window.macOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,197 @@ public Window(CGRect contentRect, NSWindowStyle aStyle, NSBackingStore buffering
Delegate = new WindowDelegate(this);
_inputPane = InputPane.GetForCurrentView();
_inputPane.Window = this;

// Register the types that can be dragging into the window (effectively 'AllowDrop').
// This must be done before dragging enters the window or dragging methods won't be invoked.
// We register for all possible types and then confirm whether they
// are actually supported within the draggingEntered and draggingUpdated methods.
// This has a minor side effect in the standard UI (zoom effect) but is considered acceptable.
RegisterForDraggedTypes(new string[]
{
NSPasteboard.NSPasteboardTypeUrl,
NSPasteboard.NSPasteboardTypeTIFF,
NSPasteboard.NSPasteboardTypePNG,
NSPasteboard.NSPasteboardTypeHTML,
NSPasteboard.NSPasteboardTypeRTF,
NSPasteboard.NSPasteboardTypeRTFD,
NSPasteboard.NSPasteboardTypeFileUrl,
NSPasteboard.NSPasteboardTypeString

/* For future use
UTType.URL,
UTType.Image,
UTType.HTML,
UTType.RTF,
UTType.FileURL,
UTType.PlainText,
*/
});
}

/// <summary>
/// Method invoked when a drag operation enters the <see cref="NSWindow"/>.
/// </summary>
/// <remarks>
///
/// While it is never documented directly, DraggingEntered can be added to NSWindow just like NSView.
/// Key docs telling this story include:
///
/// (1) NSWindow documentation does not include most NSView drag/drop methods
/// https://developer.apple.com/documentation/appkit/nswindow
/// (2) Since NSWindow documentation doesn't reference them, they also aren't included in Xamarin
/// https://docs.microsoft.com/en-us/dotnet/api/appkit.nswindow?view=xamarin-mac-sdk-14
/// (3) However, the 'draggingEntered' method docs reference 'Window' at the very end
/// "Invoked when the mouse pointer enters the destination’s bounds rectangle (if it is a view object)
/// or its frame rectangle (if it is a window object). "
/// https://developer.apple.com/documentation/appkit/nsdraggingdestination/1416019-draggingentered
/// (4) In legacy macOS docs this is a little more explitly stated:
/// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DragandDrop/Concepts/dragdestination.html#//apple_ref/doc/uid/20000977-BAJBJFBG
/// (5) Other macOS drag/drop articles and blogs refer to dragging/dropping directly from Window - although
/// give no examples.
///
/// The Export "method_name" attribute is fundamentally important to make this work, removing it will
/// break functionality and the method will never be called by macOS. Again, this seemly is because
/// Xamarin.macOS is not aware of it.
///
/// </remarks>
/// <param name="info">Information about the dragging session from the sender.</param>
/// <returns>The accepted drag operation(s).</returns>
[Export("draggingEntered:")]
public virtual NSDragOperation DraggingEntered(NSDraggingInfo draggingInfo)
{
try
{
return DraggingEnteredAction.Invoke(draggingInfo);
}
catch
{
return NSDragOperation.None;
}
}

/// <summary>
/// Code to execute when the <see cref="DraggingEntered(NSDraggingInfo)"/> method is invoked.
/// This can be used in a similar way to <see cref="UIElement.DragEnter"/>.
/// </summary>
public Func<NSDraggingInfo, NSDragOperation> DraggingEnteredAction { get; set; } =
(NSDraggingInfo draggingInfo) =>
{
return NSDragOperation.None;
};

/// <summary>
/// Method invoked when a drag operation updates over the <see cref="NSWindow"/>.
/// This is periodically called when the pointer is moved.
/// </summary>
/// <remarks>
/// See remarks in <see cref="DraggingEntered(NSDraggingInfo)"/>
/// </remarks>
/// <param name="draggingInfo">Information about the dragging session from the sender.</param>
[Export("draggingUpdated:")]
public virtual NSDragOperation DraggingUpdated(NSDraggingInfo draggingInfo)
{
try
{
return DraggingUpdatedAction.Invoke(draggingInfo);
}
catch
{
return NSDragOperation.None;
}
}

/// <summary>
/// Code to execute when the <see cref="DraggingUpdated(NSDraggingInfo)"/> method is invoked.
/// This can be used in a similar way to <see cref="UIElement.DragOver"/>.
/// </summary>
public Func<NSDraggingInfo, NSDragOperation> DraggingUpdatedAction { get; set; } =
(NSDraggingInfo draggingInfo) =>
{
return NSDragOperation.None;
};

/// <summary>
/// Method invoked when a drag operation ends.
/// </summary>
/// <remarks>
/// See remarks in <see cref="DraggingEntered(NSDraggingInfo)"/>
/// </remarks>
/// <param name="draggingInfo">Information about the dragging session from the sender.</param>
[Export("draggingEnded:")]
public virtual void DraggingEnded(NSDraggingInfo draggingInfo)
{
// Not used in this context.
// These methods within NSWindow are only used for external drop into Uno apps.
// If Uno doesn't accept and handle the drop it doesn't need to be aware of when it ends.
return;
}

/// <summary>
/// Method invoked when a drag operation exited the <see cref="NSWindow"/>.
/// </summary>
/// <remarks>
/// See remarks in <see cref="DraggingEntered(NSDraggingInfo)"/>
/// </remarks>
/// <param name="draggingInfo">Information about the dragging session from the sender.</param>
[Export("draggingExited:")]
public virtual void DraggingExited(NSDraggingInfo draggingInfo)
{
return;
}

/// <summary>
/// Code to execute when the <see cref="DraggingExited(NSDraggingInfo)"/> method is invoked.
/// This can be used in a similar way to <see cref="UIElement.DragLeave"/>.
/// </summary>
public Action<NSDraggingInfo> DraggingExitedAction { get; set; } =
(NSDraggingInfo draggingInfo) =>
{
return;
};

/// <summary>
/// Method invoked when the pointer is released over the <see cref="NSWindow"/> during a drag operation
/// This is the last chance to reject a drop.
/// </summary>
/// <param name="draggingInfo">Information about the dragging session from the sender.</param>
/// <returns>True if the destination accepts the drag operation; otherwise, false. </returns>
[Export("prepareForDragOperation:")]
public virtual bool PrepareForDragOperation(AppKit.NSDraggingInfo draggingInfo)
{
// Always return true as UWP doesn't really have an equivalent step.
// Drop is accepted within DraggingEntered (drag entered event in UWP).
return true;
}

/// <summary>
/// Method invoked when the pointer is released and data is dropped over the the <see cref="NSWindow"/>.
/// </summary>
/// <param name="draggingInfo">Information about the dragging session from the sender.</param>
/// <returns>True if the destination accepts the data; otherwise, false.</returns>
[Export("performDragOperation:")]
public virtual bool PerformDragOperation(NSDraggingInfo draggingInfo)
{
try
{
return PerformDragOperationAction.Invoke(draggingInfo);
}
catch
{
return false;
}
}

/// <summary>
/// Code to execute when the <see cref="PerformDragOperation(NSDraggingInfo)"/> method is invoked.
/// This can be used in a similar way to <see cref="UIElement.Drop"/>.
/// </summary>
public Func<NSDraggingInfo, bool> PerformDragOperationAction { get; set; } =
(NSDraggingInfo draggingInfo) =>
{
return false;
};

public static void SetNeedsKeyboard(NSView view, bool needsKeyboard)
{
if (view != null)
Expand Down
107 changes: 107 additions & 0 deletions src/Uno.UI/UI/Xaml/DragDropExtension.macOS.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#if __MACOS__
#nullable enable

using System;
using System.Numerics;
using System.Threading;
using AppKit;
using Foundation;
using MobileCoreServices;
using Uno.Extensions;
using Uno.Logging;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Point = Windows.Foundation.Point;
using UIElement = Windows.UI.Xaml.UIElement;

namespace Windows.ApplicationModel.DataTransfer.DragDrop.Core
{
internal class MacOSDragDropExtension : IDragDropExtension
{
private readonly DragDropManager _manager;
private NSWindow _nativeWindowHost;
private Uno.UI.Controls.Window _window;

public MacOSDragDropExtension(DragDropManager owner)
{
_manager = (DragDropManager)owner;

_nativeWindowHost = AppKit.NSApplication.SharedApplication.DangerousWindows[0];
_window = (Uno.UI.Controls.Window)CoreWindow.GetForCurrentThread()!._window;

_window.DraggingEnteredAction = OnDraggingEnteredEvent;
_window.DraggingUpdatedAction = OnDraggingUpdated;
_window.DraggingExitedAction = OnDraggingExited;
_window.PerformDragOperationAction = OnPerformDragOperation;
}

private NSDragOperation OnDraggingEnteredEvent(NSDraggingInfo draggingInfo)
{
return NSDragOperation.Copy;
}

private NSDragOperation OnDraggingUpdated(NSDraggingInfo draggingInfo)
{
return NSDragOperation.Copy;
}

private void OnDraggingExited(NSDraggingInfo draggingInfo)
{
return;
}

private bool OnPerformDragOperation(NSDraggingInfo draggingInfo)
{
return true;
}

public void StartNativeDrag(CoreDragInfo info)
=> CoreDispatcher.Main.RunAsync(CoreDispatcherPriority.High, async () =>
{
try
{
NSDraggingSource source = new NSDraggingSource();
NSEvent sourceEvent = new NSEvent();

var text = await info.Data.GetTextAsync();

var draggingItem = new NSDraggingItem((NSString)text);
draggingItem.DraggingFrame = new CoreGraphics.CGRect(0, 0, 1, 1);

//_nativeView?.BeginDraggingSession(new[] { draggingItem }, sourceEvent, source);
}
catch (Exception e)
{
this.Log().Error("Failed to start native Drag and Drop.", e);
}
});

private class DragEventSource : IDragEventSource
{
private static long _nextFrameId;

public DragEventSource(long pointerId)
{
Id = pointerId;
}

public long Id { get; }

public uint FrameId { get; } = (uint)Interlocked.Increment(ref _nextFrameId);

/// <inheritdoc />
public (Point location, DragDropModifiers modifier) GetState()
{
return (new Windows.Foundation.Point(0, 0), DragDropModifiers.None);
}

/// <inheritdoc />
public Point GetPosition(object? relativeTo)
{
return new Point(0, 0);
}
}
}
}

#endif
3 changes: 2 additions & 1 deletion src/Uno.UWP/UI/Core/CoreWindow.macOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ namespace Windows.UI.Core
{
public partial class CoreWindow
{
private readonly NSWindow _window;
// HACK: TODO: Find a better way to get this reference
public readonly NSWindow _window;

private bool _cursorHidden = false;
private CoreCursor _pointerCursor = new CoreCursor(CoreCursorType.Arrow, 0);
Expand Down

0 comments on commit 80381ca

Please sign in to comment.