diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 1970c5557d..effdc847a0 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -139,9 +139,15 @@ namespace Avalonia.Dialogs return builder; } + public static Task ShowManagedAsync(this FileDialog dialog, Window parent) + => new ManagedSystemDialogImpl().ShowFileDialogAsync(dialog, parent); + + public static Task ShowManagedAsync(this OpenFolderDialog dialog, Window parent) + => new ManagedSystemDialogImpl().ShowFolderDialogAsync(dialog, parent); + 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/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/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 88f3e528e5..7bc287ea28 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/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("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _fileChooser = DBusHelper.Connection?.CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _isDbusAvailable = _fileChooser is not null; } - public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) + public async Task 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 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 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("org.freedesktop.portal.Request", objectPath); - var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.TrySetResult(x.results["uris"] as string[])); + 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) + private async Task ShowNativeFolderDialogAsync(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.TrySetResult(x.results["uris"] as string[])); + 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; } @@ -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; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index ec3f29c806..8765299d1d 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/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().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new GtkSystemDialog()) + .Bind().ToConstant(new DBusSystemDialog()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind().ToConstant(new X11PlatformLifetimeEvents(this)); @@ -209,10 +208,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.