win32u: Move NtUserSendInput implementation from user32.
Signed-off-by: Jacek Caban <jacek@codeweavers.com> Signed-off-by: Huw Davies <huw@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org>
This commit is contained in:
parent
21d60952cb
commit
6ff2d287c2
|
@ -65,11 +65,6 @@ void USER_unload_driver(void)
|
|||
* These are fallbacks for entry points that are not implemented in the real driver.
|
||||
*/
|
||||
|
||||
static BOOL CDECL nulldrv_SetCursorPos( INT x, INT y )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void CDECL nulldrv_UpdateClipboard(void)
|
||||
{
|
||||
}
|
||||
|
@ -103,11 +98,6 @@ static LRESULT CDECL nulldrv_SysCommand( HWND hwnd, WPARAM wparam, LPARAM lparam
|
|||
* Each entry point simply loads the real driver and chains to it.
|
||||
*/
|
||||
|
||||
static BOOL CDECL loaderdrv_SetCursorPos( INT x, INT y )
|
||||
{
|
||||
return load_driver()->pSetCursorPos( x, y );
|
||||
}
|
||||
|
||||
static void CDECL loaderdrv_UpdateClipboard(void)
|
||||
{
|
||||
load_driver()->pUpdateClipboard();
|
||||
|
@ -130,7 +120,7 @@ static struct user_driver_funcs lazy_load_driver =
|
|||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
loaderdrv_SetCursorPos,
|
||||
NULL,
|
||||
NULL,
|
||||
/* clipboard functions */
|
||||
loaderdrv_UpdateClipboard,
|
||||
|
@ -187,7 +177,6 @@ void CDECL __wine_set_user_driver( const struct user_driver_funcs *funcs, UINT v
|
|||
#define SET_USER_FUNC(name) \
|
||||
do { if (!driver->p##name) driver->p##name = nulldrv_##name; } while(0)
|
||||
|
||||
SET_USER_FUNC(SetCursorPos);
|
||||
SET_USER_FUNC(UpdateClipboard);
|
||||
SET_USER_FUNC(MsgWaitForMultipleObjectsEx);
|
||||
SET_USER_FUNC(SetWindowIcon);
|
||||
|
|
|
@ -122,105 +122,6 @@ BOOL set_capture_window( HWND hwnd, UINT gui_flags, HWND *prev_ret )
|
|||
}
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
* update_mouse_coords
|
||||
*
|
||||
* Helper for SendInput.
|
||||
*/
|
||||
static void update_mouse_coords( INPUT *input )
|
||||
{
|
||||
if (!(input->u.mi.dwFlags & MOUSEEVENTF_MOVE)) return;
|
||||
|
||||
if (input->u.mi.dwFlags & MOUSEEVENTF_ABSOLUTE)
|
||||
{
|
||||
DPI_AWARENESS_CONTEXT context = SetThreadDpiAwarenessContext( DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE );
|
||||
RECT rc;
|
||||
|
||||
if (input->u.mi.dwFlags & MOUSEEVENTF_VIRTUALDESK)
|
||||
rc = get_virtual_screen_rect();
|
||||
else
|
||||
rc = get_primary_monitor_rect();
|
||||
|
||||
input->u.mi.dx = rc.left + ((input->u.mi.dx * (rc.right - rc.left)) >> 16);
|
||||
input->u.mi.dy = rc.top + ((input->u.mi.dy * (rc.bottom - rc.top)) >> 16);
|
||||
SetThreadDpiAwarenessContext( context );
|
||||
}
|
||||
else
|
||||
{
|
||||
int accel[3];
|
||||
|
||||
/* dx and dy can be negative numbers for relative movements */
|
||||
SystemParametersInfoW(SPI_GETMOUSE, 0, accel, 0);
|
||||
|
||||
if (!accel[2]) return;
|
||||
|
||||
if (abs(input->u.mi.dx) > accel[0])
|
||||
{
|
||||
input->u.mi.dx *= 2;
|
||||
if ((abs(input->u.mi.dx) > accel[1]) && (accel[2] == 2)) input->u.mi.dx *= 2;
|
||||
}
|
||||
if (abs(input->u.mi.dy) > accel[0])
|
||||
{
|
||||
input->u.mi.dy *= 2;
|
||||
if ((abs(input->u.mi.dy) > accel[1]) && (accel[2] == 2)) input->u.mi.dy *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
* SendInput (USER32.@)
|
||||
*/
|
||||
UINT WINAPI SendInput( UINT count, LPINPUT inputs, int size )
|
||||
{
|
||||
UINT i;
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
|
||||
if (size != sizeof(INPUT))
|
||||
{
|
||||
SetLastError( ERROR_INVALID_PARAMETER );
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!count)
|
||||
{
|
||||
SetLastError( ERROR_INVALID_PARAMETER );
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!inputs)
|
||||
{
|
||||
SetLastError( ERROR_NOACCESS );
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
INPUT input = inputs[i];
|
||||
switch (input.type)
|
||||
{
|
||||
case INPUT_MOUSE:
|
||||
/* we need to update the coordinates to what the server expects */
|
||||
update_mouse_coords( &input );
|
||||
/* fallthrough */
|
||||
case INPUT_KEYBOARD:
|
||||
status = send_hardware_message( 0, &input, NULL, SEND_HWMSG_INJECTED );
|
||||
break;
|
||||
case INPUT_HARDWARE:
|
||||
SetLastError( ERROR_CALL_NOT_IMPLEMENTED );
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (status)
|
||||
{
|
||||
SetLastError( RtlNtStatusToDosError(status) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
* keybd_event (USER32.@)
|
||||
*/
|
||||
|
@ -235,7 +136,7 @@ void WINAPI keybd_event( BYTE bVk, BYTE bScan,
|
|||
input.u.ki.dwFlags = dwFlags;
|
||||
input.u.ki.time = 0;
|
||||
input.u.ki.dwExtraInfo = dwExtraInfo;
|
||||
SendInput( 1, &input, sizeof(input) );
|
||||
NtUserSendInput( 1, &input, sizeof(input) );
|
||||
}
|
||||
|
||||
|
||||
|
@ -254,7 +155,7 @@ void WINAPI mouse_event( DWORD dwFlags, DWORD dx, DWORD dy,
|
|||
input.u.mi.dwFlags = dwFlags;
|
||||
input.u.mi.time = 0;
|
||||
input.u.mi.dwExtraInfo = dwExtraInfo;
|
||||
SendInput( 1, &input, sizeof(input) );
|
||||
NtUserSendInput( 1, &input, sizeof(input) );
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2177,123 +2177,6 @@ static BOOL send_message( struct send_message_info *info, DWORD_PTR *res_ptr, BO
|
|||
}
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
* send_hardware_message
|
||||
*/
|
||||
NTSTATUS send_hardware_message( HWND hwnd, const INPUT *input, const RAWINPUT *rawinput, UINT flags )
|
||||
{
|
||||
struct user_key_state_info *key_state_info = get_user_thread_info()->key_state;
|
||||
struct send_message_info info;
|
||||
int prev_x, prev_y, new_x, new_y;
|
||||
INT counter = NtUserCallOneParam( 0, NtUserIncrementKeyStateCounter );
|
||||
USAGE hid_usage_page, hid_usage;
|
||||
NTSTATUS ret;
|
||||
BOOL wait;
|
||||
|
||||
info.type = MSG_HARDWARE;
|
||||
info.dest_tid = 0;
|
||||
info.hwnd = hwnd;
|
||||
info.flags = 0;
|
||||
info.timeout = 0;
|
||||
|
||||
if (input->type == INPUT_HARDWARE && rawinput->header.dwType == RIM_TYPEHID)
|
||||
{
|
||||
if (input->u.hi.uMsg == WM_INPUT_DEVICE_CHANGE)
|
||||
{
|
||||
hid_usage_page = ((USAGE *)rawinput->data.hid.bRawData)[0];
|
||||
hid_usage = ((USAGE *)rawinput->data.hid.bRawData)[1];
|
||||
}
|
||||
if (input->u.hi.uMsg == WM_INPUT)
|
||||
{
|
||||
if (!rawinput_device_get_usages( rawinput->header.hDevice, &hid_usage_page, &hid_usage ))
|
||||
{
|
||||
WARN( "unable to get HID usages for device %p\n", rawinput->header.hDevice );
|
||||
return STATUS_INVALID_HANDLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SERVER_START_REQ( send_hardware_message )
|
||||
{
|
||||
req->win = wine_server_user_handle( hwnd );
|
||||
req->flags = flags;
|
||||
req->input.type = input->type;
|
||||
switch (input->type)
|
||||
{
|
||||
case INPUT_MOUSE:
|
||||
req->input.mouse.x = input->u.mi.dx;
|
||||
req->input.mouse.y = input->u.mi.dy;
|
||||
req->input.mouse.data = input->u.mi.mouseData;
|
||||
req->input.mouse.flags = input->u.mi.dwFlags;
|
||||
req->input.mouse.time = input->u.mi.time;
|
||||
req->input.mouse.info = input->u.mi.dwExtraInfo;
|
||||
break;
|
||||
case INPUT_KEYBOARD:
|
||||
req->input.kbd.vkey = input->u.ki.wVk;
|
||||
req->input.kbd.scan = input->u.ki.wScan;
|
||||
req->input.kbd.flags = input->u.ki.dwFlags;
|
||||
req->input.kbd.time = input->u.ki.time;
|
||||
req->input.kbd.info = input->u.ki.dwExtraInfo;
|
||||
break;
|
||||
case INPUT_HARDWARE:
|
||||
req->input.hw.msg = input->u.hi.uMsg;
|
||||
req->input.hw.lparam = MAKELONG( input->u.hi.wParamL, input->u.hi.wParamH );
|
||||
switch (input->u.hi.uMsg)
|
||||
{
|
||||
case WM_INPUT:
|
||||
case WM_INPUT_DEVICE_CHANGE:
|
||||
req->input.hw.rawinput.type = rawinput->header.dwType;
|
||||
switch (rawinput->header.dwType)
|
||||
{
|
||||
case RIM_TYPEHID:
|
||||
req->input.hw.rawinput.hid.device = HandleToUlong( rawinput->header.hDevice );
|
||||
req->input.hw.rawinput.hid.param = rawinput->header.wParam;
|
||||
req->input.hw.rawinput.hid.usage_page = hid_usage_page;
|
||||
req->input.hw.rawinput.hid.usage = hid_usage;
|
||||
req->input.hw.rawinput.hid.count = rawinput->data.hid.dwCount;
|
||||
req->input.hw.rawinput.hid.length = rawinput->data.hid.dwSizeHid;
|
||||
wine_server_add_data( req, rawinput->data.hid.bRawData,
|
||||
rawinput->data.hid.dwCount * rawinput->data.hid.dwSizeHid );
|
||||
break;
|
||||
default:
|
||||
assert( 0 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (key_state_info) wine_server_set_reply( req, key_state_info->state,
|
||||
sizeof(key_state_info->state) );
|
||||
ret = wine_server_call( req );
|
||||
wait = reply->wait;
|
||||
prev_x = reply->prev_x;
|
||||
prev_y = reply->prev_y;
|
||||
new_x = reply->new_x;
|
||||
new_y = reply->new_y;
|
||||
}
|
||||
SERVER_END_REQ;
|
||||
|
||||
if (!ret)
|
||||
{
|
||||
if (key_state_info)
|
||||
{
|
||||
key_state_info->time = GetTickCount();
|
||||
key_state_info->counter = counter;
|
||||
}
|
||||
if ((flags & SEND_HWMSG_INJECTED) && (prev_x != new_x || prev_y != new_y))
|
||||
USER_Driver->pSetCursorPos( new_x, new_y );
|
||||
}
|
||||
|
||||
if (wait)
|
||||
{
|
||||
LRESULT ignored;
|
||||
wait_message_reply( 0 );
|
||||
retrieve_reply( &info, 0, &ignored );
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/***********************************************************************
|
||||
* SendMessageTimeoutW (USER32.@)
|
||||
*/
|
||||
|
|
|
@ -635,7 +635,7 @@
|
|||
@ stdcall SendDlgItemMessageW(long long long long long)
|
||||
@ stdcall SendIMEMessageExA(long long)
|
||||
@ stdcall SendIMEMessageExW(long long)
|
||||
@ stdcall SendInput(long ptr long)
|
||||
@ stdcall SendInput(long ptr long) NtUserSendInput
|
||||
@ stdcall SendMessageA(long long long long)
|
||||
@ stdcall SendMessageCallbackA(long long long long ptr long)
|
||||
@ stdcall SendMessageCallbackW(long long long long ptr long)
|
||||
|
|
|
@ -165,7 +165,6 @@ static const struct user_callbacks user_funcs =
|
|||
EndMenu,
|
||||
HideCaret,
|
||||
PostMessageW,
|
||||
SendInput,
|
||||
SendMessageTimeoutW,
|
||||
SendMessageA,
|
||||
SendMessageW,
|
||||
|
|
|
@ -101,7 +101,6 @@ extern RECT get_virtual_screen_rect(void) DECLSPEC_HIDDEN;
|
|||
extern RECT get_primary_monitor_rect(void) DECLSPEC_HIDDEN;
|
||||
extern DWORD get_input_codepage( void ) DECLSPEC_HIDDEN;
|
||||
extern BOOL map_wparam_AtoW( UINT message, WPARAM *wparam, enum wm_char_mapping mapping ) DECLSPEC_HIDDEN;
|
||||
extern NTSTATUS send_hardware_message( HWND hwnd, const INPUT *input, const RAWINPUT *rawinput, UINT flags ) DECLSPEC_HIDDEN;
|
||||
extern HPEN SYSCOLOR_GetPen( INT index ) DECLSPEC_HIDDEN;
|
||||
extern HBRUSH SYSCOLOR_Get55AABrush(void) DECLSPEC_HIDDEN;
|
||||
extern void SYSPARAMS_Init(void) DECLSPEC_HIDDEN;
|
||||
|
|
|
@ -1199,6 +1199,7 @@ static struct unix_funcs unix_funcs =
|
|||
NtUserReleaseDC,
|
||||
NtUserScrollDC,
|
||||
NtUserSelectPalette,
|
||||
NtUserSendInput,
|
||||
NtUserSetActiveWindow,
|
||||
NtUserSetCapture,
|
||||
NtUserSetClassLong,
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
#pragma makedep unix
|
||||
#endif
|
||||
|
||||
#include "ntstatus.h"
|
||||
#define WIN32_NO_STATUS
|
||||
#include "win32u_private.h"
|
||||
#include "ntuser_private.h"
|
||||
#include "wine/server.h"
|
||||
|
@ -75,13 +77,100 @@ BOOL CDECL __wine_send_input( HWND hwnd, const INPUT *input, const RAWINPUT *raw
|
|||
return set_ntstatus( send_hardware_message( hwnd, input, rawinput, 0 ));
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
* update_mouse_coords
|
||||
*
|
||||
* Helper for NtUserSendInput.
|
||||
*/
|
||||
static void update_mouse_coords( INPUT *input )
|
||||
{
|
||||
if (!(input->mi.dwFlags & MOUSEEVENTF_MOVE)) return;
|
||||
|
||||
if (input->mi.dwFlags & MOUSEEVENTF_ABSOLUTE)
|
||||
{
|
||||
RECT rc;
|
||||
|
||||
if (input->mi.dwFlags & MOUSEEVENTF_VIRTUALDESK)
|
||||
rc = get_virtual_screen_rect( 0 );
|
||||
else
|
||||
rc = get_primary_monitor_rect( 0 );
|
||||
|
||||
input->mi.dx = rc.left + ((input->mi.dx * (rc.right - rc.left)) >> 16);
|
||||
input->mi.dy = rc.top + ((input->mi.dy * (rc.bottom - rc.top)) >> 16);
|
||||
}
|
||||
else
|
||||
{
|
||||
int accel[3];
|
||||
|
||||
/* dx and dy can be negative numbers for relative movements */
|
||||
NtUserSystemParametersInfo( SPI_GETMOUSE, 0, accel, 0 );
|
||||
|
||||
if (!accel[2]) return;
|
||||
|
||||
if (abs( input->mi.dx ) > accel[0])
|
||||
{
|
||||
input->mi.dx *= 2;
|
||||
if (abs( input->mi.dx ) > accel[1] && accel[2] == 2) input->mi.dx *= 2;
|
||||
}
|
||||
if (abs(input->mi.dy) > accel[0])
|
||||
{
|
||||
input->mi.dy *= 2;
|
||||
if (abs( input->mi.dy ) > accel[1] && accel[2] == 2) input->mi.dy *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
* NtUserSendInput (win32u.@)
|
||||
*/
|
||||
UINT WINAPI NtUserSendInput( UINT count, INPUT *inputs, int size )
|
||||
{
|
||||
if (!user_callbacks) return 0;
|
||||
return user_callbacks->pSendInput( count, inputs, size );
|
||||
UINT i;
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
|
||||
if (size != sizeof(INPUT))
|
||||
{
|
||||
SetLastError( ERROR_INVALID_PARAMETER );
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!count)
|
||||
{
|
||||
SetLastError( ERROR_INVALID_PARAMETER );
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!inputs)
|
||||
{
|
||||
SetLastError( ERROR_NOACCESS );
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
INPUT input = inputs[i];
|
||||
switch (input.type)
|
||||
{
|
||||
case INPUT_MOUSE:
|
||||
/* we need to update the coordinates to what the server expects */
|
||||
update_mouse_coords( &input );
|
||||
/* fallthrough */
|
||||
case INPUT_KEYBOARD:
|
||||
status = send_hardware_message( 0, &input, NULL, SEND_HWMSG_INJECTED );
|
||||
break;
|
||||
case INPUT_HARDWARE:
|
||||
SetLastError( ERROR_CALL_NOT_IMPLEMENTED );
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (status)
|
||||
{
|
||||
SetLastError( RtlNtStatusToDosError(status) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
/***********************************************************************
|
||||
|
|
|
@ -38,7 +38,6 @@ struct user_callbacks
|
|||
BOOL (WINAPI *pEndMenu)(void);
|
||||
BOOL (WINAPI *pHideCaret)( HWND hwnd );
|
||||
BOOL (WINAPI *pPostMessageW)( HWND, UINT, WPARAM, LPARAM );
|
||||
UINT (WINAPI *pSendInput)( UINT count, INPUT *inputs, int size );
|
||||
LRESULT (WINAPI *pSendMessageTimeoutW)( HWND, UINT, WPARAM, LPARAM, UINT, UINT, PDWORD_PTR );
|
||||
LRESULT (WINAPI *pSendMessageA)( HWND, UINT, WPARAM, LPARAM );
|
||||
LRESULT (WINAPI *pSendMessageW)( HWND, UINT, WPARAM, LPARAM );
|
||||
|
|
|
@ -4717,8 +4717,6 @@ ULONG_PTR WINAPI NtUserCallOneParam( ULONG_PTR arg, ULONG code )
|
|||
MSG *msg = (MSG *)arg;
|
||||
return handle_internal_message( msg->hwnd, msg->message, msg->wParam, msg->lParam );
|
||||
}
|
||||
case NtUserIncrementKeyStateCounter:
|
||||
return InterlockedAdd( &global_key_state_counter, arg );
|
||||
case NtUserLock:
|
||||
switch( arg )
|
||||
{
|
||||
|
|
|
@ -1159,7 +1159,7 @@
|
|||
@ stub NtUserScrollWindowEx
|
||||
@ stdcall NtUserSelectPalette(long long long)
|
||||
@ stub NtUserSendEventMessage
|
||||
@ stub NtUserSendInput
|
||||
@ stdcall NtUserSendInput(long ptr long)
|
||||
@ stub NtUserSendInteractiveControlHapticsReport
|
||||
@ stub NtUserSetActivationFilter
|
||||
@ stub NtUserSetActiveProcessForMonitor
|
||||
|
|
|
@ -252,6 +252,7 @@ struct unix_funcs
|
|||
BOOL (WINAPI *pNtUserScrollDC)( HDC hdc, INT dx, INT dy, const RECT *scroll, const RECT *clip,
|
||||
HRGN ret_update_rgn, RECT *update_rect );
|
||||
HPALETTE (WINAPI *pNtUserSelectPalette)( HDC hdc, HPALETTE hpal, WORD bkg );
|
||||
UINT (WINAPI *pNtUserSendInput)( UINT count, INPUT *inputs, int size );
|
||||
HWND (WINAPI *pNtUserSetActiveWindow)( HWND hwnd );
|
||||
HWND (WINAPI *pNtUserSetCapture)( HWND hwnd );
|
||||
DWORD (WINAPI *pNtUserSetClassLong)( HWND hwnd, INT offset, LONG newval, BOOL ansi );
|
||||
|
|
|
@ -1016,6 +1016,12 @@ HPALETTE WINAPI NtUserSelectPalette( HDC hdc, HPALETTE hpal, WORD bkg )
|
|||
return unix_funcs->pNtUserSelectPalette( hdc, hpal, bkg );
|
||||
}
|
||||
|
||||
UINT WINAPI NtUserSendInput( UINT count, INPUT *inputs, int size )
|
||||
{
|
||||
if (!unix_funcs) return 0;
|
||||
return unix_funcs->pNtUserSendInput( count, inputs, size );
|
||||
}
|
||||
|
||||
HWND WINAPI NtUserSetActiveWindow( HWND hwnd )
|
||||
{
|
||||
if (!unix_funcs) return 0;
|
||||
|
|
|
@ -169,7 +169,6 @@ enum
|
|||
NtUserGetDeskPattern,
|
||||
NtUserGetWinProcPtr,
|
||||
NtUserHandleInternalMessage,
|
||||
NtUserIncrementKeyStateCounter,
|
||||
NtUserLock,
|
||||
NtUserSetCallbacks,
|
||||
NtUserSpyGetVKeyName,
|
||||
|
@ -605,6 +604,7 @@ HANDLE WINAPI NtUserRemoveProp( HWND hwnd, const WCHAR *str );
|
|||
BOOL WINAPI NtUserScrollDC( HDC hdc, INT dx, INT dy, const RECT *scroll, const RECT *clip,
|
||||
HRGN ret_update_rgn, RECT *update_rect );
|
||||
HPALETTE WINAPI NtUserSelectPalette( HDC hdc, HPALETTE palette, WORD force_background );
|
||||
UINT WINAPI NtUserSendInput( UINT count, INPUT *inputs, int size );
|
||||
HWND WINAPI NtUserSetActiveWindow( HWND hwnd );
|
||||
HWND WINAPI NtUserSetCapture( HWND hwnd );
|
||||
DWORD WINAPI NtUserSetClassLong( HWND hwnd, INT offset, LONG newval, BOOL ansi );
|
||||
|
|
Loading…
Reference in New Issue