Compare commits

..

4 Commits
ctrl ... copy

Author SHA1 Message Date
Romain Vimont
7a77f3bab4 Copy on "get clipboard" if possible
Ctrl+c synchronizes the Android device clipboard to the computer
clipboard.

To make the copy more straightforward, if the device runs at least
Android 7, also send a COPY keycode before copying the clipboard.

<https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_COPY>
2020-05-28 12:02:21 +02:00
Romain Vimont
0f6cdc56fa Expose method to inject PASTE on Device
This will allow a consistent implementation for injecting COPY.
2020-05-28 12:02:21 +02:00
Romain Vimont
09bb6a68cf Add more convenience methods for injection
Expose methods to inject key events and key codes with an additional
parameter to specify the "mode" (async, wait for finish, wait for
result).
2020-05-28 12:02:21 +02:00
Romain Vimont
f9ddf31c32 Forward SHIFT to the device
This allows to select text with Shift + arrow keys.

Fixes #942 <https://github.com/Genymobile/scrcpy/issues/942>.
2020-05-28 12:02:20 +02:00
13 changed files with 130 additions and 74 deletions

View File

@@ -417,7 +417,7 @@ The secondary display may only be controlled if the device runs at least Android
#### Stay awake
To prevent the device to sleep after some delay when the device is plugged in:
To prevent the device to sleep after some delay:
```bash
scrcpy --stay-awake
@@ -560,33 +560,30 @@ Also see [issue #14].
## Shortcuts
`LCtrl` is the left `Ctrl` key (the right `Ctrl` key is forwarded to the
device).
| Action | Shortcut | Shortcut (macOS)
| ------------------------------------------- |:----------------------------- |:-----------------------------
| Switch fullscreen mode | `LCtrl`+`f` | `Cmd`+`f`
| Rotate display left | `LCtrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
| Rotate display right | `LCtrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
| Resize window to 1:1 (pixel-perfect) | `LCtrl`+`g` | `Cmd`+`g`
| Resize window to remove black borders | `LCtrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
| Click on `HOME` | `LCtrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
| Click on `BACK` | `LCtrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
| Click on `APP_SWITCH` | `LCtrl`+`s` | `Cmd`+`s`
| Click on `MENU` | `LCtrl`+`m` | `Ctrl`+`m`
| Click on `VOLUME_UP` | `LCtrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
| Click on `VOLUME_DOWN` | `LCtrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
| Click on `POWER` | `LCtrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring) | `LCtrl`+`o` | `Cmd`+`o`
| Turn device screen on | `LCtrl`+`Shift`+`o` | `Cmd`+`Shift`+`o`
| Rotate device screen | `LCtrl`+`r` | `Cmd`+`r`
| Expand notification panel | `LCtrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `LCtrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `LCtrl`+`c` | `Cmd`+`c`
| Paste computer clipboard to device | `LCtrl`+`v` | `Cmd`+`v`
| Copy computer clipboard to device and paste | `LCtrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Enable/disable FPS counter (on stdout) | `LCtrl`+`i` | `Cmd`+`i`
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o`
| Turn device screen on | `Ctrl`+`Shift`+`o` | `Cmd`+`Shift`+`o`
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
| Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._

View File

@@ -167,7 +167,7 @@ Default is "info" for release builds, "debug" for debug builds.
.TP
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running, when the device is plugged in.
Keep the device on while scrcpy is running.
.TP
.B \-\-window\-borderless
@@ -203,54 +203,52 @@ Default is 0 (automatic).\n
.SH SHORTCUTS
\fBLCtrl\fR is the left Ctrl key (the right Ctrl key is forwarded to the device).
.TP
.B LCtrl+f
.B Ctrl+f
Switch fullscreen mode
.TP
.B LCtrl+Left
.B Ctrl+Left
Rotate display left
.TP
.B LCtrl+Right
.B Ctrl+Right
Rotate display right
.TP
.B LCtrl+g
.B Ctrl+g
Resize window to 1:1 (pixel\-perfect)
.TP
.B LCtrl+x, Double\-click on black borders
.B Ctrl+x, Double\-click on black borders
Resize window to remove black borders
.TP
.B LCtrl+h, Home, Middle\-click
.B Ctrl+h, Home, Middle\-click
Click on HOME
.TP
.B LCtrl+b, Ctrl+Backspace, Right\-click (when screen is on)
.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on)
Click on BACK
.TP
.B LCtrl+s
.B Ctrl+s
Click on APP_SWITCH
.TP
.B LCtrl+m
.B Ctrl+m
Click on MENU
.TP
.B LCtrl+Up
.B Ctrl+Up
Click on VOLUME_UP
.TP
.B LCtrl+Down
.B Ctrl+Down
Click on VOLUME_DOWN
.TP
.B LCtrl+p
.B Ctrl+p
Click on POWER (turn screen on/off)
.TP
@@ -258,39 +256,39 @@ Click on POWER (turn screen on/off)
Turn screen on
.TP
.B LCtrl+o
.B Ctrl+o
Turn device screen off (keep mirroring)
.TP
.B LCtrl+Shift+o
.B Ctrl+Shift+o
Turn device screen on
.TP
.B LCtrl+r
.B Ctrl+r
Rotate device screen
.TP
.B LCtrl+n
.B Ctrl+n
Expand notification panel
.TP
.B LCtrl+Shift+n
.B Ctrl+Shift+n
Collapse notification panel
.TP
.B LCtrl+c
.B Ctrl+c
Copy device clipboard to computer
.TP
.B LCtrl+v
.B Ctrl+v
Paste computer clipboard to device
.TP
.B LCtrl+Shift+v
.B Ctrl+Shift+v
Copy computer clipboard to device (and paste if the device runs Android >= 7)
.TP
.B LCtrl+i
.B Ctrl+i
Enable/disable FPS counter (print frames/second in logs)
.TP

View File

@@ -15,7 +15,7 @@ scrcpy_print_usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "LCtrl"
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr,
"Usage: %s [options]\n"
@@ -159,8 +159,7 @@ scrcpy_print_usage(const char *arg0) {
#endif
"\n"
" -w, --stay-awake\n"
" Keep the device on while scrcpy is running, when the device\n"
" is plugged in.\n"
" Keep the device on while scrcpy is running.\n"
"\n"
" --window-borderless\n"
" Disable window decorations (display borderless window).\n"
@@ -186,9 +185,6 @@ scrcpy_print_usage(const char *arg0) {
"\n"
"Shortcuts:\n"
"\n"
" LCtrl is the left Ctrl key (the right Ctrl key is forwarded to\n"
" the device).\n"
"\n"
" " CTRL_OR_CMD "+f\n"
" Switch fullscreen mode\n"
"\n"
@@ -205,7 +201,7 @@ scrcpy_print_usage(const char *arg0) {
" Double-click on black borders\n"
" Resize window to remove black borders\n"
"\n"
" LCtrl+h\n"
" Ctrl+h\n"
" Middle-click\n"
" Click on HOME\n"
"\n"
@@ -217,7 +213,7 @@ scrcpy_print_usage(const char *arg0) {
" " CTRL_OR_CMD "+s\n"
" Click on APP_SWITCH\n"
"\n"
" LCtrl+m\n"
" Ctrl+m\n"
" Click on MENU\n"
"\n"
" " CTRL_OR_CMD "+Up\n"

View File

@@ -66,6 +66,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buffer_write32be(&buf[17],
(uint32_t) msg->inject_scroll_event.vscroll);
return 21;
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
buf[1] = msg->get_clipboard.copy;
return 2;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
buf[1] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text,
@@ -79,7 +82,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data
return 1;

View File

@@ -60,6 +60,9 @@ struct control_msg {
int32_t hscroll;
int32_t vscroll;
} inject_scroll_event;
struct {
bool copy;
} get_clipboard;
struct {
char *text; // owned, to be freed by SDL_free()
bool paste;

View File

@@ -92,7 +92,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT);
MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT);
MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT);
}
if (!(mod & (KMOD_NUM | KMOD_SHIFT))) {

View File

@@ -102,9 +102,10 @@ collapse_notification_panel(struct controller *controller) {
}
static void
request_device_clipboard(struct controller *controller) {
request_device_clipboard(struct controller *controller, bool copy) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
msg.get_clipboard.copy = copy;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device clipboard");
@@ -259,8 +260,7 @@ input_manager_process_key(struct input_manager *im,
// control: indicates the state of the command-line option --no-control
// ctrl: the Ctrl key
// Only capture Left-Ctrl, Right-Ctrl is forwarded to the device
bool ctrl = event->keysym.mod & KMOD_LCTRL;
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);
@@ -353,7 +353,8 @@ input_manager_process_key(struct input_manager *im,
return;
case SDLK_c:
if (control && cmd && !shift && !repeat && down) {
request_device_clipboard(controller);
// press COPY and get the clipboard content
request_device_clipboard(controller, true);
}
return;
case SDLK_v:

View File

@@ -185,14 +185,18 @@ static void test_serialize_collapse_notification_panel(void) {
static void test_serialize_get_clipboard(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
.get_clipboard = {
.copy = true,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
assert(size == 2);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_GET_CLIPBOARD,
1, // copy
};
assert(!memcmp(buf, expected, sizeof(expected)));
}

View File

@@ -18,6 +18,7 @@ public final class ControlMessage {
public static final int TYPE_ROTATE_DEVICE = 10;
public static final int FLAGS_PASTE = 1;
public static final int FLAGS_COPY = 2;
private int type;
private String text;
@@ -81,6 +82,15 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createGetClipboard(boolean copy) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_GET_CLIPBOARD;
if (copy) {
msg.flags = FLAGS_COPY;
}
return msg;
}
/**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/

View File

@@ -12,6 +12,7 @@ public class ControlMessageReader {
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int GET_CLIPBOARD_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4092; // 4096 - 1 (type) - 1 (parse flag) - 2 (length)
@@ -67,6 +68,9 @@ public class ControlMessageReader {
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
msg = parseGetClipboard();
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
break;
@@ -76,7 +80,6 @@ public class ControlMessageReader {
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);
break;
@@ -148,6 +151,14 @@ public class ControlMessageReader {
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
}
private ControlMessage parseGetClipboard() {
if (buffer.remaining() < GET_CLIPBOARD_PAYLOAD_LENGTH) {
return null;
}
boolean copy = buffer.get() != 0;
return ControlMessage.createGetClipboard(copy);
}
private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;

View File

@@ -104,10 +104,8 @@ public class Controller {
device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
if (clipboardText != null) {
sender.pushClipboardText(clipboardText);
}
boolean copy = (msg.getFlags() & ControlMessage.FLAGS_COPY) != 0;
getClipboard(copy);
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
@@ -229,6 +227,19 @@ public class Controller {
return device.injectKeycode(keycode);
}
private void getClipboard(boolean copy) {
// On Android >= 7, also press the COPY key if requested
if (copy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
// Must wait until the COPY has been executed
device.injectCopyKeycode();
}
String clipboardText = device.getClipboardText();
if (clipboardText != null) {
sender.pushClipboardText(clipboardText);
}
}
private boolean setClipboard(String text, boolean paste) {
boolean ok = device.setClipboardText(text);
if (ok) {
@@ -237,7 +248,7 @@ public class Controller {
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
device.injectPasteKeycode();
}
return ok;

View File

@@ -167,15 +167,35 @@ public final class Device {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int mode) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEvent(event);
return injectEvent(event, mode);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
return injectKeyEvent(action, keyCode, repeat, metaState, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectKeycode(int keyCode, int mode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, mode);
}
public boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
return injectKeycode(keyCode, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectCopyKeycode() {
isSettingClipboard.set(true);
// Must wait until the COPY has been executed
boolean ret = injectKeycode(KeyEvent.KEYCODE_COPY, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
isSettingClipboard.set(false);
return ret;
}
public boolean injectPasteKeycode() {
return injectKeycode(KeyEvent.KEYCODE_PASTE);
}
public boolean isScreenOn() {

View File

@@ -200,6 +200,7 @@ public class ControlMessageReaderTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
dos.writeByte(1); // copy
byte[] packet = bos.toByteArray();
@@ -207,6 +208,7 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
Assert.assertEquals(1, (event.getFlags() & ControlMessage.FLAGS_COPY));
}
@Test