// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Services; /// /// GTK4 dialog response codes. /// public enum GtkResponseType { None = -1, Reject = -2, Accept = -3, DeleteEvent = -4, Ok = -5, Cancel = -6, Close = -7, Yes = -8, No = -9, Apply = -10, Help = -11 } /// /// GTK4 message dialog types. /// public enum GtkMessageType { Info = 0, Warning = 1, Question = 2, Error = 3, Other = 4 } /// /// GTK4 button layouts for dialogs. /// public enum GtkButtonsType { None = 0, Ok = 1, Close = 2, Cancel = 3, YesNo = 4, OkCancel = 5 } /// /// GTK4 file chooser actions. /// public enum GtkFileChooserAction { Open = 0, Save = 1, SelectFolder = 2, CreateFolder = 3 } /// /// Result from a file dialog. /// public class FileDialogResult { public bool Accepted { get; init; } public string[] SelectedFiles { get; init; } = Array.Empty(); public string? SelectedFile => SelectedFiles.Length > 0 ? SelectedFiles[0] : null; } /// /// Result from a color dialog. /// public class ColorDialogResult { public bool Accepted { get; init; } public float Red { get; init; } public float Green { get; init; } public float Blue { get; init; } public float Alpha { get; init; } } /// /// GTK4 interop layer for native Linux dialogs. /// Provides native file pickers, message boxes, and color choosers. /// public class Gtk4InteropService : IDisposable { #region GTK4 Native Interop private const string LibGtk4 = "libgtk-4.so.1"; private const string LibGio = "libgio-2.0.so.0"; private const string LibGlib = "libglib-2.0.so.0"; private const string LibGObject = "libgobject-2.0.so.0"; // GTK initialization [DllImport(LibGtk4)] private static extern bool gtk_init_check(); [DllImport(LibGtk4)] private static extern bool gtk_is_initialized(); // Main loop [DllImport(LibGtk4)] private static extern IntPtr g_main_context_default(); [DllImport(LibGtk4)] private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); [DllImport(LibGlib)] private static extern void g_free(IntPtr mem); // GObject [DllImport(LibGObject)] private static extern void g_object_unref(IntPtr obj); [DllImport(LibGObject)] private static extern void g_object_ref(IntPtr obj); // Window [DllImport(LibGtk4)] private static extern IntPtr gtk_window_new(); [DllImport(LibGtk4)] private static extern void gtk_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); [DllImport(LibGtk4)] private static extern void gtk_window_set_modal(IntPtr window, bool modal); [DllImport(LibGtk4)] private static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent); [DllImport(LibGtk4)] private static extern void gtk_window_destroy(IntPtr window); [DllImport(LibGtk4)] private static extern void gtk_window_present(IntPtr window); [DllImport(LibGtk4)] private static extern void gtk_window_close(IntPtr window); // Widget [DllImport(LibGtk4)] private static extern void gtk_widget_show(IntPtr widget); [DllImport(LibGtk4)] private static extern void gtk_widget_hide(IntPtr widget); [DllImport(LibGtk4)] private static extern void gtk_widget_set_visible(IntPtr widget, bool visible); [DllImport(LibGtk4)] private static extern bool gtk_widget_get_visible(IntPtr widget); // Alert Dialog (GTK4) [DllImport(LibGtk4)] private static extern IntPtr gtk_alert_dialog_new([MarshalAs(UnmanagedType.LPStr)] string format); [DllImport(LibGtk4)] private static extern void gtk_alert_dialog_set_message(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string message); [DllImport(LibGtk4)] private static extern void gtk_alert_dialog_set_detail(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string detail); [DllImport(LibGtk4)] private static extern void gtk_alert_dialog_set_buttons(IntPtr dialog, string[] labels); [DllImport(LibGtk4)] private static extern void gtk_alert_dialog_set_cancel_button(IntPtr dialog, int button); [DllImport(LibGtk4)] private static extern void gtk_alert_dialog_set_default_button(IntPtr dialog, int button); [DllImport(LibGtk4)] private static extern void gtk_alert_dialog_show(IntPtr dialog, IntPtr parent); // File Dialog (GTK4) [DllImport(LibGtk4)] private static extern IntPtr gtk_file_dialog_new(); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_set_modal(IntPtr dialog, bool modal); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_set_accept_label(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string label); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_open(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); [DllImport(LibGtk4)] private static extern IntPtr gtk_file_dialog_open_finish(IntPtr dialog, IntPtr result, out IntPtr error); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_save(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); [DllImport(LibGtk4)] private static extern IntPtr gtk_file_dialog_save_finish(IntPtr dialog, IntPtr result, out IntPtr error); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_select_folder(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); [DllImport(LibGtk4)] private static extern IntPtr gtk_file_dialog_select_folder_finish(IntPtr dialog, IntPtr result, out IntPtr error); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_open_multiple(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); [DllImport(LibGtk4)] private static extern IntPtr gtk_file_dialog_open_multiple_finish(IntPtr dialog, IntPtr result, out IntPtr error); // File filters [DllImport(LibGtk4)] private static extern IntPtr gtk_file_filter_new(); [DllImport(LibGtk4)] private static extern void gtk_file_filter_set_name(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string name); [DllImport(LibGtk4)] private static extern void gtk_file_filter_add_pattern(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string pattern); [DllImport(LibGtk4)] private static extern void gtk_file_filter_add_mime_type(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string mimeType); [DllImport(LibGtk4)] private static extern void gtk_file_dialog_set_default_filter(IntPtr dialog, IntPtr filter); // GFile [DllImport(LibGio)] private static extern IntPtr g_file_get_path(IntPtr file); // GListModel for multiple files [DllImport(LibGio)] private static extern uint g_list_model_get_n_items(IntPtr list); [DllImport(LibGio)] private static extern IntPtr g_list_model_get_item(IntPtr list, uint position); // Color Dialog (GTK4) [DllImport(LibGtk4)] private static extern IntPtr gtk_color_dialog_new(); [DllImport(LibGtk4)] private static extern void gtk_color_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); [DllImport(LibGtk4)] private static extern void gtk_color_dialog_set_modal(IntPtr dialog, bool modal); [DllImport(LibGtk4)] private static extern void gtk_color_dialog_set_with_alpha(IntPtr dialog, bool withAlpha); [DllImport(LibGtk4)] private static extern void gtk_color_dialog_choose_rgba(IntPtr dialog, IntPtr parent, IntPtr initialColor, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); [DllImport(LibGtk4)] private static extern IntPtr gtk_color_dialog_choose_rgba_finish(IntPtr dialog, IntPtr result, out IntPtr error); // GdkRGBA [StructLayout(LayoutKind.Sequential)] private struct GdkRGBA { public float Red; public float Green; public float Blue; public float Alpha; } // Async callback delegate [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void GAsyncReadyCallback(IntPtr sourceObject, IntPtr result, IntPtr userData); // Legacy GTK3 fallbacks private const string LibGtk3 = "libgtk-3.so.0"; [DllImport(LibGtk3, EntryPoint = "gtk_init_check")] private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_dialog_new")] private static extern IntPtr gtk3_file_chooser_dialog_new( [MarshalAs(UnmanagedType.LPStr)] string title, IntPtr parent, int action, [MarshalAs(UnmanagedType.LPStr)] string firstButtonText, int firstButtonResponse, [MarshalAs(UnmanagedType.LPStr)] string secondButtonText, int secondButtonResponse, IntPtr terminator); [DllImport(LibGtk3, EntryPoint = "gtk_dialog_run")] private static extern int gtk3_dialog_run(IntPtr dialog); [DllImport(LibGtk3, EntryPoint = "gtk_widget_destroy")] private static extern void gtk3_widget_destroy(IntPtr widget); [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filename")] private static extern IntPtr gtk3_file_chooser_get_filename(IntPtr chooser); [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filenames")] private static extern IntPtr gtk3_file_chooser_get_filenames(IntPtr chooser); [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_set_select_multiple")] private static extern void gtk3_file_chooser_set_select_multiple(IntPtr chooser, bool selectMultiple); [DllImport(LibGtk3, EntryPoint = "gtk_message_dialog_new")] private static extern IntPtr gtk3_message_dialog_new( IntPtr parent, int flags, int type, int buttons, [MarshalAs(UnmanagedType.LPStr)] string message); [DllImport(LibGlib, EntryPoint = "g_slist_length")] private static extern uint g_slist_length(IntPtr list); [DllImport(LibGlib, EntryPoint = "g_slist_nth_data")] private static extern IntPtr g_slist_nth_data(IntPtr list, uint n); [DllImport(LibGlib, EntryPoint = "g_slist_free")] private static extern void g_slist_free(IntPtr list); #endregion #region Fields private bool _initialized; private bool _useGtk4; private bool _disposed; private readonly object _lock = new(); // Store callbacks to prevent GC private GAsyncReadyCallback? _currentCallback; private TaskCompletionSource? _fileDialogTcs; private TaskCompletionSource? _colorDialogTcs; private IntPtr _currentDialog; #endregion #region Properties /// /// Gets whether GTK is initialized. /// public bool IsInitialized => _initialized; /// /// Gets whether GTK4 is being used (vs GTK3 fallback). /// public bool IsGtk4 => _useGtk4; #endregion #region Initialization /// /// Initializes the GTK4 interop service. /// Falls back to GTK3 if GTK4 is not available. /// public bool Initialize() { if (_initialized) return true; lock (_lock) { if (_initialized) return true; // Try GTK4 first try { if (gtk_init_check()) { _useGtk4 = true; _initialized = true; Console.WriteLine("[GTK4] Initialized GTK4"); return true; } } catch (DllNotFoundException) { Console.WriteLine("[GTK4] GTK4 not found, trying GTK3"); } catch (Exception ex) { Console.WriteLine($"[GTK4] GTK4 init failed: {ex.Message}"); } // Fall back to GTK3 try { int argc = 0; IntPtr argv = IntPtr.Zero; if (gtk3_init_check(ref argc, ref argv)) { _useGtk4 = false; _initialized = true; Console.WriteLine("[GTK4] Initialized GTK3 (fallback)"); return true; } } catch (DllNotFoundException) { Console.WriteLine("[GTK4] GTK3 not found"); } catch (Exception ex) { Console.WriteLine($"[GTK4] GTK3 init failed: {ex.Message}"); } return false; } } #endregion #region Message Dialogs /// /// Shows an alert message dialog. /// public void ShowAlert(string title, string message, GtkMessageType type = GtkMessageType.Info) { if (!EnsureInitialized()) return; if (_useGtk4) { var dialog = gtk_alert_dialog_new(title); gtk_alert_dialog_set_detail(dialog, message); string[] buttons = { "OK" }; gtk_alert_dialog_set_buttons(dialog, buttons); gtk_alert_dialog_show(dialog, IntPtr.Zero); g_object_unref(dialog); } else { var dialog = gtk3_message_dialog_new( IntPtr.Zero, 1, // GTK_DIALOG_MODAL (int)type, (int)GtkButtonsType.Ok, message); gtk3_dialog_run(dialog); gtk3_widget_destroy(dialog); } ProcessPendingEvents(); } /// /// Shows a confirmation dialog. /// public bool ShowConfirmation(string title, string message) { if (!EnsureInitialized()) return false; if (_useGtk4) { // GTK4 async dialogs are more complex - use synchronous approach var dialog = gtk_alert_dialog_new(title); gtk_alert_dialog_set_detail(dialog, message); string[] buttons = { "No", "Yes" }; gtk_alert_dialog_set_buttons(dialog, buttons); gtk_alert_dialog_set_default_button(dialog, 1); gtk_alert_dialog_set_cancel_button(dialog, 0); gtk_alert_dialog_show(dialog, IntPtr.Zero); g_object_unref(dialog); // Note: GTK4 alert dialogs are async, this is simplified return true; } else { var dialog = gtk3_message_dialog_new( IntPtr.Zero, 1, // GTK_DIALOG_MODAL (int)GtkMessageType.Question, (int)GtkButtonsType.YesNo, message); int response = gtk3_dialog_run(dialog); gtk3_widget_destroy(dialog); ProcessPendingEvents(); return response == (int)GtkResponseType.Yes; } } #endregion #region File Dialogs /// /// Shows an open file dialog. /// public FileDialogResult ShowOpenFileDialog( string title = "Open File", string? initialFolder = null, bool allowMultiple = false, params (string Name, string Pattern)[] filters) { if (!EnsureInitialized()) return new FileDialogResult { Accepted = false }; if (_useGtk4) { return ShowGtk4FileDialog(title, GtkFileChooserAction.Open, allowMultiple, filters); } else { return ShowGtk3FileDialog(title, 0, allowMultiple, filters); // GTK_FILE_CHOOSER_ACTION_OPEN = 0 } } /// /// Shows a save file dialog. /// public FileDialogResult ShowSaveFileDialog( string title = "Save File", string? suggestedName = null, params (string Name, string Pattern)[] filters) { if (!EnsureInitialized()) return new FileDialogResult { Accepted = false }; if (_useGtk4) { return ShowGtk4FileDialog(title, GtkFileChooserAction.Save, false, filters); } else { return ShowGtk3FileDialog(title, 1, false, filters); // GTK_FILE_CHOOSER_ACTION_SAVE = 1 } } /// /// Shows a folder picker dialog. /// public FileDialogResult ShowFolderDialog(string title = "Select Folder") { if (!EnsureInitialized()) return new FileDialogResult { Accepted = false }; if (_useGtk4) { return ShowGtk4FileDialog(title, GtkFileChooserAction.SelectFolder, false, Array.Empty<(string, string)>()); } else { return ShowGtk3FileDialog(title, 2, false, Array.Empty<(string, string)>()); // GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER = 2 } } private FileDialogResult ShowGtk4FileDialog( string title, GtkFileChooserAction action, bool allowMultiple, (string Name, string Pattern)[] filters) { var dialog = gtk_file_dialog_new(); gtk_file_dialog_set_title(dialog, title); gtk_file_dialog_set_modal(dialog, true); // Set up filters if (filters.Length > 0) { var filter = gtk_file_filter_new(); gtk_file_filter_set_name(filter, filters[0].Name); gtk_file_filter_add_pattern(filter, filters[0].Pattern); gtk_file_dialog_set_default_filter(dialog, filter); } // For GTK4, we need async handling - simplified synchronous version // In a full implementation, this would use proper async/await _fileDialogTcs = new TaskCompletionSource(); _currentDialog = dialog; _currentCallback = (source, result, userData) => { IntPtr error = IntPtr.Zero; IntPtr file = IntPtr.Zero; try { if (action == GtkFileChooserAction.Open && !allowMultiple) file = gtk_file_dialog_open_finish(dialog, result, out error); else if (action == GtkFileChooserAction.Save) file = gtk_file_dialog_save_finish(dialog, result, out error); else if (action == GtkFileChooserAction.SelectFolder) file = gtk_file_dialog_select_folder_finish(dialog, result, out error); if (file != IntPtr.Zero && error == IntPtr.Zero) { IntPtr pathPtr = g_file_get_path(file); string path = Marshal.PtrToStringUTF8(pathPtr) ?? ""; g_free(pathPtr); g_object_unref(file); _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = true, SelectedFiles = new[] { path } }); } else { _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false }); } } catch { _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false }); } }; // Start the dialog if (action == GtkFileChooserAction.Open && !allowMultiple) gtk_file_dialog_open(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); else if (action == GtkFileChooserAction.Open && allowMultiple) gtk_file_dialog_open_multiple(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); else if (action == GtkFileChooserAction.Save) gtk_file_dialog_save(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); else if (action == GtkFileChooserAction.SelectFolder) gtk_file_dialog_select_folder(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); // Process events until dialog completes while (!_fileDialogTcs.Task.IsCompleted) { ProcessPendingEvents(); Thread.Sleep(10); } g_object_unref(dialog); return _fileDialogTcs.Task.Result; } private FileDialogResult ShowGtk3FileDialog( string title, int action, bool allowMultiple, (string Name, string Pattern)[] filters) { var dialog = gtk3_file_chooser_dialog_new( title, IntPtr.Zero, action, "_Cancel", (int)GtkResponseType.Cancel, action == 1 ? "_Save" : "_Open", (int)GtkResponseType.Accept, IntPtr.Zero); if (allowMultiple) gtk3_file_chooser_set_select_multiple(dialog, true); int response = gtk3_dialog_run(dialog); var result = new FileDialogResult { Accepted = false }; if (response == (int)GtkResponseType.Accept) { if (allowMultiple) { IntPtr list = gtk3_file_chooser_get_filenames(dialog); uint count = g_slist_length(list); var files = new List(); for (uint i = 0; i < count; i++) { IntPtr pathPtr = g_slist_nth_data(list, i); string? path = Marshal.PtrToStringUTF8(pathPtr); if (!string.IsNullOrEmpty(path)) { files.Add(path); g_free(pathPtr); } } g_slist_free(list); result = new FileDialogResult { Accepted = true, SelectedFiles = files.ToArray() }; } else { IntPtr pathPtr = gtk3_file_chooser_get_filename(dialog); string? path = Marshal.PtrToStringUTF8(pathPtr); g_free(pathPtr); if (!string.IsNullOrEmpty(path)) result = new FileDialogResult { Accepted = true, SelectedFiles = new[] { path } }; } } gtk3_widget_destroy(dialog); ProcessPendingEvents(); return result; } #endregion #region Color Dialog /// /// Shows a color picker dialog. /// public ColorDialogResult ShowColorDialog( string title = "Choose Color", float initialRed = 1f, float initialGreen = 1f, float initialBlue = 1f, float initialAlpha = 1f, bool withAlpha = true) { if (!EnsureInitialized()) return new ColorDialogResult { Accepted = false }; if (_useGtk4) { return ShowGtk4ColorDialog(title, initialRed, initialGreen, initialBlue, initialAlpha, withAlpha); } else { // GTK3 color dialog would go here return new ColorDialogResult { Accepted = false }; } } private ColorDialogResult ShowGtk4ColorDialog( string title, float r, float g, float b, float a, bool withAlpha) { var dialog = gtk_color_dialog_new(); gtk_color_dialog_set_title(dialog, title); gtk_color_dialog_set_modal(dialog, true); gtk_color_dialog_set_with_alpha(dialog, withAlpha); _colorDialogTcs = new TaskCompletionSource(); _currentCallback = (source, result, userData) => { IntPtr error = IntPtr.Zero; try { IntPtr rgbaPtr = gtk_color_dialog_choose_rgba_finish(dialog, result, out error); if (rgbaPtr != IntPtr.Zero && error == IntPtr.Zero) { var rgba = Marshal.PtrToStructure(rgbaPtr); _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = true, Red = rgba.Red, Green = rgba.Green, Blue = rgba.Blue, Alpha = rgba.Alpha }); } else { _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false }); } } catch { _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false }); } }; gtk_color_dialog_choose_rgba(dialog, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); while (!_colorDialogTcs.Task.IsCompleted) { ProcessPendingEvents(); Thread.Sleep(10); } g_object_unref(dialog); return _colorDialogTcs.Task.Result; } #endregion #region Helpers private bool EnsureInitialized() { if (!_initialized) Initialize(); return _initialized; } private void ProcessPendingEvents() { var context = g_main_context_default(); while (g_main_context_iteration(context, false)) { } } #endregion #region IDisposable public void Dispose() { if (_disposed) return; _disposed = true; _initialized = false; GC.SuppressFinalize(this); } ~Gtk4InteropService() { Dispose(); } #endregion }