Browse Source

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

Max Katz 2 years ago
parent
commit
104023bfc8

+ 23 - 18
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);

+ 2 - 1
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>

+ 40 - 8
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;

+ 9 - 1
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);
     }
 }

+ 11 - 14
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 - 0
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;
+        }
+    }
+}

+ 5 - 12
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);
     }
 }

+ 0 - 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));

+ 0 - 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 - 0
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 - 0
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 - 0
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));
     }
 

+ 26 - 15
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 - 0
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
         };
 
 

+ 5 - 10
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);

+ 8 - 10
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);