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.foxe@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Foxe Chen
2026-06-09 18:58:08 +00:00
committed by Christian Brabandt
parent 7daab2ad98
commit beef02a975
4 changed files with 245 additions and 67 deletions
+238 -66
View File
@@ -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 <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);
}
// 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;
}
/*
+1 -1
View File
@@ -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);
+4
View File
@@ -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.*
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
607,
/**/
606,
/**/