diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj
index 087ba017ae..1629890568 100644
--- a/src/Avalonia.X11/Avalonia.X11.csproj
+++ b/src/Avalonia.X11/Avalonia.X11.csproj
@@ -7,7 +7,6 @@
-
diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs
new file mode 100644
index 0000000000..100996984b
--- /dev/null
+++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Platform.Interop;
+// ReSharper disable IdentifierTypo
+namespace Avalonia.X11.NativeDialogs
+{
+
+ static unsafe class Glib
+ {
+ private const string GlibName = "libglib-2.0.so.0";
+ private const string GObjectName = "libgobject-2.0.so.0";
+
+ [DllImport(GlibName)]
+ public static extern void g_slist_free(GSList* data);
+
+ [DllImport(GObjectName)]
+ private static extern void g_object_ref(IntPtr instance);
+
+ [DllImport(GObjectName)]
+ private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal,
+ IntPtr handler, IntPtr userData, int flags);
+
+ [DllImport(GObjectName)]
+ private static extern void g_object_unref(IntPtr instance);
+
+ [DllImport(GObjectName)]
+ private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId);
+
+ private delegate bool timeout_callback(IntPtr data);
+
+ [DllImport(GlibName)]
+ private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data,
+ IntPtr destroy);
+
+
+ class ConnectedSignal : IDisposable
+ {
+ private readonly IntPtr _instance;
+ private GCHandle _handle;
+ private readonly ulong _id;
+
+ public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id)
+ {
+ _instance = instance;
+ g_object_ref(instance);
+ _handle = handle;
+ _id = id;
+ }
+
+ public void Dispose()
+ {
+ if (_handle.IsAllocated)
+ {
+ g_signal_handler_disconnect(_instance, _id);
+ g_object_unref(_instance);
+ _handle.Free();
+ }
+ }
+ }
+
+ public static IDisposable ConnectSignal(IntPtr obj, string name, T handler)
+ {
+ var handle = GCHandle.Alloc(handler);
+ var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler);
+ using (var utf = new Utf8Buffer(name))
+ {
+ var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0);
+ if (id == 0)
+ throw new ArgumentException("Unable to connect to signal " + name);
+ return new ConnectedSignal(obj, handle, id);
+ }
+ }
+
+
+ static bool TimeoutHandler(IntPtr data)
+ {
+ var handle = GCHandle.FromIntPtr(data);
+ var cb = (Func)handle.Target;
+ if (!cb())
+ {
+ handle.Free();
+ return false;
+ }
+
+ return true;
+ }
+
+ private static readonly timeout_callback s_pinnedHandler;
+
+ static Glib()
+ {
+ s_pinnedHandler = TimeoutHandler;
+ }
+
+ static void AddTimeout(int priority, uint interval, Func callback)
+ {
+ var handle = GCHandle.Alloc(callback);
+ g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero);
+ }
+
+ public static Task RunOnGlibThread(Func action)
+ {
+ var tcs = new TaskCompletionSource();
+ AddTimeout(0, 0, () =>
+ {
+
+ try
+ {
+ tcs.SetResult(action());
+ }
+ catch (Exception e)
+ {
+ tcs.TrySetException(e);
+ }
+
+ return false;
+ });
+ return tcs.Task;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ unsafe struct GSList
+ {
+ public readonly IntPtr Data;
+ public readonly GSList* Next;
+ }
+
+ enum GtkFileChooserAction
+ {
+ Open,
+ Save,
+ SelectFolder,
+ }
+
+ // ReSharper disable UnusedMember.Global
+ enum GtkResponseType
+ {
+ Help = -11,
+ Apply = -10,
+ No = -9,
+ Yes = -8,
+ Close = -7,
+ Cancel = -6,
+ Ok = -5,
+ DeleteEvent = -4,
+ Accept = -3,
+ Reject = -2,
+ None = -1,
+ }
+ // ReSharper restore UnusedMember.Global
+
+ static unsafe class Gtk
+ {
+ private static IntPtr s_display;
+ private const string GdkName = "libgdk-3.so.0";
+ private const string GtkName = "libgtk-3.so.0";
+
+ [DllImport(GtkName)]
+ static extern void gtk_main_iteration();
+
+
+ [DllImport(GtkName)]
+ public static extern void gtk_window_set_modal(IntPtr window, bool modal);
+
+ [DllImport(GtkName)]
+ public static extern void gtk_window_present(IntPtr gtkWindow);
+
+
+ public delegate bool signal_generic(IntPtr gtkWidget, IntPtr userData);
+
+ public delegate bool signal_dialog_response(IntPtr gtkWidget, GtkResponseType response, IntPtr userData);
+
+ [DllImport(GtkName)]
+ public static extern IntPtr gtk_file_chooser_dialog_new(Utf8Buffer title, IntPtr parent,
+ GtkFileChooserAction action, IntPtr ignore);
+
+ [DllImport(GtkName)]
+ public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow);
+
+ [DllImport(GtkName)]
+ public static extern void
+ gtk_dialog_add_button(IntPtr raw, Utf8Buffer button_text, GtkResponseType response_id);
+
+ [DllImport(GtkName)]
+ public static extern GSList* gtk_file_chooser_get_filenames(IntPtr chooser);
+
+ [DllImport(GtkName)]
+ public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file);
+
+ [DllImport(GtkName)]
+ public static extern void gtk_widget_realize(IntPtr gtkWidget);
+
+ [DllImport(GtkName)]
+ public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget);
+
+ [DllImport(GtkName)]
+ public static extern void gtk_widget_hide(IntPtr gtkWidget);
+
+ [DllImport(GtkName)]
+ static extern bool gtk_init_check(int argc, IntPtr argv);
+
+ [DllImport(GdkName)]
+ static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid);
+
+ [DllImport(GdkName)]
+ static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends);
+
+ [DllImport(GdkName)]
+ static extern IntPtr gdk_display_get_default();
+
+ [DllImport(GtkName)]
+ static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags);
+
+ [DllImport(GdkName)]
+ public static extern void gdk_window_set_transient_for(IntPtr window, IntPtr parent);
+
+ public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid);
+
+ public static Task StartGtk()
+ {
+ var tcs = new TaskCompletionSource();
+ new Thread(() =>
+ {
+ try
+ {
+ using (var backends = new Utf8Buffer("x11"))
+ gdk_set_allowed_backends(backends);
+ }
+ catch
+ {
+ //Ignore
+ }
+
+ Environment.SetEnvironmentVariable("WAYLAND_DISPLAY",
+ "/proc/fake-display-to-prevent-wayland-initialization-by-gtk3");
+
+ if (!gtk_init_check(0, IntPtr.Zero))
+ {
+ tcs.SetResult(false);
+ return;
+ }
+
+ IntPtr app;
+ using (var utf = new Utf8Buffer($"avalonia.app.a{Guid.NewGuid():N}"))
+ app = gtk_application_new(utf, 0);
+ if (app == IntPtr.Zero)
+ {
+ tcs.SetResult(false);
+ return;
+ }
+
+ s_display = gdk_display_get_default();
+ tcs.SetResult(true);
+ while (true)
+ gtk_main_iteration();
+ }) {Name = "GTK3THREAD", IsBackground = true}.Start();
+ return tcs.Task;
+ }
+ }
+}
diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
new file mode 100644
index 0000000000..61047ef2a9
--- /dev/null
+++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
@@ -0,0 +1,122 @@
+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 static Avalonia.X11.NativeDialogs.Glib;
+using static Avalonia.X11.NativeDialogs.Gtk;
+// ReSharper disable AccessToModifiedClosure
+namespace Avalonia.X11.NativeDialogs
+{
+ class GtkSystemDialog : ISystemDialogImpl
+ {
+ private Task _initialized;
+ private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
+ bool multiSelect, string initialFileName)
+ {
+ 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();
+ List disposables = null;
+
+ void Dispose()
+ {
+ // ReSharper disable once PossibleNullReferenceException
+ foreach (var d in disposables) d.Dispose();
+ disposables.Clear();
+ }
+
+ disposables = new List
+ {
+ ConnectSignal(dlg, "close", delegate
+ {
+ tcs.TrySetResult(null);
+ Dispose();
+ return false;
+ }),
+ ConnectSignal(dlg, "response", (_, resp, __) =>
+ {
+ string[] result = null;
+ if (resp == GtkResponseType.Accept)
+ {
+ var resultList = new List();
+ var gs = gtk_file_chooser_get_filenames(dlg);
+ var cgs = gs;
+ while (cgs != null)
+ {
+ if (cgs->Data != IntPtr.Zero)
+ resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data));
+ cgs = cgs->Next;
+ }
+ g_slist_free(gs);
+ result = resultList.ToArray();
+ }
+
+ gtk_widget_hide(dlg);
+ Dispose();
+ tcs.TrySetResult(result);
+ return false;
+ })
+ };
+ using (var open = new Utf8Buffer("Open"))
+ gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
+ using (var open = new Utf8Buffer("Cancel"))
+ gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
+ if (initialFileName != null)
+ using (var fn = new Utf8Buffer(initialFileName))
+ gtk_file_chooser_set_filename(dlg, fn);
+ gtk_window_present(dlg);
+ return tcs.Task;
+ }
+
+ public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+ {
+ await EnsureInitialized();
+ return await await RunOnGlibThread(
+ () => ShowDialog(dialog.Title, parent,
+ dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
+ (dialog as OpenFileDialog)?.AllowMultiple ?? false,
+ Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
+ string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName)));
+ }
+
+ public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+ {
+ await EnsureInitialized();
+ return await await RunOnGlibThread(async () =>
+ {
+ var res = await ShowDialog(dialog.Title, parent,
+ GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory);
+ 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)
+ {
+ 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);
+ }
+ }
+}
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 323411e319..5a19187a22 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
-using Avalonia.Gtk3;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL;
@@ -10,6 +9,7 @@ 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
{
@@ -45,7 +45,7 @@ namespace Avalonia.X11
.Bind().ToConstant(new X11Clipboard(this))
.Bind().ToConstant(new PlatformSettingsStub())
.Bind().ToConstant(new X11IconLoader(Info))
- .Bind().ToConstant(new Gtk3ForeignX11SystemDialog());
+ .Bind().ToConstant(new GtkSystemDialog());
X11Screens = Avalonia.X11.X11Screens.Init(this);
Screens = new X11Screens(X11Screens);