Compare commits

...

11 Commits

Author SHA1 Message Date
Romain Vimont
a01e9c2812 Avoid additional copy on Java text parsing
Directly pass the buffer range to the String constructor.
2020-07-09 12:28:42 +02:00
Romain Vimont
80f5a7c43d Update copy-paste section in README
Update documentation regarding the recent copy-paste changes.
2020-07-09 12:28:42 +02:00
Romain Vimont
ddb36e3436 Forward copy-cut shortcuts
For convenience, forward RCtrl+c (or Cmd+c) and and RCtrl+x (or Cmd+x)
as LCtrl+c and LCtrl+x to the device.

This allows to use the "natural" keys for copy-paste (especially on
macOS).
2020-07-09 12:28:42 +02:00
Romain Vimont
cac1765091 Change "resize to fit" shortcut to RCtrl+w
For convenience, RCtrl+x (and Cmd+x) will be used for "cut text to
clipboard", in addition to LCtrl+x.
2020-07-09 12:28:42 +02:00
Romain Vimont
f5a14b285b Accept Cmd for shortcuts on macOS
For convenience (and to keep the existing behavior), also accept
shortcuts using Cmd instead of RCtrl.
2020-07-09 12:28:42 +02:00
Romain Vimont
65edae0ca6 Reformulate RCtrl+v description
RCtrl+v is the only scrcpy shortcut related to copy-paste. Since it's
not a real "paste", reformulate to indicate that it injects the
clipboard content as text events.
2020-07-09 12:28:42 +02:00
Romain Vimont
e4a0fada10 Remove RCtrl+c copy shortcut
Now that Ctrl+c is forwarded to the device, and that every device
clipboard change is automatically synchronized to the computer, RCtrl+c
is useless.
2020-07-09 12:28:42 +02:00
Romain Vimont
8a037e3d9b Remove RCtrl+Shift+v paste shortcut
Now that LCtrl+v synchronize the computer clipboard to the device
clipboard, RCtrl+Shift+v is not needed anymore.

Note: RCtrl+v is kept to send the computer clipboard content as a
sequence of keys.
2020-07-09 12:28:42 +02:00
Romain Vimont
b3aa88c751 Set device clipboard only if necessary
Do not explicitly set the clipboard text if it already contains the
expected content. This avoids possible copy-paste loops between the
computer and the device.
2020-07-09 12:28:37 +02:00
Romain Vimont
b9602e56d9 Synchronize clipboard on Ctrl+v
Pressing Ctrl+v on the device will typically paste the clipboard
content.

Before sending the key event, synchronize the computer clipboard to the
device clipboard to allow seamless copy-paste.
2020-06-02 18:27:24 +02:00
Romain Vimont
0c01ac34b4 Simplify PASTE option for "set clipboard"
When the client requests to set the clipboard, it may request to press
the PASTE key in addition. To be a bit generic, it was stored as a flag
in ControlMessage.java.

But flags suggest that it represents a bitwise union, which could become
confusing when we add a COPY option for getting the clipboard.
2020-06-02 18:27:24 +02:00
9 changed files with 91 additions and 82 deletions

View File

@@ -490,17 +490,15 @@ requested orientation.
#### Copy-paste #### Copy-paste
It is possible to synchronize clipboards between the computer and the device, in Any time the Android clipboard changes, it is automatically synchronized to the
both directions: computer clipboard.
- `RCtrl`+`c` copies the device clipboard to the computer clipboard; `Ctrl`+`c` (copy), `Ctrl`+`x` (cut) and `LCtrl`+`v` (paste) work as you expect.
- `RCtrl`+`Shift`+`v` copies the computer clipboard to the device clipboard
(and pastes if the device runs Android >= 7); In addition, `RCtrl`+`v` allows to inject the computer clipboard content as a
- `RCtrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but sequence of text event. Even if it can break non-ASCII content, this is
breaks non-ASCII characters). sometimes necessary when pasting directly is not possible.
Moreover, any time the Android clipboard changes, it is automatically
synchronized to the computer clipboard.
#### Text injection preference #### Text injection preference
@@ -563,13 +561,16 @@ Also see [issue #14].
`RCtrl` is the right `Ctrl` key (the left `Ctrl` key is forwarded to the `RCtrl` is the right `Ctrl` key (the left `Ctrl` key is forwarded to the
device). device).
On macOS, `Cmd` also works (for shortcuts which are not already captured by the
system).
| Action | Shortcut | Action | Shortcut
| ------------------------------------------- |:----------------------------- | ------------------------------------------- |:-----------------------------
| Switch fullscreen mode | `RCtrl`+`f` | Switch fullscreen mode | `RCtrl`+`f`
| Rotate display left | `RCtrl`+`←` | Rotate display left | `RCtrl`+`←`
| Rotate display right | `RCtrl`+`→` | Rotate display right | `RCtrl`+`→`
| Resize window to 1:1 (pixel-perfect) | `RCtrl`+`g` | Resize window to 1:1 (pixel-perfect) | `RCtrl`+`g`
| Resize window to remove black borders | `RCtrl`+`x` \| _Double-click¹_ | Resize window to remove black borders | `RCtrl`+`w` \| _Double-click¹_
| Click on `HOME` | `RCtrl`+`h` \| _Middle-click_ | Click on `HOME` | `RCtrl`+`h` \| _Middle-click_
| Click on `BACK` | `RCtrl`+`b` \| _Right-click²_ | Click on `BACK` | `RCtrl`+`b` \| _Right-click²_
| Click on `APP_SWITCH` | `RCtrl`+`s` | Click on `APP_SWITCH` | `RCtrl`+`s`
@@ -583,9 +584,7 @@ device).
| Rotate device screen | `RCtrl`+`r` | Rotate device screen | `RCtrl`+`r`
| Expand notification panel | `RCtrl`+`n` | Expand notification panel | `RCtrl`+`n`
| Collapse notification panel | `RCtrl`+`Shift`+`n` | Collapse notification panel | `RCtrl`+`Shift`+`n`
| Copy device clipboard to computer | `RCtrl`+`c` | Inject computer clipboard text | `RCtrl`+`v`
| Paste computer clipboard to device | `RCtrl`+`v`
| Copy computer clipboard to device and paste | `RCtrl`+`Shift`+`v`
| Enable/disable FPS counter (on stdout) | `RCtrl`+`i` | Enable/disable FPS counter (on stdout) | `RCtrl`+`i`
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._

View File

@@ -222,7 +222,7 @@ Rotate display right
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
.TP .TP
.B RCtrl+x, Double\-click on black borders .B RCtrl+w, Double\-click on black borders
Resize window to remove black borders Resize window to remove black borders
.TP .TP
@@ -277,17 +277,9 @@ Expand notification panel
.B RCtrl+Shift+n .B RCtrl+Shift+n
Collapse notification panel Collapse notification panel
.TP
.B RCtrl+c
Copy device clipboard to computer
.TP .TP
.B RCtrl+v .B RCtrl+v
Paste computer clipboard to device Inject computer clipboard text
.TP
.B RCtrl+Shift+v
Copy computer clipboard to device (and paste if the device runs Android >= 7)
.TP .TP
.B RCtrl+i .B RCtrl+i

View File

@@ -196,7 +196,7 @@ scrcpy_print_usage(const char *arg0) {
" RCtrl+g\n" " RCtrl+g\n"
" Resize window to 1:1 (pixel-perfect)\n" " Resize window to 1:1 (pixel-perfect)\n"
"\n" "\n"
" RCtrl+x\n" " RCtrl+w\n"
" Double-click on black borders\n" " Double-click on black borders\n"
" Resize window to remove black borders\n" " Resize window to remove black borders\n"
"\n" "\n"
@@ -242,15 +242,8 @@ scrcpy_print_usage(const char *arg0) {
" RCtrl+Shift+n\n" " RCtrl+Shift+n\n"
" Collapse notification panel\n" " Collapse notification panel\n"
"\n" "\n"
" RCtrl+c\n"
" Copy device clipboard to computer\n"
"\n"
" RCtrl+v\n" " RCtrl+v\n"
" Paste computer clipboard to device\n" " Inject computer clipboard text\n"
"\n"
" RCtrl+Shift+v\n"
" Copy computer clipboard to device (and paste if the device\n"
" runs Android >= 7)\n"
"\n" "\n"
" RCtrl+i\n" " RCtrl+i\n"
" Enable/disable FPS counter (print frames/second in logs)\n" " Enable/disable FPS counter (print frames/second in logs)\n"

View File

@@ -101,16 +101,6 @@ collapse_notification_panel(struct controller *controller) {
} }
} }
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 static void
set_device_clipboard(struct controller *controller, bool paste) { set_device_clipboard(struct controller *controller, bool paste) {
char *text = SDL_GetClipboardText(); char *text = SDL_GetClipboardText();
@@ -252,6 +242,25 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
return true; return true;
} }
static void
inject_as_ctrl(struct input_manager *im, const SDL_KeyboardEvent *event) {
struct control_msg msg;
if (!convert_input_key(event, &msg, false)) {
return;
}
// Disable RCtrl and Meta
msg.inject_keycode.metastate &=
~(AMETA_CTRL_RIGHT_ON | AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON);
// Enable LCtrl
msg.inject_keycode.metastate |= AMETA_CTRL_LEFT_ON;
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject keycode'");
}
}
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event,
@@ -259,25 +268,36 @@ input_manager_process_key(struct input_manager *im,
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
// ctrl: the Ctrl key // ctrl: the Ctrl key
bool lctrl = event->keysym.mod & KMOD_LCTRL;
bool rctrl = event->keysym.mod & KMOD_RCTRL; bool rctrl = event->keysym.mod & KMOD_RCTRL;
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (alt || meta) { bool shortcut_key = rctrl;
// No shortcuts involve Alt or Meta, and they are not forwarded to the #ifdef __APPLE__
// device shortcut_key |= meta;
#else
if (meta) {
// No shortcut involve Meta, and it is not forwarded to the device
return;
}
#endif
if (alt) {
// No shortcuts involve Alt, and it is not forwarded to the device
return; return;
} }
struct controller *controller = im->controller; struct controller *controller = im->controller;
SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN;
// Capture all RCtrl events // Capture all RCtrl events
if (rctrl) { if (shortcut_key) {
SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN;
int action = down ? ACTION_DOWN : ACTION_UP; int action = down ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat; bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) { switch (keycode) {
case SDLK_h: case SDLK_h:
// Ctrl+h on all platform, since Cmd+h is already captured by // Ctrl+h on all platform, since Cmd+h is already captured by
@@ -339,20 +359,10 @@ input_manager_process_key(struct input_manager *im,
rotate_client_right(im->screen); rotate_client_right(im->screen);
} }
return; return;
case SDLK_c:
if (control && !shift && !repeat && down) {
request_device_clipboard(controller);
}
return;
case SDLK_v: case SDLK_v:
if (control && !repeat && down) { if (control && !shift && !repeat && down) {
if (shift) { // Inject the text as input events
// store the text in the device clipboard and paste clipboard_paste(controller);
set_device_clipboard(controller, true);
} else {
// inject the text as input events
clipboard_paste(controller);
}
} }
return; return;
case SDLK_f: case SDLK_f:
@@ -360,7 +370,7 @@ input_manager_process_key(struct input_manager *im,
screen_switch_fullscreen(im->screen); screen_switch_fullscreen(im->screen);
} }
return; return;
case SDLK_x: case SDLK_w:
if (!shift && !repeat && down) { if (!shift && !repeat && down) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(im->screen);
} }
@@ -391,6 +401,15 @@ input_manager_process_key(struct input_manager *im,
rotate_device(controller); rotate_device(controller);
} }
return; return;
case SDLK_c:
case SDLK_x:
if (control && !shift) {
// For convenience, forward shortcut_key+c and
// shortcut_key+x as Ctrl+c and Ctrl+x (typically "copy" and
// "cut", but not always) to the device
inject_as_ctrl(im, event);
}
return;
} }
return; return;
@@ -400,6 +419,12 @@ input_manager_process_key(struct input_manager *im,
return; return;
} }
if (lctrl && !shift && keycode == SDLK_v && down) {
// Synchronize the computer clipboard to the device clipboard before
// sending Ctrl+V, to allow seamless copy-paste.
set_device_clipboard(controller, false);
}
struct control_msg msg; struct control_msg msg;
if (convert_input_key(event, &msg, im->prefer_text)) { if (convert_input_key(event, &msg, im->prefer_text)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {

View File

@@ -17,8 +17,6 @@ public final class ControlMessage {
public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10; public static final int TYPE_ROTATE_DEVICE = 10;
public static final int FLAGS_PASTE = 1;
private int type; private int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* private int metaState; // KeyEvent.META_*
@@ -30,7 +28,7 @@ public final class ControlMessage {
private Position position; private Position position;
private int hScroll; private int hScroll;
private int vScroll; private int vScroll;
private int flags; private boolean paste;
private ControlMessage() { private ControlMessage() {
} }
@@ -75,9 +73,7 @@ public final class ControlMessage {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD; msg.type = TYPE_SET_CLIPBOARD;
msg.text = text; msg.text = text;
if (paste) { msg.paste = paste;
msg.flags = FLAGS_PASTE;
}
return msg; return msg;
} }
@@ -141,7 +137,7 @@ public final class ControlMessage {
return vScroll; return vScroll;
} }
public int getFlags() { public boolean getPaste() {
return flags; return paste;
} }
} }

View File

@@ -21,7 +21,6 @@ public class ControlMessageReader {
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlMessageReader() { public ControlMessageReader() {
// invariant: the buffer is always in "get" mode // invariant: the buffer is always in "get" mode
@@ -111,8 +110,10 @@ public class ControlMessageReader {
if (buffer.remaining() < len) { if (buffer.remaining() < len) {
return null; return null;
} }
buffer.get(textBuffer, 0, len); int position = buffer.position();
return new String(textBuffer, 0, len, StandardCharsets.UTF_8); // Move the buffer position to consume the text
buffer.position(position + len);
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
} }
private ControlMessage parseInjectText() { private ControlMessage parseInjectText() {

View File

@@ -110,8 +110,7 @@ public class Controller {
} }
break; break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0; setClipboard(msg.getText(), msg.getPaste());
setClipboard(msg.getText(), paste);
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {

View File

@@ -207,6 +207,14 @@ public final class Device {
} }
public boolean setClipboardText(String text) { public boolean setClipboardText(String text) {
String currentClipboard = getClipboardText();
if (currentClipboard == null || currentClipboard.equals(text)) {
// The clipboard already contains the requested text.
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
// problem, do not explicitly set the clipboard text if it already contains the expected content.
return false;
}
isSettingClipboard.set(true); isSettingClipboard.set(true);
boolean ok = serviceManager.getClipboardManager().setText(text); boolean ok = serviceManager.getClipboardManager().setText(text);
isSettingClipboard.set(false); isSettingClipboard.set(false);

View File

@@ -228,9 +228,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals("testé", event.getText()); Assert.assertEquals("testé", event.getText());
Assert.assertTrue(event.getPaste());
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
Assert.assertTrue(parse);
} }
@Test @Test
@@ -256,9 +254,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(text, event.getText()); Assert.assertEquals(text, event.getText());
Assert.assertTrue(event.getPaste());
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
Assert.assertTrue(parse);
} }
@Test @Test