From efb4eeab50b5e3fb456b0bf882063e8d90f6ec31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:12:18 +0000 Subject: [PATCH] move DnD testing infrastructure from glfw.c to dnd.c, add init_dnd() Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/78192f4b-c0b7-4c34-9073-4ea717b531da Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com> --- kitty/data-types.c | 2 + kitty/dnd.c | 224 ++++++++++++++++++++++++++++++++++++++++++++- kitty/dnd.h | 2 - kitty/glfw.c | 212 ------------------------------------------ 4 files changed, 225 insertions(+), 215 deletions(-) diff --git a/kitty/data-types.c b/kitty/data-types.c index c38810eb0..a31fe667b 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -759,6 +759,7 @@ extern bool init_crypto_library(PyObject*); extern bool init_desktop(PyObject*); extern bool init_fonts(PyObject*); extern bool init_glfw(PyObject *m); +extern bool init_dnd(PyObject *m); extern bool init_child(PyObject *m); extern bool init_state(PyObject *module); extern bool init_keys(PyObject *module); @@ -812,6 +813,7 @@ PyInit_fast_data_types(void) { if (!init_ColorProfile(m)) return NULL; if (!init_Screen(m)) return NULL; if (!init_glfw(m)) return NULL; + if (!init_dnd(m)) return NULL; if (!init_child(m)) return NULL; if (!init_state(m)) return NULL; if (!init_keys(m)) return NULL; diff --git a/kitty/dnd.c b/kitty/dnd.c index 74a13970e..660595488 100644 --- a/kitty/dnd.c +++ b/kitty/dnd.c @@ -166,7 +166,7 @@ sanitized_filename_from_url(const char *url) { } -void +static void dnd_set_test_write_func(PyObject *func, size_t mime_list_size_cap, size_t present_data_cap, size_t remote_drag_limit) { (void)machine_id; Py_CLEAR(g_dnd_test_write_func); @@ -1911,3 +1911,225 @@ drag_remote_file_data( #undef abrt #undef ds // }}} + +// DnD testing infrastructure {{{ + +static PyObject * +py_dnd_set_test_write_func(PyObject *self UNUSED, PyObject *args) { + PyObject *func = Py_None; unsigned mime_list_size_cap = 0, present_data_cap = 0, remote_drag_limit = 0; + if (!PyArg_ParseTuple(args, "|OIII", &func, &mime_list_size_cap, &present_data_cap, &remote_drag_limit)) return NULL; + // Pass None to clear the interceptor and restore normal operation. + dnd_set_test_write_func(func == Py_None ? NULL : func, mime_list_size_cap, present_data_cap, remote_drag_limit); + Py_RETURN_NONE; +} + +static void +destroy_fake_window_contents(Window *w) { + // Free window resources without touching GPU objects (none allocated for fake windows). + drop_free_data(w); + drag_free_offer(w); + free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks); + free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys); + Py_CLEAR(w->render_data.screen); + Py_CLEAR(w->title); + Py_CLEAR(w->title_bar_data.last_drawn_title_object_id); + free(w->title_bar_data.buf); w->title_bar_data.buf = NULL; + Py_CLEAR(w->url_target_bar_data.last_drawn_title_object_id); + free(w->url_target_bar_data.buf); w->url_target_bar_data.buf = NULL; + // render_data.vao_idx is -1 so release_gpu_resources_for_window is safe, but we skip it + // since we never allocated those resources. +} + +static PyObject * +dnd_test_create_fake_window(PyObject *self UNUSED, PyObject *args UNUSED) { + // Create a minimal OS window + tab + window without any OpenGL/GPU resources. + // Returns (os_window_id, window_id). + ensure_space_for(&global_state, os_windows, OSWindow, global_state.num_os_windows + 1, capacity, 1, true); + OSWindow *osw = global_state.os_windows + global_state.num_os_windows++; + zero_at_ptr(osw); + osw->id = ++global_state.os_window_id_counter; + osw->tab_bar_render_data.vao_idx = -1; + osw->background_opacity.alpha = OPT(background_opacity); + osw->created_at = monotonic(); + // osw->handle intentionally left NULL - no real GLFW window + + ensure_space_for(osw, tabs, Tab, 1, capacity, 1, true); + Tab *tab = &osw->tabs[0]; + zero_at_ptr(tab); + tab->id = ++global_state.tab_id_counter; + tab->border_rects.vao_idx = -1; + osw->num_tabs = 1; + osw->active_tab = 0; + + ensure_space_for(tab, windows, Window, 1, capacity, 1, true); + Window *w = &tab->windows[0]; + zero_at_ptr(w); + w->id = ++global_state.window_id_counter; + w->visible = true; + w->render_data.vao_idx = -1; + w->window_title_render_data.vao_idx = -1; + w->drop.wanted = true; + tab->num_windows = 1; + tab->active_window = 0; + + global_state.mouse_hover_in_window = w->id; + return Py_BuildValue("KK", (unsigned long long)osw->id, (unsigned long long)w->id); +} + +static PyObject * +dnd_test_cleanup_fake_window(PyObject *self UNUSED, PyObject *args) { + unsigned long long os_window_id; + if (!PyArg_ParseTuple(args, "K", &os_window_id)) return NULL; + for (size_t i = 0; i < global_state.num_os_windows; i++) { + if (global_state.os_windows[i].id == (id_type)os_window_id) { + OSWindow *osw = global_state.os_windows + i; + for (size_t t = 0; t < osw->num_tabs; t++) { + Tab *tab = osw->tabs + t; + for (size_t j = 0; j < tab->num_windows; j++) { + Window *win = tab->windows + j; + if (global_state.mouse_hover_in_window == win->id) + global_state.mouse_hover_in_window = 0; + destroy_fake_window_contents(win); + } + free(tab->border_rects.rect_buf); tab->border_rects.rect_buf = NULL; + free(tab->windows); tab->windows = NULL; + } + Py_CLEAR(osw->window_title); + Py_CLEAR(osw->tab_bar_render_data.screen); + free(osw->tabs); osw->tabs = NULL; + remove_i_from_array(global_state.os_windows, i, global_state.num_os_windows); + break; + } + } + Py_RETURN_NONE; +} + +static PyObject * +dnd_test_set_mouse_pos(PyObject *self UNUSED, PyObject *args) { + unsigned long long window_id; + int cell_x, cell_y, pixel_x, pixel_y; + if (!PyArg_ParseTuple(args, "Kiiii", &window_id, &cell_x, &cell_y, &pixel_x, &pixel_y)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + w->mouse_pos.cell_x = (unsigned int)cell_x; + w->mouse_pos.cell_y = (unsigned int)cell_y; + w->mouse_pos.global_x = pixel_x; + w->mouse_pos.global_y = pixel_y; + Py_RETURN_NONE; +} + +static PyObject * +dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) { + // Simulate a drop enter/move/drop event. mimes_seq must be a sequence of str, or + // None to simulate a leave event. + unsigned long long window_id; + int is_drop; + PyObject *mimes_seq; + if (!PyArg_ParseTuple(args, "KpO", &window_id, &is_drop, &mimes_seq)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + if (mimes_seq == Py_None) { + drop_left_child(w); + Py_RETURN_NONE; + } + RAII_PyObject(fast_seq, PySequence_Fast(mimes_seq, "mimes must be a sequence")); + if (!fast_seq) return NULL; + Py_ssize_t num_mimes = PySequence_Fast_GET_SIZE(fast_seq); + RAII_ALLOC(const char*, mimes, malloc(sizeof(const char*) * (num_mimes ? num_mimes : 1))); + if (!mimes) return PyErr_NoMemory(); + for (Py_ssize_t i = 0; i < num_mimes; i++) { + mimes[i] = PyUnicode_AsUTF8(PySequence_Fast_GET_ITEM(fast_seq, i)); + if (!mimes[i]) return NULL; + } + drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false); + Py_RETURN_NONE; +} + +static PyObject * +dnd_test_fake_drop_data(PyObject *self UNUSED, PyObject *args) { + // Simulate OS delivering drop data for the given MIME type. + // If error_code > 0, simulate an error (e.g. ENOENT=2, EIO=5, EPERM=1). + // Otherwise deliver data and the mandatory end-of-data signal. + unsigned long long window_id; + const char *mime; + RAII_PY_BUFFER(data); + int error_code = 0; + if (!PyArg_ParseTuple(args, "Ksy*|i", &window_id, &mime, &data, &error_code)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + if (error_code > 0) { + drop_dispatch_data(w, mime, NULL, -(ssize_t)error_code); + } else if (data.len > 0) { + drop_dispatch_data(w, mime, (const char*)data.buf, (ssize_t)data.len); + drop_dispatch_data(w, mime, NULL, 0); // mandatory end-of-data signal + } else { + // Empty data: just the end-of-data signal (sz=0 is the sentinel for "no more data"). + drop_dispatch_data(w, mime, NULL, 0); + } + Py_RETURN_NONE; +} + +static PyObject * +dnd_test_force_drag_dropped(PyObject *self UNUSED, PyObject *args) { + // Force the drag source state to DROPPED for testing purposes. + // This simulates what would happen after start_window_drag() succeeds + // and the drop target receives the data. + unsigned long long window_id; + if (!PyArg_ParseTuple(args, "K", &window_id)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + if (w->drag_source.state != DRAG_SOURCE_BEING_BUILT) { + PyErr_SetString(PyExc_ValueError, "Drag source state is not BEING_BUILT"); + return NULL; + } + // Simulate what drag_start does on success, without calling start_window_drag + for (size_t i = 0; i < w->drag_source.num_mimes; i++) { + free(w->drag_source.items[i].optional_data); + w->drag_source.items[i].optional_data = NULL; + w->drag_source.items[i].data_size = 0; + w->drag_source.items[i].data_capacity = 0; + w->drag_source.items[i].data_decode_initialized = false; + } + for (size_t i = 0; i < arraysz(w->drag_source.images); i++) { + if (w->drag_source.images[i].data) free(w->drag_source.images[i].data); + zero_at_ptr(w->drag_source.images + i); + } + w->drag_source.state = DRAG_SOURCE_DROPPED; + Py_RETURN_NONE; +} + +static PyObject * +dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) { + // Simulate what drag_get_data does initially: find the MIME item at the + // given index, set requested_remote_files if appropriate, and return the + // escape code that would be sent to the client. + unsigned long long window_id; + unsigned idx; + if (!PyArg_ParseTuple(args, "KI", &window_id, &idx)) return NULL; + Window *w = window_for_window_id((id_type)window_id); + if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } + if (w->drag_source.state < DRAG_SOURCE_DROPPED || idx >= w->drag_source.num_mimes || !w->drag_source.items) { + PyErr_SetString(PyExc_ValueError, "Invalid state or index"); return NULL; + } + w->drag_source.items[idx].requested_remote_files = w->drag_source.is_remote_client && w->drag_source.items[idx].is_uri_list; + Py_RETURN_NONE; +} + +static PyMethodDef dnd_methods[] = { + {"dnd_set_test_write_func", (PyCFunction)py_dnd_set_test_write_func, METH_VARARGS, ""}, + METHODB(dnd_test_create_fake_window, METH_NOARGS), + METHODB(dnd_test_cleanup_fake_window, METH_VARARGS), + METHODB(dnd_test_set_mouse_pos, METH_VARARGS), + METHODB(dnd_test_fake_drop_event, METH_VARARGS), + METHODB(dnd_test_fake_drop_data, METH_VARARGS), + METHODB(dnd_test_force_drag_dropped, METH_VARARGS), + METHODB(dnd_test_request_drag_data, METH_VARARGS), + {NULL, NULL, 0, NULL} +}; + +bool +init_dnd(PyObject *m) { + if (PyModule_AddFunctions(m, dnd_methods) != 0) return false; + return true; +} +// }}} diff --git a/kitty/dnd.h b/kitty/dnd.h index 50c5db252..27a7012e2 100644 --- a/kitty/dnd.h +++ b/kitty/dnd.h @@ -21,10 +21,8 @@ void drop_set_status(Window *w, int operation, const char *payload, size_t paylo size_t drop_update_mimes(Window *w, const char **allowed_mimes, size_t allowed_mimes_count); void drop_dispatch_data(Window *w, const char *mime_type, const char *data, ssize_t sz); void drop_finish(Window *w); -void dnd_set_test_write_func(PyObject*, size_t, size_t, size_t); bool dnd_is_test_mode(void); - typedef enum { DRAG_NOTIFY_ACCEPTED, DRAG_NOTIFY_ACTION_CHANGED, DRAG_NOTIFY_DROPPED, DRAG_NOTIFY_FINISHED } DragNotifyType; void drag_free_offer(Window *w); void drag_add_mimes(Window *w, int allowed_operations, uint32_t client_id, const char *data, size_t sz, bool has_more); diff --git a/kitty/glfw.c b/kitty/glfw.c index e94bd9db2..6584c3c01 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -895,210 +895,6 @@ request_drop_status_update(OSWindow *osw) { } } -// DnD testing infrastructure {{{ - -static PyObject * -py_dnd_set_test_write_func(PyObject *self UNUSED, PyObject *args) { - PyObject *func = Py_None; unsigned mime_list_size_cap = 0, present_data_cap = 0, remote_drag_limit = 0; - if (!PyArg_ParseTuple(args, "|OIII", &func, &mime_list_size_cap, &present_data_cap, &remote_drag_limit)) return NULL; - // Pass None to clear the interceptor and restore normal operation. - dnd_set_test_write_func(func == Py_None ? NULL : func, mime_list_size_cap, present_data_cap, remote_drag_limit); - Py_RETURN_NONE; -} - -static void -destroy_fake_window_contents(Window *w) { - // Free window resources without touching GPU objects (none allocated for fake windows). - drop_free_data(w); - drag_free_offer(w); - free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks); - free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys); - Py_CLEAR(w->render_data.screen); - Py_CLEAR(w->title); - Py_CLEAR(w->title_bar_data.last_drawn_title_object_id); - free(w->title_bar_data.buf); w->title_bar_data.buf = NULL; - Py_CLEAR(w->url_target_bar_data.last_drawn_title_object_id); - free(w->url_target_bar_data.buf); w->url_target_bar_data.buf = NULL; - // render_data.vao_idx is -1 so release_gpu_resources_for_window is safe, but we skip it - // since we never allocated those resources. -} - -static PyObject * -dnd_test_create_fake_window(PyObject *self UNUSED, PyObject *args UNUSED) { - // Create a minimal OS window + tab + window without any OpenGL/GPU resources. - // Returns (os_window_id, window_id). - ensure_space_for(&global_state, os_windows, OSWindow, global_state.num_os_windows + 1, capacity, 1, true); - OSWindow *osw = global_state.os_windows + global_state.num_os_windows++; - zero_at_ptr(osw); - osw->id = ++global_state.os_window_id_counter; - osw->tab_bar_render_data.vao_idx = -1; - osw->background_opacity.alpha = OPT(background_opacity); - osw->created_at = monotonic(); - // osw->handle intentionally left NULL - no real GLFW window - - ensure_space_for(osw, tabs, Tab, 1, capacity, 1, true); - Tab *tab = &osw->tabs[0]; - zero_at_ptr(tab); - tab->id = ++global_state.tab_id_counter; - tab->border_rects.vao_idx = -1; - osw->num_tabs = 1; - osw->active_tab = 0; - - ensure_space_for(tab, windows, Window, 1, capacity, 1, true); - Window *w = &tab->windows[0]; - zero_at_ptr(w); - w->id = ++global_state.window_id_counter; - w->visible = true; - w->render_data.vao_idx = -1; - w->window_title_render_data.vao_idx = -1; - w->drop.wanted = true; - tab->num_windows = 1; - tab->active_window = 0; - - global_state.mouse_hover_in_window = w->id; - return Py_BuildValue("KK", (unsigned long long)osw->id, (unsigned long long)w->id); -} - -static PyObject * -dnd_test_cleanup_fake_window(PyObject *self UNUSED, PyObject *args) { - unsigned long long os_window_id; - if (!PyArg_ParseTuple(args, "K", &os_window_id)) return NULL; - for (size_t i = 0; i < global_state.num_os_windows; i++) { - if (global_state.os_windows[i].id == (id_type)os_window_id) { - OSWindow *osw = global_state.os_windows + i; - for (size_t t = 0; t < osw->num_tabs; t++) { - Tab *tab = osw->tabs + t; - for (size_t j = 0; j < tab->num_windows; j++) { - Window *win = tab->windows + j; - if (global_state.mouse_hover_in_window == win->id) - global_state.mouse_hover_in_window = 0; - destroy_fake_window_contents(win); - } - free(tab->border_rects.rect_buf); tab->border_rects.rect_buf = NULL; - free(tab->windows); tab->windows = NULL; - } - Py_CLEAR(osw->window_title); - Py_CLEAR(osw->tab_bar_render_data.screen); - free(osw->tabs); osw->tabs = NULL; - remove_i_from_array(global_state.os_windows, i, global_state.num_os_windows); - break; - } - } - Py_RETURN_NONE; -} - -static PyObject * -dnd_test_set_mouse_pos(PyObject *self UNUSED, PyObject *args) { - unsigned long long window_id; - int cell_x, cell_y, pixel_x, pixel_y; - if (!PyArg_ParseTuple(args, "Kiiii", &window_id, &cell_x, &cell_y, &pixel_x, &pixel_y)) return NULL; - Window *w = window_for_window_id((id_type)window_id); - if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } - w->mouse_pos.cell_x = (unsigned int)cell_x; - w->mouse_pos.cell_y = (unsigned int)cell_y; - w->mouse_pos.global_x = pixel_x; - w->mouse_pos.global_y = pixel_y; - Py_RETURN_NONE; -} - -static PyObject * -dnd_test_fake_drop_event(PyObject *self UNUSED, PyObject *args) { - // Simulate a drop enter/move/drop event. mimes_seq must be a sequence of str, or - // None to simulate a leave event. - unsigned long long window_id; - int is_drop; - PyObject *mimes_seq; - if (!PyArg_ParseTuple(args, "KpO", &window_id, &is_drop, &mimes_seq)) return NULL; - Window *w = window_for_window_id((id_type)window_id); - if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } - if (mimes_seq == Py_None) { - drop_left_child(w); - Py_RETURN_NONE; - } - RAII_PyObject(fast_seq, PySequence_Fast(mimes_seq, "mimes must be a sequence")); - if (!fast_seq) return NULL; - Py_ssize_t num_mimes = PySequence_Fast_GET_SIZE(fast_seq); - RAII_ALLOC(const char*, mimes, malloc(sizeof(const char*) * (num_mimes ? num_mimes : 1))); - if (!mimes) return PyErr_NoMemory(); - for (Py_ssize_t i = 0; i < num_mimes; i++) { - mimes[i] = PyUnicode_AsUTF8(PySequence_Fast_GET_ITEM(fast_seq, i)); - if (!mimes[i]) return NULL; - } - drop_move_on_child(w, mimes, (size_t)num_mimes, is_drop ? true : false); - Py_RETURN_NONE; -} - -static PyObject * -dnd_test_fake_drop_data(PyObject *self UNUSED, PyObject *args) { - // Simulate OS delivering drop data for the given MIME type. - // If error_code > 0, simulate an error (e.g. ENOENT=2, EIO=5, EPERM=1). - // Otherwise deliver data and the mandatory end-of-data signal. - unsigned long long window_id; - const char *mime; - RAII_PY_BUFFER(data); - int error_code = 0; - if (!PyArg_ParseTuple(args, "Ksy*|i", &window_id, &mime, &data, &error_code)) return NULL; - Window *w = window_for_window_id((id_type)window_id); - if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } - if (error_code > 0) { - drop_dispatch_data(w, mime, NULL, -(ssize_t)error_code); - } else if (data.len > 0) { - drop_dispatch_data(w, mime, (const char*)data.buf, (ssize_t)data.len); - drop_dispatch_data(w, mime, NULL, 0); // mandatory end-of-data signal - } else { - // Empty data: just the end-of-data signal (sz=0 is the sentinel for "no more data"). - drop_dispatch_data(w, mime, NULL, 0); - } - Py_RETURN_NONE; -} - -static PyObject * -dnd_test_force_drag_dropped(PyObject *self UNUSED, PyObject *args) { - // Force the drag source state to DROPPED for testing purposes. - // This simulates what would happen after start_window_drag() succeeds - // and the drop target receives the data. - unsigned long long window_id; - if (!PyArg_ParseTuple(args, "K", &window_id)) return NULL; - Window *w = window_for_window_id((id_type)window_id); - if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } - if (w->drag_source.state != DRAG_SOURCE_BEING_BUILT) { - PyErr_SetString(PyExc_ValueError, "Drag source state is not BEING_BUILT"); - return NULL; - } - // Simulate what drag_start does on success, without calling start_window_drag - for (size_t i = 0; i < w->drag_source.num_mimes; i++) { - free(w->drag_source.items[i].optional_data); - w->drag_source.items[i].optional_data = NULL; - w->drag_source.items[i].data_size = 0; - w->drag_source.items[i].data_capacity = 0; - w->drag_source.items[i].data_decode_initialized = false; - } - for (size_t i = 0; i < arraysz(w->drag_source.images); i++) { - if (w->drag_source.images[i].data) free(w->drag_source.images[i].data); - zero_at_ptr(w->drag_source.images + i); - } - w->drag_source.state = DRAG_SOURCE_DROPPED; - Py_RETURN_NONE; -} - -static PyObject * -dnd_test_request_drag_data(PyObject *self UNUSED, PyObject *args) { - // Simulate what drag_get_data does initially: find the MIME item at the - // given index, set requested_remote_files if appropriate, and return the - // escape code that would be sent to the client. - unsigned long long window_id; - unsigned idx; - if (!PyArg_ParseTuple(args, "KI", &window_id, &idx)) return NULL; - Window *w = window_for_window_id((id_type)window_id); - if (!w) { PyErr_SetString(PyExc_ValueError, "Window not found"); return NULL; } - if (w->drag_source.state < DRAG_SOURCE_DROPPED || idx >= w->drag_source.num_mimes || !w->drag_source.items) { - PyErr_SetString(PyExc_ValueError, "Invalid state or index"); return NULL; - } - w->drag_source.items[idx].requested_remote_files = w->drag_source.is_remote_client && w->drag_source.items[idx].is_uri_list; - Py_RETURN_NONE; -} -// }}} - static void application_close_requested_callback(int flags) { if (flags) { @@ -3403,14 +3199,6 @@ static PyMethodDef module_methods[] = { {"glfw_get_monitor_workarea", (PyCFunction)get_monitor_workarea, METH_NOARGS, ""}, {"glfw_get_monitor_names", (PyCFunction)get_monitor_names, METH_NOARGS, ""}, {"glfw_primary_monitor_content_scale", (PyCFunction)primary_monitor_content_scale, METH_NOARGS, ""}, - {"dnd_set_test_write_func", (PyCFunction)py_dnd_set_test_write_func, METH_VARARGS, ""}, - METHODB(dnd_test_create_fake_window, METH_NOARGS), - METHODB(dnd_test_cleanup_fake_window, METH_VARARGS), - METHODB(dnd_test_set_mouse_pos, METH_VARARGS), - METHODB(dnd_test_fake_drop_event, METH_VARARGS), - METHODB(dnd_test_fake_drop_data, METH_VARARGS), - METHODB(dnd_test_force_drag_dropped, METH_VARARGS), - METHODB(dnd_test_request_drag_data, METH_VARARGS), {NULL, NULL, 0, NULL} /* Sentinel */ };