Skip to content

Commit

Permalink
Fix macOS clipboard formats mapping (#13197)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Dan Walmsley <[email protected]>
  • Loading branch information
3 people authored Oct 12, 2023
1 parent b6b0168 commit adb97bd
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 35 deletions.
16 changes: 16 additions & 0 deletions native/Avalonia.Native/src/OSX/clipboard.mm
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ virtual HRESULT GetText (char* type, IAvnString**ppv) override
}
}

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;
Expand Down
6 changes: 5 additions & 1 deletion src/Avalonia.Base/Input/DataFormats.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using Avalonia.Metadata;

namespace Avalonia.Input
{
Expand All @@ -18,7 +19,10 @@ public static class DataFormats
/// <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 changes: 3 additions & 4 deletions src/Avalonia.Native/AvaloniaNativeDragSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ public Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObje
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)));
Expand Down
92 changes: 62 additions & 30 deletions src/Avalonia.Native/ClipboardImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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;
Expand All @@ -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";

Expand Down Expand Up @@ -46,7 +48,7 @@ public unsafe Task SetTextAsync(string text)

public IEnumerable<string> GetFormats()
{
var rv = new List<string>();
var rv = new HashSet<string>();
using (var formats = _native.ObtainFormats())
{
var cnt = formats.Count;
Expand All @@ -58,11 +60,11 @@ public IEnumerable<string> GetFormats()
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);
}
}
}
Expand Down Expand Up @@ -91,32 +93,71 @@ public IEnumerable<IStorageItem> GetFiles()
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))
Expand Down Expand Up @@ -146,17 +187,8 @@ public void Dispose()

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 change: 1 addition & 0 deletions src/Avalonia.Native/avn.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit adb97bd

Please sign in to comment.