diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
index a311efdfb0..80159c82d7 100644
--- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
+++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
@@ -14,6 +14,10 @@
+
+
+
+
diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
index 1970c5557d..e4025453c4 100644
--- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
+++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Dialogs
{
public static class ManagedFileDialogExtensions
{
- private class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new()
+ internal class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new()
{
async Task Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null)
{
@@ -141,7 +141,7 @@ namespace Avalonia.Dialogs
public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) => ShowManagedAsync(dialog, parent, options);
-
+
public static Task ShowManagedAsync(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) where TWindow : Window, new()
{
diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
index e9d6394aa5..a5cb207223 100644
--- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
+++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
@@ -2,10 +2,12 @@
net6.0;netstandard2.0
+ enable
+
diff --git a/src/Avalonia.FreeDesktop/DBusFileChooser.cs b/src/Avalonia.FreeDesktop/DBusFileChooser.cs
new file mode 100644
index 0000000000..24db614a02
--- /dev/null
+++ b/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 OpenFileAsync(string ParentWindow, string Title, IDictionary Options);
+ Task SaveFileAsync(string ParentWindow, string Title, IDictionary Options);
+ Task SaveFilesAsync(string ParentWindow, string Title, IDictionary Options);
+ Task GetAsync(string prop);
+ Task GetAllAsync();
+ Task SetAsync(string prop, object val);
+ Task WatchPropertiesAsync(Action handler);
+ }
+
+ [Dictionary]
+ internal class FileChooserProperties
+ {
+ public uint Version { get; set; }
+ }
+
+ internal static class FileChooserExtensions
+ {
+ public static Task GetVersionAsync(this IFileChooser o) => o.GetAsync("version");
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
index c14539d7bf..7204e51dbd 100644
--- a/src/Avalonia.FreeDesktop/DBusHelper.cs
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -6,7 +6,7 @@ using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
- public class DBusHelper
+ public static class DBusHelper
{
///
/// This class uses synchronous execution at DBus connection establishment stage
@@ -14,14 +14,14 @@ namespace Avalonia.FreeDesktop
///
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
{
diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs
new file mode 100644
index 0000000000..940a476916
--- /dev/null
+++ b/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 WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action onError = null);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
new file mode 100644
index 0000000000..d1905a4569
--- /dev/null
+++ b/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("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 ShowFileDialogAsync(FileDialog dialog, Window parent)
+ {
+ var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
+ ObjectPath objectPath;
+ var options = new Dictionary();
+ 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("org.freedesktop.portal.Request", objectPath);
+ var tsc = new TaskCompletionSource();
+ 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 ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
+ {
+ var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
+ var options = new Dictionary
+ {
+ { "directory", true }
+ };
+ var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options);
+ var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath);
+ var tsc = new TaskCompletionSource();
+ 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;
+ }
+ }
+}
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index ec3f29c806..fa7ae69759 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/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().ToConstant(new X11Clipboard(this))
.Bind().ToConstant(new PlatformSettingsStub())
.Bind().ToConstant(new X11IconLoader(Info))
- .Bind().ToConstant(new GtkSystemDialog())
+ .Bind().ToConstant(DBusSystemDialog.TryCreate() as ISystemDialogImpl ?? new ManagedFileDialogExtensions.ManagedSystemDialogImpl())
.Bind().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind().ToConstant(new X11PlatformLifetimeEvents(this));
@@ -209,10 +209,10 @@ namespace Avalonia
public bool OverlayPopups { get; set; }
///
- /// 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.
///
- public bool UseDBusMenu { get; set; }
+ public bool UseDBusMenu { get; set; } = true;
///
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.