Browse Source

Merge pull request #8217 from affederaffe/feature/xdg-desktop-portal-file-chooser

Add xdg-desktop-portal file chooser
pull/8250/head
Max Katz 4 years ago
committed by GitHub
parent
commit
76fa3c56a0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  2. 4
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  3. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  4. 32
      src/Avalonia.FreeDesktop/DBusFileChooser.cs
  5. 24
      src/Avalonia.FreeDesktop/DBusHelper.cs
  6. 16
      src/Avalonia.FreeDesktop/DBusRequest.cs
  7. 102
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  8. 10
      src/Avalonia.X11/X11Platform.cs

4
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -14,6 +14,10 @@
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
</Project>

4
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -8,7 +8,7 @@ namespace Avalonia.Dialogs
{
public static class ManagedFileDialogExtensions
{
private class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
internal class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
{
async Task<string[]> Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null)
{
@ -141,7 +141,7 @@ namespace Avalonia.Dialogs
public static Task<string[]> ShowManagedAsync(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) => ShowManagedAsync<Window>(dialog, parent, options);
public static Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) where TWindow : Window, new()
{

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -2,10 +2,12 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<PackageReference Include="Tmds.DBus" Version="0.9.0" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">

32
src/Avalonia.FreeDesktop/DBusFileChooser.cs

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
[DBusInterface("org.freedesktop.portal.FileChooser")]
internal interface IFileChooser : IDBusObject
{
Task<ObjectPath> OpenFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFilesAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<T> GetAsync<T>(string prop);
Task<FileChooserProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[Dictionary]
internal class FileChooserProperties
{
public uint Version { get; set; }
}
internal static class FileChooserExtensions
{
public static Task<uint> GetVersionAsync(this IFileChooser o) => o.GetAsync<uint>("version");
}
}

24
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -6,7 +6,7 @@ using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
public class DBusHelper
public static class DBusHelper
{
/// <summary>
/// This class uses synchronous execution at DBus connection establishment stage
@ -14,14 +14,14 @@ namespace Avalonia.FreeDesktop
/// </summary>
private class DBusSyncContext : SynchronizationContext
{
private SynchronizationContext _ctx;
private object _lock = new object();
private readonly object _lock = new();
private SynchronizationContext? _ctx;
public override void Post(SendOrPostCallback d, object state)
{
lock (_lock)
{
if (_ctx != null)
if (_ctx is not null)
_ctx?.Post(d, state);
else
lock (_lock)
@ -33,10 +33,9 @@ namespace Avalonia.FreeDesktop
{
lock (_lock)
{
if (_ctx != null)
if (_ctx is not null)
_ctx?.Send(d, state);
else
d(state);
}
}
@ -47,15 +46,14 @@ namespace Avalonia.FreeDesktop
_ctx = new AvaloniaSynchronizationContext();
}
}
public static Connection Connection { get; private set; }
public static Connection TryInitialize(string dbusAddress = null)
public static Connection? Connection { get; private set; }
public static Connection? TryInitialize(string? dbusAddress = null)
=> Connection ?? TryCreateNewConnection(dbusAddress);
public static Connection? TryCreateNewConnection(string? dbusAddress = null)
{
return Connection ?? TryCreateNewConnection(dbusAddress);
}
public static Connection TryCreateNewConnection(string dbusAddress = null)
{
var oldContext = SynchronizationContext.Current;
try
{

16
src/Avalonia.FreeDesktop/DBusRequest.cs

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
[DBusInterface("org.freedesktop.portal.Request")]
internal interface IRequest : IDBusObject
{
Task CloseAsync();
Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception> onError = null);
}
}

102
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : ISystemDialogImpl
{
private readonly IFileChooser _fileChooser;
internal static DBusSystemDialog? TryCreate()
{
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);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
}
private DBusSystemDialog(IFileChooser fileChooser)
{
_fileChooser = fileChooser;
}
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
ObjectPath objectPath;
var options = new Dictionary<string, object>();
if (dialog.Filters is not null)
options.Add("filters", ParseFilters(dialog));
switch (dialog)
{
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;
}
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;
for (var i = 0; i < uris.Length; i++)
uris[i] = new Uri(uris[i]).AbsolutePath;
return uris;
}
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
var options = new Dictionary<string, object>
{
{ "directory", true }
};
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options);
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;
}
private static (string name, (uint style, string extension)[])[] ParseFilters(FileDialog dialog)
{
var filters = new (string name, (uint style, string extension)[])[dialog.Filters!.Count];
for (var i = 0; i < filters.Length; i++)
{
var extensions = dialog.Filters[i].Extensions.Select(static x => (0u, x)).ToArray();
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions);
}
return filters;
}
}
}

10
src/Avalonia.X11/X11Platform.cs

@ -5,6 +5,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Dialogs;
using Avalonia.FreeDesktop;
using Avalonia.FreeDesktop.DBusIme;
using Avalonia.Input;
@ -15,7 +16,6 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.X11;
using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
@ -80,7 +80,7 @@ namespace Avalonia.X11
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog())
.Bind<ISystemDialogImpl>().ToConstant(DBusSystemDialog.TryCreate() as ISystemDialogImpl ?? new ManagedFileDialogExtensions.ManagedSystemDialogImpl<Window>())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
@ -209,10 +209,10 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
/// <summary>
/// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is false.
/// Enables native file dialogs as well as global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc).
/// The default value is true.
/// </summary>
public bool UseDBusMenu { get; set; }
public bool UseDBusMenu { get; set; } = true;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.

Loading…
Cancel
Save