Browse Source

[X11] Use simple DllImport-based GTK file dialog instead of GTK3 backend

Nikita Tsukanov 6 years ago
parent
commit
d2e930af38

+ 0 - 1
src/Avalonia.X11/Avalonia.X11.csproj

@@ -7,7 +7,6 @@
 
     <ItemGroup>
         <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
-        <ProjectReference Include="..\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj" />
         <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
     </ItemGroup>
 

+ 263 - 0
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<T>(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<bool>)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<bool> callback)
+        {
+            var handle = GCHandle.Alloc(callback);
+            g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero);
+        }
+
+        public static Task<T> RunOnGlibThread<T>(Func<T> action)
+        {
+            var tcs = new TaskCompletionSource<T>();
+            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<bool> StartGtk()
+        {
+            var tcs = new TaskCompletionSource<bool>();
+            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;
+        }
+    }
+}

+ 122 - 0
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<bool> _initialized;
+        private unsafe  Task<string[]> 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<string[]>();
+            List<IDisposable> disposables = null;
+
+            void Dispose()
+            {
+                // ReSharper disable once PossibleNullReferenceException
+                foreach (var d in disposables) d.Dispose();
+                disposables.Clear();
+            }
+
+            disposables = new List<IDisposable>
+            {
+                ConnectSignal<signal_generic>(dlg, "close", delegate
+                {
+                    tcs.TrySetResult(null);
+                    Dispose();
+                    return false;
+                }),
+                ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) =>
+                {
+                    string[] result = null;
+                    if (resp == GtkResponseType.Accept)
+                    {
+                        var resultList = new List<string>();
+                        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<string[]> 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<string> 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);
+        }
+    }
+}

+ 2 - 2
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<IClipboard>().ToConstant(new X11Clipboard(this))
                 .Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
                 .Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
-                .Bind<ISystemDialogImpl>().ToConstant(new Gtk3ForeignX11SystemDialog());
+                .Bind<ISystemDialogImpl>().ToConstant(new GtkSystemDialog());
             
             X11Screens = Avalonia.X11.X11Screens.Init(this);
             Screens = new X11Screens(X11Screens);