Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: drop Tmds.DBus in favor of the AOT-friendlly Tmds.DBus.Protocol #19101

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,465 changes: 2,465 additions & 0 deletions src/Uno.UI.Runtime.Skia.X11/DBus/Desktop.DBus.cs

Large diffs are not rendered by default.

745 changes: 0 additions & 745 deletions src/Uno.UI.Runtime.Skia.X11/DBus/OrgFreedesktopPortalDesktop.cs

This file was deleted.

28 changes: 0 additions & 28 deletions src/Uno.UI.Runtime.Skia.X11/DBus/OrgFreedesktopPortalRequest.cs

This file was deleted.

213 changes: 120 additions & 93 deletions src/Uno.UI.Runtime.Skia.X11/LinuxFilePickerExtension.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
Expand All @@ -8,12 +9,12 @@
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI.ViewManagement;
using Tmds.DBus;
using Tmds.DBus.Protocol;
using Uno.Extensions.Storage.Pickers;
using Uno.Foundation.Logging;
using Uno.UI.Helpers;
using Uno.UI.Helpers.WinUI;
using Uno.WinUI.Runtime.Skia.X11.Dbus;
using Uno.WinUI.Runtime.Skia.X11.DBus;

namespace Uno.WinUI.Runtime.Skia.X11;

Expand Down Expand Up @@ -43,133 +44,161 @@ public async Task<IReadOnlyList<StorageFile>> PickMultipleFilesAsync(Cancellatio

public async Task<IReadOnlyList<string>> PickFilesAsync(CancellationToken token, bool multiple, bool directory)
{
Connection? connection;
ConnectionInfo? info;
try
{
connection = Connection.Session;
info = await connection.ConnectAsync();
}
catch (Exception e)
var sessionsAddressBus = Address.Session;
if (sessionsAddressBus is null)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"Unable to connect to DBus, see https://aka.platform.uno/x11-dbus-troubleshoot for troubleshooting information.", e);
this.Log().Error($"Can not determine DBus session bus address");
}

return await Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
return ImmutableList<string>.Empty;
}

var fileChooser = connection.CreateProxy<IFileChooser>(Service, ObjectPath);

if (fileChooser is null)
if (token.IsCancellationRequested)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"Unable to find object {ObjectPath} at DBus service {Service}, make sure you have an xdg-desktop-portal implementation installed. For more information, visit https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces");
}

return await Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
return ImmutableList<string>.Empty;
}

if (!X11Helper.XamlRootHostFromApplicationView(ApplicationView.GetForCurrentViewSafe(), out var host))
using var connection = new Connection(sessionsAddressBus);
var connectionTcs = new TaskCompletionSource();
// ConnectAsync calls ConfigureAwait(false), so we need this TCS dance to make the continuation continue on the UI thread
_ = connection.ConnectAsync().AsTask().ContinueWith(_ => connectionTcs.TrySetResult(), token);
var timeoutTask = Task.Delay(1000, token);
var finishedTask = await Task.WhenAny(connectionTcs.Task, timeoutTask);
if (finishedTask == timeoutTask)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"Unable to get the {nameof(X11XamlRootHost)} instance from the application view.");
this.Log().Error($"Timed out while trying to connect to DBus");
}

return await Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
return ImmutableList<string>.Empty;
}

var handleToken = "UnoFileChooser" + Random.Shared.Next();
try
{
var requestPath = $"{ResultObjectPathPrefix}/{info.LocalName[1..].Replace(".", "_")}/{handleToken}";
var desktopService = new DesktopService(connection, Service);
var chooser = desktopService.CreateFileChooser(ObjectPath);

var result = connection.CreateProxy<IRequest>(Service, requestPath);
if (token.IsCancellationRequested)
{
return ImmutableList<string>.Empty;
}

var tcs = new TaskCompletionSource<IReadOnlyList<string>>();
var version = await chooser.GetVersionAsync();
if (version != 3)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"File pickers are only implemented for version 3 of the File chooser portal, but version {version} was found");
}
return ImmutableList<string>.Empty;
}

token.Register(() =>
if (!X11Helper.XamlRootHostFromApplicationView(ApplicationView.GetForCurrentViewSafe(), out var host))
{
if (this.Log().IsEnabled(LogLevel.Debug))
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Debug($"File picker cancelled.");
this.Log().Error($"Unable to get the {nameof(X11XamlRootHost)} instance from the application view.");
}

tcs.TrySetResult(Array.Empty<string>());
});
return ImmutableList<string>.Empty;
}

var handleToken = "UnoFileChooser" + Random.Shared.NextInt64();
var requestPath = $"{ResultObjectPathPrefix}/{connection.UniqueName![1..].Replace(".", "_")}/{handleToken}";

if (token.IsCancellationRequested)
{
return ImmutableList<string>.Empty;
}

// We listen for the signal BEFORE we send the request. The spec API reference
// points out the race condition that could occur otherwise.
using var _ = await result.WatchResponseAsync(r =>
var request = desktopService.CreateRequest(requestPath);
var responseTcs = new TaskCompletionSource<(uint Response, Dictionary<string, VariantValue> Results)>();
_ = request.WatchResponseAsync((exception, tuple) =>
{
if (r.Response is Response.Success)
if (exception is not null)
{
tcs.TrySetResult(((IReadOnlyList<string>)r.results["uris"]).Select(s => new Uri(s).AbsolutePath).ToList());
responseTcs.SetException(exception);
}
else
{
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug($"File picker received an unsuccessful response {r.Response}.");
}

tcs.TrySetResult(Array.Empty<string>());
responseTcs.SetResult(tuple);
}
}, e =>
});

if (token.IsCancellationRequested)
{
return ImmutableList<string>.Empty;
}

var actualRequestPath = await chooser.OpenFileAsync(
parentWindow: "x11:" + host.RootX11Window.Window.ToString("X", CultureInfo.InvariantCulture),
title: (multiple, directory) switch
{
(true, true) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_DIRECTORY_MULTIPLE"),
(true, false) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_FILE_MULTIPLE"),
(false, true) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_DIRECTORY_SINGLE"),
(false, false) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_FILE_SINGLE")
},
options: new Dictionary<string, VariantValue>
{
{ "handle_token", handleToken },
{ "accept_label", string.IsNullOrEmpty(picker.CommitButtonTextInternal) ? ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_ACCEPT_LABEL") : picker.CommitButtonTextInternal },
{ "multiple", multiple },
{ "directory", directory },
{ "filters", GetPortalFilters() },
{ "current_folder", new Array<byte>(Encoding.UTF8.GetBytes(PickerHelpers.GetInitialDirectory(picker.SuggestedStartLocationInternal)).Append((byte)'\0')) }
});

if (actualRequestPath != requestPath)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"File picker failed to receive a response.", e);
this.Log().Error($"{nameof(chooser.OpenFileAsync)} returned a path '{actualRequestPath}' that is different from supplied handle_token -based path '{requestPath}'");
}

tcs.TrySetResult(Array.Empty<string>());
});
return ImmutableList<string>.Empty;
}

var title = (multiple, directory) switch
{
(true, true) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_DIRECTORY_MULTIPLE"),
(true, false) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_FILE_MULTIPLE"),
(false, true) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_DIRECTORY_SINGLE"),
(false, false) => ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_TITLE_FILE_SINGLE")
};
var window = "x11:" + host.RootX11Window.Window.ToString("X", CultureInfo.InvariantCulture);
var requestPath2 = await fileChooser.OpenFileAsync(window, title, new Dictionary<string, object>
{
{ "handle_token", handleToken },
{ "accept_label", string.IsNullOrEmpty(picker.CommitButtonTextInternal) ? ResourceAccessor.GetLocalizedStringResource("FILE_PICKER_ACCEPT_LABEL") : picker.CommitButtonTextInternal },
{ "multiple", multiple },
{ "directory", directory },
{ "filters", GetPortalFilters(picker.FileTypeFilterInternal) },
{ "current_folder", Encoding.UTF8.GetBytes(PickerHelpers.GetInitialDirectory(picker.SuggestedStartLocationInternal)).Append((byte)'\0') }
});
var (response, results) = await responseTcs.Task;

if (requestPath != requestPath2)
if (!Enum.IsDefined(typeof(Response), response))
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"We are waiting at a wrong path {requestPath} instead of {requestPath2}");
this.Log().Error($"FileChooser returned an invalid response number {response}.");
}

tcs.TrySetResult(Array.Empty<string>());
return ImmutableList<string>.Empty;
}

return await tcs.Task;

if ((Response)response is not Response.Success)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would not unboxing response earlier, prevent unboxing the same object in lines 180 and 182?

{
if ((Response)response is not Response.UserCancelled && this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"FileChooser's response indicates an unsuccessful operation {(Response)response}");
}

return ImmutableList<string>.Empty;
}

return results["uris"].GetArray<string>().Select(s => new Uri(s).AbsolutePath).ToImmutableList();
}
catch (Exception e)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"Failed to pick file", e);
this.Log().Error($"DBus FileChooser error, see https://aka.platform.uno/x11-dbus-troubleshoot for troubleshooting information.", e);
}

return await Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
return ImmutableList<string>.Empty;
}
}

private (string, (uint, string)[])[] GetPortalFilters(IList<string> filters)
private Array<Struct<string, Array<Struct<uint, string>>>> GetPortalFilters()
{
// Except from the API reference
// filters (a(sa(us)))
Expand All @@ -185,35 +214,33 @@ public async Task<IReadOnlyList<string>> PickFilesAsync(CancellationToken token,
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
//
// Note that filters are purely there to aid the user in making a useful selection. The portal may still allow the user to select files that don’t match any filter criteria, and applications must be prepared to handle that.
//
var list = new List<(uint kind, string pattern)>();
foreach (var filter in filters)

// We don't have a way to map a filter to a category (e.g. image/png -> Images), so we make every filter its own category
var list = new Array<Struct<string, Array<Struct<uint, string>>>>();
foreach (var pattern in picker.FileTypeFilterInternal.Distinct())
{
try
if (pattern is null)
{
// will throw if not a valid MIME type
list.Add((1, new System.Net.Mime.ContentType(filter).ToString()));
continue;
}
catch (Exception)

if (pattern == "*")
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"Failed to parse portal filer {filter} as a MIME type, falling back to glob.");
}
// The portal accepts any glob pattern, but to be similar to other platforms, we
// assume the filter is of the form `.extension`.
if (filter == "*")
{
list.Add((0, filter));
}
else
list.Add(Struct.Create("All Files", new Array<Struct<uint, string>>(new[] { Struct.Create((uint)0, "*.*") })));
}
else if (pattern.StartsWith('.') && pattern[1..] is var ext && ext.All(char.IsLetterOrDigit))
{
list.Add(Struct.Create($"{ext.ToUpperInvariant()} Files", new Array<Struct<uint, string>>(new[] { Struct.Create((uint)0, $"*.{ext}") })));
}
else
{
if (this.Log().IsEnabled(LogLevel.Error))
{
list.Add((0, $"*{filter}"));
this.Log().Error($"Skipping invalid file extension filter: '{pattern}'");
}
}
}

// We don't have a way to map a filter to a category (e.g. image/png -> Images), so we make every filter its own category
return list.Select(f => (f.pattern, new[] { f })).ToArray();
return list;
}
}
Loading
Loading