From e425a21a479b94899c641ee10eb69e1363b4e050 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Mon, 31 Jul 2023 10:31:47 -0700 Subject: [PATCH] DataObject/Clipboard enhancements - Switches out BinarySerializer to DataContractSerializer for GetObject/SetObject - Support [DataContract] types for GetObject/SetObject (in addition to Serializable) - Add GetObject() override that takes desired type - GetObject() returns object for known format types for String, Bitmap, Color (mac) - Wpf/WinForms: GetData() returns byte data for more types - Wpf: Prevent crash getting data for FileContents, and return MemoryStream[] with GetObject() --- lib/monomac | 2 +- src/Eto.Gtk/Forms/ClipboardHandler.cs | 23 +- src/Eto.Gtk/Forms/DataObjectHandler.cs | 24 ++- src/Eto.Mac/Forms/DataObjectHandler.cs | 35 ++- src/Eto.Mac/Forms/MemoryDataObjectHandler.cs | 38 +++- src/Eto.WinForms/Win32.cs | 9 + src/Eto.Wpf/Forms/ClipboardHandler.cs | 2 + src/Eto.Wpf/Forms/DataObjectHandler.cs | 200 +++++++++++++++++- src/Eto/Forms/Clipboard.cs | 93 ++++---- src/Eto/Forms/DataObject.cs | 119 +++++++---- src/Eto/Forms/ObjectData.cs | 93 ++++++++ .../Sections/Behaviors/ClipboardSection.cs | 17 +- .../Sections/Behaviors/DragDropSection.cs | 48 ++++- .../UnitTests/Forms/ClipboardTests.cs | 76 ++++++- 14 files changed, 669 insertions(+), 110 deletions(-) create mode 100755 src/Eto/Forms/ObjectData.cs diff --git a/lib/monomac b/lib/monomac index fffcaf035..2bc2eda35 160000 --- a/lib/monomac +++ b/lib/monomac @@ -1 +1 @@ -Subproject commit fffcaf035ce5df78c597363ba3f26f0a4059b6e5 +Subproject commit 2bc2eda3561248ef9fda4241a0532c0c556b7450 diff --git a/src/Eto.Gtk/Forms/ClipboardHandler.cs b/src/Eto.Gtk/Forms/ClipboardHandler.cs index da3cbf400..1286c7351 100644 --- a/src/Eto.Gtk/Forms/ClipboardHandler.cs +++ b/src/Eto.Gtk/Forms/ClipboardHandler.cs @@ -177,15 +177,34 @@ public bool Contains(string type) public bool TrySetObject(object value, string type) => false; - public bool TryGetObject(string type, out object value) + public bool TryGetObject(string type, Type objectType, out object value) { + if (objectType == null || objectType == typeof(string)) + { + if (DataObjectHandler.string_types.Contains(type, StringComparer.OrdinalIgnoreCase)) + { + value = GetString(type); + if (value != null) + return true; + } + } + if (objectType == null || objectType == typeof(Bitmap)) + { + if (DataObjectHandler.image_types.Contains(type, StringComparer.OrdinalIgnoreCase)) + { + value = new Bitmap(GetData(type)); + return true; + } + } + value = null; return false; } public void SetObject(object value, string type) => Widget.SetObject(value, type); - public T GetObject(string type) => Widget.GetObject(type); + public object GetObject(string type, Type objectType) => Widget.GetObject(type, objectType); + public object GetObject(string type) => Widget.GetObject(type); public string[] Types { diff --git a/src/Eto.Gtk/Forms/DataObjectHandler.cs b/src/Eto.Gtk/Forms/DataObjectHandler.cs index a3dfe96ae..7565c18fb 100644 --- a/src/Eto.Gtk/Forms/DataObjectHandler.cs +++ b/src/Eto.Gtk/Forms/DataObjectHandler.cs @@ -211,8 +211,28 @@ public bool Contains(params string[] types) public bool TrySetObject(object value, string type) => false; - public bool TryGetObject(string type, out object value) + internal static string[] string_types = { "UTF8_STRING", "TEXT", "STRING", "text/html", "text/plain" }; + internal static string[] image_types = { "image/pixbuf", "image/png", "image/tiff", "image/bmp", "image/jpeg" }; + + public bool TryGetObject(string type, Type objectType, out object value) { + if (objectType == null || objectType == typeof(string)) + { + if (string_types.Contains(type, StringComparer.OrdinalIgnoreCase)) + { + value = GetString(type); + if (value != null) + return true; + } + } + if (objectType == null || objectType == typeof(Bitmap)) + { + if (image_types.Contains(type, StringComparer.OrdinalIgnoreCase)) + { + value = new Bitmap(GetData(type)); + return true; + } + } value = null; return false; } @@ -220,6 +240,8 @@ public bool TryGetObject(string type, out object value) public void SetObject(object value, string type) => Widget.SetObject(value, type); public T GetObject(string type) => Widget.GetObject(type); + public object GetObject(string type, Type objectType) => Widget.GetObject(type, objectType); + public object GetObject(string type) => Widget.GetObject(type); public string[] Types { diff --git a/src/Eto.Mac/Forms/DataObjectHandler.cs b/src/Eto.Mac/Forms/DataObjectHandler.cs index d6b5b9467..8c846101c 100644 --- a/src/Eto.Mac/Forms/DataObjectHandler.cs +++ b/src/Eto.Mac/Forms/DataObjectHandler.cs @@ -195,12 +195,37 @@ public bool Contains(string type) return Control.GetAvailableTypeFromArray(new[] { type }) != null; } - public bool TryGetObject(string type, out object value) + public bool TryGetObject(string type, Type objectType, out object value) { - if (type == NSPasteboard.NSPasteboardTypeColor) + if (objectType == null || objectType == typeof(Color)) { - value = NSColor.FromPasteboard(Control)?.ToEto(); - return true; + if (type == NSPasteboard.NSPasteboardTypeColor) + { + value = NSColor.FromPasteboard(Control)?.ToEto(); + return true; + } + } + if (objectType == null || objectType == typeof(string)) + { + if (type == NSPasteboard.NSPasteboardTypeString + || type == NSPasteboard.NSPasteboardTypeTabularText + || type == NSPasteboard.NSPasteboardTypeUrl + || type == NSPasteboard.NSPasteboardTypeFileUrl + || type == NSPasteboard.NSPasteboardTypeRTF + || type == NSPasteboard.NSPasteboardTypeHTML) + { + value = GetString(type); + return true; + } + } + if (objectType == null || objectType == typeof(Bitmap)) + { + if (type == NSPasteboard.NSPasteboardTypeTIFF + || type == NSPasteboard.NSPasteboardTypePNG) + { + value = new Bitmap(new MemoryStream(GetData(type))); + return true; + } } value = null; return false; @@ -219,5 +244,7 @@ public bool TrySetObject(object value, string type) public void SetObject(object value, string type) => Widget.SetObject(value, type); public T GetObject(string type) => Widget.GetObject(type); + public object GetObject(string type, Type objectType) => Widget.GetObject(type, objectType); + public object GetObject(string type) => Widget.GetObject(type); } } diff --git a/src/Eto.Mac/Forms/MemoryDataObjectHandler.cs b/src/Eto.Mac/Forms/MemoryDataObjectHandler.cs index 1a77a4094..c50a62ab9 100644 --- a/src/Eto.Mac/Forms/MemoryDataObjectHandler.cs +++ b/src/Eto.Mac/Forms/MemoryDataObjectHandler.cs @@ -193,14 +193,40 @@ public bool TrySetObject(object value, string type) return false; } - public bool TryGetObject(string type, out object value) + public bool TryGetObject(string type, Type objectType, out object value) { - var colorItem = GetDataItem(type); - if (colorItem != null) + if (objectType == null || objectType == typeof(Color)) { - value = colorItem.Value.ToEto(); - return true; + var colorItem = GetDataItem(type); + if (colorItem != null) + { + value = colorItem.Value.ToEto(); + return true; + } + } + if (objectType == null || objectType == typeof(string)) + { + if (type == NSPasteboard.NSPasteboardTypeString + || type == NSPasteboard.NSPasteboardTypeTabularText + || type == NSPasteboard.NSPasteboardTypeUrl + || type == NSPasteboard.NSPasteboardTypeFileUrl + || type == NSPasteboard.NSPasteboardTypeRTF + || type == NSPasteboard.NSPasteboardTypeHTML) + { + value = GetString(type); + return true; + } + } + if (objectType == null || objectType == typeof(Bitmap)) + { + if (type == NSPasteboard.NSPasteboardTypeTIFF + || type == NSPasteboard.NSPasteboardTypePNG) + { + value = new Bitmap(new MemoryStream(GetData(type))); + return true; + } } + value = null; return false; } @@ -208,5 +234,7 @@ public bool TryGetObject(string type, out object value) public void SetObject(object value, string type) => Widget.SetObject(value, type); public T GetObject(string type) => Widget.GetObject(type); + public object GetObject(string type, Type objectType) => Widget.GetObject(type, objectType); + public object GetObject(string type) => Widget.GetObject(type); } } diff --git a/src/Eto.WinForms/Win32.cs b/src/Eto.WinForms/Win32.cs index a9e8a7bf5..04bc32bf9 100755 --- a/src/Eto.WinForms/Win32.cs +++ b/src/Eto.WinForms/Win32.cs @@ -554,5 +554,14 @@ public enum SC : uint { CLOSE = 0xF060 } + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr GlobalLock(IntPtr handle); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern bool GlobalUnlock(IntPtr handle); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern int GlobalSize(IntPtr handle); } } diff --git a/src/Eto.Wpf/Forms/ClipboardHandler.cs b/src/Eto.Wpf/Forms/ClipboardHandler.cs index 7e0550708..86b917e0d 100755 --- a/src/Eto.Wpf/Forms/ClipboardHandler.cs +++ b/src/Eto.Wpf/Forms/ClipboardHandler.cs @@ -8,6 +8,8 @@ public ClipboardHandler() Control = new sw.DataObject(); } + public override sw.IDataObject ReadingDataObject => sw.Clipboard.GetDataObject(); + public override string[] Types => sw.Clipboard.GetDataObject()?.GetFormats(); protected override void Update() diff --git a/src/Eto.Wpf/Forms/DataObjectHandler.cs b/src/Eto.Wpf/Forms/DataObjectHandler.cs index cc4cdf687..5e9753268 100755 --- a/src/Eto.Wpf/Forms/DataObjectHandler.cs +++ b/src/Eto.Wpf/Forms/DataObjectHandler.cs @@ -1,14 +1,20 @@ #if WPF +using System.Runtime.InteropServices.ComTypes; using static System.Windows.WpfDataObjectExtensions; using BitmapSource = System.Windows.Media.Imaging.BitmapSource; +using IDataObject = Eto.Forms.IDataObject; +using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; namespace Eto.Wpf.Forms { #elif WINFORMS +using System.Runtime.InteropServices.ComTypes; using static System.Windows.Forms.SwfDataObjectExtensions; using sw = System.Windows.Forms; using BitmapSource = System.Drawing.Image; +using IDataObject = Eto.Forms.IDataObject; +using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; namespace Eto.WinForms.Forms { @@ -49,7 +55,30 @@ public DataObjectHandler(sw.DataObject data) { } - protected override object InnerGetData(string type) => Control.GetData(type); + protected override object InnerGetData(string type) + { + try + { + // WPF can throw exceptions here for FileContents, maybe others. + var data = Control.GetData(type); + if (data != null) + return data; + } + catch + { + } + + try + { + // fallback to native stream reading + return GetStream(type, -1); + } + catch + { + } + + return null; + } public override void Clear() { @@ -74,6 +103,8 @@ public abstract class DataObjectHandler : WidgetHandler Control; + public DataObjectHandler() { } @@ -141,7 +172,7 @@ public Image Image if (img != null) return img; } - if (Contains(sw.DataFormats.Dib) && InnerGetData(sw.DataFormats.Dib) is Stream stream) + if (Contains(sw.DataFormats.Dib) && GetObjectData(sw.DataFormats.Dib) is Stream stream) return Win32.FromDIB(stream); return null; } @@ -250,12 +281,55 @@ public Uri[] Uris public abstract bool Contains(string type); protected abstract object InnerGetData(string type); + + object GetObjectData(string type) + { + try + { + // WPF can throw exceptions here for FileContents, maybe others. + var data = InnerGetData(type); + if (data != null) + return data; + } + catch + { + } + + try + { + // fallback to native stream reading + return GetStream(type, -1); + } + catch + { + } + + return null; + + } public byte[] GetData(string type) { if (Contains(type)) { - return GetAsData(InnerGetData(type)); + if (type == "FileContents") + { + // special case for FileContents, it needs an index. + var fileDescriptorStream = GetStream("FileGroupDescriptorW"); + if (fileDescriptorStream != null) + { + var reader = new BinaryReader(fileDescriptorStream); + // get count of number of files + var count = reader.ReadUInt32(); + if (count == 1) + { + // Get the contents of the first file only. One should use GetObject() to get an array of memory streams. + return GetAsData(GetStream(type, 0)); + } + } + } + + return GetAsData(GetObjectData(type)); } return null; } @@ -303,7 +377,7 @@ protected string GetString(string type, Encoding encoding) return Text; if (!Contains(type)) return null; - return GetAsString(InnerGetData(type), encoding); + return GetAsString(GetObjectData(type), encoding); } protected string GetAsString(object data, Encoding encoding) @@ -367,14 +441,128 @@ public bool TrySetObject(object value, string type) return false; } - public bool TryGetObject(string type, out object value) + public bool TryGetObject(string type, Type objectType, out object value) { + try + { + if (type == "FileContents") + { + // special case for FileContents, it needs an index. + var fileDescriptorStream = GetStream("FileGroupDescriptorW"); + if (fileDescriptorStream != null) + { + var reader = new BinaryReader(fileDescriptorStream); + // get count of number of files + var count = reader.ReadUInt32(); + var contents = new MemoryStream[count]; + for (int i = 0; i < count; i++) + { + var stream = GetStream(type, i); + contents[i] = stream; + } + value = contents; + return true; + } + } + } + catch + { + // ignore errors + } + + try + { + var obj = ReadingDataObject.GetData(type, true); + if (obj != null && (objectType == null || objectType.IsAssignableFrom(obj.GetType()))) + { + value = obj; + return true; + } + } + catch + { + // ignore errors + } + value = null; return false; } public void SetObject(object value, string type) => Widget.SetObject(value, type); - public T GetObject(string type) => Widget.GetObject(type); + public object GetObject(string type) => Widget.GetObject(type); + public object GetObject(string type, Type objectType) => Widget.GetObject(type, objectType); + + internal MemoryStream GetStream(string format, int index = -1) + { + var comDataObject = ReadingDataObject as System.Runtime.InteropServices.ComTypes.IDataObject; + if (comDataObject == null) + return null; + +#if WPF + var dataFormat = sw.DataFormats.GetDataFormat(format); +#elif WINFORMS + var dataFormat = sw.DataFormats.GetFormat(format); +#endif + if (dataFormat == null) + return null; + + var formatetc = new FORMATETC(); + formatetc.cfFormat = (short)dataFormat.Id; + formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT; + formatetc.lindex = index; + formatetc.tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_HGLOBAL; + + + var medium = new STGMEDIUM(); + + //using the com IDataObject interface get the data using the defined FORMATETC + comDataObject.GetData(ref formatetc, out medium); + + if (medium.tymed == TYMED.TYMED_ISTREAM) + return ReadIStream(medium); + + if (medium.tymed == TYMED.TYMED_HGLOBAL) + return ReadHGlobal(medium); + + return null; + } + + + MemoryStream ReadHGlobal(STGMEDIUM medium) + { + IntPtr source = Win32.GlobalLock(medium.unionmember); + + // can't lock? Abort + if (source == IntPtr.Zero) + return null; + + try + { + int length = Win32.GlobalSize(medium.unionmember); + + byte[] bytes = new byte[length]; + Marshal.Copy(source, bytes, 0, length); + return new MemoryStream(bytes); + } + finally + { + Win32.GlobalUnlock(medium.unionmember); + } + } + + MemoryStream ReadIStream(STGMEDIUM medium) + { + var istream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember); + Marshal.Release(medium.unionmember); + + var stat = new STATSTG(); + istream.Stat(out stat, 0); + + var bytes = new byte[stat.cbSize]; + istream.Read(bytes, bytes.Length, IntPtr.Zero); + + return new MemoryStream(bytes); + } } } diff --git a/src/Eto/Forms/Clipboard.cs b/src/Eto/Forms/Clipboard.cs index 8d9dcffe0..23f04326b 100644 --- a/src/Eto/Forms/Clipboard.cs +++ b/src/Eto/Forms/Clipboard.cs @@ -195,15 +195,12 @@ public void SetObject(object value, string type) var baseType = value.GetType(); baseType = Nullable.GetUnderlyingType(baseType) ?? baseType; - if (baseType.GetTypeInfo().IsSerializable) + if (baseType.IsSerializable || baseType.GetCustomAttribute() != null) { - using (var ms = new MemoryStream()) + var data = ObjectData.Serialize(value, baseType); + if (data != null) { -#pragma warning disable SYSLIB0011 - var binaryFormatter = new BinaryFormatter(); - binaryFormatter.Serialize(ms, value); - SetDataStream(ms, type); -#pragma warning restore SYSLIB0011 + SetData(data, type); return; } } @@ -228,29 +225,9 @@ public void SetObject(object value, string type) /// An instance of the object to recieve, or the default value. public T GetObject(string type) { - if (Handler.TryGetObject(type, out var obj) && obj is T handlerValue) - return handlerValue; - - var baseType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - - try - { - if (baseType.GetTypeInfo().IsSerializable && GetObject(type) is T value) - { - return value; - } - - var converter = System.ComponentModel.TypeDescriptor.GetConverter(baseType); - if (converter?.CanConvertFrom(typeof(string)) == true) - { - return (T)converter.ConvertFromString(GetString(type)); - } - } - catch (Exception ex) - { - // log error in debug - Debug.WriteLine(ex); - } + var val = GetObject(type, typeof(T)); + if (val is T output) + return output; return default; } @@ -259,27 +236,60 @@ public T GetObject(string type) /// /// type identifier to get the value for. /// Value of the object if deserializable, otherwise null. - public object GetObject(string type) + public object GetObject(string type) => GetObject(type, null); + + /// + /// Gets an object from the data object with the specified type + /// + /// + /// This is useful when you know the type of object, and it is serializable or has a type converter to convert from string. + /// If it cannot be converted it will return the default value. + /// + /// Type identifier to get from the data object + /// Type of the object to get, or null to detect type + /// An instance of the object to recieve, or the default value. + public object GetObject(string type, Type objectType) { - if (Handler.TryGetObject(type, out var value)) + if (objectType != null) + objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + + if (Handler.TryGetObject(type, objectType, out var value) && !(value is Stream || value is byte[])) return value; - var stream = GetDataStream(type); - if (stream == null) - return null; try { -#pragma warning disable SYSLIB0011 - var binaryFormatter = new BinaryFormatter(); - return binaryFormatter.Deserialize(stream); -#pragma warning restore SYSLIB0011 + if (ObjectData.CanSerialize(objectType)) + { + var stream = value switch + { + Stream s => s, + byte[] bytes => new MemoryStream(bytes), + _ => GetDataStream(type) + }; + + if (stream != null) + { + value = ObjectData.Deserialize(stream, objectType); + if (value != null) + return value; + } + } + + if (objectType != null) + { + var converter = System.ComponentModel.TypeDescriptor.GetConverter(objectType); + if (converter?.CanConvertFrom(typeof(string)) == true) + { + return converter.ConvertFromString(GetString(type)); + } + } } catch (Exception ex) { // log error in debug Debug.WriteLine(ex); - return null; } + return default; } /// @@ -303,8 +313,9 @@ public object GetObject(string type) /// Attempts to get the specified value from the clipboard in a native-supplied way /// /// Data format type to get the value + /// Type that is requested /// Value returned /// True if the value was returned, false otherwise - bool TryGetObject(string type, out object value); + bool TryGetObject(string type, Type objectType, out object value); } } \ No newline at end of file diff --git a/src/Eto/Forms/DataObject.cs b/src/Eto/Forms/DataObject.cs index 1ce8ea084..5603b21f4 100644 --- a/src/Eto/Forms/DataObject.cs +++ b/src/Eto/Forms/DataObject.cs @@ -137,6 +137,25 @@ public interface IDataObject /// Type identifier to get from the data object /// An instance of the object to recieve, or the default value. T GetObject(string type); + + /// + /// Gets a serialized value with the specified identifier. + /// + /// type identifier to get the value for. + /// Value of the object if deserializable, otherwise null. + object GetObject(string type); + + /// + /// Gets an object from the data object with the specified type + /// + /// + /// This is useful when you know the type of object, and it is serializable or has a type converter to convert from string. + /// If it cannot be converted it will return the default value. + /// + /// Type identifier to get from the data object + /// Type of the object to get, or null to detect type + /// An instance of the object to recieve, or the default value. + object GetObject(string type, Type objectType); } /// @@ -228,6 +247,13 @@ public void SetDataStream(Stream stream, string type) } } } + + [Serializable] + class CustomType + { + public string TypeName { get; set; } + public object Value { get; set; } + } /// /// Sets the into the data object with the specified using serialization or type converter @@ -248,15 +274,12 @@ public void SetObject(object value, string type) var baseType = value.GetType(); baseType = Nullable.GetUnderlyingType(baseType) ?? baseType; - if (baseType.GetTypeInfo().IsSerializable) + if (ObjectData.CanSerialize(baseType)) { - using (var ms = new MemoryStream()) + var data = ObjectData.Serialize(value, baseType); + if (data != null) { -#pragma warning disable SYSLIB0011 - var binaryFormatter = new BinaryFormatter(); - binaryFormatter.Serialize(ms, value); - SetDataStream(ms, type); -#pragma warning restore SYSLIB0011 + SetData(data, type); return; } } @@ -281,29 +304,9 @@ public void SetObject(object value, string type) /// An instance of the object to recieve, or the default value. public T GetObject(string type) { - if (Handler.TryGetObject(type, out var obj) && obj is T handlerValue) - return handlerValue; - - var baseType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); - - try - { - if (baseType.GetTypeInfo().IsSerializable && GetObject(type) is T value) - { - return value; - } - - var converter = System.ComponentModel.TypeDescriptor.GetConverter(baseType); - if (converter?.CanConvertFrom(typeof(string)) == true) - { - return (T)converter.ConvertFromString(GetString(type)); - } - } - catch (Exception ex) - { - // log error in debug - Debug.WriteLine(ex); - } + var val = GetObject(type, typeof(T)); + if (val is T output) + return output; return default; } @@ -312,27 +315,60 @@ public T GetObject(string type) /// /// type identifier to get the value for. /// Value of the object if deserializable, otherwise null. - public object GetObject(string type) + public object GetObject(string type) => GetObject(type, null); + + /// + /// Gets an object from the data object with the specified type + /// + /// + /// This is useful when you know the type of object, and it is serializable or has a type converter to convert from string. + /// If it cannot be converted it will return the default value. + /// + /// Type identifier to get from the data object + /// Type of the object to get, or null to detect type + /// An instance of the object to recieve, or the default value. + public object GetObject(string type, Type objectType) { - if (Handler.TryGetObject(type, out var value)) + if (objectType != null) + objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + + if (Handler.TryGetObject(type, objectType, out var value) && !(value is Stream || value is byte[])) return value; - var stream = GetDataStream(type); - if (stream == null) - return null; try { -#pragma warning disable SYSLIB0011 - var binaryFormatter = new BinaryFormatter(); - return binaryFormatter.Deserialize(stream); -#pragma warning restore SYSLIB0011 + if (ObjectData.CanSerialize(objectType)) + { + var stream = value switch + { + Stream s => s, + byte[] bytes => new MemoryStream(bytes), + _ => GetDataStream(type) + }; + + if (stream != null) + { + value = ObjectData.Deserialize(stream, objectType); + if (value != null) + return value; + } + } + + if (objectType != null) + { + var converter = System.ComponentModel.TypeDescriptor.GetConverter(objectType); + if (converter?.CanConvertFrom(typeof(string)) == true) + { + return converter.ConvertFromString(GetString(type)); + } + } } catch (Exception ex) { // log error in debug Debug.WriteLine(ex); - return null; } + return default; } /// @@ -441,8 +477,9 @@ public Uri[] Uris /// Attempts to get the specified value from the clipboard in a native-supplied way /// /// Data format type to get the value + /// Type that is requested, or null for any type /// Value returned /// True if the value was returned, false otherwise - bool TryGetObject(string type, out object value); + bool TryGetObject(string type, Type objectType, out object value); } } \ No newline at end of file diff --git a/src/Eto/Forms/ObjectData.cs b/src/Eto/Forms/ObjectData.cs new file mode 100755 index 000000000..f751a42ac --- /dev/null +++ b/src/Eto/Forms/ObjectData.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Runtime.Serialization; +using System.Text; + +namespace Eto.Forms; + +[DataContract(Name = nameof(ObjectData))] +class ObjectDataSurrogate +{ + [DataMember] + public string TypeName { get; set; } +} + +[DataContract] +class ObjectData +{ + internal static bool CanSerialize(Type type) => type == null || type.IsSerializable || type.GetCustomAttribute() != null; + internal static byte[] Serialize(object value, Type baseType) + { + var data = new ObjectData + { + TypeName = baseType.AssemblyQualifiedName, + Value = value + }; + + using (var ms = new MemoryStream()) + { + var serializer = new DataContractSerializer(typeof(ObjectData), new [] { baseType }); + serializer.WriteObject(ms, data); + + // for debugging + // var str = Encoding.UTF8.GetString(ms.ToArray()); + + return ms.ToArray(); + } + } + + internal static object Deserialize(Stream stream, Type objectType) + { + MemoryStream ms = null; + + // for debugging + // var str = new StreamReader(stream).ReadToEnd(); + // stream.Position = 0; + + if (objectType == null) + { + // we need to be able to seek so we can read twice + if (!stream.CanSeek) + { + ms = new MemoryStream(); + stream.CopyTo(ms); + stream = ms; + } + + // we don't know the type, read it from the stream first + var serializer = new DataContractSerializer(typeof(ObjectDataSurrogate)); + var dataType = serializer.ReadObject(stream) as ObjectDataSurrogate; + if (dataType == null) + { + ms?.Dispose(); + return null; + } + objectType = Type.GetType(dataType.TypeName, false); + if (objectType == null) + { + ms?.Dispose(); + return null; + } + } + + { + // read again, but with known type populated + stream.Position = 0; + var serializer = new DataContractSerializer(typeof(ObjectData), new[] { objectType }); + var data = serializer.ReadObject(stream) as ObjectData; + + ms?.Dispose(); + + if (data == null) + return null; + + return data.Value; + } + } + + [DataMember] + public string TypeName { get; set; } + [DataMember] + public object Value { get; set; } + +} diff --git a/test/Eto.Test/Sections/Behaviors/ClipboardSection.cs b/test/Eto.Test/Sections/Behaviors/ClipboardSection.cs index 87def57b3..03c856ac6 100644 --- a/test/Eto.Test/Sections/Behaviors/ClipboardSection.cs +++ b/test/Eto.Test/Sections/Behaviors/ClipboardSection.cs @@ -32,6 +32,12 @@ public ClipboardSection() clipboard.SetString("my value", "my.custom.type"); Update(); }; + var copyObjectButton = new Button { Text = "Copy Object" }; + copyObjectButton.Click += (sender, e) => + { + clipboard.SetObject(new DragDropSection.CustomSerializableType { Name = "Woot" }, "my.custom.object"); + Update(); + }; var pasteTextButton = new Button { Text = "Paste" }; pasteTextButton.Click += (sender, e) => Update(); @@ -54,7 +60,7 @@ public ClipboardSection() Orientation = Orientation.Horizontal, Spacing = 5, Padding = new Padding(10), - Items = { copyTextButton, copyHtmlButton, copyImageButton, copyCustomButton, pasteTextButton, clearButton } + Items = { copyTextButton, copyHtmlButton, copyImageButton, copyCustomButton, copyObjectButton, pasteTextButton, clearButton } }, new StackLayoutItem(pasteData, expand: true) } @@ -98,16 +104,21 @@ void Update() var data = clipboard.GetData(type); if (data != null) { - panel.Items.Add(string.Format("- Data, Length: {0}", data.Length)); + panel.Items.Add($"- Data, Length: {data.Length}"); var hexString = BitConverter.ToString(data); panel.Items.Add(hexString.Substring(0, Math.Min(hexString.Length, 1000))); } var str = clipboard.GetString(type); if (str != null) { - panel.Items.Add(string.Format("- String, Length: {0}", str.Length)); + panel.Items.Add($"- String, Length: {str.Length}"); panel.Items.Add(str); } + var obj = clipboard.GetObject(type); + if (obj != null) + { + panel.Items.Add($"- Object, Type: {obj.GetType()}: {obj}"); + } } } pasteData.Content = panel; diff --git a/test/Eto.Test/Sections/Behaviors/DragDropSection.cs b/test/Eto.Test/Sections/Behaviors/DragDropSection.cs index 7c13796ce..61c7ee814 100644 --- a/test/Eto.Test/Sections/Behaviors/DragDropSection.cs +++ b/test/Eto.Test/Sections/Behaviors/DragDropSection.cs @@ -12,6 +12,23 @@ public class DragDropSection : Panel TextBox innerTextBox; CheckBox writeDataCheckBox; EnumDropDown allowedEffectDropDown; + + [DataContract] + public class CustomDataContractType + { + [DataMember] + public string Name { get; set; } + + public override string ToString() => Name; + } + + [Serializable] + public class CustomSerializableType + { + public string Name { get; set; } + + public override string ToString() => Name; + } public DragDropSection() { @@ -61,6 +78,9 @@ DataObject CreateDataObject() data.Html = htmlTextArea.Text; if (includeImageCheck.Checked == true) data.Image = TestIcons.Logo; + + data.SetObject(new CustomDataContractType { Name = "Hello Data Contract!" }, "my.custom.datacontract.type"); + data.SetObject(new CustomSerializableType { Name = "Hello Serializable!" }, "my.custom.serializable.type"); return data; } @@ -445,15 +465,33 @@ void WriteData(DataObject data) var d = data.GetData(format); if (d != null) { - var s = string.Join(",", d.Select(r => r.ToString()).Take(1000)); - Log.Write(null, $"\t{format}: {s}"); + var s = string.Join(",", d.Select(r => r.ToString()).Take(10)); + Log.Write(null, $"\t{format}: data: {d.Length} bytes ({s})"); } else - Log.Write(null, $"\t{format}: {d}"); + Log.Write(null, $"\t{format}: data: "); } - catch + catch (Exception ex) { - + Log.Write(null, $"Error getting data for {format}, {ex.Message}"); + } + + try + { + var obj = data.GetObject(format); + if (obj != null) + { + object val = null; + if (obj.ToString() != obj.GetType().ToString()) + val = obj; + Log.Write(null, $"\t{format}: object: {obj.GetType()} {val}"); + if (obj is string[] strings) + Log.Write(null, $"\t\tvalues: {string.Join(";", strings)}"); + } + } + catch (Exception ex) + { + Log.Write(null, $"Error getting object for {format}, {ex.Message}"); } } } diff --git a/test/Eto.Test/UnitTests/Forms/ClipboardTests.cs b/test/Eto.Test/UnitTests/Forms/ClipboardTests.cs index ecd8fded9..451c4e6e9 100755 --- a/test/Eto.Test/UnitTests/Forms/ClipboardTests.cs +++ b/test/Eto.Test/UnitTests/Forms/ClipboardTests.cs @@ -43,13 +43,45 @@ public enum DataType String, Data, Uris, + SerializableObject, + NormalObject, + UnsafeObject } const string SampleText = "Hello"; const string SampleStringType = "eto-string"; const string SampleDataType = "eto-data"; + const string SampleSerializableObjectType = "eto-serializable-object"; + const string SampleObjectType = "eto-object"; + const string SampleUnsafeObjectType = "eto-unsafe-object"; const string SampleHtml = "Some Html"; + [Serializable] + public class SerializableObject : ISerializable + { + + public string SomeValue { get; set; } + public SerializableObject() + { + } + + public SerializableObject(SerializationInfo info, StreamingContext context) + { + SomeValue = info.GetString("SomeValue"); + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("SomeValue", SomeValue); + } + } + + [Serializable] + public class SomeOtherObject + { + public string SomeValue { get; set; } + } + static byte[] SampleByteData => new byte[] { 10, 20, 30 }; // note: windows only supports a single web uri, but multiple file uris @@ -105,6 +137,18 @@ void TestIsNull(T dataObject, DataType type) Assert.IsFalse(dataObject.ContainsUris); Assert.IsNull(dataObject.Uris); break; + case DataType.SerializableObject: + CollectionAssert.DoesNotContain(SampleSerializableObjectType, dataObject.Types); + Assert.IsNull(dataObject.GetObject(SampleSerializableObjectType)); + break; + case DataType.NormalObject: + CollectionAssert.DoesNotContain(SampleObjectType, dataObject.Types); + Assert.IsNull(dataObject.GetObject(SampleObjectType)); + break; + case DataType.UnsafeObject: + CollectionAssert.DoesNotContain(SampleUnsafeObjectType, dataObject.Types); + Assert.IsNull(dataObject.GetObject(SampleUnsafeObjectType)); + break; default: throw new NotSupportedException(); } @@ -155,6 +199,24 @@ void TestValue(T dataObject, DataType type) else CollectionAssert.AreEquivalent(SampleBothUris, dataObject.Uris); break; + case DataType.SerializableObject: + Assert.Contains(SampleSerializableObjectType, dataObject.Types); + var obj = dataObject.GetObject(SampleSerializableObjectType); + Assert.IsNotNull(obj); + Assert.AreEqual(obj.SomeValue, SampleText); + break; + case DataType.NormalObject: + Assert.Contains(SampleObjectType, dataObject.Types); + var obj2 = dataObject.GetObject(SampleObjectType); + Assert.IsNotNull(obj2); + Assert.AreEqual(obj2.SomeValue, SampleText); + break; + case DataType.UnsafeObject: + Assert.Contains(SampleUnsafeObjectType, dataObject.Types); + var obj3 = dataObject.GetObject(SampleUnsafeObjectType) as SomeOtherObject; + Assert.IsNotNull(obj3); + Assert.AreEqual(obj3.SomeValue, SampleText); + break; default: throw new NotSupportedException(); } @@ -191,6 +253,15 @@ void SetValue(T dataObject, DataType type) case DataType.Uris: dataObject.Uris = SampleBothUris; break; + case DataType.SerializableObject: + dataObject.SetObject(new SerializableObject { SomeValue = SampleText }, SampleSerializableObjectType); + break; + case DataType.NormalObject: + dataObject.SetObject(new SomeOtherObject { SomeValue = SampleText }, SampleObjectType); + break; + case DataType.UnsafeObject: + dataObject.SetObject(new SomeOtherObject { SomeValue = SampleText }, SampleUnsafeObjectType); + break; default: throw new NotSupportedException(); } @@ -256,7 +327,10 @@ public void SettingMultipleFormatsShouldWork() DataType.Text, DataType.Html, DataType.String, - DataType.Data + DataType.Data, + DataType.SerializableObject, + DataType.NormalObject, + DataType.UnsafeObject }; Invoke(() =>