Skip to content

Commit

Permalink
Update GTK and FreeDesktop implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkatz6 committed Jun 24, 2022
1 parent 06e141b commit eacd679
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 140 deletions.
159 changes: 114 additions & 45 deletions src/Avalonia.FreeDesktop/DBusSystemDialog.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
3 changes: 3 additions & 0 deletions src/Avalonia.X11/NativeDialogs/Gtk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ public static extern void
[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_add_pattern(IntPtr filter, Utf8Buffer pattern);

[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_add_mime_type (IntPtr filter, Utf8Buffer mimeType);

[DllImport(GtkName)]
public static extern IntPtr gtk_file_chooser_add_filter(IntPtr chooser, IntPtr filter);

Expand Down
Loading

0 comments on commit eacd679

Please sign in to comment.