From: Foxe Chen Date: Mon, 29 Jun 2026 21:11:23 +0000 (+0000) Subject: patch 9.2.0752: GTK4: drag-and-drop does not support HTML X-Git-Tag: v9.2.0752^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1372aafa543215e9e7e91d88db4d02801433db2f;p=thirdparty%2Fvim.git patch 9.2.0752: GTK4: drag-and-drop does not support HTML Problem: In the GTK4 GUI drag-and-drop does not support HTML. Solution: Refactor GTK 4 drag-and-drop and clipboard handling. closes: #20637 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- diff --git a/src/clipboard.c b/src/clipboard.c index 5ac185cb53..b96b245c31 100644 --- a/src/clipboard.c +++ b/src/clipboard.c @@ -3293,12 +3293,19 @@ did_set_clipboard(optset_T *args UNUSED) vim_regfree(clip_exclude_prog); clip_exclude_prog = new_exclude_prog; # endif -# if defined(FEAT_GUI_GTK) && !defined(USE_GTK4) +# if defined(FEAT_GUI_GTK) if (gui.in_use) { +# ifdef USE_GTK4 + gui_gtk_update_selection_formats(&clip_plus); + gui_gtk_update_selection_formats(&clip_star); +# else gui_gtk_set_selection_targets((GdkAtom)GDK_SELECTION_PRIMARY); gui_gtk_set_selection_targets((GdkAtom)clip_plus.gtk_sel_atom); +# endif +# ifdef FEAT_DND gui_gtk_set_dnd_targets(); +# endif } # endif } @@ -3730,7 +3737,9 @@ dec_clip_provider(void) * If "vim" is TRUE, then get the motion type. If "vimenc" is TRUE, then get the * motion type and also convert "*buf". "buf" and "len_store" will be updated to * reflect the actual contents, but should be set beforehand with the initial - * contents. Returns OK on success and FAIL on failure. + * contents. Note that if "*tofree" is not NULL, use vim_free() on "*buf", + * otherwise use the free func for which "*buf" was allocated with. In both + * cases use vim_free() on "*tofree". Returns OK on success and FAIL on failure. */ int clip_convert_data( diff --git a/src/gui.h b/src/gui.h index 56e86d7a7f..aa9f94ec9b 100644 --- a/src/gui.h +++ b/src/gui.h @@ -490,6 +490,10 @@ typedef struct Gui // Used for clipboard functionality in GTK4 GUI GdkContentProvider *regular_provider; GdkContentProvider *primary_provider; + +# ifdef FEAT_DND + GtkDropTargetAsync *drop_target; +# endif #endif } gui_T; diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index 5a4e78481d..2bcb31ce9e 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -284,7 +284,7 @@ static gboolean scroll_event(GtkEventControllerScroll *controller, double dx, do static void focus_in_event(GtkEventControllerFocus *controller, gpointer data); static void focus_out_event(GtkEventControllerFocus *controller, gpointer data); #ifdef FEAT_DND -static gboolean drop_cb(GtkDropTarget *target, const GValue *value, double x, double y, gpointer data); +static gboolean drop_cb(GtkDropTargetAsync *target, GdkDrop *drop, double x, double y, void *data); #endif #ifdef FEAT_GUI_TABLINE static void tabline_enter_cb(GtkEventController *controller, double x, double y, void *udata); @@ -295,6 +295,7 @@ static void tabline_menu_press_event(GtkGestureClick *gesture, int n_press, doub #endif static void mainwin_destroy_cb(GObject *object, gpointer data); static gboolean delete_event_cb(GtkWindow *window, gpointer data); +static int query_pointer_pos(int *x, int *y, GdkModifierType *state); static void mainwin_fullscreened_cb(GObject *obj, GParamSpec *pspec, gpointer user_data); static void drawarea_realize_cb(GtkWidget *widget, gpointer data); static void drawarea_unrealize_cb(GtkWidget *widget, gpointer data); @@ -628,14 +629,17 @@ gui_mch_init(void) } #ifdef FEAT_DND - // Set up drag-and-drop target for files and text. + // Set up drag-and-drop target for files and text. We use async variant so + // we can handle html format. { - GtkDropTarget *drop = gtk_drop_target_new(G_TYPE_INVALID, GDK_ACTION_COPY); - GType types[] = { GDK_TYPE_FILE_LIST, G_TYPE_STRING }; - gtk_drop_target_set_gtypes(drop, types, 2); - g_signal_connect(drop, "drop", + gui.drop_target = gtk_drop_target_async_new(NULL, + GDK_ACTION_COPY | GDK_ACTION_MOVE); + + gui_gtk_set_dnd_targets(); + g_signal_connect(gui.drop_target, "drop", G_CALLBACK(drop_cb), NULL); - gtk_widget_add_controller(gui.drawarea, GTK_EVENT_CONTROLLER(drop)); + gtk_widget_add_controller(gui.drawarea, + GTK_EVENT_CONTROLLER(gui.drop_target)); } #endif @@ -647,13 +651,13 @@ gui_mch_init(void) { GdkDisplay *display = gtk_widget_get_display(gui.mainwin); GdkClipboard *primary = gdk_display_get_primary_clipboard(display); - GdkClipboard *board = gdk_display_get_clipboard(display); + GdkClipboard *regular = gdk_display_get_clipboard(display); if (primary != NULL) g_signal_connect(primary, "changed", G_CALLBACK(clipboard_changed_cb), &clip_star); - if (board != NULL) - g_signal_connect(board, "changed", + if (regular != NULL) + g_signal_connect(regular, "changed", G_CALLBACK(clipboard_changed_cb), &clip_plus); } @@ -2425,14 +2429,207 @@ drawarea_scale_factor_cb(GObject *object UNUSED, } #endif +typedef enum +{ + READ_DATA_CLIP, // Clipboard data + READ_DATA_DROP, // URI or text DND data + READ_DATA_DROP_DATA // HTML DND data +} ReadDataType; + +#define READDATA_BUFSIZE 4096 + +// Used for reading clipboard and DND data. +typedef struct +{ + ReadDataType type; + + GCancellable *cancel; + GByteArray *arr; // NULL when not needed + char *buf; // NULL when not needed +} ReadData; + +#ifdef FEAT_DND +typedef struct +{ + ReadData rd; + GdkDrop *drop; + int x; + int y; +} DropReadData; +#endif + +typedef struct +{ + ReadData rd; + Clipboard_T *cbd; + char *mime_type; +} ClipReadData; + + static ReadData * +read_data_new(ReadDataType type, size_t sz) +{ + ReadData *rd = g_malloc0(sz); + + rd->type = type; + rd->cancel = g_cancellable_new(); + + if (type == READ_DATA_CLIP || type == READ_DATA_DROP_DATA) + { + rd->arr = g_byte_array_new(); + rd->buf = g_malloc(READDATA_BUFSIZE); + } + + return rd; +} + + static void +read_data_free(ReadData *rd) +{ + g_cancellable_cancel(rd->cancel); + + if (rd->type == READ_DATA_CLIP) + g_free(((ClipReadData *)rd)->mime_type); + else if (rd->type == READ_DATA_DROP || rd->type == READ_DATA_DROP_DATA) + g_object_unref(((DropReadData *)rd)->drop); + g_object_unref(rd->cancel); + if (rd->arr != NULL) + g_byte_array_free(rd->arr, TRUE); + g_free(rd->buf); + g_free(rd); +} + #ifdef FEAT_DND +static void drop_read_text(GdkDrop *drop, char_u *text); +#endif + /* - * Drag-and-drop handler for files and text. + * General purpose callback for reading data from an input stream, either from + * clipboard or DND. */ - static gboolean -drop_cb(GtkDropTarget *target UNUSED, const GValue *value, - double x, double y, gpointer data UNUSED) + static void +read_data_input_cb( + GInputStream *stream, + GAsyncResult *result, + ReadData *rd) { + ssize_t r = g_input_stream_read_finish(stream, result, NULL); + + if (r == -1) + { + // Error occured + DropReadData *drd = (DropReadData *)rd; + + if (rd->type == READ_DATA_DROP_DATA) + gdk_drop_finish(drd->drop, 0); + } + else if (r == 0) + { + // EOF, don't need to check for READ_DATA_DROP because that uses GType. + if (rd->type == READ_DATA_CLIP) + { + ClipReadData *crd = (ClipReadData *)rd; + long len; + char_u *actual, *final; + int motion_type = MAUTO; + char_u *tofree = NULL; + + len = (long)rd->arr->len; + actual = final = g_byte_array_free(rd->arr, FALSE); + rd->arr = NULL; + + if (clip_convert_data(&final, &len, &motion_type, + STRCMP(crd->mime_type, VIM_MIMETYPE_NAME) == 0, + STRCMP(crd->mime_type, VIMENC_MIMETYPE_NAME) == 0, + &tofree) == OK) + clip_yank_selection(motion_type, final, len, crd->cbd); + g_free(actual); + vim_free(tofree); + } +#ifdef FEAT_DND + else if (rd->type == READ_DATA_DROP_DATA) + { + DropReadData *drd = (DropReadData *)rd; + char_u *str; + + // Append NUL + g_byte_array_append(rd->arr, (const uint8_t *)"", 1); + str = g_byte_array_free(rd->arr, FALSE); + rd->arr = NULL; + drop_read_text(drd->drop, str); + g_free(str); + gdk_drop_finish(drd->drop, gdk_drop_get_actions(drd->drop)); + } +#endif + } + else + { + // Continue reading + g_byte_array_append(rd->arr, (const uint8_t *)rd->buf, r); + g_input_stream_read_async(stream, rd->buf, READDATA_BUFSIZE, + G_PRIORITY_HIGH, + rd->cancel, (GAsyncReadyCallback)read_data_input_cb, rd); + return; + } + + read_data_free(rd); + g_object_unref(stream); +} + +#ifdef FEAT_DND +/* + * Set up for receiving DND items. + */ + void +gui_gtk_set_dnd_targets(void) +{ + GdkContentFormatsBuilder *builder = gdk_content_formats_builder_new(); + + gdk_content_formats_builder_add_gtype(builder, GDK_TYPE_FILE_LIST); + gdk_content_formats_builder_add_gtype(builder, G_TYPE_STRING); + if (clip_html) + gdk_content_formats_builder_add_mime_type(builder, "text/html"); + + gtk_drop_target_async_set_formats(gui.drop_target, + gdk_content_formats_builder_free_to_formats(builder)); +} + +/* + * Handle textual DND data. Note that this does not finish the drop. + */ + static void +drop_read_text(GdkDrop *drop, char_u *text) +{ + GdkModifierType state; + char_u dropkey[6] = { + CSI, + KS_MODIFIER, + 0, + CSI, + KS_EXTRA, + (char_u)KE_DROP + }; + + if (text == NULL || *text == NUL || !query_pointer_pos(NULL, NULL, &state)) + return; + + dnd_yank_drag_data((char_u *)text, (long)STRLEN(text)); + + dropkey[2] = modifiers_gdk2vim(state); + if (dropkey[2] != 0) + add_to_input_buf(dropkey, (int)sizeof(dropkey)); + else + add_to_input_buf(dropkey + 3, (int)(sizeof(dropkey) - 3)); +} + + static void +drop_read_value_cb(GdkDrop *drop, GAsyncResult *result, DropReadData *drd) +{ + const GValue *value = gdk_drop_read_value_finish(drop, result, NULL); + gboolean success = FALSE; + + if (value == NULL) + goto exit; + if (G_VALUE_HOLDS(value, GDK_TYPE_FILE_LIST)) { GSList *files = g_value_get_boxed(value); @@ -2441,11 +2638,11 @@ drop_cb(GtkDropTarget *target UNUSED, const GValue *value, int i; if (nfiles <= 0) - return FALSE; + goto exit; fnames = ALLOC_MULT(char_u *, nfiles); if (fnames == NULL) - return FALSE; + goto exit; i = 0; for (GSList *l = files; l != NULL; l = l->next) @@ -2459,27 +2656,122 @@ drop_cb(GtkDropTarget *target UNUSED, const GValue *value, nfiles = i; if (nfiles > 0) - gui_handle_drop((int)x, (int)y, 0, fnames, nfiles); + gui_handle_drop(drd->x, drd->y, 0, fnames, nfiles); else vim_free(fnames); - - return TRUE; + success = TRUE; } else if (G_VALUE_HOLDS(value, G_TYPE_STRING)) { - const char *text = g_value_get_string(value); - char_u dropkey[6] = {CSI, KS_MODIFIER, 0, - CSI, KS_EXTRA, (char_u)KE_DROP}; + const char *text = g_value_get_string(value); if (text == NULL || *text == NUL) - return FALSE; + goto exit; - dnd_yank_drag_data((char_u *)text, (long)STRLEN(text)); - add_to_input_buf(dropkey + 3, 3); + drop_read_text(drop, (char_u *)text); + success = TRUE; + } - return TRUE; +exit: + gdk_drop_finish(drop, success ? gdk_drop_get_actions(drop) : 0); + read_data_free(&drd->rd); +} + + static void +drop_read_cb(GdkDrop *drop, GAsyncResult *result, DropReadData *drd) +{ + GInputStream *in_stream; + const char *m; + + in_stream = gdk_drop_read_finish(drop, result, &m, NULL); + if (in_stream == NULL || STRCMP(m, "text/html") != 0) + { + read_data_free(&drd->rd); + gdk_drop_finish(drop, 0); + return; } + assert(STRCMP(m, "text/html") == 0); + + // GTK docs says not to use blocking read calls, so do it async. + g_input_stream_read_async(in_stream, drd->rd.buf, READDATA_BUFSIZE, + G_PRIORITY_HIGH, + drd->rd.cancel, (GAsyncReadyCallback)read_data_input_cb, drd); +} + +// GCancellable of current DND operation, else NULL. +static GCancellable *dnd_cancel = NULL; +static guint dnd_timeout_id = 0; + + static gboolean +drop_timeout_cb(void *udata UNUSED) +{ + g_cancellable_cancel(dnd_cancel); + g_clear_object(&dnd_cancel); + dnd_timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +/* + * Drag-and-drop handler for files and text. + */ + static gboolean +drop_cb( + GtkDropTargetAsync *target UNUSED, + GdkDrop *drop, + double x, + double y, + void *udata UNUSED) +{ + GdkContentFormats *formats = gdk_drop_get_formats(drop); + DropReadData *drd = NULL; + + if (gdk_content_formats_contain_gtype(formats, GDK_TYPE_FILE_LIST)) + { + drd = (DropReadData *)read_data_new(READ_DATA_DROP, sizeof(*drd)); + gdk_drop_read_value_async(drop, GDK_TYPE_FILE_LIST, G_PRIORITY_HIGH, + drd->rd.cancel, (GAsyncReadyCallback)drop_read_value_cb, drd); + } + else if (clip_html + && gdk_content_formats_contain_mime_type(formats, "text/html")) + { + static const char *m[] = {"text/html", NULL}; + + drd = (DropReadData *)read_data_new(READ_DATA_DROP_DATA, sizeof(*drd)); + gdk_drop_read_async(drop, m, G_PRIORITY_HIGH, + drd->rd.cancel, (GAsyncReadyCallback)drop_read_cb, drd); + } + else if (gdk_content_formats_contain_gtype(formats, G_TYPE_STRING)) + { + drd = (DropReadData *)read_data_new(READ_DATA_DROP, sizeof(*drd)); + gdk_drop_read_value_async(drop, G_TYPE_STRING, G_PRIORITY_HIGH, + drd->rd.cancel, (GAsyncReadyCallback)drop_read_value_cb, drd); + } + + if (dnd_cancel != NULL) + g_cancellable_cancel(dnd_cancel); + g_clear_object(&dnd_cancel); + g_clear_handle_id(&dnd_timeout_id, timeout_remove); + + if (drd != NULL) + { + drd->drop = g_object_ref(drop); + drd->x = (int)x; + drd->y = (int)y; + dnd_cancel = g_object_ref(drd->rd.cancel); + + // Trying to spin the event loop here causes this GTK assertion error: + // Gtk:ERROR:../gtk/gtk/gtkdrop.c:70:gtk_drop_begin_event: assertion failed: (self->active == FALSE) + // + // This shouldn't be an issue, because Vim DND occurs asynchronously in + // the first place anyways. + + // Add a 3 second timeout + dnd_timeout_id = timeout_add(3000, drop_timeout_cb, NULL); + + return TRUE; + } return FALSE; } #endif @@ -2670,7 +2962,7 @@ gui_mch_set_foreground(void) } static int -query_pointer_pos(int *x, int *y) +query_pointer_pos(int *x, int *y, GdkModifierType *state) { GtkNative *native; GdkSurface *surface; @@ -2698,25 +2990,28 @@ query_pointer_pos(int *x, int *y) if (pointer == NULL) return FALSE; - if (!gdk_surface_get_device_position(surface, pointer, &sx, &sy, NULL)) + if (!gdk_surface_get_device_position(surface, pointer, &sx, &sy, state)) return FALSE; - gtk_native_get_surface_transform(native, &nx, &ny); - src.x = (float)(sx - nx); - src.y = (float)(sy - ny); - if (!gtk_widget_compute_point(GTK_WIDGET(native), gui.drawarea, - &src, &dst)) - return FALSE; + if (x != NULL && y != NULL) + { + gtk_native_get_surface_transform(native, &nx, &ny); + src.x = (float)(sx - nx); + src.y = (float)(sy - ny); + if (!gtk_widget_compute_point(GTK_WIDGET(native), gui.drawarea, + &src, &dst)) + return FALSE; - *x = (int)dst.x; - *y = (int)dst.y; + *x = (int)dst.x; + *y = (int)dst.y; + } return TRUE; } void gui_mch_getmouse(int *x, int *y) { - if (!query_pointer_pos(x, y)) + if (!query_pointer_pos(x, y, NULL)) { *x = 0; *y = 0; @@ -3994,6 +4289,20 @@ get_menu_tool_height(void) return height; } +/* + * Update selection targets for regular and primary selections. + */ + void +gui_gtk_update_selection_formats(Clipboard_T *cbd) +{ + // This will call the ref_formats() vfunc of the content provider, see + // gui_gtk4_cb.c + if (cbd == &clip_plus) + gdk_content_provider_content_changed(gui.regular_provider); + else + gdk_content_provider_content_changed(gui.primary_provider); +} + /* * Get the GdkClipboard and GdkContentProvider for the given Clipboard_T. * clip_star (*) uses PRIMARY, clip_plus (+) uses CLIPBOARD. @@ -4024,67 +4333,27 @@ gtk4_get_clipboard(Clipboard_T *cbd, GdkContentProvider **provider) } } -typedef struct { - Clipboard_T *cbd; - gboolean done; - gboolean abandoned; // requester timed out, callback owns "crd" -} ClipReadData; - /* * Callback for gdk_clipboard_read_async(). */ static void clip_read_cb(GdkClipboard *cb, GAsyncResult *result, ClipReadData *crd) { - Clipboard_T *cbd = crd->cbd; - GError *error = NULL; GInputStream *in_stream; const char *mime_type; - GByteArray *arr; - static char buf[512]; - ssize_t r; - char_u *actual, *final; - long len; - int motion_type = MAUTO; - char_u *tofree = NULL; - - in_stream = gdk_clipboard_read_finish(cb, result, &mime_type, &error); - if (in_stream == NULL) - { - g_error_free(error); - goto exit; - } - - arr = g_byte_array_new(); - while ((r = g_input_stream_read(in_stream, buf, 512, NULL, NULL)) > 0) - g_byte_array_append(arr, (uint8_t *)buf, r); - - if (r == -1) + in_stream = gdk_clipboard_read_finish(cb, result, &mime_type, NULL); + if (in_stream == NULL) { - g_byte_array_free(arr, TRUE); - goto exit; + read_data_free(&crd->rd); + return; } - assert(r == 0); - - len = (long)arr->len; - actual = final = g_byte_array_free(arr, FALSE); - if (!crd->abandoned && clip_convert_data(&final, &len, &motion_type, - STRCMP(mime_type, VIM_MIMETYPE_NAME) == 0, - STRCMP(mime_type, VIMENC_MIMETYPE_NAME) == 0, &tofree) == OK) - clip_yank_selection(motion_type, final, len, cbd); - g_free(actual); - vim_free(tofree); - -exit: - if (in_stream != NULL) - g_object_unref(in_stream); - // free "crd" if the requester gave up, else mark the read complete - if (crd->abandoned) - vim_free(crd); - else - crd->done = TRUE; + // Not sure if we need to copy the string, but do it anyways. + crd->mime_type = g_strdup(mime_type); + g_input_stream_read_async(in_stream, crd->rd.buf, READDATA_BUFSIZE, + G_PRIORITY_HIGH, + crd->rd.cancel, (GAsyncReadyCallback)read_data_input_cb, crd); } /* @@ -4093,45 +4362,45 @@ exit: void clip_mch_request_selection(Clipboard_T *cbd) { - static const char *mimes_no_html[] = { + static const char *mimes_no_html[] = { VIMENC_MIMETYPE_NAME, VIM_MIMETYPE_NAME, "text/plain;charset=utf-8", "text/plain", NULL }; - GdkClipboard *clipboard; - ClipReadData *crd; - time_t start; + + GdkClipboard *clipboard; + ClipReadData *crd; + GCancellable *cancel; clipboard = gtk4_get_clipboard(cbd, NULL); if (clipboard == NULL) return; - // Heap-allocate: on a timeout this returns before the read completes, - // so "crd" must outlive this stack frame. - crd = ALLOC_ONE(ClipReadData); - if (crd == NULL) - return; + crd = (ClipReadData *)read_data_new(READ_DATA_CLIP, sizeof(*crd)); crd->cbd = cbd; - crd->done = FALSE; - crd->abandoned = FALSE; + cancel = g_object_ref(crd->rd.cancel); gdk_clipboard_read_async( clipboard, clip_html ? supported_mimes : mimes_no_html, - G_PRIORITY_HIGH, NULL, (GAsyncReadyCallback)clip_read_cb, crd); + G_PRIORITY_HIGH, crd->rd.cancel, + (GAsyncReadyCallback)clip_read_cb, crd); - // Spin until the async callback fires, with a 3-second wall-clock - // timeout as a safety net. - start = time(NULL); - while (!crd->done && time(NULL) < start + 3) - g_main_context_iteration(NULL, TRUE); + { + time_t start; - if (crd->done) - vim_free(crd); - else - // timed out: hand ownership to the callback, which frees "crd" - crd->abandoned = TRUE; + // Spin until the done receiving data, with a 3-second wall-clock + // timeout as a safety net. Use the GCancellable as a way to signal if + // done (and also use it to cancel if timed out as well). + start = time(NULL); + while (!g_cancellable_is_cancelled(cancel) + && time(NULL) < start + 3) + g_main_context_iteration(NULL, TRUE); + + g_cancellable_cancel(cancel); + g_object_unref(cancel); + } } static int in_clipboard_set = FALSE; @@ -4468,7 +4737,6 @@ gui_mch_add_menu_item(vimmenu_T *menu, int idx) { if (menu_is_separator(menu->name)) { - // TODO menu->id = vim_toolbar_insert_separator(VIM_TOOLBAR(gui.toolbar), idx); } diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro index a40b102260..fddaec21a4 100644 --- a/src/proto/gui_gtk4.pro +++ b/src/proto/gui_gtk4.pro @@ -53,6 +53,7 @@ void gui_mch_draw_part_cursor(int w, int h, guicolor_T color); void gui_mch_flash(int msec); void gui_mch_invert_rectangle(int r, int c, int nr, int nc); void gui_gtk4_resize(int width, int height); +void gui_gtk_set_dnd_targets(void); void gui_mch_update(void); int gui_mch_wait_for_chars(long wtime); void gui_mch_flush(void); @@ -83,6 +84,7 @@ void gui_gtk_set_mnemonics(int enable); void gui_make_popup(char_u *path_name, int mouse_pos); int get_menu_tool_width(void); int get_menu_tool_height(void); +void gui_gtk_update_selection_formats(Clipboard_T *cbd); void clip_mch_request_selection(Clipboard_T *cbd); void clip_mch_set_selection(Clipboard_T *cbd); int clip_mch_own_selection(Clipboard_T *cbd); diff --git a/src/version.c b/src/version.c index d4c08b3eb9..48ff1025c7 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 752, /**/ 751, /**/