From adb97bd5b6bc5e7c49a8b75f7cb0ed4fb56ac214 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 12 Oct 2023 07:15:56 -0700 Subject: [PATCH] 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 --- native/Avalonia.Native/src/OSX/clipboard.mm | 16 ++++ src/Avalonia.Base/Input/DataFormats.cs | 6 +- .../AvaloniaNativeDragSource.cs | 7 +- src/Avalonia.Native/ClipboardImpl.cs | 92 +++++++++++++------ src/Avalonia.Native/avn.idl | 1 + 5 files changed, 87 insertions(+), 35 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 9a38558cf9..75c8f2a021 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/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* 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; diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index f593ed205f..8050f1b721 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/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 /// /// Dataformat for one or more filenames /// - [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)] + /// + /// This data format is supported only on desktop platforms. + /// + [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); } } diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs index 5063c7a0a0..3b4fd80775 100644 --- a/src/Avalonia.Native/AvaloniaNativeDragSource.cs +++ b/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))); diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 5a6b0df801..5b9c3acaaf 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/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 GetFormats() { - var rv = new List(); + var rv = new HashSet(); 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 storageItems: + using (var strings = new AvnStringArray(storageItems + .Select(s => s.TryGetLocalPath()) + .Where(p => p is not null))) + { + _native.SetStrings(toFormat, strings); + } + break; + case IEnumerable 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 GetFormatsAsync() { - using (var n = _native.ObtainFormats()) - return Task.FromResult(n.ToStringArray()); + return Task.FromResult(GetFormats().ToArray()); } public async Task 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); } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index e2b1b17aa6..036273fc01 100644 --- a/src/Avalonia.Native/avn.idl +++ b/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);