Browse Source

Fix macOS clipboard formats mapping (#13197)

* Implement macOS clipboard formats mapping

* Mark DataFormats unstable instead of obsolete (removes warnings in our code base)

* Support non-text data formats in macOS drag source

* Implement SetStrings for IAvnClipboard to support files properly

* Add comments to a confusing part of code

* Update src/Avalonia.Base/Input/DataFormats.cs

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Co-authored-by: Dan Walmsley <dan@walms.co.uk>
pull/13255/head
Max Katz 3 years ago
committed by GitHub
parent
commit
adb97bd5b6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      native/Avalonia.Native/src/OSX/clipboard.mm
  2. 6
      src/Avalonia.Base/Input/DataFormats.cs
  3. 7
      src/Avalonia.Native/AvaloniaNativeDragSource.cs
  4. 92
      src/Avalonia.Native/ClipboardImpl.cs
  5. 1
      src/Avalonia.Native/avn.idl

16
native/Avalonia.Native/src/OSX/clipboard.mm

@ -42,6 +42,22 @@ public:
}
}
virtual HRESULT SetStrings(char* type, IAvnStringArray*ppv) override
{
START_COM_CALL;
@autoreleasepool
{
NSArray<NSString*>* data = GetNSArrayOfStringsAndRelease(ppv);
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
if(_item == nil)
[_pb setPropertyList: data forType: typeString];
else
[_item setPropertyList: data forType:typeString];
return S_OK;
}
}
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override
{
START_COM_CALL;

6
src/Avalonia.Base/Input/DataFormats.cs

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using Avalonia.Metadata;
namespace Avalonia.Input
{
@ -18,7 +19,10 @@ namespace Avalonia.Input
/// <summary>
/// Dataformat for one or more filenames
/// </summary>
[Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)]
/// <remarks>
/// This data format is supported only on desktop platforms.
/// </remarks>
[Unstable("Use DataFormats.Files, this format is supported only on desktop platforms. And it will be removed in 12.0."), EditorBrowsable(EditorBrowsableState.Never)]
public static readonly string FileNames = nameof(FileNames);
}
}

7
src/Avalonia.Native/AvaloniaNativeDragSource.cs

@ -48,10 +48,9 @@ namespace Avalonia.Native
using (var clipboard = new ClipboardImpl(clipboardImpl))
using (var cb = new DndCallback(tcs))
{
if (data.Contains(DataFormats.Text))
// API is synchronous, so it's OK
clipboard.SetTextAsync(data.GetText()).Wait();
// Native API is synchronous, so it's OK. For now.
clipboard.SetDataObjectAsync(data).GetAwaiter().GetResult();
view.BeginDraggingSession((AvnDragDropEffects)allowedEffects,
triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb,
GCHandle.ToIntPtr(GCHandle.Alloc(data)));

92
src/Avalonia.Native/ClipboardImpl.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Logging;
using Avalonia.Native.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
@ -13,6 +14,7 @@ namespace Avalonia.Native
class ClipboardImpl : IClipboard, IDisposable
{
private IAvnClipboard _native;
// TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
private const string NSPasteboardTypeString = "public.utf8-plain-text";
private const string NSFilenamesPboardType = "NSFilenamesPboardType";
@ -46,7 +48,7 @@ namespace Avalonia.Native
public IEnumerable<string> GetFormats()
{
var rv = new List<string>();
var rv = new HashSet<string>();
using (var formats = _native.ObtainFormats())
{
var cnt = formats.Count;
@ -58,11 +60,11 @@ namespace Avalonia.Native
rv.Add(DataFormats.Text);
if (fmt.String == NSFilenamesPboardType)
{
#pragma warning disable CS0618 // Type or member is obsolete
rv.Add(DataFormats.FileNames);
#pragma warning restore CS0618 // Type or member is obsolete
rv.Add(DataFormats.Files);
rv.Add(DataFormats.FileNames);
rv.Add(DataFormats.Files);
}
else
rv.Add(fmt.String);
}
}
}
@ -91,32 +93,71 @@ namespace Avalonia.Native
public unsafe Task SetDataObjectAsync(IDataObject data)
{
_native.Clear();
foreach (var fmt in data.GetDataFormats())
// If there is multiple values with the same "to" format, prefer these that were not mapped.
var formats = data.GetDataFormats().Select(f =>
{
string from, to;
bool mapped;
if (f == DataFormats.Text)
(from, to, mapped) = (f, NSPasteboardTypeString, true);
else if (f == DataFormats.Files || f == DataFormats.FileNames)
(from, to, mapped) = (f, NSFilenamesPboardType, true);
else (from, to, mapped) = (f, f, false);
return (from, to, mapped);
})
.GroupBy(p => p.to)
.Select(g => g.OrderBy(f => f.mapped).First());
foreach (var (fromFormat, toFormat, _) in formats)
{
var o = data.Get(fmt);
if(o is string s)
_native.SetText(fmt, s);
else if(o is byte[] bytes)
fixed (byte* pbytes = bytes)
_native.SetBytes(fmt, pbytes, bytes.Length);
var o = data.Get(fromFormat);
switch (o)
{
case string s:
_native.SetText(toFormat, s);
break;
case IEnumerable<IStorageItem> storageItems:
using (var strings = new AvnStringArray(storageItems
.Select(s => s.TryGetLocalPath())
.Where(p => p is not null)))
{
_native.SetStrings(toFormat, strings);
}
break;
case IEnumerable<string> managedStrings:
using (var strings = new AvnStringArray(managedStrings))
{
_native.SetStrings(toFormat, strings);
}
break;
case byte[] bytes:
{
fixed (byte* pbytes = bytes)
_native.SetBytes(toFormat, pbytes, bytes.Length);
break;
}
default:
Logger.TryGet(LogEventLevel.Warning, LogArea.macOSPlatform)?.Log(this,
"Unsupported IDataObject value type: {0}", o?.GetType().FullName ?? "(null)");
break;
}
}
return Task.CompletedTask;
}
public Task<string[]> GetFormatsAsync()
{
using (var n = _native.ObtainFormats())
return Task.FromResult(n.ToStringArray());
return Task.FromResult(GetFormats().ToArray());
}
public async Task<object> GetDataAsync(string format)
{
if (format == DataFormats.Text)
if (format == DataFormats.Text || format == NSPasteboardTypeString)
return await GetTextAsync();
#pragma warning disable CS0618 // Type or member is obsolete
if (format == DataFormats.FileNames)
if (format == DataFormats.FileNames || format == NSFilenamesPboardType)
return GetFileNames();
#pragma warning restore CS0618 // Type or member is obsolete
if (format == DataFormats.Files)
return GetFiles();
using (var n = _native.GetBytes(format))
@ -146,17 +187,8 @@ namespace Avalonia.Native
public bool Contains(string dataFormat) => Formats.Contains(dataFormat);
public object Get(string dataFormat)
{
if (dataFormat == DataFormats.Text)
return _clipboard.GetTextAsync().Result;
if (dataFormat == DataFormats.Files)
return _clipboard.GetFiles();
#pragma warning disable CS0618
if (dataFormat == DataFormats.FileNames)
#pragma warning restore CS0618
return _clipboard.GetFileNames();
return null;
}
public object Get(string dataFormat) => _clipboard.GetDataAsync(dataFormat).GetAwaiter().GetResult();
public Task SetFromDataObjectAsync(IDataObject dataObject) => _clipboard.SetDataObjectAsync(dataObject);
}
}

1
src/Avalonia.Native/avn.idl

@ -908,6 +908,7 @@ interface IAvnClipboard : IUnknown
HRESULT SetText(char* type, char* utf8Text);
HRESULT ObtainFormats(IAvnStringArray**ppv);
HRESULT GetStrings(char* type, IAvnStringArray**ppv);
HRESULT SetStrings(char* type, IAvnStringArray*ppv);
HRESULT SetBytes(char* type, void* utf8Text, int len);
HRESULT GetBytes(char* type, IAvnString**ppv);

Loading…
Cancel
Save