Compare commits
11 Commits
clipboard.
...
left_ctrl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a01e9c2812 | ||
|
|
80f5a7c43d | ||
|
|
ddb36e3436 | ||
|
|
cac1765091 | ||
|
|
f5a14b285b | ||
|
|
65edae0ca6 | ||
|
|
e4a0fada10 | ||
|
|
8a037e3d9b | ||
|
|
b3aa88c751 | ||
|
|
b9602e56d9 | ||
|
|
0c01ac34b4 |
25
README.md
25
README.md
@@ -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._
|
||||||
|
|||||||
12
app/scrcpy.1
12
app/scrcpy.1
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user