From e9c5e42de9c4cfabbafd3db14e1c510c04809bc6 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 13 Jan 2019 22:51:25 +0300 Subject: [PATCH] [X11] For now use GtkFileChooser on a separate thread --- src/Avalonia.X11/Avalonia.X11.csproj | 1 + src/Avalonia.X11/X11Platform.cs | 4 +- .../Gtk3ForeignX11SystemDialog.cs | 115 ++++++++++++++++++ src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 13 +- src/Gtk/Avalonia.Gtk3/SystemDialogs.cs | 36 ++++-- 5 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 src/Gtk/Avalonia.Gtk3/Gtk3ForeignX11SystemDialog.cs diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 3e75180468..40db6d8c62 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 79aef75e4c..675ed4bd53 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.Gtk3; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -41,7 +42,8 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new SystemDialogsStub()) - .Bind().ToConstant(new IconLoaderStub()); + .Bind().ToConstant(new IconLoaderStub()) + .Bind().ToConstant(new Gtk3ForeignX11SystemDialog()); X11Screens.Init(this); if (Info.XInputVersion != null) { diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3ForeignX11SystemDialog.cs b/src/Gtk/Avalonia.Gtk3/Gtk3ForeignX11SystemDialog.cs new file mode 100644 index 0000000000..b80914572b --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Gtk3ForeignX11SystemDialog.cs @@ -0,0 +1,115 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Gtk3.Interop; +using Avalonia.Platform; +using Avalonia.Platform.Interop; + +namespace Avalonia.Gtk3 +{ + public class Gtk3ForeignX11SystemDialog : ISystemDialogImpl + { + private Task _initialized; + private SystemDialogBase _inner = new SystemDialogBase(); + + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + await EnsureInitialized(); + var xid = parent.Handle.Handle; + return await await RunOnGtkThread( + () => _inner.ShowFileDialogAsync(dialog, GtkWindow.Null, chooser => UpdateParent(chooser, xid))); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + await EnsureInitialized(); + var xid = parent.Handle.Handle; + return await await RunOnGtkThread( + () => _inner.ShowFolderDialogAsync(dialog, GtkWindow.Null, chooser => UpdateParent(chooser, xid))); + } + + void UpdateParent(GtkFileChooser chooser, IntPtr xid) + { + Native.GtkWidgetRealize(chooser); + var window = Native.GtkWidgetGetWindow(chooser); + var parent = Native.GdkWindowForeignNewForDisplay(GdkDisplay, xid); + if (window != IntPtr.Zero && parent != IntPtr.Zero) + Native.GdkWindowSetTransientFor(window, parent); + } + + async Task EnsureInitialized() + { + if (_initialized == null) + { + var tcs = new TaskCompletionSource(); + _initialized = tcs.Task; + new Thread(() => GtkThread(tcs)) + { + IsBackground = true + }.Start(); + } + + if (!(await _initialized)) + throw new Exception("Unable to initialize GTK on separate thread"); + + } + + Task RunOnGtkThread(Func action) + { + var tcs = new TaskCompletionSource(); + GlibTimeout.Add(0, 0, () => + { + + try + { + tcs.SetResult(action()); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + + return false; + }); + return tcs.Task; + } + + + void GtkThread(TaskCompletionSource tcs) + { + try + { + X11.XInitThreads(); + }catch{} + Resolver.Resolve(); + if (Native.GdkWindowForeignNewForDisplay == null) + throw new Exception("gdk_x11_window_foreign_new_for_display is not found in your libgdk-3.so"); + using (var backends = new Utf8Buffer("x11")) + Native.GdkSetAllowedBackends?.Invoke(backends); + if (!Native.GtkInitCheck(0, IntPtr.Zero)) + { + tcs.SetResult(false); + return; + } + + using (var utf = new Utf8Buffer($"avalonia.app.a{Guid.NewGuid().ToString("N")}")) + App = Native.GtkApplicationNew(utf, 0); + if (App == IntPtr.Zero) + { + tcs.SetResult(false); + return; + } + GdkDisplay = Native.GdkGetDefaultDisplay(); + tcs.SetResult(true); + while (true) + Native.GtkMainIteration(); + } + + private IntPtr GdkDisplay { get; set; } + private IntPtr App { get; set; } + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index bf973119a9..765c19a796 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -50,6 +50,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate IntPtr gtk_init(int argc, IntPtr argv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate bool gtk_init_check(int argc, IntPtr argv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk, optional: true)] public delegate IntPtr gdk_set_allowed_backends (Utf8Buffer backends); @@ -288,6 +291,12 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate void gdk_window_end_paint(IntPtr window); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk, optional: true)] + public delegate IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate void gdk_window_set_transient_for(IntPtr window, IntPtr parent); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate void gdk_event_request_motions(IntPtr ev); @@ -414,6 +423,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_set_transient_for GtkWindowSetTransientFor; public static D.gdk_set_allowed_backends GdkSetAllowedBackends; public static D.gtk_init GtkInit; + public static D.gtk_init_check GtkInitCheck; public static D.gtk_window_present GtkWindowPresent; public static D.gtk_widget_hide GtkWidgetHide; public static D.gtk_widget_show GtkWidgetShow; @@ -483,8 +493,9 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_window_process_updates GdkWindowProcessUpdates; public static D.gdk_window_begin_paint_rect GdkWindowBeginPaintRect; public static D.gdk_window_end_paint GdkWindowEndPaint; + public static D.gdk_x11_window_foreign_new_for_display GdkWindowForeignNewForDisplay; + public static D.gdk_window_set_transient_for GdkWindowSetTransientFor; - public static D.gdk_pixbuf_new_from_file GdkPixbufNewFromFile; public static D.gtk_icon_theme_get_default GtkIconThemeGetDefault; public static D.gtk_icon_theme_load_icon GtkIconThemeLoadIcon; diff --git a/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs b/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs index 5e6615f92d..1e85eaa156 100644 --- a/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs +++ b/src/Gtk/Avalonia.Gtk3/SystemDialogs.cs @@ -11,16 +11,17 @@ using Avalonia.Platform.Interop; namespace Avalonia.Gtk3 { - class SystemDialog : ISystemDialogImpl + class SystemDialogBase { - unsafe static Task ShowDialog(string title, GtkWindow parent, GtkFileChooserAction action, - bool multiselect, string initialFileName) + public unsafe static Task ShowDialog(string title, GtkWindow parent, GtkFileChooserAction action, + bool multiselect, string initialFileName, Action modify) { GtkFileChooser dlg; parent = parent ?? GtkWindow.Null; using (var name = new Utf8Buffer(title)) dlg = Native.GtkFileChooserDialogNew(name, parent, action, IntPtr.Zero); + modify?.Invoke(dlg); if (multiselect) Native.GtkFileChooserSetSelectMultiple(dlg, true); @@ -42,7 +43,7 @@ namespace Avalonia.Gtk3 dispose(); return false; }), - Signal.Connect(dlg, "response", (_, resp, __)=> + Signal.Connect(dlg, "response", (_, resp, __) => { string[] result = null; if (resp == GtkResponseType.Accept) @@ -56,9 +57,11 @@ namespace Avalonia.Gtk3 rlst.Add(Utf8Buffer.StringFromPtr(cgs->Data)); cgs = cgs->Next; } + Native.GSlistFree(gs); result = rlst.ToArray(); } + Native.GtkWidgetHide(dlg); dispose(); tcs.TrySetResult(result); @@ -70,27 +73,38 @@ namespace Avalonia.Gtk3 Native.GtkDialogAddButton(dlg, open, GtkResponseType.Accept); using (var open = new Utf8Buffer("Cancel")) Native.GtkDialogAddButton(dlg, open, GtkResponseType.Cancel); - if(initialFileName!=null) + if (initialFileName != null) using (var fn = new Utf8Buffer(initialFileName)) Native.GtkFileChooserSetFilename(dlg, fn); Native.GtkWindowPresent(dlg); return tcs.Task; } - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public Task ShowFileDialogAsync(FileDialog dialog, GtkWindow parent, + Action modify = null) { - return ShowDialog(dialog.Title, ((WindowBaseImpl)parent)?.GtkWidget, + return 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)); + string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), modify); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, GtkWindow parent, + Action modify = null) { - var res = await ShowDialog(dialog.Title, ((WindowBaseImpl) parent)?.GtkWidget, - GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory); + var res = await ShowDialog(dialog.Title, parent, + GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, modify); return res?.FirstOrDefault(); } } + + class SystemDialog : SystemDialogBase, ISystemDialogImpl + { + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + => ShowFolderDialogAsync(dialog, ((WindowBaseImpl)parent)?.GtkWidget); + + public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + => ShowFileDialogAsync(dialog, ((WindowBaseImpl)parent)?.GtkWidget); + } }