822 lines
25 KiB
C#
822 lines
25 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// GTK4 dialog response codes.
|
|
/// </summary>
|
|
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
|
|
}
|
|
|
|
/// <summary>
|
|
/// GTK4 message dialog types.
|
|
/// </summary>
|
|
public enum GtkMessageType
|
|
{
|
|
Info = 0,
|
|
Warning = 1,
|
|
Question = 2,
|
|
Error = 3,
|
|
Other = 4
|
|
}
|
|
|
|
/// <summary>
|
|
/// GTK4 button layouts for dialogs.
|
|
/// </summary>
|
|
public enum GtkButtonsType
|
|
{
|
|
None = 0,
|
|
Ok = 1,
|
|
Close = 2,
|
|
Cancel = 3,
|
|
YesNo = 4,
|
|
OkCancel = 5
|
|
}
|
|
|
|
/// <summary>
|
|
/// GTK4 file chooser actions.
|
|
/// </summary>
|
|
public enum GtkFileChooserAction
|
|
{
|
|
Open = 0,
|
|
Save = 1,
|
|
SelectFolder = 2,
|
|
CreateFolder = 3
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result from a file dialog.
|
|
/// </summary>
|
|
public class FileDialogResult
|
|
{
|
|
public bool Accepted { get; init; }
|
|
public string[] SelectedFiles { get; init; } = Array.Empty<string>();
|
|
public string? SelectedFile => SelectedFiles.Length > 0 ? SelectedFiles[0] : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result from a color dialog.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// GTK4 interop layer for native Linux dialogs.
|
|
/// Provides native file pickers, message boxes, and color choosers.
|
|
/// </summary>
|
|
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<FileDialogResult>? _fileDialogTcs;
|
|
private TaskCompletionSource<ColorDialogResult>? _colorDialogTcs;
|
|
private IntPtr _currentDialog;
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// Gets whether GTK is initialized.
|
|
/// </summary>
|
|
public bool IsInitialized => _initialized;
|
|
|
|
/// <summary>
|
|
/// Gets whether GTK4 is being used (vs GTK3 fallback).
|
|
/// </summary>
|
|
public bool IsGtk4 => _useGtk4;
|
|
|
|
#endregion
|
|
|
|
#region Initialization
|
|
|
|
/// <summary>
|
|
/// Initializes the GTK4 interop service.
|
|
/// Falls back to GTK3 if GTK4 is not available.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Shows an alert message dialog.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a confirmation dialog.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Shows an open file dialog.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a save file dialog.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows a folder picker dialog.
|
|
/// </summary>
|
|
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<FileDialogResult>();
|
|
_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<string>();
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// Shows a color picker dialog.
|
|
/// </summary>
|
|
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<ColorDialogResult>();
|
|
|
|
_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<GdkRGBA>(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
|
|
}
|