Browse Source

Update GTK and FreeDesktop implementations

pull/8303/head
Max Katz 4 years ago
parent
commit
eacd679649
  1. 159
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  2. 3
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  3. 225
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  4. 7
      src/Avalonia.X11/X11Platform.cs
  5. 9
      src/Avalonia.X11/X11Window.cs

159
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -1,102 +1,171 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : ISystemDialogImpl
internal class DBusSystemDialog : BclStorageProvider
{
private readonly IFileChooser _fileChooser;
internal static DBusSystemDialog? TryCreate()
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() =>
{
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);
_ = fileChooser.GetVersionAsync();
return fileChooser;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
});
internal static DBusSystemDialog? TryCreate(IPlatformHandle handle)
{
return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser
? new DBusSystemDialog(fileChooser, handle) : null;
}
private DBusSystemDialog(IFileChooser fileChooser)
private readonly IFileChooser _fileChooser;
private readonly IPlatformHandle _handle;
private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle)
{
_fileChooser = fileChooser;
_handle = handle;
}
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
public override bool CanOpen => true;
public override bool CanSave => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var options = new Dictionary<string, object>();
if (dialog.Filters is not null)
options.Add("filters", ParseFilters(dialog));
var chooserOptions = new Dictionary<string, object>();
var filters = ParseFilters(options.FileTypeFilter);
if (filters.Any())
{
chooserOptions.Add("filters", filters);
}
chooserOptions.Add("multiple", options.AllowMultiple);
objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
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 ?? Array.Empty<string>();
return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).AbsolutePath))).ToList();
}
switch (dialog)
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, object>();
var filters = ParseFilters(options.FileTypeChoices);
if (filters.Any())
{
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;
chooserOptions.Add("filters", filters);
}
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", currentName);
if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true)
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString()));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
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)
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).AbsolutePath : null;
if (path is null)
{
return null;
for (var i = 0; i < uris.Length; i++)
uris[i] = new Uri(uris[i]).AbsolutePath;
return uris;
}
else
{
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null);
return new BclStorageFile(new FileInfo(path));
}
}
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
var options = new Dictionary<string, object>
var parentWindow = $"x11:{_handle.Handle:X}";
var chooserOptions = new Dictionary<string, object>
{
{ "directory", true }
{ "directory", true },
{ "multiple", options.AllowMultiple }
};
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options);
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
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;
var uris = await tsc.Task ?? Array.Empty<string>();
return uris
.Select(path => new Uri(path).AbsolutePath)
// WSL2 freedesktop allows to select files as well in directory picker, filter it out.
.Where(Directory.Exists)
.Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
}
private static (string name, (uint style, string extension)[])[] ParseFilters(FileDialog dialog)
private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes)
{
var filters = new (string name, (uint style, string extension)[])[dialog.Filters!.Count];
for (var i = 0; i < filters.Length; i++)
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
if (fileTypes is null)
{
var extensions = dialog.Filters[i].Extensions.Select(static x => (0u, x)).ToArray();
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions);
return Array.Empty<(string name, (uint style, string extension)[])>();
}
var filters = new List<(string name, (uint style, string extension)[])>();
foreach (var fileType in fileTypes)
{
const uint globStyle = 0u;
const uint mimeStyle = 1u;
var extensions = Enumerable.Empty<(uint, string)>();
if (fileType.Patterns is { } patterns)
{
extensions = extensions.Concat(patterns.Select(static x => (globStyle, x)));
}
else if (fileType.MimeTypes is { } mimeTypes)
{
extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x)));
}
if (extensions.Any())
{
filters.Add((fileType.Name, extensions.ToArray()));
}
}
return filters;
return filters.ToArray();
}
}
}

3
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -208,6 +208,9 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_add_pattern(IntPtr filter, Utf8Buffer pattern);
[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_add_mime_type (IntPtr filter, Utf8Buffer mimeType);
[DllImport(GtkName)]
public static extern IntPtr gtk_file_chooser_add_filter(IntPtr chooser, IntPtr filter);

225
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -1,57 +1,147 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using static Avalonia.X11.NativeDialogs.Glib;
using static Avalonia.X11.NativeDialogs.Gtk;
// ReSharper disable AccessToModifiedClosure
namespace Avalonia.X11.NativeDialogs
{
class GtkSystemDialog : ISystemDialogImpl
internal class GtkSystemDialog : BclStorageProvider
{
private Task<bool> _initialized;
private Task<bool>? _initialized;
private readonly X11Window _window;
public GtkSystemDialog(X11Window window)
{
_window = window;
}
public override bool CanOpen => true;
public override bool CanSave => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
options.AllowMultiple, options.SuggestedStartLocation, null, options.FileTypeFilter, null, false)
.ConfigureAwait(false);
return res?.Select(f => new BclStorageFile(new FileInfo(f))).ToArray() ?? Array.Empty<IStorageFile>();
});
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
options.AllowMultiple, options.SuggestedStartLocation, null,
null, null, false).ConfigureAwait(false);
return res?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray() ?? Array.Empty<IStorageFolder>();
});
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices, options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
return res?.FirstOrDefault() is { } file
? new BclStorageFile(new FileInfo(file))
: null;
});
}
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, string initialDirectory, string initialFileName, IEnumerable<FileDialogFilter> filters, string defaultExtension, bool overwritePrompt)
private unsafe Task<string[]?> ShowDialog(string? title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName,
IEnumerable<FilePickerFileType>? filters, string? defaultExtension, bool overwritePrompt)
{
IntPtr dlg;
using (var name = new Utf8Buffer(title))
{
dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero);
}
UpdateParent(dlg, parent);
if (multiSelect)
{
gtk_file_chooser_set_select_multiple(dlg, true);
}
gtk_window_set_modal(dlg, true);
var tcs = new TaskCompletionSource<string[]>();
List<IDisposable> disposables = null;
var tcs = new TaskCompletionSource<string[]?>();
List<IDisposable>? disposables = null;
void Dispose()
{
// ReSharper disable once PossibleNullReferenceException
foreach (var d in disposables) d.Dispose();
foreach (var d in disposables!)
{
d.Dispose();
}
disposables.Clear();
}
var filtersDic = new Dictionary<IntPtr, FileDialogFilter>();
if(filters != null)
var filtersDic = new Dictionary<IntPtr, FilePickerFileType>();
if (filters != null)
{
foreach (var f in filters)
{
var filter = gtk_file_filter_new();
filtersDic[filter] = f;
using (var b = new Utf8Buffer(f.Name))
gtk_file_filter_set_name(filter, b);
if (f.Patterns?.Any() == true || f.MimeTypes?.Any() == true)
{
var filter = gtk_file_filter_new();
filtersDic[filter] = f;
using (var b = new Utf8Buffer(f.Name))
{
gtk_file_filter_set_name(filter, b);
}
foreach (var e in f.Extensions)
using (var b = new Utf8Buffer("*." + e))
gtk_file_filter_add_pattern(filter, b);
if (f.Patterns is not null)
{
foreach (var e in f.Patterns)
{
using (var b = new Utf8Buffer(e))
{
gtk_file_filter_add_pattern(filter, b);
}
}
}
gtk_file_chooser_add_filter(dlg, filter);
if (f.MimeTypes is not null)
{
foreach (var e in f.MimeTypes)
{
using (var b = new Utf8Buffer(e))
{
gtk_file_filter_add_mime_type(filter, b);
}
}
}
gtk_file_chooser_add_filter(dlg, filter);
}
}
}
disposables = new List<IDisposable>
{
@ -63,7 +153,7 @@ namespace Avalonia.X11.NativeDialogs
}),
ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) =>
{
string[] result = null;
string[]? result = null;
if (resp == GtkResponseType.Accept)
{
var resultList = new List<string>();
@ -71,20 +161,18 @@ namespace Avalonia.X11.NativeDialogs
var cgs = gs;
while (cgs != null)
{
if (cgs->Data != IntPtr.Zero)
resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data));
cgs = cgs->Next;
if (cgs->Data != IntPtr.Zero
&& Utf8Buffer.StringFromPtr(cgs->Data) is string str) { resultList.Add(str); } cgs = cgs->Next;
}
g_slist_free(gs);
result = resultList.ToArray();
// GTK doesn't auto-append the extension, so we need to do that manually
if (action == GtkFileChooserAction.Save)
{
var currentFilter = gtk_file_chooser_get_filter(dlg);
filtersDic.TryGetValue(currentFilter, out var selectedFilter);
for (var c = 0; c < result.Length; c++)
result[c] = NameWithExtension(result[c], defaultExtension, selectedFilter);
for (var c = 0; c < result.Length; c++) { result[c] = StorageProviderHelpers.NameWithExtension(result[c], defaultExtension, selectedFilter); }
}
}
@ -98,13 +186,19 @@ namespace Avalonia.X11.NativeDialogs
action == GtkFileChooserAction.Save ? "Save"
: action == GtkFileChooserAction.SelectFolder ? "Select"
: "Open"))
{
gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
}
using (var open = new Utf8Buffer("Cancel"))
{
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
}
if (initialDirectory != null)
Uri? folderPath = null;
if (initialFolder?.TryGetUri(out folderPath) == true)
{
using var dir = new Utf8Buffer(initialDirectory);
using var dir = new Utf8Buffer(folderPath.LocalPath);
gtk_file_chooser_set_current_folder(dlg, dir);
}
@ -112,7 +206,7 @@ namespace Avalonia.X11.NativeDialogs
{
// gtk_file_chooser_set_filename() expects full path
using var fn = action == GtkFileChooserAction.Open
? new Utf8Buffer(Path.Combine(initialDirectory ?? "", initialFileName))
? new Utf8Buffer(Path.Combine(folderPath?.LocalPath ?? "", initialFileName))
: new Utf8Buffer(initialFileName);
if (action == GtkFileChooserAction.Save)
@ -131,84 +225,29 @@ namespace Avalonia.X11.NativeDialogs
return tcs.Task;
}
string NameWithExtension(string path, string defaultExtension, FileDialogFilter filter)
private async Task EnsureInitialized()
{
var name = Path.GetFileName(path);
if (name != null && !name.Contains("."))
if (_initialized == null)
{
if (filter?.Extensions?.Count > 0)
{
if (defaultExtension != null
&& filter.Extensions.Contains(defaultExtension))
return path + "." + defaultExtension.TrimStart('.');
var ext = filter.Extensions.FirstOrDefault(x => x != "*");
if (ext != null)
return path + "." + ext.TrimStart('.');
}
if (defaultExtension != null)
path += "." + defaultExtension.TrimStart('.');
_initialized = StartGtk();
}
return path;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(() => ShowDialog(
dialog.Title, platformImpl,
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
(dialog as OpenFileDialog)?.AllowMultiple ?? false,
dialog.Directory,
dialog.InitialFileName,
dialog.Filters,
(dialog as SaveFileDialog)?.DefaultExtension,
(dialog as SaveFileDialog)?.ShowOverwritePrompt ?? false));
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(
dialog.Title,
platformImpl,GtkFileChooserAction.SelectFolder,
false,
dialog.Directory,
null,
null,
null,
false);
return res?.FirstOrDefault();
});
}
async Task EnsureInitialized()
{
if (_initialized == null) _initialized = StartGtk();
if (!(await _initialized))
{
throw new Exception("Unable to initialize GTK on separate thread");
}
}
void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = parentWindow.Handle.Handle;
gtk_widget_realize(chooser);
var window = gtk_widget_get_window(chooser);
var parent = GetForeignWindow(xid);
if (window != IntPtr.Zero && parent != IntPtr.Zero)
{
gdk_window_set_transient_for(window, parent);
}
}
}
}

7
src/Avalonia.X11/X11Platform.cs

@ -80,7 +80,6 @@ namespace Avalonia.X11
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<ISystemDialogImpl>().ToConstant(DBusSystemDialog.TryCreate() as ISystemDialogImpl ?? new ManagedFileDialogExtensions.ManagedSystemDialogImpl<Window>())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
@ -214,6 +213,12 @@ namespace Avalonia
/// </summary>
public bool UseDBusMenu { get; set; } = true;
/// <summary>
/// Enables GTK file picker instead of default FreeDesktop.
/// The default value is true. And FreeDesktop file picker is used instead if available.
/// </summary>
public bool UseGtkFilePicker { get; set; } = false;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
/// </summary>

9
src/Avalonia.X11/X11Window.cs

@ -17,6 +17,7 @@ using Avalonia.Input.TextInput;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.X11.Glx;
@ -28,7 +29,8 @@ namespace Avalonia.X11
unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client,
ITopLevelImplWithNativeMenuExporter,
ITopLevelImplWithNativeControlHost,
ITopLevelImplWithTextInputMethod
ITopLevelImplWithTextInputMethod,
ITopLevelImplWithStorageProvider
{
private readonly AvaloniaX11Platform _platform;
private readonly bool _popup;
@ -211,6 +213,10 @@ namespace Avalonia.X11
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_SYNC_REQUEST_COUNTER,
_x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1);
}
var canUseFreeDekstopPicker = !platform.Options.UseGtkFilePicker && platform.Options.UseDBusMenu;
StorageProvider = canUseFreeDekstopPicker && DBusSystemDialog.TryCreate(Handle) is {} dBusStorage
? dBusStorage : new NativeDialogs.GtkSystemDialog(this);
}
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
@ -1192,6 +1198,7 @@ namespace Avalonia.X11
public bool NeedsManagedDecorations => false;
public IStorageProvider StorageProvider { get; }
public class SurfacePlatformHandle : IPlatformNativeSurfaceHandle
{

Loading…
Cancel
Save