mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-21 03:40:40 +08:00
[fix] support Unicode text input on Windows hosts, refs #120
This commit is contained in:
@@ -151,7 +151,7 @@ gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
|
|||||||
|
|
||||||
身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。
|
身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。
|
||||||
|
|
||||||
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的 Ctrl / Win / Alt / Shift 可锁定组合键;macOS 的 Command 对应 Win。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
|
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的 Ctrl / Win / Alt / Shift 可锁定组合键;macOS 的 Command 对应 Win。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。Windows 通过当前已解锁用户桌面的 Unicode 键盘事件输入文本,无需切换电脑输入法或覆盖剪贴板;锁屏和 UAC 安全桌面不接受此文本通道。Windows 被控端也必须更新到包含文本输入支持的版本,仅更新 Android 不会启用该能力。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
|
||||||
|
|
||||||
“发送文件”调用系统文件选择器,后台读取所选文件后使用桌面端相同的 `file` / `file_feedback` 协议传输,支持空文件和中文文件名。每次发送一个文件,以 64 KiB 分块、最多 1 MiB 未确认数据限制发送队列;进度来自接收端确认,30 秒没有有效进展则报告失败。接收时校验大小并追踪完整字节范围,允许跨线程回调乱序,未完成的文件不会提供保存入口。同名文件分开存储,不覆盖已有文件;可通过“保存收到的文件”导出最近收到的文件,连续接收多个文件时应逐个保存。
|
“发送文件”调用系统文件选择器,后台读取所选文件后使用桌面端相同的 `file` / `file_feedback` 协议传输,支持空文件和中文文件名。每次发送一个文件,以 64 KiB 分块、最多 1 MiB 未确认数据限制发送队列;进度来自接收端确认,30 秒没有有效进展则报告失败。接收时校验大小并追踪完整字节范围,允许跨线程回调乱序,未完成的文件不会提供保存入口。同名文件分开存储,不覆盖已有文件;可通过“保存收到的文件”导出最近收到的文件,连续接收多个文件时应逐个保存。
|
||||||
|
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
|||||||
private boolean sendTextEdit(int count,String value){
|
private boolean sendTextEdit(int count,String value){
|
||||||
if(session==null)return false;
|
if(session==null)return false;
|
||||||
if(session.textEdit(count,value))return true;
|
if(session.textEdit(count,value))return true;
|
||||||
toast("当前远端不支持此文本,中文或表情需更新 macOS 被控端,或通过剪贴板发送");return false;
|
toast("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送");return false;
|
||||||
}
|
}
|
||||||
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,false);}}
|
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,false);}}
|
||||||
private void shortcut(int code){if(session!=null){int modifier=hostPlatform.equals("macos")?0x5B:0x11;session.key(modifier,true);tapKey(code);session.key(modifier,false);}}
|
private void shortcut(int code){if(session!=null){int modifier=hostPlatform.equals("macos")?0x5B:0x11;session.key(modifier,true);tapKey(code);session.key(modifier,false);}}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "keyboard_capturer.h"
|
#include "keyboard_capturer.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "windows_input_injector.h"
|
#include "windows_input_injector.h"
|
||||||
#include "windows_input_marker.h"
|
#include "windows_input_marker.h"
|
||||||
@@ -388,4 +391,60 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PlatformKeyboardCapturer::SendTextCommand(const std::string& text) {
|
||||||
|
if (text.empty() || text.size() > kMaxTextInputBytes ||
|
||||||
|
text.find('\0') != std::string::npos) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Validate the entire UTF-8 commit before injecting any characters.
|
||||||
|
const int byte_count = static_cast<int>(text.size());
|
||||||
|
const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||||
|
text.data(), byte_count, nullptr, 0);
|
||||||
|
if (length == 0) return -1;
|
||||||
|
std::wstring characters(length, L'\0');
|
||||||
|
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(),
|
||||||
|
byte_count, characters.data(), length) != length) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<INPUT> inputs;
|
||||||
|
inputs.reserve(characters.size() * 2);
|
||||||
|
for (size_t index = 0; index < characters.size(); ++index) {
|
||||||
|
const wchar_t character = characters[index];
|
||||||
|
INPUT input{};
|
||||||
|
input.type = INPUT_KEYBOARD;
|
||||||
|
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
|
||||||
|
if (character == L'\r' || character == L'\n' || character == L'\t') {
|
||||||
|
input.ki.wVk = character == L'\t' ? VK_TAB : VK_RETURN;
|
||||||
|
if (character == L'\r' && index + 1 < characters.size() &&
|
||||||
|
characters[index + 1] == L'\n') {
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// VK_PACKET carries UTF-16 units, including both halves of emoji.
|
||||||
|
input.ki.wScan = static_cast<WORD>(character);
|
||||||
|
input.ki.dwFlags = KEYEVENTF_UNICODE;
|
||||||
|
}
|
||||||
|
inputs.push_back(input);
|
||||||
|
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||||
|
inputs.push_back(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One batch keeps surrogate pairs and key releases together with the text.
|
||||||
|
const UINT count = static_cast<UINT>(inputs.size());
|
||||||
|
const UINT sent = SendInputOnUserDesktop(inputs.data(), count);
|
||||||
|
if (sent != count) {
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
if (sent < count && sent % 2 != 0) {
|
||||||
|
SendInputOnUserDesktop(inputs[sent]); // Best-effort release.
|
||||||
|
}
|
||||||
|
LOG_WARN(
|
||||||
|
"SendInput failed for committed text, sent={}, expected={}, err={}",
|
||||||
|
sent, count, error);
|
||||||
|
SetLastError(error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: DI JUNKUN
|
* @Author: DI JUNKUN
|
||||||
* @Date: 2024-11-22
|
* @Date: 2024-11-22
|
||||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef _KEYBOARD_CAPTURER_H_
|
#ifndef _KEYBOARD_CAPTURER_H_
|
||||||
@@ -29,6 +29,8 @@ class PlatformKeyboardCapturer final : public KeyboardCapturer {
|
|||||||
int Hook(OnKeyAction on_key_action, void* user_ptr) override;
|
int Hook(OnKeyAction on_key_action, void* user_ptr) override;
|
||||||
int Unhook() override;
|
int Unhook() override;
|
||||||
bool IsHookActive() const override;
|
bool IsHookActive() const override;
|
||||||
|
bool SupportsTextInput() const override { return true; }
|
||||||
|
int SendTextCommand(const std::string& text) override;
|
||||||
int SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
int SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||||
bool extended = false) override;
|
bool extended = false) override;
|
||||||
|
|
||||||
|
|||||||
@@ -13,26 +13,32 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
DWORD InjectOnUserDesktop(INPUT input) {
|
struct InputResult {
|
||||||
|
UINT sent;
|
||||||
|
DWORD error;
|
||||||
|
};
|
||||||
|
|
||||||
|
InputResult InjectOnUserDesktop(INPUT* inputs, UINT count) {
|
||||||
if (!IsCurrentSessionUnlocked()) {
|
if (!IsCurrentSessionUnlocked()) {
|
||||||
return ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
|
return {0, ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION};
|
||||||
}
|
}
|
||||||
ScopedInteractiveDesktop desktop;
|
ScopedInteractiveDesktop desktop;
|
||||||
if (!desktop.Bind(L"")) {
|
if (!desktop.Bind(L"")) {
|
||||||
const DWORD error = GetLastError();
|
const DWORD error = GetLastError();
|
||||||
return error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE;
|
return {0, error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE};
|
||||||
}
|
}
|
||||||
if (!desktop.IsReceivingInput()) {
|
if (!desktop.IsReceivingInput()) {
|
||||||
return ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
|
return {0, ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION};
|
||||||
}
|
}
|
||||||
// Lock screens and UAC still require the session-checked service path.
|
// Lock screens and UAC still require the session-checked service path.
|
||||||
if (_wcsicmp(desktop.name().c_str(), L"Default") != 0) {
|
if (_wcsicmp(desktop.name().c_str(), L"Default") != 0) {
|
||||||
return ERROR_ACCESS_DENIED;
|
return {0, ERROR_ACCESS_DENIED};
|
||||||
}
|
}
|
||||||
SetLastError(ERROR_SUCCESS);
|
SetLastError(ERROR_SUCCESS);
|
||||||
if (SendInput(1, &input, sizeof(INPUT)) == 1) return ERROR_SUCCESS;
|
const UINT sent = SendInput(count, inputs, sizeof(INPUT));
|
||||||
|
if (sent == count) return {sent, ERROR_SUCCESS};
|
||||||
const DWORD error = GetLastError();
|
const DWORD error = GetLastError();
|
||||||
return error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE;
|
return {sent, error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE};
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserDesktopInputWorker {
|
class UserDesktopInputWorker {
|
||||||
@@ -48,9 +54,9 @@ class UserDesktopInputWorker {
|
|||||||
thread_.join();
|
thread_.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
DWORD Send(INPUT input) {
|
InputResult Send(INPUT* inputs, UINT count) {
|
||||||
std::packaged_task<DWORD()> task(
|
std::packaged_task<InputResult()> task(
|
||||||
[input] { return InjectOnUserDesktop(input); });
|
[inputs, count] { return InjectOnUserDesktop(inputs, count); });
|
||||||
auto result = task.get_future();
|
auto result = task.get_future();
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
@@ -76,7 +82,7 @@ class UserDesktopInputWorker {
|
|||||||
|
|
||||||
std::mutex mutex_;
|
std::mutex mutex_;
|
||||||
std::condition_variable wake_;
|
std::condition_variable wake_;
|
||||||
std::queue<std::packaged_task<DWORD()>> pending_;
|
std::queue<std::packaged_task<InputResult()>> pending_;
|
||||||
bool stopping_ = false;
|
bool stopping_ = false;
|
||||||
std::thread thread_;
|
std::thread thread_;
|
||||||
};
|
};
|
||||||
@@ -84,8 +90,18 @@ class UserDesktopInputWorker {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
UINT SendInputOnUserDesktop(const INPUT& input) {
|
UINT SendInputOnUserDesktop(const INPUT& input) {
|
||||||
|
INPUT copy = input;
|
||||||
|
return SendInputOnUserDesktop(©, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
UINT SendInputOnUserDesktop(INPUT* inputs, UINT count) {
|
||||||
|
if (!inputs || count == 0) {
|
||||||
|
SetLastError(ERROR_INVALID_PARAMETER);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
static UserDesktopInputWorker worker;
|
static UserDesktopInputWorker worker;
|
||||||
const DWORD error = worker.Send(input);
|
const InputResult result = worker.Send(inputs, count);
|
||||||
|
const DWORD error = result.error;
|
||||||
if (error != ERROR_SUCCESS) {
|
if (error != ERROR_SUCCESS) {
|
||||||
// Include session identity even when Windows reports no useful SendInput
|
// Include session identity even when Windows reports no useful SendInput
|
||||||
// error (for example UIPI). Keep repeated mouse-move failures bounded.
|
// error (for example UIPI). Keep repeated mouse-move failures bounded.
|
||||||
@@ -102,7 +118,7 @@ UINT SendInputOnUserDesktop(const INPUT& input) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetLastError(error);
|
SetLastError(error);
|
||||||
return error == ERROR_SUCCESS ? 1 : 0;
|
return result.sent;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ namespace crossdesk {
|
|||||||
// worker's failure code in the calling thread's last-error value.
|
// worker's failure code in the calling thread's last-error value.
|
||||||
UINT SendInputOnUserDesktop(const INPUT& input);
|
UINT SendInputOnUserDesktop(const INPUT& input);
|
||||||
|
|
||||||
|
// Synchronously injects one ordered batch. The caller owns the buffer until
|
||||||
|
// this function returns, including while the worker is using it.
|
||||||
|
UINT SendInputOnUserDesktop(INPUT* inputs, UINT count);
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user