patch 9.2.0607: GTK4: inputdialog() does not work as expected
Commit:
https://github.com/vim/vim/commit/beef02a975815cb03d9a7159ede72629d498ddf2
Author: Foxe Chen <
chen...@gmail.com>
Date: Tue Jun 9 18:58:08 2026 +0000
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 <
chen...@gmail.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c
index 033b3f7ba..5328a4b5b 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;
+
+ 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);
- title = CONVERT_TO_UTF8(title);
- message = CONVERT_TO_UTF8(message);
+ // 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);
- // Parse button labels from the "&Yes
&No
&Cancel" format
- if (buttons != NULL)
+ // 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)
{
- buf = vim_strsave(buttons);
- if (buf != NULL)
- {
- p = buf;
- while (*p != NUL && butcount < 63)
- {
- char_u *start = p;
- while (*p != NUL && *p != '
')
- ++p;
- if (*p == '
')
- *p++ = NUL;
- // Skip '&' mnemonic marker
- if (*start == '&')
- ++start;
- btn_conv[butcount] = CONVERT_TO_UTF8(start);
- btn_labels[butcount] = (const char *)btn_conv[butcount];
- butcount++;
- }
- }
+ 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);
}
- 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);
+ 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 <Esc> key is pressed. the GTK3 GUI also allows
+ // mnemonics without <Alt> 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);
+ }
- 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);
+ if (buttons != NULL)
+ {
+ GtkWidget *but_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ char **buttons_arr; // Note that array is allocated, not strings
+ int n_buttons;
- add.response = -1;
- add.done = FALSE;
+ 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++)
+ {
+ 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);
+ }
- gtk_alert_dialog_choose(dlg, GTK_WINDOW(gui.mainwin), NULL,
- alert_dialog_cb, &add);
+ gtk_window_present(win);
- 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_window_destroy(win);
+ }
+ g_free(but_states);
+ g_free(tofree);
- // GTK returns 0-based index, Vim wants 1-based
- return add.response >= 0 ? add.response + 1 : 0;
+ // 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 0a1ba1296..653b5132b 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 7083c1bbb..e6c2af18d 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 7633b7e54..e35cb0962 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,
/**/