API to start a drag
This commit is contained in:
parent
5ea35cbbfc
commit
07a9f2bcaa
11 changed files with 158 additions and 54 deletions
2
glfw/glfw3.h
vendored
2
glfw/glfw3.h
vendored
|
|
@ -2068,7 +2068,7 @@ typedef struct GLFWimage
|
|||
int height;
|
||||
/*! The pixel data of this image, arranged left-to-right, top-to-bottom.
|
||||
*/
|
||||
unsigned char* pixels;
|
||||
const unsigned char* pixels;
|
||||
} GLFWimage;
|
||||
|
||||
/*! @brief Gamepad input state
|
||||
|
|
|
|||
4
glfw/wl_init.c
vendored
4
glfw/wl_init.c
vendored
|
|
@ -608,9 +608,7 @@ static void registryHandleGlobal(void* data UNUSED,
|
|||
else if (is(wl_data_device_manager))
|
||||
{
|
||||
_glfw.wl.dataDeviceManager =
|
||||
wl_registry_bind(registry, name,
|
||||
&wl_data_device_manager_interface,
|
||||
1);
|
||||
wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3);
|
||||
if (_glfw.wl.seat && _glfw.wl.dataDeviceManager && !_glfw.wl.dataDevice) {
|
||||
_glfwSetupWaylandDataDevice();
|
||||
}
|
||||
|
|
|
|||
4
glfw/wl_platform.h
vendored
4
glfw/wl_platform.h
vendored
|
|
@ -414,8 +414,10 @@ typedef struct _GLFWlibraryWayland
|
|||
struct wl_data_source* source;
|
||||
char** mimes; // Array of MIME type strings
|
||||
int mime_count; // Number of MIME types
|
||||
_GLFWwindow* window; // Window that initiated the drag
|
||||
GLFWid window_id; // Window that initiated the drag
|
||||
GLFWDragSourceData* current_request; // Current data request being processed
|
||||
struct wl_surface *drag_icon;
|
||||
struct wp_viewport *drag_viewport;
|
||||
} drag;
|
||||
} _GLFWlibraryWayland;
|
||||
|
||||
|
|
|
|||
86
glfw/wl_window.c
vendored
86
glfw/wl_window.c
vendored
|
|
@ -3006,7 +3006,8 @@ GLFWAPI bool glfwWaylandBeep(GLFWwindow *handle) {
|
|||
|
||||
// Drag operation implementation
|
||||
|
||||
static void cleanup_drag_source_data(GLFWDragSourceData* data) {
|
||||
static void
|
||||
cleanup_drag_source_data(GLFWDragSourceData* data) {
|
||||
if (!data) return;
|
||||
if (data->write_fd >= 0) {
|
||||
close(data->write_fd);
|
||||
|
|
@ -3016,11 +3017,11 @@ static void cleanup_drag_source_data(GLFWDragSourceData* data) {
|
|||
free(data);
|
||||
}
|
||||
|
||||
static void cleanup_drag(void) {
|
||||
static void
|
||||
cleanup_drag(struct wl_data_source *source) {
|
||||
// Notify the application that the drag source is closed
|
||||
if (_glfw.wl.drag.window && _glfw.wl.drag.window->callbacks.dragSource) {
|
||||
_glfwInputDragSourceRequest(_glfw.wl.drag.window, NULL, NULL);
|
||||
}
|
||||
_GLFWwindow *window = _glfwWindowForId(_glfw.wl.drag.window_id);
|
||||
if (window && window->callbacks.dragSource) _glfwInputDragSourceRequest(window, NULL, NULL);
|
||||
|
||||
// Clean up any pending data request
|
||||
if (_glfw.wl.drag.current_request) {
|
||||
|
|
@ -3029,18 +3030,23 @@ static void cleanup_drag(void) {
|
|||
}
|
||||
|
||||
// Clean up MIME type strings
|
||||
for (int i = 0; i < _glfw.wl.drag.mime_count; i++) {
|
||||
free(_glfw.wl.drag.mimes[i]);
|
||||
}
|
||||
for (int i = 0; i < _glfw.wl.drag.mime_count; i++) free(_glfw.wl.drag.mimes[i]);
|
||||
free(_glfw.wl.drag.mimes);
|
||||
_glfw.wl.drag.mimes = NULL;
|
||||
_glfw.wl.drag.mime_count = 0;
|
||||
_glfw.wl.drag.window = NULL;
|
||||
_glfw.wl.drag.window_id = 0;
|
||||
if (_glfw.wl.drag.drag_viewport) wp_viewport_destroy(_glfw.wl.drag.drag_viewport);
|
||||
if (_glfw.wl.drag.drag_icon) wl_surface_destroy(_glfw.wl.drag.drag_icon);
|
||||
_glfw.wl.drag.drag_icon = NULL; _glfw.wl.drag.drag_viewport = NULL;
|
||||
if (_glfw.wl.drag.source && _glfw.wl.drag.source != source) wl_data_source_destroy(_glfw.wl.drag.source);
|
||||
_glfw.wl.drag.source = NULL;
|
||||
if (source) wl_data_source_destroy(source);
|
||||
}
|
||||
|
||||
static void
|
||||
drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const char *mime_type, int fd) {
|
||||
if (!_glfw.wl.drag.window) {
|
||||
_GLFWwindow *window = _glfwWindowForId(_glfw.wl.drag.window_id);
|
||||
if (!window) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
|
@ -3052,7 +3058,7 @@ drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const
|
|||
return;
|
||||
}
|
||||
|
||||
request->window_id = _glfw.wl.drag.window ? _glfw.wl.drag.window->id : 0;
|
||||
request->window_id = _glfw.wl.drag.window_id;
|
||||
request->mime_type = _glfw_strdup(mime_type);
|
||||
request->write_fd = fd;
|
||||
request->finished = false;
|
||||
|
|
@ -3064,25 +3070,16 @@ drag_source_send(void *data UNUSED, struct wl_data_source *source UNUSED, const
|
|||
}
|
||||
|
||||
// Store as current request
|
||||
if (_glfw.wl.drag.current_request) {
|
||||
cleanup_drag_source_data(_glfw.wl.drag.current_request);
|
||||
}
|
||||
if (_glfw.wl.drag.current_request) cleanup_drag_source_data(_glfw.wl.drag.current_request);
|
||||
_glfw.wl.drag.current_request = request;
|
||||
|
||||
// Notify the application via callback
|
||||
_glfwInputDragSourceRequest(_glfw.wl.drag.window, mime_type, request);
|
||||
_glfwInputDragSourceRequest(window, mime_type, request);
|
||||
}
|
||||
|
||||
static void
|
||||
drag_source_cancelled(void *data UNUSED, struct wl_data_source *source) {
|
||||
// Clean up drag data
|
||||
if (_glfw.wl.drag.source == source) {
|
||||
cleanup_drag();
|
||||
_glfw.wl.drag.source = NULL;
|
||||
}
|
||||
if (source) {
|
||||
wl_data_source_destroy(source);
|
||||
}
|
||||
cleanup_drag(source);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -3113,10 +3110,7 @@ static const struct wl_data_source_listener drag_source_listener = {
|
|||
|
||||
void
|
||||
_glfwPlatformCancelDrag(_GLFWwindow* window UNUSED) {
|
||||
if (_glfw.wl.drag.source) {
|
||||
drag_source_cancelled(NULL, _glfw.wl.drag.source);
|
||||
_glfw.wl.drag.source = NULL;
|
||||
}
|
||||
cleanup_drag(_glfw.wl.drag.source);
|
||||
}
|
||||
|
||||
int
|
||||
|
|
@ -3154,7 +3148,7 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const char* const* mime_types, int m
|
|||
// Allocate storage for MIME types
|
||||
_glfw.wl.drag.mimes = calloc(mime_count, sizeof(char*));
|
||||
_glfw.wl.drag.mime_count = mime_count;
|
||||
_glfw.wl.drag.window = window;
|
||||
_glfw.wl.drag.window_id = window->id;
|
||||
|
||||
if (!_glfw.wl.drag.mimes) {
|
||||
_glfwPlatformCancelDrag(window);
|
||||
|
|
@ -3176,33 +3170,33 @@ _glfwPlatformStartDrag(_GLFWwindow* window, const char* const* mime_types, int m
|
|||
wl_data_source_add_listener(_glfw.wl.drag.source, &drag_source_listener, NULL);
|
||||
|
||||
// Set up the drag icon surface if thumbnail is provided
|
||||
struct wl_surface* icon_surface = NULL;
|
||||
struct wl_buffer* icon_buffer = NULL;
|
||||
|
||||
if (thumbnail && thumbnail->pixels) {
|
||||
icon_surface = wl_compositor_create_surface(_glfw.wl.compositor);
|
||||
if (icon_surface) {
|
||||
struct wl_buffer* icon_buffer = NULL;
|
||||
_glfw.wl.drag.drag_icon = wl_compositor_create_surface(_glfw.wl.compositor);
|
||||
if (_glfw.wl.drag.drag_icon) {
|
||||
icon_buffer = createShmBuffer(thumbnail, false, true);
|
||||
if (icon_buffer) {
|
||||
wl_surface_attach(icon_surface, icon_buffer, 0, 0);
|
||||
wl_surface_commit(icon_surface);
|
||||
if (_glfw.wl.wp_viewporter) {
|
||||
double f_scale = _glfwWaylandWindowScale(window);
|
||||
int logical_width = (int)(thumbnail->width / f_scale);
|
||||
int logical_height = (int)(thumbnail->height / f_scale);
|
||||
_glfw.wl.drag.drag_viewport = wp_viewporter_get_viewport(
|
||||
_glfw.wl.wp_viewporter, _glfw.wl.drag.drag_icon);
|
||||
wp_viewport_set_destination(_glfw.wl.drag.drag_viewport, logical_width, logical_height);
|
||||
} else {
|
||||
int scale = _glfwWaylandIntegerWindowScale(window);
|
||||
wl_surface_set_buffer_scale(_glfw.wl.drag.drag_icon, scale);
|
||||
}
|
||||
wl_surface_attach(_glfw.wl.drag.drag_icon, icon_buffer, 0, 0);
|
||||
wl_surface_commit(_glfw.wl.drag.drag_icon);
|
||||
wl_buffer_destroy(icon_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the drag operation
|
||||
wl_data_device_start_drag(_glfw.wl.dataDevice, _glfw.wl.drag.source,
|
||||
window->wl.surface, icon_surface,
|
||||
wl_data_device_start_drag(_glfw.wl.dataDevice, _glfw.wl.drag.source, window->wl.surface, _glfw.wl.drag.drag_icon,
|
||||
_glfw.wl.pointer_serial);
|
||||
|
||||
// Clean up icon resources (the compositor takes ownership)
|
||||
if (icon_buffer) {
|
||||
wl_buffer_destroy(icon_buffer);
|
||||
}
|
||||
if (icon_surface) {
|
||||
wl_surface_destroy(icon_surface);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
2
glfw/x11_window.c
vendored
2
glfw/x11_window.c
vendored
|
|
@ -2438,7 +2438,7 @@ void _glfwPlatformSetWindowIcon(_GLFWwindow* window,
|
|||
|
||||
for (j = 0; j < images[i].width * images[i].height; j++)
|
||||
{
|
||||
unsigned char *p = images->pixels + j * 4;
|
||||
const unsigned char *p = images->pixels + j * 4;
|
||||
const unsigned char r = *p++, g = *p++, b = *p++, a = *p++;
|
||||
*target++ = a << 24 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@
|
|||
grab_keyboard,
|
||||
is_layer_shell_supported,
|
||||
last_focused_os_window_id,
|
||||
load_png_data,
|
||||
macos_cycle_through_os_windows,
|
||||
mark_os_window_for_close,
|
||||
monitor_pid,
|
||||
|
|
@ -109,6 +110,7 @@
|
|||
set_os_window_chrome,
|
||||
set_os_window_size,
|
||||
set_os_window_title,
|
||||
start_drag_with_data,
|
||||
thread_write,
|
||||
toggle_fullscreen,
|
||||
toggle_maximized,
|
||||
|
|
@ -3301,6 +3303,14 @@ def simulate_color_scheme_preference_change(self, which: str) -> None:
|
|||
case _:
|
||||
self.show_error(_('Unknown color scheme type'), _('{} is not a valid color scheme type').format(which))
|
||||
|
||||
@ac('debug', ''' Start a test drag operation, for use with mouse_map ''')
|
||||
def test_dragging(self) -> None:
|
||||
if wid := current_os_window():
|
||||
with open(logo_png_file, 'rb') as f:
|
||||
rgba, width, height = load_png_data(f.read())
|
||||
drag_data = {'text/plain': b'This is a test drag of some basic text with the kitty logo as the drag icon.'}
|
||||
start_drag_with_data(wid, drag_data, rgba, width, height)
|
||||
|
||||
def launch_urls(self, *urls: str, no_replace_window: bool = False) -> None:
|
||||
from .launch import force_window_launch
|
||||
from .open_actions import actions_for_launch
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ COLOR_IS_SPECIAL: int
|
|||
COLOR_NOT_SET: int
|
||||
COLOR_IS_RGB: int
|
||||
COLOR_IS_INDEX: int
|
||||
GLFW_DRAG_OPERATION_MOVE: int
|
||||
GLFW_DRAG_OPERATION_COPY: int
|
||||
GLFW_DRAG_OPERATION_GENERIC: int
|
||||
GLFW_LAYER_SHELL_NONE: int
|
||||
GLFW_LAYER_SHELL_PANEL: int
|
||||
GLFW_LAYER_SHELL_TOP: int
|
||||
|
|
@ -1808,3 +1811,9 @@ class StreamingBase64Encodeer:
|
|||
def reset(self) -> bytes: ...
|
||||
# encode the specified data, return number of bytes written dest should be at least 4/3 *src + 2 bytes in size
|
||||
def encode_into(self, dest: WriteableBuffer, src: ReadableBuffer) -> int: ...
|
||||
|
||||
|
||||
def start_drag_with_data(
|
||||
os_window_id: int, data_map: dict[str, bytes], thumbnail: bytes = b'', width: int = 0, height: int = 0,
|
||||
operations: int = GLFW_DRAG_OPERATION_MOVE
|
||||
) -> None: ...
|
||||
|
|
|
|||
2
kitty/glfw-wrapper.h
generated
2
kitty/glfw-wrapper.h
generated
|
|
@ -1796,7 +1796,7 @@ typedef struct GLFWimage
|
|||
int height;
|
||||
/*! The pixel data of this image, arranged left-to-right, top-to-bottom.
|
||||
*/
|
||||
unsigned char* pixels;
|
||||
const unsigned char* pixels;
|
||||
} GLFWimage;
|
||||
|
||||
/*! @brief Gamepad input state
|
||||
|
|
|
|||
79
kitty/glfw.c
79
kitty/glfw.c
|
|
@ -771,6 +771,52 @@ application_close_requested_callback(int flags) {
|
|||
}
|
||||
}
|
||||
|
||||
#define ds (global_state.drag_source)
|
||||
static void
|
||||
try_sending_drag_source_data(id_type timer_id UNUSED, void *callback_data UNUSED) {
|
||||
bool incomplete = false;
|
||||
for (size_t i = 0; i < ds.num_ongoing_transfers; i++) {
|
||||
#define t ds.ongoing_transfers[i]
|
||||
size_t sz = PyBytes_GET_SIZE(t.weakref_to_data_object);
|
||||
ssize_t ret;
|
||||
if (sz > t.offset) {
|
||||
const char *data = PyBytes_AS_STRING(t.weakref_to_data_object);
|
||||
ret = glfwSendDragData(t.platform_data, data + t.offset, sz - t.offset);
|
||||
} else ret = glfwSendDragData(t.platform_data, NULL, 0);
|
||||
if (ret >= 0) {
|
||||
t.offset += ret;
|
||||
if (t.offset < sz) incomplete = true;
|
||||
else glfwSendDragData(t.platform_data, NULL, 0); // tell glfw transfer is complete
|
||||
} else {
|
||||
log_error("Failed to send data from drag source with error: %s", strerror(-ret));
|
||||
t.offset = sz;
|
||||
}
|
||||
#undef t
|
||||
}
|
||||
if (incomplete) add_main_loop_timer(ms_double_to_monotonic_t(2), false, try_sending_drag_source_data, NULL, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
drag_source_callback(GLFWwindow *window UNUSED, const char* mime_type, GLFWDragSourceData* source_data) {
|
||||
PyObject *data;
|
||||
if (mime_type == NULL) {
|
||||
ds.is_active = false;
|
||||
Py_CLEAR(ds.drag_data);
|
||||
return;
|
||||
}
|
||||
if (!ds.is_active || !ds.drag_data || !(data = PyDict_GetItemString(ds.drag_data, mime_type))) {
|
||||
glfwSendDragData(source_data, NULL, EINVAL);
|
||||
return;
|
||||
}
|
||||
ensure_space_for(&ds, ongoing_transfers, ds.ongoing_transfers[0], ds.num_ongoing_transfers + 1, ongoing_transfers_capacity, 8, true);
|
||||
ds.ongoing_transfers[ds.num_ongoing_transfers].platform_data = source_data;
|
||||
ds.ongoing_transfers[ds.num_ongoing_transfers].weakref_to_data_object = data;
|
||||
ds.ongoing_transfers[ds.num_ongoing_transfers].offset = 0;
|
||||
ds.num_ongoing_transfers++;
|
||||
try_sending_drag_source_data(0, NULL);
|
||||
}
|
||||
#undef ds
|
||||
|
||||
static char*
|
||||
get_current_selection(void) {
|
||||
if (!global_state.boss) return NULL;
|
||||
|
|
@ -1629,6 +1675,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
|
|||
glfwSetKeyboardCallback(glfw_window, key_callback);
|
||||
glfwSetDropCallback(glfw_window, drop_callback);
|
||||
glfwSetDragCallback(glfw_window, drag_callback);
|
||||
glfwSetDragSourceCallback(glfw_window, drag_source_callback);
|
||||
monotonic_t now = monotonic();
|
||||
w->is_focused = true;
|
||||
w->cursor_blink_zero_time = now;
|
||||
|
|
@ -2704,6 +2751,32 @@ grab_keyboard(PyObject *self UNUSED, PyObject *action) {
|
|||
return Py_NewRef(glfwGrabKeyboard(action == Py_None ? 2 : PyObject_IsTrue(action)) ? Py_True : Py_False);
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
start_drag_with_data(PyObject *self UNUSED, PyObject *args, PyObject *kw) {
|
||||
static const char* kwlist[] = {"os_window_id", "data_map", "thumbnail", "width", "height", "operations", NULL};
|
||||
unsigned long long os_window_id; PyObject *data_map;
|
||||
const unsigned char *thumbnail_data = NULL; Py_ssize_t thumbnail_sz = 0; int height = 0, width = 0;
|
||||
int operations = GLFW_DRAG_OPERATION_MOVE;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kw, "KO!|y#iii", (char**)kwlist,
|
||||
&os_window_id, &PyDict_Type, &data_map, &thumbnail_data, &thumbnail_sz, &width, &height, &operations)) return NULL;
|
||||
OSWindow *w = os_window_for_id(os_window_id);
|
||||
if (!w || !w->handle) { PyErr_SetString(PyExc_KeyError, "OS Window with specified id does not exist"); return NULL; }
|
||||
RAII_ALLOC(const char*, mime_types, calloc(PyDict_Size(data_map), sizeof(const char*)));
|
||||
if (!mime_types) { PyErr_NoMemory(); return NULL; }
|
||||
PyObject *key, *value; Py_ssize_t pos = 0; int num = 0;
|
||||
while (PyDict_Next(data_map, &pos, &key, &value)) {
|
||||
if (!PyUnicode_Check(key)) { PyErr_SetString(PyExc_TypeError, "data_map must have string keys"); return NULL; }
|
||||
if (!PyBytes_Check(value)) { PyErr_SetString(PyExc_TypeError, "data_map must have bytes values"); return NULL; }
|
||||
mime_types[num++] = PyUnicode_AsUTF8(key);
|
||||
}
|
||||
GLFWimage thumbnail = {.pixels=thumbnail_data, .width=width, .height=height};
|
||||
global_state.drag_source.is_active = true;
|
||||
Py_CLEAR(global_state.drag_source.drag_data); global_state.drag_source.drag_data = Py_NewRef(data_map);
|
||||
global_state.drag_source.num_ongoing_transfers = 0;
|
||||
glfwStartDrag(w->handle, mime_types, num, thumbnail_data ? &thumbnail : NULL, operations);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Boilerplate {{{
|
||||
|
||||
static PyMethodDef module_methods[] = {
|
||||
|
|
@ -2715,6 +2788,7 @@ static PyMethodDef module_methods[] = {
|
|||
METHODB(grab_keyboard, METH_O),
|
||||
METHODB(pointer_name_to_css_name, METH_O),
|
||||
{"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
{"start_drag_with_data", (PyCFunction)(void (*) (void))(start_drag_with_data), METH_VARARGS | METH_KEYWORDS, NULL},
|
||||
METHODB(set_default_window_icon, METH_VARARGS),
|
||||
METHODB(set_os_window_icon, METH_VARARGS),
|
||||
METHODB(set_clipboard_data_types, METH_VARARGS),
|
||||
|
|
@ -2758,7 +2832,7 @@ static PyMethodDef module_methods[] = {
|
|||
};
|
||||
|
||||
void cleanup_glfw(void) {
|
||||
if (logo.pixels) free(logo.pixels);
|
||||
if (logo.pixels) free((void*)logo.pixels);
|
||||
logo.pixels = NULL;
|
||||
Py_CLEAR(edge_spacing_func);
|
||||
#ifndef __APPLE__
|
||||
|
|
@ -2775,6 +2849,9 @@ init_glfw(PyObject *m) {
|
|||
|
||||
// constants {{{
|
||||
#define ADDC(n) if(PyModule_AddIntConstant(m, #n, n) != 0) return false;
|
||||
ADDC(GLFW_DRAG_OPERATION_MOVE);
|
||||
ADDC(GLFW_DRAG_OPERATION_COPY);
|
||||
ADDC(GLFW_DRAG_OPERATION_GENERIC);
|
||||
ADDC(GLFW_RELEASE);
|
||||
ADDC(GLFW_PRESS);
|
||||
ADDC(GLFW_REPEAT);
|
||||
|
|
|
|||
|
|
@ -1581,6 +1581,9 @@ finalize(void) {
|
|||
free_bgimage(&global_state.bgimage, false);
|
||||
free_window_logo_table(&global_state.all_window_logos);
|
||||
global_state.bgimage = NULL;
|
||||
free(global_state.drag_source.ongoing_transfers);
|
||||
Py_CLEAR(global_state.drag_source.drag_data);
|
||||
zero_at_ptr(&global_state.drag_source);
|
||||
|
||||
free_allocs_in_options(&global_state.opts);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -374,6 +374,17 @@ typedef struct GlobalState {
|
|||
int gl_version;
|
||||
bool supports_framebuffer_srgb;
|
||||
PyObject *options_object;
|
||||
|
||||
struct {
|
||||
bool is_active;
|
||||
PyObject *drag_data;
|
||||
struct {
|
||||
void *platform_data;
|
||||
size_t offset;
|
||||
PyObject *weakref_to_data_object;
|
||||
} *ongoing_transfers;
|
||||
size_t num_ongoing_transfers, ongoing_transfers_capacity;
|
||||
} drag_source;
|
||||
} GlobalState;
|
||||
|
||||
extern GlobalState global_state;
|
||||
|
|
|
|||
Loading…
Reference in a new issue