wails/v3/pkg/application/linux_cgo_gtk4.c
Lea Anthony 716507b488
fix(linux/gtk4): window position, max size, and show/hide on X11 (#4965)
* fix(linux/gtk4): window position, max size, and show/hide on X11 (#4957)

- Use gtk_window_present() in windowShow() to properly restore windows
  after hide instead of gtk_widget_set_visible() which could leave
  windows minimized
- Add X11-conditional window position get/set using XTranslateCoordinates
  and XMoveWindow, gated behind GDK_IS_X11_DISPLAY runtime checks and
  GDK_WINDOWING_X11 compile-time guards. Wayland continues to return 0,0
  gracefully since position is not available there.
- Implement max window size enforcement via notify::default-width/height
  signal handlers that clamp dimensions, replacing the removed
  gtk_window_set_geometry_hints from GTK3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: treat skipped cross-compile job as success in CI

The cross_compile_results job treats "skipped" as a failure, but the
cross_compile job is legitimately skipped when the PR hasn't been
approved yet. Accept "skipped" alongside "success".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:27:20 +11:00

997 lines
36 KiB
C

//go:build linux && gtk4
#include "linux_cgo_gtk4.h"
#ifdef GDK_WINDOWING_X11
#include <gdk/x11/gdkx.h>
#include <dlfcn.h>
// Function pointer types for Xlib functions loaded at runtime via dlsym.
// This avoids a direct link dependency on libX11 - the symbols are resolved
// from GTK4's already-loaded X11 backend.
typedef int (*WailsXMoveWindowFunc)(Display*, Window, int, int);
typedef int (*WailsXFlushFunc)(Display*);
typedef Bool (*WailsXTranslateCoordinatesFunc)(Display*, Window, Window, int, int, int*, int*, Window*);
static WailsXMoveWindowFunc wails_XMoveWindow = NULL;
static WailsXFlushFunc wails_XFlush = NULL;
static WailsXTranslateCoordinatesFunc wails_XTranslateCoordinates = NULL;
static gboolean x11_funcs_resolved = FALSE;
static void resolve_x11_funcs(void) {
if (x11_funcs_resolved) return;
x11_funcs_resolved = TRUE;
wails_XMoveWindow = (WailsXMoveWindowFunc)dlsym(RTLD_DEFAULT, "XMoveWindow");
wails_XFlush = (WailsXFlushFunc)dlsym(RTLD_DEFAULT, "XFlush");
wails_XTranslateCoordinates = (WailsXTranslateCoordinatesFunc)dlsym(RTLD_DEFAULT, "XTranslateCoordinates");
}
#endif
#ifdef WAILS_GTK_DEBUG
#define DEBUG_LOG(fmt, ...) fprintf(stderr, "[GTK4] " fmt "\n", ##__VA_ARGS__)
#else
#define DEBUG_LOG(fmt, ...)
#endif
// ============================================================================
// Global state
// ============================================================================
static GMenu *app_menu_model = NULL;
static GSimpleActionGroup *app_action_group = NULL;
// ============================================================================
// Main thread dispatch
// ============================================================================
static gboolean dispatchCallback(gpointer data) {
struct CallbackID *args = data;
unsigned int cid = args->value;
dispatchOnMainThreadCallback(cid);
free(args);
return G_SOURCE_REMOVE;
}
void dispatchOnMainThread(unsigned int id) {
CallbackID *args = malloc(sizeof(CallbackID));
args->value = id;
g_idle_add((GSourceFunc)dispatchCallback, (gpointer)args);
}
// ============================================================================
// Signal handling
// ============================================================================
static void fix_signal(int signum) {
struct sigaction st;
if (sigaction(signum, NULL, &st) < 0) {
goto fix_signal_error;
}
st.sa_flags |= SA_ONSTACK;
if (sigaction(signum, &st, NULL) < 0) {
goto fix_signal_error;
}
return;
fix_signal_error:
fprintf(stderr, "error fixing handler for signal %d, please "
"report this issue to "
"https://github.com/wailsapp/wails: %s\n",
signum, strerror(errno));
}
void install_signal_handlers(void) {
#if defined(SIGCHLD)
fix_signal(SIGCHLD);
#endif
#if defined(SIGHUP)
fix_signal(SIGHUP);
#endif
#if defined(SIGINT)
fix_signal(SIGINT);
#endif
#if defined(SIGQUIT)
fix_signal(SIGQUIT);
#endif
#if defined(SIGABRT)
fix_signal(SIGABRT);
#endif
#if defined(SIGFPE)
fix_signal(SIGFPE);
#endif
#if defined(SIGTERM)
fix_signal(SIGTERM);
#endif
#if defined(SIGBUS)
fix_signal(SIGBUS);
#endif
#if defined(SIGSEGV)
fix_signal(SIGSEGV);
#endif
#if defined(SIGXCPU)
fix_signal(SIGXCPU);
#endif
#if defined(SIGXFSZ)
fix_signal(SIGXFSZ);
#endif
}
// ============================================================================
// Object data helpers
// ============================================================================
void save_window_id(void *object, uint value) {
g_object_set_data((GObject *)object, "windowid", GUINT_TO_POINTER((guint)value));
}
guint get_window_id(void *object) {
return GPOINTER_TO_UINT(g_object_get_data((GObject *)object, "windowid"));
}
void save_webview_to_content_manager(void *contentManager, void *webview) {
g_object_set_data(G_OBJECT((WebKitUserContentManager *)contentManager), "webview", webview);
}
WebKitWebView* get_webview_from_content_manager(void *contentManager) {
return WEBKIT_WEB_VIEW(g_object_get_data(G_OBJECT(contentManager), "webview"));
}
// ============================================================================
// Signal connection (wrapper for macro)
// ============================================================================
void signal_connect(void *widget, char *event, void *cb, uintptr_t data) {
g_signal_connect(widget, event, cb, (gpointer)data);
}
// ============================================================================
// WebView helpers
// ============================================================================
WebKitWebView* webkit_web_view(GtkWidget *webview) {
return WEBKIT_WEB_VIEW(webview);
}
// WebKitGTK 6.0: webkit_web_view_new_with_user_content_manager() was removed
// Use g_object_new() with the "user-content-manager" property instead
GtkWidget* create_webview_with_user_content_manager(WebKitUserContentManager *manager) {
return GTK_WIDGET(g_object_new(WEBKIT_TYPE_WEB_VIEW,
"user-content-manager", manager,
NULL));
}
// ============================================================================
// Menu system (GMenu/GAction for GTK4)
// ============================================================================
static void on_action_activated(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
MenuItemData *data = (MenuItemData *)user_data;
if (data != NULL) {
menuActionActivated(data->id);
}
}
void init_app_action_group(void) {
if (app_action_group == NULL) {
app_action_group = g_simple_action_group_new();
}
}
void set_app_menu_model(GMenu *menu) {
app_menu_model = menu;
}
GMenuItem* create_menu_item(const char *label, const char *action_name, guint item_id) {
init_app_action_group();
char full_action_name[256];
snprintf(full_action_name, sizeof(full_action_name), "app.%s", action_name);
GMenuItem *item = g_menu_item_new(label, full_action_name);
GSimpleAction *action = g_simple_action_new(action_name, NULL);
MenuItemData *data = g_new0(MenuItemData, 1);
data->id = item_id;
data->action = action;
g_signal_connect(action, "activate", G_CALLBACK(on_action_activated), data);
g_action_map_add_action(G_ACTION_MAP(app_action_group), G_ACTION(action));
return item;
}
GMenuItem* create_check_menu_item(const char *label, const char *action_name, guint item_id, gboolean initial_state) {
init_app_action_group();
char full_action_name[256];
snprintf(full_action_name, sizeof(full_action_name), "app.%s", action_name);
GMenuItem *item = g_menu_item_new(label, full_action_name);
GSimpleAction *action = g_simple_action_new_stateful(action_name, NULL, g_variant_new_boolean(initial_state));
MenuItemData *data = g_new0(MenuItemData, 1);
data->id = item_id;
data->action = action;
g_signal_connect(action, "activate", G_CALLBACK(on_action_activated), data);
g_action_map_add_action(G_ACTION_MAP(app_action_group), G_ACTION(action));
return item;
}
static void on_radio_action_activated(GSimpleAction *action, GVariant *parameter, gpointer user_data) {
const gchar *target = g_variant_get_string(parameter, NULL);
g_simple_action_set_state(action, g_variant_new_string(target));
guint item_id = (guint)atoi(target);
menuActionActivated(item_id);
}
GMenuItem* create_radio_menu_item(const char *label, const char *action_name, const char *target, const char *initial_value, guint item_id) {
init_app_action_group();
char full_action_name[256];
snprintf(full_action_name, sizeof(full_action_name), "app.%s", action_name);
GMenuItem *item = g_menu_item_new(label, NULL);
g_menu_item_set_action_and_target(item, full_action_name, "s", target);
GAction *existing = g_action_map_lookup_action(G_ACTION_MAP(app_action_group), action_name);
if (existing == NULL) {
GSimpleAction *action = g_simple_action_new_stateful(
action_name,
G_VARIANT_TYPE_STRING,
g_variant_new_string(initial_value)
);
MenuItemData *data = g_new0(MenuItemData, 1);
data->id = item_id;
data->action = action;
g_signal_connect(action, "activate", G_CALLBACK(on_radio_action_activated), data);
g_action_map_add_action(G_ACTION_MAP(app_action_group), G_ACTION(action));
}
return item;
}
GtkWidget* create_menu_bar_from_model(GMenu *menu_model) {
return gtk_popover_menu_bar_new_from_model(G_MENU_MODEL(menu_model));
}
GtkWidget* create_header_bar_with_menu(GMenu *menu_model) {
GtkWidget *header_bar = gtk_header_bar_new();
GtkWidget *menu_button = gtk_menu_button_new();
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(menu_button), "open-menu-symbolic");
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(menu_button), G_MENU_MODEL(menu_model));
gtk_widget_set_tooltip_text(menu_button, "Main Menu");
gtk_accessible_update_property(GTK_ACCESSIBLE(menu_button),
GTK_ACCESSIBLE_PROPERTY_LABEL, "Main Menu", -1);
gtk_header_bar_pack_end(GTK_HEADER_BAR(header_bar), menu_button);
return header_bar;
}
void attach_action_group_to_widget(GtkWidget *widget) {
init_app_action_group();
gtk_widget_insert_action_group(widget, "app", G_ACTION_GROUP(app_action_group));
}
void set_action_accelerator(GtkApplication *app, const char *action_name, const char *accel) {
if (app == NULL || accel == NULL || strlen(accel) == 0) return;
char full_action_name[256];
snprintf(full_action_name, sizeof(full_action_name), "app.%s", action_name);
const char *accels[] = { accel, NULL };
gtk_application_set_accels_for_action(app, full_action_name, accels);
}
char* build_accelerator_string(guint key, GdkModifierType mods) {
return gtk_accelerator_name(key, mods);
}
void set_action_enabled(const char *action_name, gboolean enabled) {
if (app_action_group == NULL) return;
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(app_action_group), action_name);
if (action != NULL && G_IS_SIMPLE_ACTION(action)) {
g_simple_action_set_enabled(G_SIMPLE_ACTION(action), enabled);
}
}
void set_action_state(const char *action_name, gboolean state) {
if (app_action_group == NULL) return;
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(app_action_group), action_name);
if (action != NULL && G_IS_SIMPLE_ACTION(action)) {
g_simple_action_set_state(G_SIMPLE_ACTION(action), g_variant_new_boolean(state));
}
}
gboolean get_action_state(const char *action_name) {
if (app_action_group == NULL) return FALSE;
GAction *action = g_action_map_lookup_action(G_ACTION_MAP(app_action_group), action_name);
if (action != NULL) {
GVariant *state = g_action_get_state(action);
if (state != NULL) {
gboolean result = g_variant_get_boolean(state);
g_variant_unref(state);
return result;
}
}
return FALSE;
}
void menu_remove_item(GMenu *menu, gint position) {
g_menu_remove(menu, position);
}
void menu_insert_item(GMenu *menu, gint position, GMenuItem *item) {
g_menu_insert_item(menu, position, item);
}
// ============================================================================
// Window event controllers (GTK4 style)
// ============================================================================
void setupWindowEventControllers(GtkWindow *window, GtkWidget *webview, uintptr_t winID) {
// Close request (replaces delete-event)
g_signal_connect(window, "close-request", G_CALLBACK(handleCloseRequest), (gpointer)winID);
// Window state changes (maximize, fullscreen, etc)
g_signal_connect(window, "notify::maximized", G_CALLBACK(handleNotifyState), (gpointer)winID);
g_signal_connect(window, "notify::fullscreened", G_CALLBACK(handleNotifyState), (gpointer)winID);
// Focus controller for window
GtkEventController *focus_controller = gtk_event_controller_focus_new();
gtk_widget_add_controller(GTK_WIDGET(window), focus_controller);
g_signal_connect(focus_controller, "enter", G_CALLBACK(handleFocusEnter), (gpointer)winID);
g_signal_connect(focus_controller, "leave", G_CALLBACK(handleFocusLeave), (gpointer)winID);
// Click gesture for webview (button press/release)
GtkGesture *click_gesture = gtk_gesture_click_new();
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click_gesture), 0); // Listen to all buttons
gtk_widget_add_controller(webview, GTK_EVENT_CONTROLLER(click_gesture));
g_signal_connect(click_gesture, "pressed", G_CALLBACK(handleButtonPressed), (gpointer)winID);
g_signal_connect(click_gesture, "released", G_CALLBACK(handleButtonReleased), (gpointer)winID);
// Key controller for webview
GtkEventController *key_controller = gtk_event_controller_key_new();
gtk_widget_add_controller(webview, key_controller);
g_signal_connect(key_controller, "key-pressed", G_CALLBACK(handleKeyPressed), (gpointer)winID);
}
// ============================================================================
// Window drag/resize (GdkToplevel for GTK4)
// ============================================================================
void beginWindowDrag(GtkWindow *window, int button, double x, double y, guint32 timestamp) {
GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window));
if (native == NULL) return;
GdkSurface *surface = gtk_native_get_surface(native);
if (surface == NULL || !GDK_IS_TOPLEVEL(surface)) return;
GdkToplevel *toplevel = GDK_TOPLEVEL(surface);
GdkDevice *device = NULL;
GdkDisplay *display = gdk_surface_get_display(surface);
GdkSeat *seat = gdk_display_get_default_seat(display);
if (seat) {
device = gdk_seat_get_pointer(seat);
}
gdk_toplevel_begin_move(toplevel, device, button, x, y, timestamp);
}
void beginWindowResize(GtkWindow *window, GdkSurfaceEdge edge, int button, double x, double y, guint32 timestamp) {
GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window));
if (native == NULL) return;
GdkSurface *surface = gtk_native_get_surface(native);
if (surface == NULL || !GDK_IS_TOPLEVEL(surface)) return;
GdkToplevel *toplevel = GDK_TOPLEVEL(surface);
GdkDevice *device = NULL;
GdkDisplay *display = gdk_surface_get_display(surface);
GdkSeat *seat = gdk_display_get_default_seat(display);
if (seat) {
device = gdk_seat_get_pointer(seat);
}
gdk_toplevel_begin_resize(toplevel, edge, device, button, x, y, timestamp);
}
// ============================================================================
// Drag and drop (GtkDropTarget for GTK4)
// ============================================================================
static gboolean on_drop_accept(GtkDropTarget *target, GdkDrop *drop, gpointer data) {
GdkContentFormats *formats = gdk_drop_get_formats(drop);
return gdk_content_formats_contain_gtype(formats, GDK_TYPE_FILE_LIST);
}
static GdkDragAction on_drop_enter(GtkDropTarget *target, gdouble x, gdouble y, gpointer data) {
onDropEnter((uintptr_t)data);
return GDK_ACTION_COPY;
}
static void on_drop_leave(GtkDropTarget *target, gpointer data) {
onDropLeave((uintptr_t)data);
}
static GdkDragAction on_drop_motion(GtkDropTarget *target, gdouble x, gdouble y, gpointer data) {
onDropMotion((gint)x, (gint)y, (uintptr_t)data);
return GDK_ACTION_COPY;
}
static gboolean on_drop(GtkDropTarget *target, const GValue *value, gdouble x, gdouble y, gpointer data) {
if (!G_VALUE_HOLDS(value, GDK_TYPE_FILE_LIST)) {
return FALSE;
}
GSList *file_list = g_value_get_boxed(value);
if (file_list == NULL) {
return FALSE;
}
guint count = g_slist_length(file_list);
if (count == 0) {
return FALSE;
}
char **paths = g_new0(char*, count + 1);
guint i = 0;
for (GSList *l = file_list; l != NULL; l = l->next) {
GFile *file = G_FILE(l->data);
paths[i++] = g_file_get_path(file);
}
paths[count] = NULL;
onDropFiles(paths, (gint)x, (gint)y, (uintptr_t)data);
for (i = 0; i < count; i++) {
g_free(paths[i]);
}
g_free(paths);
return TRUE;
}
static void on_motion_enter(GtkDropControllerMotion *ctrl, gdouble x, gdouble y, gpointer data) {
onDropEnter((uintptr_t)data);
}
static void on_motion_leave(GtkDropControllerMotion *ctrl, gpointer data) {
onDropLeave((uintptr_t)data);
}
static void on_motion_motion(GtkDropControllerMotion *ctrl, gdouble x, gdouble y, gpointer data) {
onDropMotion((gint)x, (gint)y, (uintptr_t)data);
}
void enableDND(GtkWidget *widget, uintptr_t winID) {
GtkEventController *motion_ctrl = gtk_drop_controller_motion_new();
gtk_event_controller_set_propagation_phase(motion_ctrl, GTK_PHASE_CAPTURE);
g_signal_connect(motion_ctrl, "enter", G_CALLBACK(on_motion_enter), (gpointer)winID);
g_signal_connect(motion_ctrl, "leave", G_CALLBACK(on_motion_leave), (gpointer)winID);
g_signal_connect(motion_ctrl, "motion", G_CALLBACK(on_motion_motion), (gpointer)winID);
gtk_widget_add_controller(widget, motion_ctrl);
GtkDropTarget *target = gtk_drop_target_new(GDK_TYPE_FILE_LIST, GDK_ACTION_COPY);
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(target), GTK_PHASE_CAPTURE);
g_signal_connect(target, "accept", G_CALLBACK(on_drop_accept), (gpointer)winID);
g_signal_connect(target, "enter", G_CALLBACK(on_drop_enter), (gpointer)winID);
g_signal_connect(target, "leave", G_CALLBACK(on_drop_leave), (gpointer)winID);
g_signal_connect(target, "motion", G_CALLBACK(on_drop_motion), (gpointer)winID);
g_signal_connect(target, "drop", G_CALLBACK(on_drop), (gpointer)winID);
gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(target));
}
void disableDND(GtkWidget *widget, uintptr_t winID) {
}
// ============================================================================
// File dialogs (GtkFileDialog for GTK4)
// ============================================================================
GtkFileDialog* create_file_dialog(const char *title) {
GtkFileDialog *dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog, title);
return dialog;
}
void add_file_filter(GtkFileDialog *dialog, GListStore *filters, const char *name, const char *pattern) {
GtkFileFilter *filter = gtk_file_filter_new();
gtk_file_filter_set_name(filter, name);
gchar **patterns = g_strsplit(pattern, ";", -1);
for (int i = 0; patterns[i] != NULL; i++) {
gchar *p = g_strstrip(patterns[i]);
if (strlen(p) > 0) {
gtk_file_filter_add_pattern(filter, p);
}
}
g_strfreev(patterns);
g_list_store_append(filters, filter);
g_object_unref(filter);
}
void set_file_dialog_filters(GtkFileDialog *dialog, GListStore *filters) {
gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters));
}
// File dialog callbacks
static void on_file_dialog_open_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
FileDialogData *data = (FileDialogData *)user_data;
GtkFileDialog *dialog = GTK_FILE_DIALOG(source);
GError *error = NULL;
GFile *file = gtk_file_dialog_open_finish(dialog, result, &error);
if (error != NULL) {
DEBUG_LOG("open_finish error: %s", error->message);
fileDialogCallback(data->request_id, NULL, 0, TRUE);
g_error_free(error);
} else if (file != NULL) {
char *path = g_file_get_path(file);
char *files[1] = { path };
fileDialogCallback(data->request_id, files, 1, FALSE);
g_free(path);
g_object_unref(file);
} else {
// Cancelled - no error but no file
fileDialogCallback(data->request_id, NULL, 0, TRUE);
}
g_free(data);
}
static void on_file_dialog_open_multiple_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
FileDialogData *data = (FileDialogData *)user_data;
GtkFileDialog *dialog = GTK_FILE_DIALOG(source);
GError *error = NULL;
GListModel *files = gtk_file_dialog_open_multiple_finish(dialog, result, &error);
if (error != NULL) {
DEBUG_LOG("open_multiple_finish error: %s", error->message);
fileDialogCallback(data->request_id, NULL, 0, TRUE);
g_error_free(error);
} else if (files != NULL) {
guint n = g_list_model_get_n_items(files);
char **paths = g_new0(char*, n + 1);
for (guint i = 0; i < n; i++) {
GFile *file = G_FILE(g_list_model_get_item(files, i));
paths[i] = g_file_get_path(file);
g_object_unref(file);
}
fileDialogCallback(data->request_id, paths, (int)n, FALSE);
for (guint i = 0; i < n; i++) {
g_free(paths[i]);
}
g_free(paths);
g_object_unref(files);
} else {
fileDialogCallback(data->request_id, NULL, 0, TRUE);
}
g_free(data);
}
static void on_file_dialog_select_folder_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
FileDialogData *data = (FileDialogData *)user_data;
GtkFileDialog *dialog = GTK_FILE_DIALOG(source);
GError *error = NULL;
GFile *file = gtk_file_dialog_select_folder_finish(dialog, result, &error);
if (error != NULL) {
DEBUG_LOG("select_folder_finish error: %s", error->message);
fileDialogCallback(data->request_id, NULL, 0, TRUE);
g_error_free(error);
} else if (file != NULL) {
char *path = g_file_get_path(file);
char *files[1] = { path };
fileDialogCallback(data->request_id, files, 1, FALSE);
g_free(path);
g_object_unref(file);
} else {
fileDialogCallback(data->request_id, NULL, 0, TRUE);
}
g_free(data);
}
static void on_file_dialog_select_multiple_folders_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
FileDialogData *data = (FileDialogData *)user_data;
GtkFileDialog *dialog = GTK_FILE_DIALOG(source);
GError *error = NULL;
GListModel *files = gtk_file_dialog_select_multiple_folders_finish(dialog, result, &error);
if (error != NULL) {
DEBUG_LOG("select_multiple_folders_finish error: %s", error->message);
fileDialogCallback(data->request_id, NULL, 0, TRUE);
g_error_free(error);
} else if (files != NULL) {
guint n = g_list_model_get_n_items(files);
char **paths = g_new0(char*, n + 1);
for (guint i = 0; i < n; i++) {
GFile *file = G_FILE(g_list_model_get_item(files, i));
paths[i] = g_file_get_path(file);
g_object_unref(file);
}
fileDialogCallback(data->request_id, paths, (int)n, FALSE);
for (guint i = 0; i < n; i++) {
g_free(paths[i]);
}
g_free(paths);
g_object_unref(files);
} else {
fileDialogCallback(data->request_id, NULL, 0, TRUE);
}
g_free(data);
}
static void on_file_dialog_save_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
FileDialogData *data = (FileDialogData *)user_data;
GtkFileDialog *dialog = GTK_FILE_DIALOG(source);
GError *error = NULL;
GFile *file = gtk_file_dialog_save_finish(dialog, result, &error);
if (error != NULL) {
DEBUG_LOG("save_finish error: %s", error->message);
fileDialogCallback(data->request_id, NULL, 0, TRUE);
g_error_free(error);
} else if (file != NULL) {
char *path = g_file_get_path(file);
char *files[1] = { path };
fileDialogCallback(data->request_id, files, 1, FALSE);
g_free(path);
g_object_unref(file);
} else {
fileDialogCallback(data->request_id, NULL, 0, TRUE);
}
g_free(data);
}
void show_open_file_dialog(GtkWindow *parent, GtkFileDialog *dialog, guint request_id, gboolean allow_multiple, gboolean is_folder) {
FileDialogData *data = g_new0(FileDialogData, 1);
data->request_id = request_id;
data->allow_multiple = allow_multiple;
data->is_folder = is_folder;
if (is_folder && allow_multiple) {
gtk_file_dialog_select_multiple_folders(dialog, parent, NULL, on_file_dialog_select_multiple_folders_finish, data);
} else if (is_folder) {
gtk_file_dialog_select_folder(dialog, parent, NULL, on_file_dialog_select_folder_finish, data);
} else if (allow_multiple) {
gtk_file_dialog_open_multiple(dialog, parent, NULL, on_file_dialog_open_multiple_finish, data);
} else {
gtk_file_dialog_open(dialog, parent, NULL, on_file_dialog_open_finish, data);
}
}
void show_save_file_dialog(GtkWindow *parent, GtkFileDialog *dialog, guint request_id) {
FileDialogData *data = g_new0(FileDialogData, 1);
data->request_id = request_id;
data->is_save = TRUE;
gtk_file_dialog_save(dialog, parent, NULL, on_file_dialog_save_finish, data);
}
// ============================================================================
// Custom Message Dialogs (GtkWindow-based for proper styling)
// ============================================================================
typedef struct {
GtkWindow *dialog;
guint request_id;
int button_count;
int cancel_button;
GtkWidget **buttons;
} MessageDialogData;
static void message_dialog_cleanup(MessageDialogData *data) {
if (data->buttons != NULL) {
g_free(data->buttons);
}
g_free(data);
}
static void on_message_dialog_button_clicked(GtkButton *button, gpointer user_data) {
MessageDialogData *data = (MessageDialogData *)user_data;
int index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "button-index"));
alertDialogCallback(data->request_id, index);
gtk_window_destroy(data->dialog);
message_dialog_cleanup(data);
}
static gboolean on_message_dialog_close(GtkWindow *window, gpointer user_data) {
MessageDialogData *data = (MessageDialogData *)user_data;
int result = (data->cancel_button >= 0) ? data->cancel_button : -1;
alertDialogCallback(data->request_id, result);
message_dialog_cleanup(data);
return TRUE;
}
static gboolean on_message_dialog_key_pressed(GtkEventControllerKey *controller,
guint keyval, guint keycode,
GdkModifierType state, gpointer user_data) {
MessageDialogData *data = (MessageDialogData *)user_data;
if (keyval == GDK_KEY_Escape && data->cancel_button >= 0 && data->cancel_button < data->button_count) {
gtk_widget_activate(data->buttons[data->cancel_button]);
return TRUE;
}
return FALSE;
}
void show_message_dialog(GtkWindow *parent, const char *heading, const char *body,
const char *icon_name, const unsigned char *icon_data, int icon_data_len,
const char **buttons, int button_count,
int default_button, int cancel_button, int destructive_button,
guint request_id) {
GtkWidget *dialog = gtk_window_new();
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
gtk_window_set_resizable(GTK_WINDOW(dialog), FALSE);
gtk_window_set_decorated(GTK_WINDOW(dialog), TRUE);
gtk_widget_add_css_class(dialog, "message");
gtk_widget_set_size_request(dialog, 300, -1);
if (parent != NULL) {
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
}
MessageDialogData *data = g_new0(MessageDialogData, 1);
data->dialog = GTK_WINDOW(dialog);
data->request_id = request_id;
data->button_count = button_count;
data->cancel_button = cancel_button;
data->buttons = (button_count > 0) ? g_new0(GtkWidget*, button_count) : NULL;
GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_margin_start(content, 24);
gtk_widget_set_margin_end(content, 24);
gtk_widget_set_margin_top(content, 24);
gtk_widget_set_margin_bottom(content, 24);
const int symbolic_icon_size = 32;
GtkWidget *icon_widget = NULL;
if (icon_data != NULL && icon_data_len > 0) {
GBytes *bytes = g_bytes_new(icon_data, icon_data_len);
GdkTexture *texture = gdk_texture_new_from_bytes(bytes, NULL);
g_bytes_unref(bytes);
if (texture != NULL) {
int tex_size = gdk_texture_get_width(texture);
GtkWidget *image = gtk_image_new_from_paintable(GDK_PAINTABLE(texture));
gtk_image_set_pixel_size(GTK_IMAGE(image), tex_size);
icon_widget = image;
g_object_unref(texture);
}
} else if (icon_name != NULL && strlen(icon_name) > 0) {
icon_widget = gtk_image_new_from_icon_name(icon_name);
gtk_image_set_pixel_size(GTK_IMAGE(icon_widget), symbolic_icon_size);
}
if (icon_widget != NULL) {
gtk_widget_set_halign(icon_widget, GTK_ALIGN_CENTER);
gtk_widget_set_margin_bottom(icon_widget, 12);
gtk_box_append(GTK_BOX(content), icon_widget);
}
if (heading != NULL && strlen(heading) > 0) {
GtkWidget *heading_label = gtk_label_new(heading);
gtk_widget_add_css_class(heading_label, "title-2");
gtk_widget_set_halign(heading_label, GTK_ALIGN_CENTER);
gtk_label_set_wrap(GTK_LABEL(heading_label), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(heading_label), 50);
gtk_box_append(GTK_BOX(content), heading_label);
}
if (body != NULL && strlen(body) > 0) {
GtkWidget *body_label = gtk_label_new(body);
gtk_widget_set_halign(body_label, GTK_ALIGN_CENTER);
gtk_label_set_wrap(GTK_LABEL(body_label), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(body_label), 50);
gtk_widget_add_css_class(body_label, "dim-label");
gtk_box_append(GTK_BOX(content), body_label);
}
if (button_count > 0) {
GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(button_box, GTK_ALIGN_CENTER);
gtk_widget_set_margin_top(button_box, 12);
for (int i = 0; i < button_count; i++) {
GtkWidget *btn = gtk_button_new_with_label(buttons[i]);
g_object_set_data(G_OBJECT(btn), "button-index", GINT_TO_POINTER(i));
g_signal_connect(btn, "clicked", G_CALLBACK(on_message_dialog_button_clicked), data);
data->buttons[i] = btn;
if (i == default_button) {
gtk_widget_add_css_class(btn, "suggested-action");
gtk_widget_add_css_class(btn, "default");
}
if (i == destructive_button) {
gtk_widget_add_css_class(btn, "destructive-action");
}
gtk_box_append(GTK_BOX(button_box), btn);
}
gtk_box_append(GTK_BOX(content), button_box);
}
gtk_window_set_child(GTK_WINDOW(dialog), content);
GtkEventController *key_controller = gtk_event_controller_key_new();
g_signal_connect(key_controller, "key-pressed", G_CALLBACK(on_message_dialog_key_pressed), data);
gtk_widget_add_controller(dialog, key_controller);
g_signal_connect(dialog, "close-request", G_CALLBACK(on_message_dialog_close), data);
gtk_window_present(GTK_WINDOW(dialog));
if (default_button >= 0 && default_button < button_count) {
gtk_window_set_default_widget(GTK_WINDOW(dialog), data->buttons[default_button]);
gtk_widget_grab_focus(data->buttons[default_button]);
}
}
// ============================================================================
// Clipboard (async API for GTK4)
// ============================================================================
static char *clipboard_sync_result = NULL;
static gboolean clipboard_sync_done = FALSE;
static void on_clipboard_sync_finish(GObject *source, GAsyncResult *result, gpointer user_data) {
GdkClipboard *clipboard = GDK_CLIPBOARD(source);
GError *error = NULL;
clipboard_sync_result = gdk_clipboard_read_text_finish(clipboard, result, &error);
if (error != NULL) {
DEBUG_LOG("clipboard read error: %s", error->message);
g_error_free(error);
clipboard_sync_result = NULL;
}
clipboard_sync_done = TRUE;
}
char* clipboard_get_text_sync(void) {
GdkDisplay *display = gdk_display_get_default();
GdkClipboard *clipboard = gdk_display_get_clipboard(display);
clipboard_sync_done = FALSE;
clipboard_sync_result = NULL;
gdk_clipboard_read_text_async(clipboard, NULL, on_clipboard_sync_finish, NULL);
GMainContext *ctx = g_main_context_default();
while (!clipboard_sync_done) {
g_main_context_iteration(ctx, TRUE);
}
return clipboard_sync_result;
}
void clipboard_free_text(char *text) {
if (text != NULL) {
g_free(text);
}
}
// ============================================================================
// Window position (X11 only)
// ============================================================================
void window_move_x11(GtkWindow *window, int x, int y) {
#ifdef GDK_WINDOWING_X11
GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window));
if (native == NULL) return;
GdkSurface *surface = gtk_native_get_surface(native);
if (surface == NULL) return;
GdkDisplay *display = gdk_surface_get_display(surface);
if (!GDK_IS_X11_DISPLAY(display)) return;
resolve_x11_funcs();
if (wails_XMoveWindow == NULL) return;
Display *xdisplay = gdk_x11_display_get_xdisplay(display);
Window xwindow = gdk_x11_surface_get_xid(GDK_X11_SURFACE(surface));
wails_XMoveWindow(xdisplay, xwindow, x, y);
if (wails_XFlush != NULL) wails_XFlush(xdisplay);
#endif
}
void window_get_position_x11(GtkWindow *window, int *x, int *y) {
*x = 0;
*y = 0;
#ifdef GDK_WINDOWING_X11
GtkNative *native = gtk_widget_get_native(GTK_WIDGET(window));
if (native == NULL) return;
GdkSurface *surface = gtk_native_get_surface(native);
if (surface == NULL) return;
GdkDisplay *display = gdk_surface_get_display(surface);
if (!GDK_IS_X11_DISPLAY(display)) return;
resolve_x11_funcs();
if (wails_XTranslateCoordinates == NULL) return;
Display *xdisplay = gdk_x11_display_get_xdisplay(display);
Window xwindow = gdk_x11_surface_get_xid(GDK_X11_SURFACE(surface));
Window child;
Window root = DefaultRootWindow(xdisplay);
int abs_x, abs_y;
if (wails_XTranslateCoordinates(xdisplay, xwindow, root, 0, 0, &abs_x, &abs_y, &child)) {
*x = abs_x;
*y = abs_y;
}
#endif
}
// ============================================================================
// Window size constraints (max size enforcement for GTK4)
// ============================================================================
static void on_window_size_changed(GObject *object, GParamSpec *pspec, gpointer data) {
GtkWindow *window = GTK_WINDOW(object);
// Don't clamp during fullscreen or maximize - these should bypass max size
// constraints, matching V2 behaviour where geometry hints are suspended.
if (gtk_window_is_fullscreen(window) || gtk_window_is_maximized(window)) return;
int maxW = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(window), "wails-max-width"));
int maxH = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(window), "wails-max-height"));
if (maxW <= 0 && maxH <= 0) return;
int w = gtk_widget_get_width(GTK_WIDGET(window));
int h = gtk_widget_get_height(GTK_WIDGET(window));
gboolean needs_clamp = FALSE;
if (maxW > 0 && w > maxW) { w = maxW; needs_clamp = TRUE; }
if (maxH > 0 && h > maxH) { h = maxH; needs_clamp = TRUE; }
if (needs_clamp) {
gtk_window_set_default_size(window, w, h);
}
}
void window_set_max_size(GtkWindow *window, int maxWidth, int maxHeight) {
g_object_set_data(G_OBJECT(window), "wails-max-width", GINT_TO_POINTER(maxWidth));
g_object_set_data(G_OBJECT(window), "wails-max-height", GINT_TO_POINTER(maxHeight));
// Check if we already connected the signal
gpointer connected = g_object_get_data(G_OBJECT(window), "wails-max-size-connected");
if (connected == NULL) {
g_signal_connect(window, "notify::default-width", G_CALLBACK(on_window_size_changed), NULL);
g_signal_connect(window, "notify::default-height", G_CALLBACK(on_window_size_changed), NULL);
g_object_set_data(G_OBJECT(window), "wails-max-size-connected", GINT_TO_POINTER(1));
}
}
// ============================================================================
// Misc
// ============================================================================
int GetNumScreens(void) {
return 0;
}