Browse Source

Remove specific data type methods from the IDataObject, add new Files format

pull/10389/head
Max Katz 3 years ago
parent
commit
104023bfc8
  1. 41
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  2. 3
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  3. 48
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  4. 10
      src/Avalonia.Base/Input/DataFormats.cs
  5. 25
      src/Avalonia.Base/Input/DataObject.cs
  6. 50
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  7. 17
      src/Avalonia.Base/Input/IDataObject.cs
  8. 5
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  9. 9
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  10. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  11. 2
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  12. 12
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  13. 41
      src/Avalonia.Native/ClipboardImpl.cs
  14. 3
      src/Windows/Avalonia.Win32/ClipboardFormats.cs
  15. 15
      src/Windows/Avalonia.Win32/DataObject.cs
  16. 18
      src/Windows/Avalonia.Win32/OleDataObject.cs

41
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -306,25 +306,8 @@ namespace ControlCatalog.Pages
resultText += @$"
Content:
";
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
resultText += await ReadTextFromFile(file, 10000);
}
openedFileContent.Text = resultText;
@ -354,6 +337,28 @@ namespace ControlCatalog.Pages
}
}
public static async Task<string> ReadTextFromFile(IStorageFile file, int length)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
return new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);

3
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -25,7 +25,6 @@
BorderThickness="2">
<TextBlock Name="DragStateCustom" TextWrapping="Wrap">Drag Me (custom)</TextBlock>
</Border>
<TextBlock Name="DropState" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Margin="8"
@ -47,5 +46,7 @@
</Border>
</StackPanel>
</WrapPanel>
<TextBlock x:Name="DropState" TextWrapping="Wrap" />
</StackPanel>
</UserControl>

48
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -1,27 +1,29 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
namespace ControlCatalog.Pages
{
public class DragAndDropPage : UserControl
{
TextBlock _DropState;
private readonly TextBlock _dropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Get<TextBlock>("DropState");
_dropState = this.Get<TextBlock>("DropState");
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move);
SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
}
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects)
@ -68,12 +70,12 @@ namespace ControlCatalog.Pages
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames)
&& !e.Data.Contains(DataFormats.Files)
&& !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
void Drop(object? sender, DragEventArgs e)
async void Drop(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
@ -85,11 +87,41 @@ namespace ControlCatalog.Pages
}
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
{
_dropState.Text = e.Data.GetText();
}
else if (e.Data.Contains(DataFormats.Files))
{
var files = e.Data.GetFiles() ?? Array.Empty<IStorageItem>();
var contentStr = "";
foreach (var item in files)
{
if (item is IStorageFile file)
{
var content = await DialogsPage.ReadTextFromFile(file, 1000);
contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}";
}
else if (item is IStorageFolder folder)
{
var items = await folder.GetItemsAsync();
contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
}
}
_dropState.Text = contentStr;
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty<string>());
{
var files = e.Data.GetFileNames();
_dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty<string>());
}
#pragma warning restore CS0618 // Type or member is obsolete
else if (e.Data.Contains(CustomFormat))
_DropState.Text = "Custom: " + e.Data.Get(CustomFormat);
{
_dropState.Text = "Custom: " + e.Data.Get(CustomFormat);
}
}
dragMe.PointerPressed += DoDrag;

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

@ -1,4 +1,6 @@
namespace Avalonia.Input
using System;
namespace Avalonia.Input
{
public static class DataFormats
{
@ -7,9 +9,15 @@
/// </summary>
public static readonly string Text = nameof(Text);
/// <summary>
/// Dataformat for one or more files.
/// </summary>
public static readonly string Files = nameof(Files);
/// <summary>
/// Dataformat for one or more filenames
/// </summary>
[Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")]
public static readonly string FileNames = nameof(FileNames);
}
}

25
src/Avalonia.Base/Input/DataObject.cs

@ -2,37 +2,34 @@
namespace Avalonia.Input
{
/// <summary>
/// Specific and mutable implementation of the IDataObject interface.
/// </summary>
public class DataObject : IDataObject
{
private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
private readonly Dictionary<string, object> _items = new();
/// <inheritdoc />
public bool Contains(string dataFormat)
{
return _items.ContainsKey(dataFormat);
}
/// <inheritdoc />
public object? Get(string dataFormat)
{
if (_items.ContainsKey(dataFormat))
return _items[dataFormat];
return null;
return _items.TryGetValue(dataFormat, out var item) ? item : null;
}
/// <inheritdoc />
public IEnumerable<string> GetDataFormats()
{
return _items.Keys;
}
public IEnumerable<string>? GetFileNames()
{
return Get(DataFormats.FileNames) as IEnumerable<string>;
}
public string? GetText()
{
return Get(DataFormats.Text) as string;
}
/// <summary>
/// Sets a value to the internal store of the data object with <see cref="DataFormats"/> as a key.
/// </summary>
public void Set(string dataFormat, object value)
{
_items[dataFormat] = value;

50
src/Avalonia.Base/Input/DataObjectExtensions.cs

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
public static class DataObjectExtensions
{
/// <summary>
/// Returns a list of files if the DataObject contains files or filenames.
/// <seealso cref="DataFormats.Files"/>.
/// </summary>
/// <returns>
/// Collection of storage items - files or folders. If format isn't avaialble, returns null.
/// </returns>
public static IEnumerable<IStorageItem>? GetFiles(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Files) as IEnumerable<IStorageItem>;
}
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
/// <returns>
/// Collection of file names. If format isn't avaialble, returns null.
/// </returns>
[System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")]
public static IEnumerable<string>? GetFileNames(this IDataObject dataObject)
{
return (dataObject.Get(DataFormats.FileNames) as IEnumerable<string>)
?? dataObject.GetFiles()?
.Select(f => f.TryGetLocalPath())
.Where(p => !string.IsNullOrEmpty(p))
.OfType<string>();
}
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
/// <returns>
/// A text string. If format isn't avaialble, returns null.
/// </returns>
public static string? GetText(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Text) as string;
}
}
}

17
src/Avalonia.Base/Input/IDataObject.cs

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
@ -19,21 +21,12 @@ namespace Avalonia.Input
/// </summary>
bool Contains(string dataFormat);
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
string? GetText();
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
IEnumerable<string>? GetFileNames();
/// <summary>
/// Tries to get the data of the given DataFormat.
/// </summary>
/// <returns>
/// Object data. If format isn't avaialble, returns null.
/// </returns>
object? Get(string dataFormat);
}
}

5
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFile : IStorageBookmarkFile
{
public BclStorageFile(string fileName)
{
FileInfo = new FileInfo(fileName);
}
public BclStorageFile(FileInfo fileInfo)
{
FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));

9
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFolder : IStorageBookmarkFolder
{
public BclStorageFolder(string path)
{
DirectoryInfo = new DirectoryInfo(path);
if (!DirectoryInfo.Exists)
{
throw new ArgumentException("Directory must exist");
}
}
public BclStorageFolder(DirectoryInfo directoryInfo)
{
DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));

17
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO;
internal static class StorageProviderHelpers
{
public static IStorageItem? TryCreateBclStorageItem(string path)
{
var directory = new DirectoryInfo(path);
if (directory.Exists)
{
return new BclStorageFolder(directory);
}
var file = new FileInfo(path);
if (file.Exists)
{
return new BclStorageFile(file);
}
return null;
}
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)

2
src/Avalonia.Base/Platform/Storage/PickerOptions.cs

@ -12,6 +12,8 @@ public class PickerOptions
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// Can be obtained from previously picked folder or using <see cref="IStorageProvider.TryGetFolderFromPathAsync"/>
/// or <see cref="IStorageProvider.TryGetWellKnownFolderAsync"/>.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

12
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -11,12 +11,24 @@ public static class StorageProviderExtensions
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPathAsync"/>
public static Task<IStorageFile?> TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
}
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
public static Task<IStorageFolder?> TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
}
return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
}

41
src/Avalonia.Native/ClipboardImpl.cs

@ -2,11 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native
{
@ -56,8 +56,13 @@ namespace Avalonia.Native
{
if(fmt.String == NSPasteboardTypeString)
rv.Add(DataFormats.Text);
if(fmt.String == NSFilenamesPboardType)
rv.Add(DataFormats.FileNames);
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);
}
}
}
}
@ -74,7 +79,13 @@ namespace Avalonia.Native
public IEnumerable<string> GetFileNames()
{
using (var strings = _native.GetStrings(NSFilenamesPboardType))
return strings.ToStringArray();
return strings?.ToStringArray();
}
public IEnumerable<IStorageItem> GetFiles()
{
return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
.Where(f => f is not null);
}
public unsafe Task SetDataObjectAsync(IDataObject data)
@ -102,8 +113,12 @@ namespace Avalonia.Native
{
if (format == DataFormats.Text)
return await GetTextAsync();
#pragma warning disable CS0618 // Type or member is obsolete
if (format == DataFormats.FileNames)
return GetFileNames();
#pragma warning restore CS0618 // Type or member is obsolete
if (format == DataFormats.Files)
return GetFiles();
using (var n = _native.GetBytes(format))
return n.Bytes;
}
@ -131,20 +146,16 @@ namespace Avalonia.Native
public bool Contains(string dataFormat) => Formats.Contains(dataFormat);
public string GetText()
{
// bad idea in general, but API is synchronous anyway
return _clipboard.GetTextAsync().Result;
}
public IEnumerable<string> GetFileNames() => _clipboard.GetFileNames();
public object Get(string dataFormat)
{
if (dataFormat == DataFormats.Text)
return GetText();
return _clipboard.GetTextAsync().Result;
if (dataFormat == DataFormats.Files)
return _clipboard.GetFiles();
#pragma warning disable CS0618
if (dataFormat == DataFormats.FileNames)
return GetFileNames();
#pragma warning restore CS0618
return _clipboard.GetFileNames();
return null;
}
}

3
src/Windows/Avalonia.Win32/ClipboardFormats.cs

@ -29,7 +29,10 @@ namespace Avalonia.Win32
private static readonly List<ClipboardFormat> s_formatList = new()
{
new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT),
new ClipboardFormat(DataFormats.Files, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP),
#pragma warning disable CS0618 // Type or member is obsolete
new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP),
#pragma warning restore CS0618 // Type or member is obsolete
};

15
src/Windows/Avalonia.Win32/DataObject.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using Avalonia.Input;
using Avalonia.MicroCom;
using Avalonia.Platform.Storage;
using Avalonia.Win32.Interop;
using FORMATETC = Avalonia.Win32.Interop.FORMATETC;
@ -124,16 +125,6 @@ namespace Avalonia.Win32
return _wrapped.GetDataFormats();
}
IEnumerable<string>? IDataObject.GetFileNames()
{
return _wrapped.GetFileNames();
}
string? IDataObject.GetText()
{
return _wrapped.GetText();
}
object? IDataObject.Get(string dataFormat)
{
return _wrapped.Get(dataFormat);
@ -260,8 +251,12 @@ namespace Avalonia.Win32
object data = _wrapped.Get(dataFormat)!;
if (dataFormat == DataFormats.Text || data is string)
return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty);
#pragma warning disable CS0618 // Type or member is obsolete
if (dataFormat == DataFormats.FileNames && data is IEnumerable<string> files)
return WriteFileListToHGlobal(ref hGlobal, files);
#pragma warning restore CS0618 // Type or member is obsolete
if (dataFormat == DataFormats.Files && data is IEnumerable<IStorageItem> items)
return WriteFileListToHGlobal(ref hGlobal, items.Select(f => f.TryGetLocalPath()).Where(f => f is not null)!);
if (data is Stream stream)
{
var length = (int)(stream.Length - stream.Position);

18
src/Windows/Avalonia.Win32/OleDataObject.cs

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using Avalonia.Input;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Utilities;
using Avalonia.Win32.Interop;
using MicroCom.Runtime;
@ -34,16 +35,6 @@ namespace Avalonia.Win32
return GetDataFormatsCore().Distinct();
}
public string? GetText()
{
return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT);
}
public IEnumerable<string>? GetFileNames()
{
return (IEnumerable<string>?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT);
}
public object? Get(string dataFormat)
{
return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT);
@ -67,8 +58,15 @@ namespace Avalonia.Win32
{
if (format == DataFormats.Text)
return ReadStringFromHGlobal(medium.unionmember);
#pragma warning disable CS0618
if (format == DataFormats.FileNames)
#pragma warning restore CS0618
return ReadFileNamesFromHGlobal(medium.unionmember);
if (format == DataFormats.Files)
return ReadFileNamesFromHGlobal(medium.unionmember)
.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
.Where(f => f is not null);
byte[] data = ReadBytesFromHGlobal(medium.unionmember);

Loading…
Cancel
Save