From d2e930af3800614f1e33823cb320a42710c06dd5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Jan 2019 19:14:33 +0300 Subject: [PATCH] [X11] Use simple DllImport-based GTK file dialog instead of GTK3 backend --- src/Avalonia.X11/Avalonia.X11.csproj | 1 - src/Avalonia.X11/NativeDialogs/Gtk.cs | 263 ++++++++++++++++++ .../NativeDialogs/GtkNativeFileDialogs.cs | 122 ++++++++ src/Avalonia.X11/X11Platform.cs | 4 +- 4 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.X11/NativeDialogs/Gtk.cs create mode 100644 src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs 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);