From 0289a515b38984601d36186a260c44edf5acd8be Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Jun 2022 00:59:16 -0400 Subject: [PATCH 1/9] Add file picker interface definitions --- src/Avalonia.Base/Logging/LogArea.cs | 10 ++ .../Platform/Storage/FileIO/BclStorageFile.cs | 107 ++++++++++++++++++ .../Storage/FileIO/BclStorageFolder.cs | 88 ++++++++++++++ .../Storage/FileIO/BclStorageProvider.cs | 35 ++++++ .../Storage/FileIO/StorageProviderHelpers.cs | 40 +++++++ .../Platform/Storage/FilePickerFileType.cs | 44 +++++++ .../Platform/Storage/FilePickerFileTypes.cs | 48 ++++++++ .../Platform/Storage/FilePickerOpenOptions.cs | 29 +++++ .../Platform/Storage/FilePickerSaveOptions.cs | 39 +++++++ .../Storage/FolderPickerOpenOptions.cs | 22 ++++ .../Platform/Storage/IStorageBookmarkItem.cs | 21 ++++ .../Platform/Storage/IStorageFile.cs | 32 ++++++ .../Platform/Storage/IStorageFolder.cs | 12 ++ .../Platform/Storage/IStorageItem.cs | 53 +++++++++ .../Platform/Storage/IStorageProvider.cs | 56 +++++++++ .../Platform/Storage/StorageItemProperties.cs | 43 +++++++ .../Dialogs/IStorageProviderFactory.cs | 12 ++ .../{ => Dialogs}/ISystemDialogImpl.cs | 2 + .../Platform/Dialogs/SystemDialogImpl.cs | 74 ++++++++++++ .../ITopLevelImplWithStorageProvider.cs | 11 ++ src/Avalonia.Controls/SystemDialog.cs | 57 ++++++++++ src/Avalonia.Controls/TopLevel.cs | 9 +- 22 files changed, 843 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageFile.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageFolder.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageItem.cs create mode 100644 src/Avalonia.Base/Platform/Storage/IStorageProvider.cs create mode 100644 src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs create mode 100644 src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs rename src/Avalonia.Controls/Platform/{ => Dialogs}/ISystemDialogImpl.cs (88%) create mode 100644 src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs create mode 100644 src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index c049f9e7638..98ef6d25309 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -44,5 +44,15 @@ public static class LogArea /// The log event comes from X11Platform. /// public const string X11Platform = nameof(X11Platform); + + /// + /// The log event comes from AndroidPlatform. + /// + public const string AndroidPlatform = nameof(AndroidPlatform); + + /// + /// The log event comes from IOSPlatform. + /// + public const string IOSPlatform = nameof(IOSPlatform); } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs new file mode 100644 index 00000000000..5af02219cec --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public class BclStorageFile : IStorageBookmarkFile +{ + private readonly FileInfo _fileInfo; + + public BclStorageFile(FileInfo fileInfo) + { + _fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); + } + + public bool CanOpenRead => true; + + public bool CanOpenWrite => true; + + public string Name => _fileInfo.Name; + + public virtual bool CanBookmark => true; + + public Task GetBasicPropertiesAsync() + { + var props = new StorageItemProperties(); + if (_fileInfo.Exists) + { + props = new StorageItemProperties( + (ulong)_fileInfo.Length, + _fileInfo.CreationTimeUtc, + _fileInfo.LastAccessTimeUtc); + } + return Task.FromResult(props); + } + + public Task GetParentAsync() + { + if (_fileInfo.Directory is { } directory) + { + return Task.FromResult(new BclStorageFolder(directory)); + } + return Task.FromResult(null); + } + + public Task OpenRead() + { + return Task.FromResult(_fileInfo.OpenRead()); + } + + public Task OpenWrite() + { + return Task.FromResult(_fileInfo.OpenWrite()); + } + + public virtual Task SaveBookmark() + { + return Task.FromResult(_fileInfo.FullName); + } + + public Task ReleaseBookmark() + { + // No-op + return Task.CompletedTask; + } + + public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + { + try + { + if (_fileInfo.Directory is not null) + { + uri = Path.IsPathRooted(_fileInfo.FullName) ? + new Uri(new Uri("file://"), _fileInfo.FullName) : + new Uri(_fileInfo.FullName, UriKind.Relative); + return true; + } + + uri = null; + return false; + } + catch (SecurityException) + { + uri = null; + return false; + } + } + + protected virtual void Dispose(bool disposing) + { + } + + ~BclStorageFile() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs new file mode 100644 index 00000000000..7267017eaf2 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public class BclStorageFolder : IStorageBookmarkFolder +{ + private readonly DirectoryInfo _directoryInfo; + + public BclStorageFolder(DirectoryInfo directoryInfo) + { + _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); + if (!_directoryInfo.Exists) + { + throw new ArgumentException("Directory must exist", nameof(directoryInfo)); + } + } + + public string Name => _directoryInfo.Name; + + public bool CanBookmark => true; + + public Task GetBasicPropertiesAsync() + { + var props = new StorageItemProperties( + null, + _directoryInfo.CreationTimeUtc, + _directoryInfo.LastAccessTimeUtc); + return Task.FromResult(props); + } + + public Task GetParentAsync() + { + if (_directoryInfo.Parent is { } directory) + { + return Task.FromResult(new BclStorageFolder(directory)); + } + return Task.FromResult(null); + } + + public virtual Task SaveBookmark() + { + return Task.FromResult(_directoryInfo.FullName); + } + + public Task ReleaseBookmark() + { + // No-op + return Task.CompletedTask; + } + + public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + { + try + { + uri = Path.IsPathRooted(_directoryInfo.FullName) ? + new Uri(new Uri("file://"), _directoryInfo.FullName) : + new Uri(_directoryInfo.FullName, UriKind.Relative); + + return true; + } + catch (SecurityException) + { + uri = null; + return false; + } + } + + protected virtual void Dispose(bool disposing) + { + } + + ~BclStorageFolder() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs new file mode 100644 index 00000000000..469388021ef --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public abstract class BclStorageProvider : IStorageProvider +{ + public abstract bool CanOpen { get; } + public abstract Task> OpenFilePickerAsync(FilePickerOpenOptions options); + + public abstract bool CanSave { get; } + public abstract Task SaveFilePickerAsync(FilePickerSaveOptions options); + + public abstract bool CanPickFolder { get; } + public abstract Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); + + public virtual Task OpenFileBookmarkAsync(string bookmark) + { + var file = new FileInfo(bookmark); + return file.Exists + ? Task.FromResult(new BclStorageFile(file)) + : Task.FromResult(null); + } + + public virtual Task OpenFolderBookmarkAsync(string bookmark) + { + var folder = new DirectoryInfo(bookmark); + return folder.Exists + ? Task.FromResult(new BclStorageFolder(folder)) + : Task.FromResult(null); + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs new file mode 100644 index 00000000000..f90d0a5a2ff --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.Linq; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage.FileIO; + +[Unstable] +public static class StorageProviderHelpers +{ + public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) + { + var name = Path.GetFileName(path); + if (name != null && !Path.HasExtension(name)) + { + if (filter?.Patterns?.Count > 0) + { + if (defaultExtension != null + && filter.Patterns.Contains(defaultExtension)) + { + return Path.ChangeExtension(path, defaultExtension.TrimStart('.')); + } + + var ext = filter.Patterns.FirstOrDefault(x => x != "*.*"); + ext = ext?.Split(new[] { "*." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); + if (ext != null) + { + return Path.ChangeExtension(path, ext); + } + } + + if (defaultExtension != null) + { + return Path.ChangeExtension(path, defaultExtension); + } + } + + return path; + } +} diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs new file mode 100644 index 00000000000..98848ac9f76 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Represents a name mapped to the associated file types (extensions). +/// +public class FilePickerFileType +{ + public FilePickerFileType(string name) + { + Name = name; + } + + /// + /// File type name. + /// + public string Name { get; } + + /// + /// List of extensions in GLOB format. I.e. "*.png" or "*.*". + /// + /// + /// Used on Windows and Linux systems. + /// + public IReadOnlyList? Patterns { get; set; } + + /// + /// List of extensions in MIME format. + /// + /// + /// Used on Android, Browser and Linux systems. + /// + public IReadOnlyList? MimeTypes { get; set; } + + /// + /// List of extensions in Apple uniform format. + /// + /// + /// Used only on Apple devices. + /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. + /// + public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs new file mode 100644 index 00000000000..5da037999a1 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs @@ -0,0 +1,48 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Dictionary of well known file types. +/// +public static class FilePickerFileTypes +{ + public static FilePickerFileType All { get; } = new("All") + { + Patterns = new[] { "*.*" }, + MimeTypes = new[] { "*/*" } + }; + + public static FilePickerFileType TextPlain { get; } = new("Plain Text") + { + Patterns = new[] { "*.txt" }, + AppleUniformTypeIdentifiers = new[] { "public.plain-text" }, + MimeTypes = new[] { "text/plain" } + }; + + public static FilePickerFileType ImageAll { get; } = new("All Images") + { + Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" }, + AppleUniformTypeIdentifiers = new[] { "public.image" }, + MimeTypes = new[] { "image/*" } + }; + + public static FilePickerFileType ImageJpg { get; } = new("JPEG image") + { + Patterns = new[] { "*.jpg", "*.jpeg" }, + AppleUniformTypeIdentifiers = new[] { "public.jpeg" }, + MimeTypes = new[] { "image/jpeg" } + }; + + public static FilePickerFileType ImagePng { get; } = new("PNG image") + { + Patterns = new[] { "*.png" }, + AppleUniformTypeIdentifiers = new[] { "public.png" }, + MimeTypes = new[] { "image/png" } + }; + + public static FilePickerFileType Pdf { get; } = new("PDF document") + { + Patterns = new[] { "*.pdf" }, + AppleUniformTypeIdentifiers = new[] { "com.adobe.pdf" }, + MimeTypes = new[] { "application/pdf" } + }; +} diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs new file mode 100644 index 00000000000..1f9202b0e74 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Options class for method. +/// +public class FilePickerOpenOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a file dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } + + /// + /// Gets or sets an option indicating whether open picker allows users to select multiple files. + /// + public bool AllowMultiple { get; set; } + + /// + /// Gets or sets the collection of file types that the file open picker displays. + /// + public IReadOnlyList? FileTypeFilter { get; set; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs new file mode 100644 index 00000000000..0f4d690f7ad --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Avalonia.Platform.Storage; + +/// +/// Options class for method. +/// +public class FilePickerSaveOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a file dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the file name that the file save picker suggests to the user. + /// + public string? SuggestedFileName { get; set; } + + /// + /// Gets or sets the default extension to be used to save the file. + /// + public string? DefaultExtension { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } + + /// + /// Gets or sets the collection of valid file types that the user can choose to assign to a file. + /// + public IReadOnlyList? FileTypeChoices { get; set; } + + /// + /// Gets or sets a value indicating whether file open picker displays a warning if the user specifies the name of a file that already exists. + /// + public bool? ShowOverwritePrompt { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs new file mode 100644 index 00000000000..de90da30b2e --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs @@ -0,0 +1,22 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Options class for method. +/// +public class FolderPickerOpenOptions +{ + /// + /// Gets or sets the text that appears in the title bar of a folder dialog. + /// + public string? Title { get; set; } + + /// + /// Gets or sets an option indicating whether open picker allows users to select multiple folders. + /// + public bool AllowMultiple { get; set; } + + /// + /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// + public IStorageFolder? SuggestedStartLocation { get; set; } +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs new file mode 100644 index 00000000000..65811b7fbda --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +[NotClientImplementable] +public interface IStorageBookmarkItem : IStorageItem +{ + Task ReleaseBookmark(); +} + +[NotClientImplementable] +public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem +{ +} + +[NotClientImplementable] +public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem +{ + +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs new file mode 100644 index 00000000000..2f12514e505 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +/// +/// Represents a file. Provides information about the file and its contents, and ways to manipulate them. +/// +[NotClientImplementable] +public interface IStorageFile : IStorageItem +{ + /// + /// Returns true, if file is readable. + /// + bool CanOpenRead { get; } + + /// + /// Opens a stream for read access. + /// + Task OpenRead(); + + /// + /// Returns true, if file is writeable. + /// + bool CanOpenWrite { get; } + + /// + /// Opens stream for writing to the file. + /// + Task OpenWrite(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs new file mode 100644 index 00000000000..83b316bc3b4 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageFolder.cs @@ -0,0 +1,12 @@ +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +/// +/// Manipulates folders and their contents, and provides information about them. +/// +[NotClientImplementable] +public interface IStorageFolder : IStorageItem +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs new file mode 100644 index 00000000000..078311a2862 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -0,0 +1,53 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +/// +/// Manipulates storage items (files and folders) and their contents, and provides information about them +/// +/// +/// This interface inherits . It's recommended to dispose when it's not used anymore. +/// +[NotClientImplementable] +public interface IStorageItem : IDisposable +{ + /// + /// Gets the name of the item including the file name extension if there is one. + /// + string Name { get; } + + /// + /// Gets the full file-system path of the item, if the item has a path. + /// + /// + /// Android backend might return file path with "content:" scheme. + /// Browser and iOS backends might return relative uris. + /// + bool TryGetUri([NotNullWhen(true)] out Uri? uri); + + /// + /// Gets the basic properties of the current item. + /// + Task GetBasicPropertiesAsync(); + + /// + /// Returns true is item can be bookmarked and reused later. + /// + bool CanBookmark { get; } + + /// + /// Saves items to a bookmark. + /// + /// + /// Returns identifier of a bookmark. Can be null if OS denied request. + /// + Task SaveBookmark(); + + /// + /// Gets the parent folder of the current storage item. + /// + Task GetParentAsync(); +} diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs new file mode 100644 index 00000000000..32fb148790e --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Platform.Storage; + +[NotClientImplementable] +public interface IStorageProvider +{ + /// + /// Returns true if it's possible to open file picker on the current platform. + /// + bool CanOpen { get; } + + /// + /// Opens file picker dialog. + /// + /// Array of selected or empty collection if user canceled the dialog. + Task> OpenFilePickerAsync(FilePickerOpenOptions options); + + /// + /// Returns true if it's possible to open save file picker on the current platform. + /// + bool CanSave { get; } + + /// + /// Opens save file picker dialog. + /// + /// Saved or null if user canceled the dialog. + Task SaveFilePickerAsync(FilePickerSaveOptions options); + + /// + /// Returns true if it's possible to open folder picker on the current platform. + /// + bool CanPickFolder { get; } + + /// + /// Opens folder picker dialog. + /// + /// Array of selected or empty collection if user canceled the dialog. + Task> OpenFolderPickerAsync(FolderPickerOpenOptions options); + + /// + /// Open from the bookmark ID. + /// + /// Bookmark ID. + /// Bookmarked file or null if OS denied request. + Task OpenFileBookmarkAsync(string bookmark); + + /// + /// Open from the bookmark ID. + /// + /// Bookmark ID. + /// Bookmarked folder or null if OS denied request. + Task OpenFolderBookmarkAsync(string bookmark); +} diff --git a/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs new file mode 100644 index 00000000000..41b9bfa9417 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs @@ -0,0 +1,43 @@ +using System; + +namespace Avalonia.Platform.Storage; + +/// +/// Provides access to the content-related properties of an item (like a file or folder). +/// +public class StorageItemProperties +{ + public StorageItemProperties( + ulong? size = null, + DateTimeOffset? dateCreated = null, + DateTimeOffset? dateModified = null) + { + Size = size; + DateCreated = dateCreated; + DateModified = dateModified; + } + + /// + /// Gets the size of the file in bytes. + /// + /// + /// Can be null if property is not available. + /// + public ulong? Size { get; } + + /// + /// Gets the date and time that the current folder was created. + /// + /// + /// Can be null if property is not available. + /// + public DateTimeOffset? DateCreated { get; } + + /// + /// Gets the date and time of the last time the file was modified. + /// + /// + /// Can be null if property is not available. + /// + public DateTimeOffset? DateModified { get; } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs new file mode 100644 index 00000000000..3eee8e848ea --- /dev/null +++ b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs @@ -0,0 +1,12 @@ +#nullable enable +using Avalonia.Platform.Storage; + +namespace Avalonia.Controls.Platform; + +/// +/// Factory allows to register custom storage provider instead of native implementation. +/// +public interface IStorageProviderFactory +{ + IStorageProvider CreateProvider(TopLevel topLevel); +} diff --git a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs similarity index 88% rename from src/Avalonia.Controls/Platform/ISystemDialogImpl.cs rename to src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs index 715eda5cfa7..996fff6775c 100644 --- a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Avalonia.Metadata; @@ -6,6 +7,7 @@ namespace Avalonia.Controls.Platform /// /// Defines a platform-specific system dialog implementation. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] [Unstable] public interface ISystemDialogImpl { diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs new file mode 100644 index 00000000000..2775c538035 --- /dev/null +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +#nullable enable + +namespace Avalonia.Controls.Platform +{ + /// + /// Defines a platform-specific system dialog implementation. + /// + [Obsolete] + internal class SystemDialogImpl : ISystemDialogImpl + { + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) + { + if (dialog is OpenFileDialog openDialog) + { + var filePicker = parent.StorageProvider; + if (!filePicker.CanOpen) + { + return null; + } + + var options = openDialog.ToFilePickerOpenOptions(); + + var files = await filePicker.OpenFilePickerAsync(options); + return files + .Select(file => file.TryGetUri(out var fullPath) + ? fullPath.LocalPath + : file.Name) + .ToArray(); + } + else if (dialog is SaveFileDialog saveDialog) + { + var filePicker = parent.StorageProvider; + if (!filePicker.CanSave) + { + return null; + } + + var options = saveDialog.ToFilePickerSaveOptions(); + + var file = await filePicker.SaveFilePickerAsync(options); + if (file is null) + { + return null; + } + + var filePath = file.TryGetUri(out var fullPath) + ? fullPath.LocalPath + : file.Name; + return new[] { filePath }; + } + return null; + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) + { + var filePicker = parent.StorageProvider; + if (!filePicker.CanPickFolder) + { + return null; + } + + var options = dialog.ToFolderPickerOpenOptions(); + + var folders = await filePicker.OpenFolderPickerAsync(options); + return folders + .Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null) + .FirstOrDefault(u => u is not null); + } + } +} diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs new file mode 100644 index 00000000000..b42040f3c3f --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs @@ -0,0 +1,11 @@ +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Platform.Storage; + +namespace Avalonia.Controls.Platform; + +[Unstable] +public interface ITopLevelImplWithStorageProvider : ITopLevelImpl +{ + public IStorageProvider StorageProvider { get; } +} diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 093f10be511..f3fb4d9a6d5 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -3,12 +3,15 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Controls { /// /// Base class for system file dialogs. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public abstract class FileDialog : FileSystemDialog { /// @@ -26,6 +29,7 @@ public abstract class FileDialog : FileSystemDialog /// /// Base class for system file and directory dialogs. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public abstract class FileSystemDialog : SystemDialog { [Obsolete("Use Directory")] @@ -45,6 +49,7 @@ public string? InitialDirectory /// /// Represents a system dialog that prompts the user to select a location for saving a file. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class SaveFileDialog : FileDialog { /// @@ -73,11 +78,27 @@ public class SaveFileDialog : FileDialog return (await service.ShowFileDialogAsync(this, parent) ?? Array.Empty()).FirstOrDefault(); } + + public FilePickerSaveOptions ToFilePickerSaveOptions() + { + return new FilePickerSaveOptions + { + SuggestedFileName = InitialFileName, + DefaultExtension = DefaultExtension, + FileTypeChoices = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(), + Title = Title, + SuggestedStartLocation = InitialDirectory is { } directory + ? new BclStorageFolder(new System.IO.DirectoryInfo(directory)) + : null, + ShowOverwritePrompt = ShowOverwritePrompt + }; + } } /// /// Represents a system dialog that allows the user to select one or more files to open. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class OpenFileDialog : FileDialog { /// @@ -100,11 +121,25 @@ public class OpenFileDialog : FileDialog var service = AvaloniaLocator.Current.GetRequiredService(); return service.ShowFileDialogAsync(this, parent); } + + public FilePickerOpenOptions ToFilePickerOpenOptions() + { + return new FilePickerOpenOptions + { + AllowMultiple = AllowMultiple, + FileTypeFilter = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(), + Title = Title, + SuggestedStartLocation = InitialDirectory is { } directory + ? new BclStorageFolder(new System.IO.DirectoryInfo(directory)) + : null + }; + } } /// /// Represents a system dialog that allows the user to select a directory. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class OpenFolderDialog : FileSystemDialog { [Obsolete("Use Directory")] @@ -129,14 +164,35 @@ public string? DefaultDirectory var service = AvaloniaLocator.Current.GetRequiredService(); return service.ShowFolderDialogAsync(this, parent); } + + public FolderPickerOpenOptions ToFolderPickerOpenOptions() + { + return new FolderPickerOpenOptions + { + Title = Title, + SuggestedStartLocation = InitialDirectory is { } directory + ? new BclStorageFolder(new System.IO.DirectoryInfo(directory)) + : null + }; + } } /// /// Base class for system dialogs. /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public abstract class SystemDialog { + static SystemDialog() + { + if (AvaloniaLocator.Current.GetService() is null) + { + // Register default implementation. + AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); + } + } + /// /// Gets or sets the dialog title. /// @@ -146,6 +202,7 @@ public abstract class SystemDialog /// /// Represents a filter in an or an . /// + [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")] public class FileDialogFilter { /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 57fb82485c3..d09b8249587 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -11,6 +11,7 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; @@ -93,7 +94,8 @@ private static readonly WeakEvent private ILayoutManager? _layoutManager; private Border? _transparencyFallbackBorder; private TargetWeakEventSubscriber? _resourcesChangesSubscriber; - + private IStorageProvider? _storageProvider; + /// /// Initializes static members of the class. /// @@ -319,6 +321,11 @@ bool IInputRoot.ShowAccessKeys double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1; IStyleHost IStyleHost.StylingParent => _globalStyles!; + + public IStorageProvider StorageProvider => _storageProvider + ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) + ?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider + ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); From e717cce7e822f25cc4290150597410489fcbdc26 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Jun 2022 01:00:07 -0400 Subject: [PATCH 2/9] Update headless implementations, managed and samples --- samples/ControlCatalog/ControlCatalog.csproj | 2 +- samples/ControlCatalog/MainView.xaml | 3 + samples/ControlCatalog/MainView.xaml.cs | 5 - samples/ControlCatalog/Pages/DialogsPage.xaml | 76 ++++-- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 234 ++++++++++++++++-- .../Pages/NumericUpDownPage.xaml | 6 +- .../Pages/NumericUpDownPage.xaml.cs | 13 +- .../Remote/PreviewerWindowImpl.cs | 6 +- .../Remote/PreviewerWindowingPlatform.cs | 1 - src/Avalonia.DesignerSupport/Remote/Stubs.cs | 32 ++- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 4 - .../ManagedFileChooserFilterViewModel.cs | 35 +-- .../ManagedFileChooserViewModel.cs | 97 +++++--- .../ManagedFileDialogExtensions.cs | 142 ++--------- .../ManagedStorageProvider.cs | 147 +++++++++++ .../AvaloniaHeadlessPlatform.cs | 1 - .../HeadlessPlatformStubs.cs | 36 ++- src/Avalonia.Headless/HeadlessWindowImpl.cs | 6 +- 18 files changed, 576 insertions(+), 270 deletions(-) create mode 100644 src/Avalonia.Dialogs/ManagedStorageProvider.cs diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 903c849834e..8358fb3cd42 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -1,6 +1,6 @@  - netstandard2.0 + netstandard2.0;net6.0 true enable diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index d8dc3bad2d5..b6ce59f15b7 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -69,6 +69,9 @@ + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d675324d9f7..47d11738bc4 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -24,11 +24,6 @@ public MainView() { IList tabItems = ((IList)sideBar.Items); tabItems.Add(new TabItem() - { - Header = "Dialogs", - Content = new DialogsPage() - }); - tabItems.Add(new TabItem() { Header = "Screens", Content = new ScreenPage() diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 8a835867b35..cc23ef796ab 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -1,29 +1,57 @@ - - - Use filters - - - - - + + - - + - - - - - - - + + + + + + + + + + + + + + Use filters + + + Force managed dialog + Open multiple + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index efa30c27416..f7b6db12556 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -1,13 +1,21 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Dialogs; using Avalonia.Layout; using Avalonia.Markup.Xaml; -#pragma warning disable 4014 +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; + +#pragma warning disable CS0618 // Type or member is obsolete +#nullable enable + namespace ControlCatalog.Pages { public class DialogsPage : UserControl @@ -18,13 +26,16 @@ public DialogsPage() var results = this.Get("PickerLastResults"); var resultsVisible = this.Get("PickerLastResultsVisible"); + var bookmarkContainer = this.Get("BookmarkContainer"); + var openedFileContent = this.Get("OpenedFileContent"); + var openMultiple = this.Get("OpenMultiple"); - string? lastSelectedDirectory = null; + IStorageFolder? lastSelectedDirectory = null; - List? GetFilters() + List GetFilters() { if (this.Get("UseFilters").IsChecked != true) - return null; + return new List(); return new List { new FileDialogFilter @@ -39,12 +50,23 @@ public DialogsPage() }; } + List? GetFileTypes() + { + if (this.Get("UseFilters").IsChecked != true) + return null; + return new List + { + FilePickerFileTypes.All, + FilePickerFileTypes.TextPlain + }; + } + this.Get