4 changed files with 387 additions and 3 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue