From beef02a975815cb03d9a7159ede72629d498ddf2 Mon Sep 17 00:00:00 2001 From: Foxe Chen Date: Tue, 9 Jun 2026 18:58:08 +0000 Subject: [PATCH] patch 9.2.0607: GTK4: inputdialog() does not work as expected Problem: GTK4: inputdialog() does not work as expected Solution: Refactor the dialog code to create a custom window instead of using GtkAlertDialog, while at it, also makes mnemonics work as expected (Foxe Chen). closes: #20448 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- src/gui_gtk4.c | 304 ++++++++++++++++++++++++++-------- src/proto/gui_gtk4.pro | 2 +- src/testdir/lsan-suppress.txt | 4 + src/version.c | 2 + 4 files changed, 245 insertions(+), 67 deletions(-) diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index 033b3f7bac..5328a4b5b1 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -4555,97 +4555,269 @@ gui_mch_browsedir(char_u *title, char_u *initdir) * ============================================================ */ -typedef struct { - int response; - gboolean done; -} AlertDialogData; +/* + * Split up button_string into individual button labels by inserting NUL bytes. + * Also replace the Vim-style mnemonic accelerator prefix '&' with '_'. + * "button_string" is duplicated; caller must free the duplicated string via + * *tofree. + */ + static char ** +split_button_string(char_u *button_string, int *n_buttons, char **tofree) +{ + char **array; + char_u *p; + unsigned int count = 1; + + button_string = (char_u *)g_strdup((const char *)button_string); + + for (p = button_string; *p != NUL; ++p) + if (*p == DLG_BUTTON_SEP) + ++count; + + array = g_malloc_n(count, sizeof(char *)); + count = 0; + + if (array != NULL) + { + array[count++] = (char *)button_string; + for (p = button_string; *p != NUL; ) + { + if (*p == DLG_BUTTON_SEP) + { + *p++ = NUL; + array[count++] = (char *)p; + } + else if (*p == DLG_HOTKEY_CHAR) + *p++ = '_'; + else + MB_PTR_ADV(p); + } + } + + *tofree = (char *)button_string; + *n_buttons = count; + return array; +} + +/* + * Convert VIM_GENERIC, VIM_ERROR, etc into an icon name. Returns NULL for + * VIM_GENERIC. + */ + static const char * +dialog_type_to_icon(int type) +{ + switch (type) + { + case VIM_ERROR: + return "dialog-error-symbolic"; + case VIM_WARNING: + return "dialog-warning-symbolic"; + case VIM_INFO: + return "dialog-information-symbolic"; + case VIM_QUESTION: + return "dialog-question-symbolic"; + default: + break; + } + return NULL; +} + +// Data associated with each button in the dialog +typedef struct +{ + int but_idx; + int *response; + gboolean *done; +} DialogButtonState; static void -alert_dialog_cb(GObject *source, GAsyncResult *res, gpointer data) +dialog_button_clicked_cb(GtkButton *button, DialogButtonState *state) { - AlertDialogData *add = (AlertDialogData *)data; - add->response = gtk_alert_dialog_choose_finish( - GTK_ALERT_DIALOG(source), res, NULL); - add->done = TRUE; + *state->response = state->but_idx; + *state->done = TRUE; +} + + static gboolean +dialog_key_pressed_cb( + GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + gboolean *done) +{ + if (keyval == GDK_KEY_Escape) + { + *done = TRUE; + return TRUE; + } + return FALSE; +} + + static gboolean +dialog_close_request_cb(GtkWindow *win, gboolean *win_closed) +{ + *win_closed = TRUE; + return FALSE; } int gui_mch_dialog( - int type UNUSED, + int type, char_u *title, char_u *message, char_u *buttons, - int dfltbutton, - char_u *textfield UNUSED, + int def_but, + char_u *textfield, int ex_cmd UNUSED) { - GtkAlertDialog *dlg; - AlertDialogData add; - char_u *p; - char_u *buf = NULL; - int butcount = 0; - int i; - const char *btn_labels[64]; - char_u *btn_conv[64]; + GtkWindow *win = GTK_WINDOW(gtk_window_new()); + GtkWidget *vertbox; + GtkWidget *message_box; + const char *icon_name; + GtkWidget *icon; + GtkWidget *label; + GtkWidget *entry = NULL; + char_u *utf8_title; + char_u *utf8_message; + GtkEventController *key_controller; + DialogButtonState *but_states = NULL; + char *tofree = NULL; + int response = -1; + gboolean done = FALSE; + gboolean win_closed = FALSE; - title = CONVERT_TO_UTF8(title); - message = CONVERT_TO_UTF8(message); + utf8_title = CONVERT_TO_UTF8(title); + if (utf8_title != NULL) + gtk_window_set_title(win, (const char *)utf8_title); + CONVERT_TO_UTF8_FREE(utf8_title); + + gtk_window_set_transient_for(win, GTK_WINDOW(gui.mainwin)); + gtk_window_set_modal(win, TRUE); + gtk_window_set_default_size(win, 300, -1); + gtk_window_set_destroy_with_parent(win, TRUE); + g_signal_connect(win, "close-request", + G_CALLBACK(dialog_close_request_cb), &win_closed); + + // Create main vertical layout container + vertbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16); + gtk_window_set_child(win, vertbox); + gtk_widget_set_margin_top(vertbox, 24); + gtk_widget_set_margin_bottom(vertbox, 24); + gtk_widget_set_margin_start(vertbox, 12); + gtk_widget_set_margin_end(vertbox, 12); + + // Add the message label + message_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); + gtk_widget_set_halign(message_box, GTK_ALIGN_CENTER); + gtk_box_append(GTK_BOX(vertbox), message_box); + + // If type is not VIM_GENERIC, add an icon to make the dialog look nicer :) + icon_name = dialog_type_to_icon(type); + if (icon_name != NULL) + { + icon = gtk_image_new_from_icon_name(icon_name); + gtk_image_set_icon_size(GTK_IMAGE(icon), GTK_ICON_SIZE_LARGE); + gtk_box_append(GTK_BOX(message_box), icon); + } + + utf8_message = CONVERT_TO_UTF8(message); + label = gtk_label_new((const char *)utf8_message); + CONVERT_TO_UTF8_FREE(utf8_message); + gtk_label_set_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(label), 40); + gtk_box_append(GTK_BOX(message_box), label); + + // Close the dialog when the key is pressed. the GTK3 GUI also allows + // mnemonics without key, but that behaviour comes from GTK+ 1.2 (from + // 1999!), so most users probably don't care... + key_controller = gtk_event_controller_key_new(); + g_signal_connect(key_controller, "key-pressed", G_CALLBACK(dialog_key_pressed_cb), &done); + gtk_widget_add_controller(GTK_WIDGET(win), key_controller); + + if (textfield != NULL) + { + // Add text entry so user can enter text + char_u *utf8_text = CONVERT_TO_UTF8(textfield); + + entry = gtk_entry_new(); + + if (utf8_text != NULL) + gtk_editable_set_text(GTK_EDITABLE(entry), (const char *)utf8_text); + else + gtk_editable_set_text(GTK_EDITABLE(entry), ""); + CONVERT_TO_UTF8_FREE(utf8_text); + + // Make it so that pressing enter key will activate "def_but" button + // (which is set as the default widget). + gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + gtk_box_append(GTK_BOX(vertbox), entry); + } - // Parse button labels from the "&Yes\n&No\n&Cancel" format if (buttons != NULL) { - buf = vim_strsave(buttons); - if (buf != NULL) + GtkWidget *but_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + char **buttons_arr; // Note that array is allocated, not strings + int n_buttons; + + gtk_widget_set_halign(but_box, GTK_ALIGN_CENTER); + gtk_box_set_homogeneous(GTK_BOX(but_box), TRUE); + gtk_box_append(GTK_BOX(vertbox), but_box); + + buttons_arr = split_button_string(buttons, &n_buttons, &tofree); + + but_states = g_malloc_n(n_buttons, sizeof(DialogButtonState)); + + for (int i = 0; i < n_buttons; i++) { - p = buf; - while (*p != NUL && butcount < 63) - { - char_u *start = p; - while (*p != NUL && *p != '\n') - ++p; - if (*p == '\n') - *p++ = NUL; - // Skip '&' mnemonic marker - if (*start == '&') - ++start; - btn_conv[butcount] = CONVERT_TO_UTF8(start); - btn_labels[butcount] = (const char *)btn_conv[butcount]; - butcount++; - } + char_u *but_label; + GtkWidget *but; + DialogButtonState *but_state = but_states + i; + + but_label = CONVERT_TO_UTF8((char_u *)buttons_arr[i]); + if (but_label == NULL) + continue; + + but = gtk_button_new_with_mnemonic((char *)but_label); + if (i == def_but - 1) + gtk_window_set_default_widget(win, but); + gtk_box_append(GTK_BOX(but_box), but); + CONVERT_TO_UTF8_FREE(but_label); + + but_state->but_idx = i; + but_state->response = &response; + but_state->done = &done; + + g_signal_connect(but, "clicked", + G_CALLBACK(dialog_button_clicked_cb), but_state); } + g_free(buttons_arr); } - btn_labels[butcount] = NULL; - dlg = gtk_alert_dialog_new("%s", message ? (char *)message : ""); - if (title != NULL) - gtk_alert_dialog_set_detail(dlg, (const char *)title); - gtk_alert_dialog_set_buttons(dlg, btn_labels); - gtk_alert_dialog_set_modal(dlg, TRUE); + gtk_window_present(win); - if (dfltbutton > 0 && dfltbutton <= butcount) - gtk_alert_dialog_set_default_button(dlg, dfltbutton - 1); - if (butcount > 0) - gtk_alert_dialog_set_cancel_button(dlg, butcount - 1); - - add.response = -1; - add.done = FALSE; - - gtk_alert_dialog_choose(dlg, GTK_WINDOW(gui.mainwin), NULL, - alert_dialog_cb, &add); - - while (!add.done) + while (!done && !win_closed) g_main_context_iteration(NULL, TRUE); - g_object_unref(dlg); + if (done) + { + if (textfield != NULL) + { + // Get the text the user entered + char_u *text; - for (i = 0; i < butcount; i++) - CONVERT_TO_UTF8_FREE(btn_conv[i]); - vim_free(buf); - CONVERT_TO_UTF8_FREE(title); - CONVERT_TO_UTF8_FREE(message); + text = (char_u *)gtk_editable_get_text(GTK_EDITABLE(entry)); + text = CONVERT_FROM_UTF8(text); + vim_strncpy(textfield, text, IOSIZE - 1); + CONVERT_FROM_UTF8_FREE(text); + } - // GTK returns 0-based index, Vim wants 1-based - return add.response >= 0 ? add.response + 1 : 0; + gtk_window_destroy(win); + } + g_free(but_states); + g_free(tofree); + + // Vim buttons are indexed starting from one. + return response == -1 ? 0 : response + 1; } /* diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro index 0a1ba1296c..653b5132be 100644 --- a/src/proto/gui_gtk4.pro +++ b/src/proto/gui_gtk4.pro @@ -103,7 +103,7 @@ void gui_mch_update_scrollbar_size(void); void gui_mch_set_text_area_pos(int x, int y, int w, int h); char_u *gui_mch_browse(int saving, char_u *title, char_u *dflt, char_u *ext, char_u *initdir, char_u *filter); char_u *gui_mch_browsedir(char_u *title, char_u *initdir); -int gui_mch_dialog(int type, char_u *title, char_u *message, char_u *buttons, int dfltbutton, char_u *textfield, int ex_cmd); +int gui_mch_dialog(int type, char_u *title, char_u *message, char_u *buttons, int def_but, char_u *textfield, int ex_cmd); void gui_mch_find_dialog(exarg_T *eap); void gui_mch_replace_dialog(exarg_T *eap); void ex_helpfind(exarg_T *eap); diff --git a/src/testdir/lsan-suppress.txt b/src/testdir/lsan-suppress.txt index 7083c1bbb1..e6c2af18d4 100644 --- a/src/testdir/lsan-suppress.txt +++ b/src/testdir/lsan-suppress.txt @@ -16,3 +16,7 @@ leak:libxcb*.so.* leak:gdk_x11_screen_init_gl # leak reported on CI for test_clipmethod leak:libgtk-3*.so.* +# Leak from GTK when creating gtk4 dialog windows? +leak:wl_proxy_marshal_array_flags +leak:gtk_widget_realize +leak:libgtk-4*.so.* diff --git a/src/version.c b/src/version.c index 7633b7e541..e35cb0962d 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 607, /**/ 606, /**/