9f4735ede3
A double-click outside the device content (in the black borders) resizes so that black borders are removed. But the display rotation was not taken into account to detect the content. Use the content size instead of the frame size to fix the issue. Ref: <https://github.com/Genymobile/scrcpy/issues/898#issuecomment-610993695>
637 lines
19 KiB
C
637 lines
19 KiB
C
#include "input_manager.h"
|
|
|
|
#include <assert.h>
|
|
|
|
#include "config.h"
|
|
#include "event_converter.h"
|
|
#include "util/lock.h"
|
|
#include "util/log.h"
|
|
|
|
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
|
// coordinates (as provided in SDL mouse events)
|
|
//
|
|
// See my question:
|
|
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
|
|
static void
|
|
convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
|
|
SDL_Rect viewport;
|
|
float scale_x, scale_y;
|
|
SDL_RenderGetViewport(renderer, &viewport);
|
|
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
|
|
*x = (int) (*x / scale_x) - viewport.x;
|
|
*y = (int) (*y / scale_y) - viewport.y;
|
|
}
|
|
|
|
static struct point
|
|
get_mouse_point(struct screen *screen) {
|
|
int x;
|
|
int y;
|
|
SDL_GetMouseState(&x, &y);
|
|
convert_to_renderer_coordinates(screen->renderer, &x, &y);
|
|
return (struct point) {
|
|
.x = x,
|
|
.y = y,
|
|
};
|
|
}
|
|
|
|
static const int ACTION_DOWN = 1;
|
|
static const int ACTION_UP = 1 << 1;
|
|
|
|
static void
|
|
send_keycode(struct controller *controller, enum android_keycode keycode,
|
|
int actions, const char *name) {
|
|
// send DOWN event
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
|
msg.inject_keycode.keycode = keycode;
|
|
msg.inject_keycode.metastate = 0;
|
|
|
|
if (actions & ACTION_DOWN) {
|
|
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'inject %s (DOWN)'", name);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (actions & ACTION_UP) {
|
|
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'inject %s (UP)'", name);
|
|
}
|
|
}
|
|
}
|
|
|
|
static inline void
|
|
action_home(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_HOME, actions, "HOME");
|
|
}
|
|
|
|
static inline void
|
|
action_back(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_BACK, actions, "BACK");
|
|
}
|
|
|
|
static inline void
|
|
action_app_switch(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH");
|
|
}
|
|
|
|
static inline void
|
|
action_power(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_POWER, actions, "POWER");
|
|
}
|
|
|
|
static inline void
|
|
action_volume_up(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP");
|
|
}
|
|
|
|
static inline void
|
|
action_volume_down(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN");
|
|
}
|
|
|
|
static inline void
|
|
action_menu(struct controller *controller, int actions) {
|
|
send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
|
|
}
|
|
|
|
// turn the screen on if it was off, press BACK otherwise
|
|
static void
|
|
press_back_or_turn_screen_on(struct controller *controller) {
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'press back or turn screen on'");
|
|
}
|
|
}
|
|
|
|
static void
|
|
expand_notification_panel(struct controller *controller) {
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'expand notification panel'");
|
|
}
|
|
}
|
|
|
|
static void
|
|
collapse_notification_panel(struct controller *controller) {
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'collapse notification panel'");
|
|
}
|
|
}
|
|
|
|
static void
|
|
request_device_clipboard(struct controller *controller) {
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request device clipboard");
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_device_clipboard(struct controller *controller) {
|
|
char *text = SDL_GetClipboardText();
|
|
if (!text) {
|
|
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
|
return;
|
|
}
|
|
if (!*text) {
|
|
// empty text
|
|
SDL_free(text);
|
|
return;
|
|
}
|
|
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
|
|
msg.set_clipboard.text = text;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
SDL_free(text);
|
|
LOGW("Could not request 'set device clipboard'");
|
|
}
|
|
}
|
|
|
|
static void
|
|
set_screen_power_mode(struct controller *controller,
|
|
enum screen_power_mode mode) {
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
|
|
msg.set_screen_power_mode.mode = mode;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'set screen power mode'");
|
|
}
|
|
}
|
|
|
|
static void
|
|
switch_fps_counter_state(struct fps_counter *fps_counter) {
|
|
// the started state can only be written from the current thread, so there
|
|
// is no ToCToU issue
|
|
if (fps_counter_is_started(fps_counter)) {
|
|
fps_counter_stop(fps_counter);
|
|
LOGI("FPS counter stopped");
|
|
} else {
|
|
if (fps_counter_start(fps_counter)) {
|
|
LOGI("FPS counter started");
|
|
} else {
|
|
LOGE("FPS counter starting failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
clipboard_paste(struct controller *controller) {
|
|
char *text = SDL_GetClipboardText();
|
|
if (!text) {
|
|
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
|
return;
|
|
}
|
|
if (!*text) {
|
|
// empty text
|
|
SDL_free(text);
|
|
return;
|
|
}
|
|
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
|
msg.inject_text.text = text;
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
SDL_free(text);
|
|
LOGW("Could not request 'paste clipboard'");
|
|
}
|
|
}
|
|
|
|
static void
|
|
rotate_device(struct controller *controller) {
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE;
|
|
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request device rotation");
|
|
}
|
|
}
|
|
|
|
static void
|
|
rotate_client_left(struct screen *screen) {
|
|
unsigned new_rotation = (screen->rotation + 1) % 4;
|
|
screen_set_rotation(screen, new_rotation);
|
|
}
|
|
|
|
static void
|
|
rotate_client_right(struct screen *screen) {
|
|
unsigned new_rotation = (screen->rotation + 3) % 4;
|
|
screen_set_rotation(screen, new_rotation);
|
|
}
|
|
|
|
void
|
|
input_manager_process_text_input(struct input_manager *im,
|
|
const SDL_TextInputEvent *event) {
|
|
if (!im->prefer_text) {
|
|
char c = event->text[0];
|
|
if (isalpha(c) || c == ' ') {
|
|
assert(event->text[1] == '\0');
|
|
// letters and space are handled as raw key event
|
|
return;
|
|
}
|
|
}
|
|
|
|
struct control_msg msg;
|
|
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
|
msg.inject_text.text = SDL_strdup(event->text);
|
|
if (!msg.inject_text.text) {
|
|
LOGW("Could not strdup input text");
|
|
return;
|
|
}
|
|
if (!controller_push_msg(im->controller, &msg)) {
|
|
SDL_free(msg.inject_text.text);
|
|
LOGW("Could not request 'inject text'");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
|
bool prefer_text) {
|
|
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
|
|
|
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
|
return false;
|
|
}
|
|
|
|
uint16_t mod = from->keysym.mod;
|
|
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
|
|
prefer_text)) {
|
|
return false;
|
|
}
|
|
|
|
to->inject_keycode.metastate = convert_meta_state(mod);
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
input_manager_process_key(struct input_manager *im,
|
|
const SDL_KeyboardEvent *event,
|
|
bool control) {
|
|
// control: indicates the state of the command-line option --no-control
|
|
// ctrl: the Ctrl key
|
|
|
|
bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
|
|
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
|
|
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
|
|
|
|
// use Cmd on macOS, Ctrl on other platforms
|
|
#ifdef __APPLE__
|
|
bool cmd = !ctrl && meta;
|
|
#else
|
|
if (meta) {
|
|
// no shortcuts involve Meta on platforms other than macOS, and it must
|
|
// not be forwarded to the device
|
|
return;
|
|
}
|
|
bool cmd = ctrl; // && !meta, already guaranteed
|
|
#endif
|
|
|
|
if (alt) {
|
|
// no shortcuts involve Alt, and it must not be forwarded to the device
|
|
return;
|
|
}
|
|
|
|
struct controller *controller = im->controller;
|
|
|
|
// capture all Ctrl events
|
|
if (ctrl || cmd) {
|
|
SDL_Keycode keycode = event->keysym.sym;
|
|
bool down = event->type == SDL_KEYDOWN;
|
|
int action = down ? ACTION_DOWN : ACTION_UP;
|
|
bool repeat = event->repeat;
|
|
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
|
|
switch (keycode) {
|
|
case SDLK_h:
|
|
// Ctrl+h on all platform, since Cmd+h is already captured by
|
|
// the system on macOS to hide the window
|
|
if (control && ctrl && !meta && !shift && !repeat) {
|
|
action_home(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_b: // fall-through
|
|
case SDLK_BACKSPACE:
|
|
if (control && cmd && !shift && !repeat) {
|
|
action_back(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_s:
|
|
if (control && cmd && !shift && !repeat) {
|
|
action_app_switch(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_m:
|
|
// Ctrl+m on all platform, since Cmd+m is already captured by
|
|
// the system on macOS to minimize the window
|
|
if (control && ctrl && !meta && !shift && !repeat) {
|
|
action_menu(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_p:
|
|
if (control && cmd && !shift && !repeat) {
|
|
action_power(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_o:
|
|
if (control && cmd && !shift && down) {
|
|
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
|
|
}
|
|
return;
|
|
case SDLK_DOWN:
|
|
if (control && cmd && !shift) {
|
|
// forward repeated events
|
|
action_volume_down(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_UP:
|
|
if (control && cmd && !shift) {
|
|
// forward repeated events
|
|
action_volume_up(controller, action);
|
|
}
|
|
return;
|
|
case SDLK_LEFT:
|
|
if (cmd && !shift && down) {
|
|
rotate_client_left(im->screen);
|
|
}
|
|
return;
|
|
case SDLK_RIGHT:
|
|
if (cmd && !shift && down) {
|
|
rotate_client_right(im->screen);
|
|
}
|
|
return;
|
|
case SDLK_c:
|
|
if (control && cmd && !shift && !repeat && down) {
|
|
request_device_clipboard(controller);
|
|
}
|
|
return;
|
|
case SDLK_v:
|
|
if (control && cmd && !repeat && down) {
|
|
if (shift) {
|
|
// store the text in the device clipboard
|
|
set_device_clipboard(controller);
|
|
} else {
|
|
// inject the text as input events
|
|
clipboard_paste(controller);
|
|
}
|
|
}
|
|
return;
|
|
case SDLK_f:
|
|
if (!shift && cmd && !repeat && down) {
|
|
screen_switch_fullscreen(im->screen);
|
|
}
|
|
return;
|
|
case SDLK_x:
|
|
if (!shift && cmd && !repeat && down) {
|
|
screen_resize_to_fit(im->screen);
|
|
}
|
|
return;
|
|
case SDLK_g:
|
|
if (!shift && cmd && !repeat && down) {
|
|
screen_resize_to_pixel_perfect(im->screen);
|
|
}
|
|
return;
|
|
case SDLK_i:
|
|
if (!shift && cmd && !repeat && down) {
|
|
struct fps_counter *fps_counter =
|
|
im->video_buffer->fps_counter;
|
|
switch_fps_counter_state(fps_counter);
|
|
}
|
|
return;
|
|
case SDLK_n:
|
|
if (control && cmd && !repeat && down) {
|
|
if (shift) {
|
|
collapse_notification_panel(controller);
|
|
} else {
|
|
expand_notification_panel(controller);
|
|
}
|
|
}
|
|
return;
|
|
case SDLK_r:
|
|
if (control && cmd && !shift && !repeat && down) {
|
|
rotate_device(controller);
|
|
}
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!control) {
|
|
return;
|
|
}
|
|
|
|
struct control_msg msg;
|
|
if (convert_input_key(event, &msg, im->prefer_text)) {
|
|
if (!controller_push_msg(controller, &msg)) {
|
|
LOGW("Could not request 'inject keycode'");
|
|
}
|
|
}
|
|
}
|
|
|
|
static struct point
|
|
rotate_position(struct screen *screen, int32_t x, int32_t y) {
|
|
unsigned rotation = screen->rotation;
|
|
assert(rotation < 4);
|
|
|
|
int32_t w = screen->content_size.width;
|
|
int32_t h = screen->content_size.height;
|
|
struct point result;
|
|
switch (rotation) {
|
|
case 0:
|
|
result.x = x;
|
|
result.y = y;
|
|
break;
|
|
case 1:
|
|
result.x = h - y;
|
|
result.y = x;
|
|
break;
|
|
case 2:
|
|
result.x = w - x;
|
|
result.y = h - y;
|
|
break;
|
|
case 3:
|
|
result.x = y;
|
|
result.y = w - x;
|
|
break;
|
|
default:
|
|
assert(!"Unreachable");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static bool
|
|
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
|
|
struct control_msg *to) {
|
|
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
|
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
|
|
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
|
to->inject_touch_event.position.screen_size = screen->frame_size;
|
|
to->inject_touch_event.position.point =
|
|
rotate_position(screen, from->x, from->y);
|
|
to->inject_touch_event.pressure = 1.f;
|
|
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
input_manager_process_mouse_motion(struct input_manager *im,
|
|
const SDL_MouseMotionEvent *event) {
|
|
if (!event->state) {
|
|
// do not send motion events when no button is pressed
|
|
return;
|
|
}
|
|
if (event->which == SDL_TOUCH_MOUSEID) {
|
|
// simulated from touch events, so it's a duplicate
|
|
return;
|
|
}
|
|
struct control_msg msg;
|
|
if (convert_mouse_motion(event, im->screen, &msg)) {
|
|
if (!controller_push_msg(im->controller, &msg)) {
|
|
LOGW("Could not request 'inject mouse motion event'");
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool
|
|
convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
|
|
struct control_msg *to) {
|
|
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
|
|
|
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
|
|
return false;
|
|
}
|
|
|
|
struct size frame_size = screen->frame_size;
|
|
|
|
to->inject_touch_event.pointer_id = from->fingerId;
|
|
to->inject_touch_event.position.screen_size = frame_size;
|
|
// SDL touch event coordinates are normalized in the range [0; 1]
|
|
float x = from->x * frame_size.width;
|
|
float y = from->y * frame_size.height;
|
|
to->inject_touch_event.position.point = rotate_position(screen, x, y);
|
|
to->inject_touch_event.pressure = from->pressure;
|
|
to->inject_touch_event.buttons = 0;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
input_manager_process_touch(struct input_manager *im,
|
|
const SDL_TouchFingerEvent *event) {
|
|
struct control_msg msg;
|
|
if (convert_touch(event, im->screen, &msg)) {
|
|
if (!controller_push_msg(im->controller, &msg)) {
|
|
LOGW("Could not request 'inject touch event'");
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool
|
|
is_outside_device_screen(struct input_manager *im, int x, int y)
|
|
{
|
|
return x < 0 || x >= im->screen->content_size.width ||
|
|
y < 0 || y >= im->screen->content_size.height;
|
|
}
|
|
|
|
static bool
|
|
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
|
struct control_msg *to) {
|
|
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
|
|
|
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
|
|
return false;
|
|
}
|
|
|
|
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
|
to->inject_touch_event.position.screen_size = screen->frame_size;
|
|
to->inject_touch_event.position.point =
|
|
rotate_position(screen, from->x, from->y);
|
|
to->inject_touch_event.pressure = 1.f;
|
|
to->inject_touch_event.buttons =
|
|
convert_mouse_buttons(SDL_BUTTON(from->button));
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
input_manager_process_mouse_button(struct input_manager *im,
|
|
const SDL_MouseButtonEvent *event,
|
|
bool control) {
|
|
if (event->which == SDL_TOUCH_MOUSEID) {
|
|
// simulated from touch events, so it's a duplicate
|
|
return;
|
|
}
|
|
if (event->type == SDL_MOUSEBUTTONDOWN) {
|
|
if (control && event->button == SDL_BUTTON_RIGHT) {
|
|
press_back_or_turn_screen_on(im->controller);
|
|
return;
|
|
}
|
|
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
|
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
|
return;
|
|
}
|
|
// double-click on black borders resize to fit the device screen
|
|
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
|
bool outside =
|
|
is_outside_device_screen(im, event->x, event->y);
|
|
if (outside) {
|
|
screen_resize_to_fit(im->screen);
|
|
return;
|
|
}
|
|
}
|
|
// otherwise, send the click event to the device
|
|
}
|
|
|
|
if (!control) {
|
|
return;
|
|
}
|
|
|
|
struct control_msg msg;
|
|
if (convert_mouse_button(event, im->screen, &msg)) {
|
|
if (!controller_push_msg(im->controller, &msg)) {
|
|
LOGW("Could not request 'inject mouse button event'");
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool
|
|
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
|
struct control_msg *to) {
|
|
struct position position = {
|
|
.screen_size = screen->frame_size,
|
|
.point = get_mouse_point(screen),
|
|
};
|
|
|
|
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
|
|
|
to->inject_scroll_event.position = position;
|
|
to->inject_scroll_event.hscroll = from->x;
|
|
to->inject_scroll_event.vscroll = from->y;
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
input_manager_process_mouse_wheel(struct input_manager *im,
|
|
const SDL_MouseWheelEvent *event) {
|
|
struct control_msg msg;
|
|
if (convert_mouse_wheel(event, im->screen, &msg)) {
|
|
if (!controller_push_msg(im->controller, &msg)) {
|
|
LOGW("Could not request 'inject mouse wheel event'");
|
|
}
|
|
}
|
|
}
|