Browse Source

Fall back to managed dialogs when xdg-desktop-portal is unavailable

pull/8217/head
affederaffe 4 years ago
parent
commit
8026fb1047
  1. 8
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  2. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  3. 24
      src/Avalonia.FreeDesktop/DBusHelper.cs
  4. 68
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  5. 9
      src/Avalonia.X11/X11Platform.cs

8
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -139,9 +139,15 @@ namespace Avalonia.Dialogs
return builder;
}
public static Task<string[]> ShowManagedAsync(this FileDialog dialog, Window parent)
=> new ManagedSystemDialogImpl<Window>().ShowFileDialogAsync(dialog, parent);
public static Task<string> ShowManagedAsync(this OpenFolderDialog dialog, Window parent)
=> new ManagedSystemDialogImpl<Window>().ShowFolderDialogAsync(dialog, parent);
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">

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
{

68
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -5,20 +5,56 @@ using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Dialogs;
using Avalonia.Logging;
using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : ISystemDialogImpl
{
private readonly IFileChooser _fileChooser;
private readonly IFileChooser? _fileChooser;
private bool _isDbusAvailable;
internal DBusSystemDialog()
{
_fileChooser = DBusHelper.Connection.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_isDbusAvailable = _fileChooser is not null;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
if (!_isDbusAvailable)
return await dialog.ShowManagedAsync(parent);
try
{
return await ShowNativeFileDialogAsync(dialog, parent);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(this, e.Message);
_isDbusAvailable = false;
return await dialog.ShowManagedAsync(parent);
}
}
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
if (!_isDbusAvailable)
return await dialog.ShowManagedAsync(parent);
try
{
return await ShowNativeFolderDialogAsync(dialog, parent);
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(this, e.Message);
_isDbusAvailable = false;
return await dialog.ShowManagedAsync(parent);
}
}
private async Task<string[]?> ShowNativeFileDialogAsync(FileDialog dialog, Window parent)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
ObjectPath objectPath;
@ -30,38 +66,42 @@ namespace Avalonia.FreeDesktop
{
case OpenFileDialog openFileDialog:
options.Add("multiple", openFileDialog.AllowMultiple);
objectPath = await _fileChooser.OpenFileAsync(parentWindow, openFileDialog.Title ?? string.Empty, options);
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);
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.TrySetResult(x.results["uris"] as string[]));
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)
private async Task<string?> ShowNativeFolderDialogAsync(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.TrySetResult(x.results["uris"] as string[]));
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;
}
@ -71,7 +111,7 @@ namespace Avalonia.FreeDesktop
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, extensions);
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions);
}
return filters;

9
src/Avalonia.X11/X11Platform.cs

@ -15,7 +15,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 +79,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(new DBusSystemDialog())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
@ -209,10 +208,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