5 changed files with 263 additions and 140 deletions
@ -1,102 +1,171 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Platform.Storage.FileIO; |
|||
|
|||
using Tmds.DBus; |
|||
|
|||
namespace Avalonia.FreeDesktop |
|||
{ |
|||
internal class DBusSystemDialog : ISystemDialogImpl |
|||
internal class DBusSystemDialog : BclStorageProvider |
|||
{ |
|||
private readonly IFileChooser _fileChooser; |
|||
|
|||
internal static DBusSystemDialog? TryCreate() |
|||
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() => |
|||
{ |
|||
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); |
|||
if (fileChooser is null) |
|||
return null; |
|||
try |
|||
{ |
|||
fileChooser.GetVersionAsync().GetAwaiter().GetResult(); |
|||
return new DBusSystemDialog(fileChooser); |
|||
_ = fileChooser.GetVersionAsync(); |
|||
return fileChooser; |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); |
|||
return null; |
|||
} |
|||
}); |
|||
|
|||
internal static DBusSystemDialog? TryCreate(IPlatformHandle handle) |
|||
{ |
|||
return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser |
|||
? new DBusSystemDialog(fileChooser, handle) : null; |
|||
} |
|||
|
|||
private DBusSystemDialog(IFileChooser fileChooser) |
|||
private readonly IFileChooser _fileChooser; |
|||
private readonly IPlatformHandle _handle; |
|||
|
|||
private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle) |
|||
{ |
|||
_fileChooser = fileChooser; |
|||
_handle = handle; |
|||
} |
|||
|
|||
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent) |
|||
public override bool CanOpen => true; |
|||
|
|||
public override bool CanSave => true; |
|||
|
|||
public override bool CanPickFolder => true; |
|||
|
|||
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
|||
{ |
|||
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}"; |
|||
var parentWindow = $"x11:{_handle.Handle:X}"; |
|||
ObjectPath objectPath; |
|||
var options = new Dictionary<string, object>(); |
|||
if (dialog.Filters is not null) |
|||
options.Add("filters", ParseFilters(dialog)); |
|||
var chooserOptions = new Dictionary<string, object>(); |
|||
var filters = ParseFilters(options.FileTypeFilter); |
|||
if (filters.Any()) |
|||
{ |
|||
chooserOptions.Add("filters", filters); |
|||
} |
|||
|
|||
chooserOptions.Add("multiple", options.AllowMultiple); |
|||
|
|||
objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); |
|||
|
|||
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath); |
|||
var tsc = new TaskCompletionSource<string[]?>(); |
|||
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
|||
var uris = await tsc.Task ?? Array.Empty<string>(); |
|||
|
|||
return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).AbsolutePath))).ToList(); |
|||
} |
|||
|
|||
switch (dialog) |
|||
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
|||
{ |
|||
var parentWindow = $"x11:{_handle.Handle:X}"; |
|||
ObjectPath objectPath; |
|||
var chooserOptions = new Dictionary<string, object>(); |
|||
var filters = ParseFilters(options.FileTypeChoices); |
|||
if (filters.Any()) |
|||
{ |
|||
case OpenFileDialog openFileDialog: |
|||
options.Add("multiple", openFileDialog.AllowMultiple); |
|||
objectPath = await _fileChooser.OpenFileAsync(parentWindow, openFileDialog.Title ?? string.Empty, options); |
|||
break; |
|||
case SaveFileDialog saveFileDialog: |
|||
if (saveFileDialog.InitialFileName is not null) |
|||
options.Add("current_name", saveFileDialog.InitialFileName); |
|||
if (saveFileDialog.Directory is not null) |
|||
options.Add("current_folder", Encoding.UTF8.GetBytes(saveFileDialog.Directory)); |
|||
objectPath = await _fileChooser.SaveFileAsync(parentWindow, saveFileDialog.Title ?? string.Empty, options); |
|||
break; |
|||
chooserOptions.Add("filters", filters); |
|||
} |
|||
|
|||
if (options.SuggestedFileName is { } currentName) |
|||
chooserOptions.Add("current_name", currentName); |
|||
if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true) |
|||
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); |
|||
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); |
|||
|
|||
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath); |
|||
var tsc = new TaskCompletionSource<string[]?>(); |
|||
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
|||
var uris = await tsc.Task; |
|||
if (uris is null) |
|||
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).AbsolutePath : null; |
|||
|
|||
if (path is null) |
|||
{ |
|||
return null; |
|||
for (var i = 0; i < uris.Length; i++) |
|||
uris[i] = new Uri(uris[i]).AbsolutePath; |
|||
return uris; |
|||
} |
|||
else |
|||
{ |
|||
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
|
|||
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); |
|||
|
|||
return new BclStorageFile(new FileInfo(path)); |
|||
} |
|||
} |
|||
|
|||
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) |
|||
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
|||
{ |
|||
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}"; |
|||
var options = new Dictionary<string, object> |
|||
var parentWindow = $"x11:{_handle.Handle:X}"; |
|||
var chooserOptions = new Dictionary<string, object> |
|||
{ |
|||
{ "directory", true } |
|||
{ "directory", true }, |
|||
{ "multiple", options.AllowMultiple } |
|||
}; |
|||
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options); |
|||
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); |
|||
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath); |
|||
var tsc = new TaskCompletionSource<string[]?>(); |
|||
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
|||
var uris = await tsc.Task; |
|||
if (uris is null) |
|||
return null; |
|||
return uris.Length != 1 ? string.Empty : new Uri(uris[0]).AbsolutePath; |
|||
var uris = await tsc.Task ?? Array.Empty<string>(); |
|||
|
|||
return uris |
|||
.Select(path => new Uri(path).AbsolutePath) |
|||
// WSL2 freedesktop allows to select files as well in directory picker, filter it out.
|
|||
.Where(Directory.Exists) |
|||
.Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); |
|||
} |
|||
|
|||
private static (string name, (uint style, string extension)[])[] ParseFilters(FileDialog dialog) |
|||
private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes) |
|||
{ |
|||
var filters = new (string name, (uint style, string extension)[])[dialog.Filters!.Count]; |
|||
for (var i = 0; i < filters.Length; i++) |
|||
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
|
|||
|
|||
if (fileTypes is null) |
|||
{ |
|||
var extensions = dialog.Filters[i].Extensions.Select(static x => (0u, x)).ToArray(); |
|||
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions); |
|||
return Array.Empty<(string name, (uint style, string extension)[])>(); |
|||
} |
|||
|
|||
var filters = new List<(string name, (uint style, string extension)[])>(); |
|||
foreach (var fileType in fileTypes) |
|||
{ |
|||
const uint globStyle = 0u; |
|||
const uint mimeStyle = 1u; |
|||
|
|||
var extensions = Enumerable.Empty<(uint, string)>(); |
|||
|
|||
if (fileType.Patterns is { } patterns) |
|||
{ |
|||
extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); |
|||
} |
|||
else if (fileType.MimeTypes is { } mimeTypes) |
|||
{ |
|||
extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); |
|||
} |
|||
|
|||
if (extensions.Any()) |
|||
{ |
|||
filters.Add((fileType.Name, extensions.ToArray())); |
|||
} |
|||
} |
|||
|
|||
return filters; |
|||
return filters.ToArray(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue