diff --git a/README.md b/README.md index 3cb9021c..7eb5578a 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ scrcpy --fullscreen scrcpy -f # short version ``` -Fullscreen can then be toggled dynamically with `Ctrl`+`f`. +Fullscreen can then be toggled dynamically with `MOD`+`f`. #### Rotation @@ -370,17 +370,17 @@ Possibles values are: - `2`: 180 degrees - `3`: 90 degrees clockwise -The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and -`Ctrl`+`→` _(right)_. +The rotation can also be changed dynamically with `MOD`+`←` _(left)_ and +`MOD`+`→` _(right)_. Note that _scrcpy_ manages 3 different rotations: - - `Ctrl`+`r` requests the device to switch between portrait and landscape (the + - `MOD`+`r` requests the device to switch between portrait and landscape (the current running app may refuse, if it does support the requested orientation). - `--lock-video-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - - `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This + - `--rotation` (or `MOD`+`←`/`MOD`+`→`) rotates only the window content. This affects only the display, not the recording. @@ -437,9 +437,9 @@ scrcpy --turn-screen-off scrcpy -S ``` -Or by pressing `Ctrl`+`o` at any time. +Or by pressing `MOD`+`o` at any time. -To turn it back on, press `Ctrl`+`Shift`+`o` (or `POWER`, `Ctrl`+`p`). +To turn it back on, press `MOD`+`Shift`+`o` (or `POWER`, `MOD`+`p`). It can be useful to also prevent the device to sleep: @@ -494,7 +494,7 @@ scrcpy --disable-screensaver #### Rotate device screen -Press `Ctrl`+`r` to switch between portrait and landscape modes. +Press `MOD`+`r` to switch between portrait and landscape modes. Note that it rotates only if the application in foreground supports the requested orientation. @@ -504,10 +504,10 @@ requested orientation. It is possible to synchronize clipboards between the computer and the device, in both directions: - - `Ctrl`+`c` copies the device clipboard to the computer clipboard; - - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard (and + - `MOD`+`c` copies the device clipboard to the computer clipboard; + - `MOD`+`Shift`+`v` copies the computer clipboard to the device clipboard (and pastes if the device runs Android >= 7); - - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but + - `MOD`+`v` _pastes_ the computer clipboard as a sequence of text events (but breaks non-ASCII characters). Moreover, any time the Android clipboard changes, it is automatically @@ -571,30 +571,48 @@ Also see [issue #14]. ## Shortcuts +In the following list, `MOD` is the shortcut modifier. By default, it's (left) +`Alt`. + +It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, +`lalt`, `ralt`, `lsuper` and `rsuper`. For example: + +```bash +# use RCtrl for shortcuts +scrcpy --shortcut-mod=rctrl + +# use either LCtrl+LAlt or LSuper for shortcuts +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] is typically the "Windows" or "Cmd" key._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + | Action | Shortcut | ------------------------------------------- |:----------------------------- - | Switch fullscreen mode | `Ctrl`+`f` - | Rotate display left | `Ctrl`+`←` _(left)_ - | Rotate display right | `Ctrl`+`→` _(right)_ - | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` - | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ - | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ - | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `Ctrl`+`s` - | Click on `MENU` | `Ctrl`+`m` - | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ - | Click on `POWER` | `Ctrl`+`p` + | Switch fullscreen mode | `MOD`+`f` + | Rotate display left | `MOD`+`←` _(left)_ + | Rotate display right | `MOD`+`→` _(right)_ + | Resize window to 1:1 (pixel-perfect) | `MOD`+`g` + | Resize window to remove black borders | `MOD`+`x` \| _Double-click¹_ + | Click on `HOME` | `MOD`+`h` \| _Middle-click_ + | Click on `BACK` | `MOD`+`b` \| _Right-click²_ + | Click on `APP_SWITCH` | `MOD`+`s` + | Click on `MENU` | `MOD`+`m` + | Click on `VOLUME_UP` | `MOD`+`↑` _(up)_ + | Click on `VOLUME_DOWN` | `MOD`+`↓` _(down)_ + | Click on `POWER` | `MOD`+`p` | Power on | _Right-click²_ - | Turn device screen off (keep mirroring) | `Ctrl`+`o` - | Turn device screen on | `Ctrl`+`Shift`+`o` - | Rotate device screen | `Ctrl`+`r` - | Expand notification panel | `Ctrl`+`n` - | Collapse notification panel | `Ctrl`+`Shift`+`n` - | Copy device clipboard to computer | `Ctrl`+`c` - | Paste computer clipboard to device | `Ctrl`+`v` - | Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` + | Turn device screen off (keep mirroring) | `MOD`+`o` + | Turn device screen on | `MOD`+`Shift`+`o` + | Rotate device screen | `MOD`+`r` + | Expand notification panel | `MOD`+`n` + | Collapse notification panel | `MOD`+`Shift`+`n` + | Copy device clipboard to computer | `MOD`+`c` + | Paste computer clipboard to device | `MOD`+`v` + | Copy computer clipboard to device and paste | `MOD`+`Shift`+`v` + | Enable/disable FPS counter (on stdout) | `MOD`+`i` _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ diff --git a/app/meson.build b/app/meson.build index 505f0819..0163dd7f 100644 --- a/app/meson.build +++ b/app/meson.build @@ -186,7 +186,7 @@ if get_option('buildtype') == 'debug' exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies, - c_args: ['-DSDL_MAIN_HANDLED']) + c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) test(t[0], exe) endforeach endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 4e3c43f2..7ea778f3 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -149,6 +149,16 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. +.TP +.BI "\-\-shortcut\-mod " key[+...]][,...] +Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". + +A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. + +For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". + +Default is "lalt" (left-Alt). + .TP .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. @@ -207,52 +217,55 @@ Default is 0 (automatic).\n .SH SHORTCUTS +In the following list, MOD is the shortcut modifier. By default, it's (left) +Alt, but it can be configured by \-\-shortcut-mod. + .TP -.B Ctrl+f +.B MOD+f Switch fullscreen mode .TP -.B Ctrl+Left +.B MOD+Left Rotate display left .TP -.B Ctrl+Right +.B MOD+Right Rotate display right .TP -.B Ctrl+g +.B MOD+g Resize window to 1:1 (pixel\-perfect) .TP -.B Ctrl+x, Double\-click on black borders +.B MOD+x, Double\-click on black borders Resize window to remove black borders .TP -.B Ctrl+h, Home, Middle\-click +.B MOD+h, Home, Middle\-click Click on HOME .TP -.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) +.B MOD+b, MOD+Backspace, Right\-click (when screen is on) Click on BACK .TP -.B Ctrl+s +.B MOD+s Click on APP_SWITCH .TP -.B Ctrl+m +.B MOD+m Click on MENU .TP -.B Ctrl+Up +.B MOD+Up Click on VOLUME_UP .TP -.B Ctrl+Down +.B MOD+Down Click on VOLUME_DOWN .TP -.B Ctrl+p +.B MOD+p Click on POWER (turn screen on/off) .TP @@ -260,39 +273,39 @@ Click on POWER (turn screen on/off) Turn screen on .TP -.B Ctrl+o +.B MOD+o Turn device screen off (keep mirroring) .TP -.B Ctrl+Shift+o +.B MOD+Shift+o Turn device screen on .TP -.B Ctrl+r +.B MOD+r Rotate device screen .TP -.B Ctrl+n +.B MOD+n Expand notification panel .TP -.B Ctrl+Shift+n +.B MOD+Shift+n Collapse notification panel .TP -.B Ctrl+c +.B MOD+c Copy device clipboard to computer .TP -.B Ctrl+v +.B MOD+v Paste computer clipboard to device .TP -.B Ctrl+Shift+v +.B MOD+Shift+v Copy computer clipboard to device (and paste if the device runs Android >= 7) .TP -.B Ctrl+i +.B MOD+i Enable/disable FPS counter (print frames/second in logs) .TP diff --git a/app/src/cli.c b/app/src/cli.c index 4b65c98f..de32810e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -138,6 +138,19 @@ scrcpy_print_usage(const char *arg0) { " The device serial number. Mandatory only if several devices\n" " are connected to adb.\n" "\n" + " --shortcut-mod key[+...]][,...]\n" + " Specify the modifiers to use for scrcpy shortcuts.\n" + " Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n" + " \"lsuper\" and \"rsuper\".\n" + "\n" + " A shortcut can consist in several keys, separated by '+'.\n" + " Several shortcuts can be specified, separated by ','.\n" + "\n" + " For example, to use either LCtrl+LAlt or LSuper for scrcpy\n" + " shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "\n" + " Default is \"lalt\" (left-Alt).\n" + "\n" " -S, --turn-screen-off\n" " Turn the device screen off immediately.\n" "\n" @@ -185,75 +198,78 @@ scrcpy_print_usage(const char *arg0) { "\n" "Shortcuts:\n" "\n" - " Ctrl+f\n" + " In the following list, MOD is the shortcut modifier. By default,\n" + " it's (left) Alt, but it can be configured by --shortcut-mod.\n" + "\n" + " MOD+f\n" " Switch fullscreen mode\n" "\n" - " Ctrl+Left\n" + " MOD+Left\n" " Rotate display left\n" "\n" - " Ctrl+Right\n" + " MOD+Right\n" " Rotate display right\n" "\n" - " Ctrl+g\n" + " MOD+g\n" " Resize window to 1:1 (pixel-perfect)\n" "\n" - " Ctrl+x\n" + " MOD+x\n" " Double-click on black borders\n" " Resize window to remove black borders\n" "\n" - " Ctrl+h\n" + " MOD+h\n" " Middle-click\n" " Click on HOME\n" "\n" - " Ctrl+b\n" - " Ctrl+Backspace\n" + " MOD+b\n" + " MOD+Backspace\n" " Right-click (when screen is on)\n" " Click on BACK\n" "\n" - " Ctrl+s\n" + " MOD+s\n" " Click on APP_SWITCH\n" "\n" - " Ctrl+m\n" + " MOD+m\n" " Click on MENU\n" "\n" - " Ctrl+Up\n" + " MOD+Up\n" " Click on VOLUME_UP\n" "\n" - " Ctrl+Down\n" + " MOD+Down\n" " Click on VOLUME_DOWN\n" "\n" - " Ctrl+p\n" + " MOD+p\n" " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" " Power on\n" "\n" - " Ctrl+o\n" + " MOD+o\n" " Turn device screen off (keep mirroring)\n" "\n" - " Ctrl+Shift+o\n" + " MOD+Shift+o\n" " Turn device screen on\n" "\n" - " Ctrl+r\n" + " MOD+r\n" " Rotate device screen\n" "\n" - " Ctrl+n\n" + " MOD+n\n" " Expand notification panel\n" "\n" - " Ctrl+Shift+n\n" + " MOD+Shift+n\n" " Collapse notification panel\n" "\n" - " Ctrl+c\n" + " MOD+c\n" " Copy device clipboard to computer\n" "\n" - " Ctrl+v\n" + " MOD+v\n" " Paste computer clipboard to device\n" "\n" - " Ctrl+Shift+v\n" + " MOD+Shift+v\n" " Copy computer clipboard to device (and paste if the device\n" " runs Android >= 7)\n" "\n" - " Ctrl+i\n" + " MOD+i\n" " Enable/disable FPS counter (print frames/second in logs)\n" "\n" " Drag & drop APK file\n" @@ -475,6 +491,101 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { return false; } +// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt") +// returns a bitwise-or of SC_MOD_* constants (or 0 on error) +static unsigned +parse_shortcut_mods_item(const char *item, size_t len) { + unsigned mod = 0; + + for (;;) { + char *plus = strchr(item, '+'); + // strchr() does not consider the "len" parameter, to it could find an + // occurrence too far in the string (there is no strnchr()) + bool has_plus = plus && plus < item + len; + + assert(!has_plus || plus > item); + size_t key_len = has_plus ? (size_t) (plus - item) : len; + +#define STREQ(literal, s, len) \ + ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) + + if (STREQ("lctrl", item, key_len)) { + mod |= SC_MOD_LCTRL; + } else if (STREQ("rctrl", item, key_len)) { + mod |= SC_MOD_RCTRL; + } else if (STREQ("lalt", item, key_len)) { + mod |= SC_MOD_LALT; + } else if (STREQ("ralt", item, key_len)) { + mod |= SC_MOD_RALT; + } else if (STREQ("lsuper", item, key_len)) { + mod |= SC_MOD_LSUPER; + } else if (STREQ("rsuper", item, key_len)) { + mod |= SC_MOD_RSUPER; + } else { + LOGW("Unknown modifier key: %.*s", (int) key_len, item); + return 0; + } +#undef STREQ + + if (!has_plus) { + break; + } + + item = plus + 1; + assert(len >= key_len + 1); + len -= key_len + 1; + } + + return mod; +} + +static bool +parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + unsigned count = 0; + unsigned current = 0; + + // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper" + + for (;;) { + char *comma = strchr(s, ','); + if (comma && count == SC_MAX_SHORTCUT_MODS - 1) { + assert(count < SC_MAX_SHORTCUT_MODS); + LOGW("Too many shortcut modifiers alternatives"); + return false; + } + + assert(!comma || comma > s); + size_t limit = comma ? (size_t) (comma - s) : strlen(s); + + unsigned mod = parse_shortcut_mods_item(s, limit); + if (!mod) { + LOGE("Invalid modifier keys: %.*s", (int) limit, s); + return false; + } + + mods->data[current++] = mod; + ++count; + + if (!comma) { + break; + } + + s = comma + 1; + } + + mods->count = count; + + return true; +} + +#ifdef SC_TEST +// expose the function to unit-tests +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + return parse_shortcut_mods(s, mods); +} +#endif + static bool parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { @@ -526,6 +637,7 @@ guess_record_format(const char *filename) { #define OPT_CODEC_OPTIONS 1018 #define OPT_FORCE_ADB_FORWARD 1019 #define OPT_DISABLE_SCREENSAVER 1020 +#define OPT_SHORTCUT_MOD 1021 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -558,6 +670,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { OPT_RENDER_EXPIRED_FRAMES}, {"rotation", required_argument, NULL, OPT_ROTATION}, {"serial", required_argument, NULL, 's'}, + {"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD}, {"show-touches", no_argument, NULL, 't'}, {"stay-awake", no_argument, NULL, 'w'}, {"turn-screen-off", no_argument, NULL, 'S'}, @@ -721,6 +834,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_DISABLE_SCREENSAVER: opts->disable_screensaver = true; break; + case OPT_SHORTCUT_MOD: + if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/cli.h b/app/src/cli.h index 2e2bfe93..73dfe228 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -18,4 +18,9 @@ scrcpy_print_usage(const char *arg0); bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); +#ifdef SC_TEST +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); +#endif + #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index e16dee52..b954fbd9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,6 +1,7 @@ #include "input_manager.h" #include +#include #include "config.h" #include "event_converter.h" @@ -10,6 +11,64 @@ static const int ACTION_DOWN = 1; static const int ACTION_UP = 1 << 1; +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +static inline uint16_t +to_sdl_mod(unsigned mod) { + uint16_t sdl_mod = 0; + if (mod & SC_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (mod & SC_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (mod & SC_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (mod & SC_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (mod & SC_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (mod & SC_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static bool +is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { + // keep only the relevant modifier keys + sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; + + assert(im->sdl_shortcut_mods.count); + assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS); + for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) { + if (im->sdl_shortcut_mods.data[i] == sdl_mod) { + return true; + } + } + + return false; +} + +void +input_manager_init(struct input_manager *im, bool prefer_text, + const struct sc_shortcut_mods *shortcut_mods) +{ + im->prefer_text = prefer_text; + + assert(shortcut_mods->count); + assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); + for (unsigned i = 0; i < shortcut_mods->count; ++i) { + uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]); + assert(sdl_mod); + im->sdl_shortcut_mods.data[i] = sdl_mod; + } + im->sdl_shortcut_mods.count = shortcut_mods->count; +} + static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) { @@ -258,22 +317,13 @@ 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); - - if (alt || meta) { - // no shortcuts involve Alt or Meta, and they must not be forwarded to - // the device - return; - } + bool smod = is_shortcut_mod(im, event->keysym.mod); struct controller *controller = im->controller; - // capture all Ctrl events - if (ctrl) { + // The shortcut modifier is pressed + if (smod) { SDL_Keycode keycode = event->keysym.sym; bool down = event->type == SDL_KEYDOWN; int action = down ? ACTION_DOWN : ACTION_UP; @@ -281,33 +331,33 @@ input_manager_process_key(struct input_manager *im, bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: - if (control && ctrl && !shift && !repeat) { + if (control && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && ctrl && !repeat && down) { + if (control && !repeat && down) { enum screen_power_mode mode = shift ? SCREEN_POWER_MODE_NORMAL : SCREEN_POWER_MODE_OFF; @@ -315,34 +365,34 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_DOWN: - if (control && ctrl && !shift) { + if (control && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (control && ctrl && !shift) { + if (control && !shift) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { rotate_client_left(im->screen); } return; case SDLK_RIGHT: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { rotate_client_right(im->screen); } return; case SDLK_c: - if (control && ctrl && !shift && !repeat && down) { + if (control && !shift && !repeat && down) { request_device_clipboard(controller); } return; case SDLK_v: - if (control && ctrl && !repeat && down) { + if (control && !repeat && down) { if (shift) { // store the text in the device clipboard and paste set_device_clipboard(controller, true); @@ -353,29 +403,29 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_f: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; case SDLK_x: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (ctrl && !shift && !repeat && down) { + if (!shift && !repeat && down) { struct fps_counter *fps_counter = im->video_buffer->fps_counter; switch_fps_counter_state(fps_counter); } return; case SDLK_n: - if (control && ctrl && !repeat && down) { + if (control && !repeat && down) { if (shift) { collapse_notification_panel(controller); } else { @@ -384,7 +434,7 @@ input_manager_process_key(struct input_manager *im, } return; case SDLK_r: - if (control && ctrl && !shift && !repeat && down) { + if (control && !shift && !repeat && down) { rotate_device(controller); } return; diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 90b74fec..c854664a 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -3,12 +3,15 @@ #include +#include + #include "config.h" #include "common.h" #include "controller.h" #include "fps_counter.h" -#include "video_buffer.h" +#include "scrcpy.h" #include "screen.h" +#include "video_buffer.h" struct input_manager { struct controller *controller; @@ -20,8 +23,17 @@ struct input_manager { unsigned repeat; bool prefer_text; + + struct { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; + } sdl_shortcut_mods; }; +void +input_manager_init(struct input_manager *im, bool prefer_text, + const struct sc_shortcut_mods *shortcut_mods); + void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event); diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 889cbb87..06a3b3d1 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -49,7 +49,13 @@ static struct input_manager input_manager = { .video_buffer = &video_buffer, .screen = &screen, .repeat = 0, - .prefer_text = false, // initialized later + + // initialized later + .prefer_text = false, + .sdl_shortcut_mods = { + .data = {0}, + .count = 0, + }, }; #ifdef _WIN32 @@ -437,7 +443,8 @@ scrcpy(const struct scrcpy_options *options) { } } - input_manager.prefer_text = options->prefer_text; + input_manager_init(&input_manager, options->prefer_text, + &options->shortcut_mods); ret = event_loop(options->display, options->control); LOGD("quit..."); diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d7eb2f58..220fcc2c 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -20,6 +20,22 @@ enum sc_record_format { SC_RECORD_FORMAT_MKV, }; +#define SC_MAX_SHORTCUT_MODS 8 + +enum sc_shortcut_mod { + SC_MOD_LCTRL = 1 << 0, + SC_MOD_RCTRL = 1 << 1, + SC_MOD_LALT = 1 << 2, + SC_MOD_RALT = 1 << 3, + SC_MOD_LSUPER = 1 << 4, + SC_MOD_RSUPER = 1 << 5, +}; + +struct sc_shortcut_mods { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; +}; + struct sc_port_range { uint16_t first; uint16_t last; @@ -38,6 +54,7 @@ struct scrcpy_options { enum sc_log_level log_level; enum sc_record_format record_format; struct sc_port_range port_range; + struct sc_shortcut_mods shortcut_mods; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; @@ -77,6 +94,10 @@ struct scrcpy_options { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ }, \ + .shortcut_mods = { \ + .data = {SC_MOD_LALT}, \ + .count = 1, \ + }, \ .max_size = DEFAULT_MAX_SIZE, \ .bit_rate = DEFAULT_BIT_RATE, \ .max_fps = 0, \ diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 7a7d408d..1024dba6 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -3,6 +3,7 @@ #include "cli.h" #include "common.h" +#include "scrcpy.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { @@ -122,6 +123,43 @@ static void test_options2(void) { assert(opts->record_format == SC_RECORD_FORMAT_MP4); } +static void test_parse_shortcut_mods(void) { + struct sc_shortcut_mods mods; + bool ok; + + ok = sc_parse_shortcut_mods("lctrl", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == SC_MOD_LCTRL); + + ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT)); + + ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); + assert(ok); + assert(mods.count == 2); + assert(mods.data[0] == SC_MOD_RCTRL); + assert(mods.data[1] == SC_MOD_LALT); + + ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); + assert(ok); + assert(mods.count == 3); + assert(mods.data[0] == SC_MOD_LSUPER); + assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT)); + assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT)); + + ok = sc_parse_shortcut_mods("", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl+", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl,", &mods); + assert(!ok); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -130,5 +168,6 @@ int main(int argc, char *argv[]) { test_flag_help(); test_options(); test_options2(); + test_parse_shortcut_mods(); return 0; };