Browse Source

Add file picker interface definitions

pull/8303/head
Max Katz 4 years ago
parent
commit
0289a515b3
  1. 10
      src/Avalonia.Base/Logging/LogArea.cs
  2. 107
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  3. 88
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  4. 35
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  5. 40
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  6. 44
      src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
  7. 48
      src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
  8. 29
      src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
  9. 39
      src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
  10. 22
      src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
  11. 21
      src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
  12. 32
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  13. 12
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  14. 53
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  15. 56
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  16. 43
      src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
  17. 12
      src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs
  18. 2
      src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs
  19. 74
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  20. 11
      src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs
  21. 57
      src/Avalonia.Controls/SystemDialog.cs
  22. 9
      src/Avalonia.Controls/TopLevel.cs

10
src/Avalonia.Base/Logging/LogArea.cs

@ -44,5 +44,15 @@ namespace Avalonia.Logging
/// The log event comes from X11Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
/// <summary>
/// The log event comes from AndroidPlatform.
/// </summary>
public const string AndroidPlatform = nameof(AndroidPlatform);
/// <summary>
/// The log event comes from IOSPlatform.
/// </summary>
public const string IOSPlatform = nameof(IOSPlatform);
}
}

107
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<StorageItemProperties> GetBasicPropertiesAsync()
{
var props = new StorageItemProperties();
if (_fileInfo.Exists)
{
props = new StorageItemProperties(
(ulong)_fileInfo.Length,
_fileInfo.CreationTimeUtc,
_fileInfo.LastAccessTimeUtc);
}
return Task.FromResult(props);
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_fileInfo.Directory is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
return Task.FromResult<IStorageFolder?>(null);
}
public Task<Stream> OpenRead()
{
return Task.FromResult<Stream>(_fileInfo.OpenRead());
}
public Task<Stream> OpenWrite()
{
return Task.FromResult<Stream>(_fileInfo.OpenWrite());
}
public virtual Task<string?> SaveBookmark()
{
return Task.FromResult<string?>(_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);
}
}

88
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<StorageItemProperties> GetBasicPropertiesAsync()
{
var props = new StorageItemProperties(
null,
_directoryInfo.CreationTimeUtc,
_directoryInfo.LastAccessTimeUtc);
return Task.FromResult(props);
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_directoryInfo.Parent is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
return Task.FromResult<IStorageFolder?>(null);
}
public virtual Task<string?> SaveBookmark()
{
return Task.FromResult<string?>(_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);
}
}

35
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<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
public abstract bool CanSave { get; }
public abstract Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
public abstract bool CanPickFolder { get; }
public abstract Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
public virtual Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var file = new FileInfo(bookmark);
return file.Exists
? Task.FromResult<IStorageBookmarkFile?>(new BclStorageFile(file))
: Task.FromResult<IStorageBookmarkFile?>(null);
}
public virtual Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var folder = new DirectoryInfo(bookmark);
return folder.Exists
? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder))
: Task.FromResult<IStorageBookmarkFolder?>(null);
}
}

40
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;
}
}

44
src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs

@ -0,0 +1,44 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Represents a name mapped to the associated file types (extensions).
/// </summary>
public class FilePickerFileType
{
public FilePickerFileType(string name)
{
Name = name;
}
/// <summary>
/// File type name.
/// </summary>
public string Name { get; }
/// <summary>
/// List of extensions in GLOB format. I.e. "*.png" or "*.*".
/// </summary>
/// <remarks>
/// Used on Windows and Linux systems.
/// </remarks>
public IReadOnlyList<string>? Patterns { get; set; }
/// <summary>
/// List of extensions in MIME format.
/// </summary>
/// <remarks>
/// Used on Android, Browser and Linux systems.
/// </remarks>
public IReadOnlyList<string>? MimeTypes { get; set; }
/// <summary>
/// List of extensions in Apple uniform format.
/// </summary>
/// <remarks>
/// Used only on Apple devices.
/// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
/// </remarks>
public IReadOnlyList<string>? AppleUniformTypeIdentifiers { get; set; }
}

48
src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs

@ -0,0 +1,48 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Dictionary of well known file types.
/// </summary>
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" }
};
}

29
src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.OpenFilePickerAsync"/> method.
/// </summary>
public class FilePickerOpenOptions
{
/// <summary>
/// Gets or sets the text that appears in the title bar of a file dialog.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple files.
/// </summary>
public bool AllowMultiple { get; set; }
/// <summary>
/// Gets or sets the collection of file types that the file open picker displays.
/// </summary>
public IReadOnlyList<FilePickerFileType>? FileTypeFilter { get; set; }
}

39
src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.SaveFilePickerAsync"/> method.
/// </summary>
public class FilePickerSaveOptions
{
/// <summary>
/// Gets or sets the text that appears in the title bar of a file dialog.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the file name that the file save picker suggests to the user.
/// </summary>
public string? SuggestedFileName { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file.
/// </summary>
public string? DefaultExtension { get; set; }
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
/// <summary>
/// Gets or sets the collection of valid file types that the user can choose to assign to a file.
/// </summary>
public IReadOnlyList<FilePickerFileType>? FileTypeChoices { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool? ShowOverwritePrompt { get; set; }
}

22
src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs

@ -0,0 +1,22 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.OpenFolderPickerAsync"/> method.
/// </summary>
public class FolderPickerOpenOptions
{
/// <summary>
/// Gets or sets the text that appears in the title bar of a folder dialog.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple folders.
/// </summary>
public bool AllowMultiple { get; set; }
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

21
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
{
}

32
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;
/// <summary>
/// Represents a file. Provides information about the file and its contents, and ways to manipulate them.
/// </summary>
[NotClientImplementable]
public interface IStorageFile : IStorageItem
{
/// <summary>
/// Returns true, if file is readable.
/// </summary>
bool CanOpenRead { get; }
/// <summary>
/// Opens a stream for read access.
/// </summary>
Task<Stream> OpenRead();
/// <summary>
/// Returns true, if file is writeable.
/// </summary>
bool CanOpenWrite { get; }
/// <summary>
/// Opens stream for writing to the file.
/// </summary>
Task<Stream> OpenWrite();
}

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

@ -0,0 +1,12 @@
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Manipulates folders and their contents, and provides information about them.
/// </summary>
[NotClientImplementable]
public interface IStorageFolder : IStorageItem
{
}

53
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;
/// <summary>
/// Manipulates storage items (files and folders) and their contents, and provides information about them
/// </summary>
/// <remarks>
/// This interface inherits <see cref="IDisposable"/> . It's recommended to dispose <see cref="IStorageItem"/> when it's not used anymore.
/// </remarks>
[NotClientImplementable]
public interface IStorageItem : IDisposable
{
/// <summary>
/// Gets the name of the item including the file name extension if there is one.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the full file-system path of the item, if the item has a path.
/// </summary>
/// <remarks>
/// Android backend might return file path with "content:" scheme.
/// Browser and iOS backends might return relative uris.
/// </remarks>
bool TryGetUri([NotNullWhen(true)] out Uri? uri);
/// <summary>
/// Gets the basic properties of the current item.
/// </summary>
Task<StorageItemProperties> GetBasicPropertiesAsync();
/// <summary>
/// Returns true is item can be bookmarked and reused later.
/// </summary>
bool CanBookmark { get; }
/// <summary>
/// Saves items to a bookmark.
/// </summary>
/// <returns>
/// Returns identifier of a bookmark. Can be null if OS denied request.
/// </returns>
Task<string?> SaveBookmark();
/// <summary>
/// Gets the parent folder of the current storage item.
/// </summary>
Task<IStorageFolder?> GetParentAsync();
}

56
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
{
/// <summary>
/// Returns true if it's possible to open file picker on the current platform.
/// </summary>
bool CanOpen { get; }
/// <summary>
/// Opens file picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFile"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
/// <summary>
/// Returns true if it's possible to open save file picker on the current platform.
/// </summary>
bool CanSave { get; }
/// <summary>
/// Opens save file picker dialog.
/// </summary>
/// <returns>Saved <see cref="IStorageFile"/> or null if user canceled the dialog.</returns>
Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
/// <summary>
/// Returns true if it's possible to open folder picker on the current platform.
/// </summary>
bool CanPickFolder { get; }
/// <summary>
/// Opens folder picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFolder"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
/// <summary>
/// Open <see cref="IStorageBookmarkFile"/> from the bookmark ID.
/// </summary>
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked file or null if OS denied request.</returns>
Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark);
/// <summary>
/// Open <see cref="IStorageBookmarkFolder"/> from the bookmark ID.
/// </summary>
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked folder or null if OS denied request.</returns>
Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark);
}

43
src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs

@ -0,0 +1,43 @@
using System;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Provides access to the content-related properties of an item (like a file or folder).
/// </summary>
public class StorageItemProperties
{
public StorageItemProperties(
ulong? size = null,
DateTimeOffset? dateCreated = null,
DateTimeOffset? dateModified = null)
{
Size = size;
DateCreated = dateCreated;
DateModified = dateModified;
}
/// <summary>
/// Gets the size of the file in bytes.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public ulong? Size { get; }
/// <summary>
/// Gets the date and time that the current folder was created.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public DateTimeOffset? DateCreated { get; }
/// <summary>
/// Gets the date and time of the last time the file was modified.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public DateTimeOffset? DateModified { get; }
}

12
src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs

@ -0,0 +1,12 @@
#nullable enable
using Avalonia.Platform.Storage;
namespace Avalonia.Controls.Platform;
/// <summary>
/// Factory allows to register custom storage provider instead of native implementation.
/// </summary>
public interface IStorageProviderFactory
{
IStorageProvider CreateProvider(TopLevel topLevel);
}

2
src/Avalonia.Controls/Platform/ISystemDialogImpl.cs → 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
/// <summary>
/// Defines a platform-specific system dialog implementation.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
[Unstable]
public interface ISystemDialogImpl
{

74
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
{
/// <summary>
/// Defines a platform-specific system dialog implementation.
/// </summary>
[Obsolete]
internal class SystemDialogImpl : ISystemDialogImpl
{
public async Task<string[]?> 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<string?> 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);
}
}
}

11
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; }
}

57
src/Avalonia.Controls/SystemDialog.cs

@ -3,12 +3,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Controls
{
/// <summary>
/// Base class for system file dialogs.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class FileDialog : FileSystemDialog
{
/// <summary>
@ -26,6 +29,7 @@ namespace Avalonia.Controls
/// <summary>
/// Base class for system file and directory dialogs.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class FileSystemDialog : SystemDialog
{
[Obsolete("Use Directory")]
@ -45,6 +49,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a system dialog that prompts the user to select a location for saving a file.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class SaveFileDialog : FileDialog
{
/// <summary>
@ -73,11 +78,27 @@ namespace Avalonia.Controls
return (await service.ShowFileDialogAsync(this, parent) ??
Array.Empty<string>()).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
};
}
}
/// <summary>
/// Represents a system dialog that allows the user to select one or more files to open.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class OpenFileDialog : FileDialog
{
/// <summary>
@ -100,11 +121,25 @@ namespace Avalonia.Controls
var service = AvaloniaLocator.Current.GetRequiredService<ISystemDialogImpl>();
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
};
}
}
/// <summary>
/// Represents a system dialog that allows the user to select a directory.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class OpenFolderDialog : FileSystemDialog
{
[Obsolete("Use Directory")]
@ -129,14 +164,35 @@ namespace Avalonia.Controls
var service = AvaloniaLocator.Current.GetRequiredService<ISystemDialogImpl>();
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
};
}
}
/// <summary>
/// Base class for system dialogs.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class SystemDialog
{
static SystemDialog()
{
if (AvaloniaLocator.Current.GetService<ISystemDialogImpl>() is null)
{
// Register default implementation.
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>();
}
}
/// <summary>
/// Gets or sets the dialog title.
/// </summary>
@ -146,6 +202,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a filter in an <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class FileDialogFilter
{
/// <summary>

9
src/Avalonia.Controls/TopLevel.cs

@ -11,6 +11,7 @@ using Avalonia.Logging;
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 @@ namespace Avalonia.Controls
private ILayoutManager? _layoutManager;
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider;
/// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class.
/// </summary>
@ -319,6 +321,11 @@ namespace Avalonia.Controls
double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1;
IStyleHost IStyleHost.StylingParent => _globalStyles!;
public IStorageProvider StorageProvider => _storageProvider
??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider
?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget();

Loading…
Cancel
Save