5 changed files with 263 additions and 140 deletions
@ -1,102 +1,171 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
using System.Linq; |
using System.Linq; |
||||
using System.Text; |
using System.Text; |
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Controls.Platform; |
|
||||
using Avalonia.Logging; |
using Avalonia.Logging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Platform.Storage; |
||||
|
using Avalonia.Platform.Storage.FileIO; |
||||
|
|
||||
using Tmds.DBus; |
using Tmds.DBus; |
||||
|
|
||||
namespace Avalonia.FreeDesktop |
namespace Avalonia.FreeDesktop |
||||
{ |
{ |
||||
internal class DBusSystemDialog : ISystemDialogImpl |
internal class DBusSystemDialog : BclStorageProvider |
||||
{ |
{ |
||||
private readonly IFileChooser _fileChooser; |
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() => |
||||
|
|
||||
internal static DBusSystemDialog? TryCreate() |
|
||||
{ |
{ |
||||
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); |
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); |
||||
if (fileChooser is null) |
if (fileChooser is null) |
||||
return null; |
return null; |
||||
try |
try |
||||
{ |
{ |
||||
fileChooser.GetVersionAsync().GetAwaiter().GetResult(); |
_ = fileChooser.GetVersionAsync(); |
||||
return new DBusSystemDialog(fileChooser); |
return fileChooser; |
||||
} |
} |
||||
catch (Exception e) |
catch (Exception e) |
||||
{ |
{ |
||||
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); |
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); |
||||
return null; |
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; |
_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; |
ObjectPath objectPath; |
||||
var options = new Dictionary<string, object>(); |
var chooserOptions = new Dictionary<string, object>(); |
||||
if (dialog.Filters is not null) |
var filters = ParseFilters(options.FileTypeFilter); |
||||
options.Add("filters", ParseFilters(dialog)); |
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: |
chooserOptions.Add("filters", filters); |
||||
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; |
|
||||
} |
} |
||||
|
|
||||
|
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 request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath); |
||||
var tsc = new TaskCompletionSource<string[]?>(); |
var tsc = new TaskCompletionSource<string[]?>(); |
||||
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
||||
var uris = await tsc.Task; |
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; |
return null; |
||||
for (var i = 0; i < uris.Length; i++) |
} |
||||
uris[i] = new Uri(uris[i]).AbsolutePath; |
else |
||||
return uris; |
{ |
||||
|
// 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 parentWindow = $"x11:{_handle.Handle:X}"; |
||||
var options = new Dictionary<string, object> |
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 request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath); |
||||
var tsc = new TaskCompletionSource<string[]?>(); |
var tsc = new TaskCompletionSource<string[]?>(); |
||||
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); |
||||
var uris = await tsc.Task; |
var uris = await tsc.Task ?? Array.Empty<string>(); |
||||
if (uris is null) |
|
||||
return null; |
return uris |
||||
return uris.Length != 1 ? string.Empty : new Uri(uris[0]).AbsolutePath; |
.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]; |
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
|
||||
for (var i = 0; i < filters.Length; i++) |
|
||||
|
if (fileTypes is null) |
||||
{ |
{ |
||||
var extensions = dialog.Filters[i].Extensions.Select(static x => (0u, x)).ToArray(); |
return Array.Empty<(string name, (uint style, string extension)[])>(); |
||||
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions); |
} |
||||
|
|
||||
|
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