feat(macos): Add clipboard support to match Linux implementation

This commit is contained in:
yuanyuanxiang
2026-05-03 13:53:58 +02:00
parent 3d8e90da14
commit 1df2a7b321
3 changed files with 258 additions and 1 deletions

186
macos/ClipboardHandler.h Normal file
View File

@@ -0,0 +1,186 @@
#pragma once
#import <Cocoa/Cocoa.h>
#include <string>
#include <vector>
// macOS 剪贴板操作封装
// 使用 NSPasteboard API 实现
class ClipboardHandler
{
public:
// 检查剪贴板功能是否可用 (macOS 总是可用)
static bool IsAvailable()
{
return true;
}
// 获取剪贴板中的文件列表
// 返回文件的完整路径列表UTF-8失败返回空列表
static std::vector<std::string> GetFiles()
{
std::vector<std::string> files;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
// 方法1: 尝试获取文件 URL 列表 (macOS 10.13+)
NSArray<NSURL*>* urls = [pasteboard readObjectsForClasses:@[[NSURL class]]
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
if (urls && urls.count > 0) {
for (NSURL* url in urls) {
if (url.isFileURL) {
NSString* path = url.path;
if (path) {
files.push_back([path UTF8String]);
}
}
}
return files;
}
// 方法2: 兼容旧版 API (NSFilenamesPboardType)
NSArray* filenames = [pasteboard propertyListForType:NSFilenamesPboardType];
if (filenames && [filenames isKindOfClass:[NSArray class]]) {
for (NSString* path in filenames) {
if ([path isKindOfClass:[NSString class]]) {
files.push_back([path UTF8String]);
}
}
}
}
return files;
}
// 获取剪贴板文本
// 返回 UTF-8 编码的文本,失败返回空字符串
static std::string GetText()
{
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSString* text = [pasteboard stringForType:NSPasteboardTypeString];
if (text) {
return [text UTF8String];
}
}
return "";
}
// 设置剪贴板文本
// text: UTF-8 编码的文本
// 返回是否成功
static bool SetText(const std::string& text)
{
if (text.empty()) return true;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSString* nsText = [NSString stringWithUTF8String:text.c_str()];
if (nsText) {
return [pasteboard setString:nsText forType:NSPasteboardTypeString];
}
}
return false;
}
// 设置剪贴板文本(从原始字节)
// data: 文本数据(可能是 GBK 或 UTF-8
// len: 数据长度
static bool SetTextRaw(const char* data, size_t len)
{
if (!data || len == 0) return true;
// 服务端发来的文本可能是 GBK 编码,尝试转换为 UTF-8
std::string text = ConvertToUtf8(data, len);
return SetText(text);
}
// 设置剪贴板文件列表
// files: UTF-8 编码的文件路径列表
// 返回是否成功
static bool SetFiles(const std::vector<std::string>& files)
{
if (files.empty()) return true;
@autoreleasepool {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
NSMutableArray<NSURL*>* urls = [NSMutableArray arrayWithCapacity:files.size()];
for (const auto& path : files) {
NSString* nsPath = [NSString stringWithUTF8String:path.c_str()];
if (nsPath) {
NSURL* url = [NSURL fileURLWithPath:nsPath];
if (url) {
[urls addObject:url];
}
}
}
if (urls.count > 0) {
return [pasteboard writeObjects:urls];
}
}
return false;
}
private:
// 检查是否是有效的 UTF-8 序列
static bool IsValidUtf8(const char* data, size_t len)
{
const unsigned char* bytes = (const unsigned char*)data;
size_t i = 0;
while (i < len) {
if (bytes[i] <= 0x7F) {
// ASCII
i++;
} else if ((bytes[i] & 0xE0) == 0xC0) {
// 2-byte sequence
if (i + 1 >= len || (bytes[i + 1] & 0xC0) != 0x80) return false;
i += 2;
} else if ((bytes[i] & 0xF0) == 0xE0) {
// 3-byte sequence
if (i + 2 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80) return false;
i += 3;
} else if ((bytes[i] & 0xF8) == 0xF0) {
// 4-byte sequence
if (i + 3 >= len || (bytes[i + 1] & 0xC0) != 0x80 ||
(bytes[i + 2] & 0xC0) != 0x80 || (bytes[i + 3] & 0xC0) != 0x80) return false;
i += 4;
} else {
return false;
}
}
return true;
}
// 尝试将 GBK 转换为 UTF-8
// 如果已经是 UTF-8直接返回
static std::string ConvertToUtf8(const char* data, size_t len)
{
// 检查是否已经是有效的 UTF-8
if (IsValidUtf8(data, len)) {
return std::string(data, len);
}
// 使用 NSString 进行编码转换 (GBK = CFStringEncodingGB_18030_2000)
@autoreleasepool {
// 尝试 GBK (GB18030) 编码
NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);
NSString* str = [[NSString alloc] initWithBytes:data length:len encoding:gbkEncoding];
if (str) {
const char* utf8 = [str UTF8String];
if (utf8) {
return std::string(utf8);
}
}
}
// 转换失败,返回原始数据
return std::string(data, len);
}
};

View File

@@ -1,6 +1,7 @@
#import "ScreenHandler.h"
#import "H264Encoder.h"
#import "InputHandler.h"
#import "ClipboardHandler.h"
#import "../client/IOCPClient.h"
#import "../common/commands.h"
#import "../common/FileTransferV2.h"
@@ -282,6 +283,56 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
}
break;
case COMMAND_SCREEN_SET_CLIPBOARD:
// 服务端设置剪贴板: [cmd:1][text:N]
if (size > 1) {
if (ClipboardHandler::SetTextRaw((const char*)(data + 1), size - 1)) {
NSLog(@">>> Clipboard SET: %zu bytes", size - 1);
} else {
NSLog(@"*** Clipboard SET failed");
}
}
break;
case COMMAND_SCREEN_GET_CLIPBOARD:
// 服务端请求剪贴板: [cmd:1][hash:64][hmac:16]
// 返回: [TOKEN_CLIPBOARD_TEXT:1][text:N] 或 [COMMAND_GET_FOLDER:1][files]
{
// 优先检查剪贴板中的文件
auto files = ClipboardHandler::GetFiles();
if (!files.empty()) {
// 返回 COMMAND_GET_FOLDER + 文件列表多字符串格式file1\0file2\0\0
std::vector<uint8_t> buf;
buf.push_back(COMMAND_GET_FOLDER);
for (const auto& f : files) {
// 文件路径需要转换为 GBK 编码(服务端预期)
std::string gbkPath = FileTransferV2::utf8ToGbk(f);
buf.insert(buf.end(), gbkPath.begin(), gbkPath.end());
buf.push_back(0); // 每个路径后的 null 终止符
}
buf.push_back(0); // 结束标记
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@">>> Clipboard GET: %zu files", files.size());
break;
}
// 没有文件,返回文本
std::string text = ClipboardHandler::GetText();
if (!text.empty()) {
std::vector<uint8_t> buf(1 + text.size());
buf[0] = TOKEN_CLIPBOARD_TEXT;
memcpy(&buf[1], text.data(), text.size());
m_client->Send2Server((char*)buf.data(), buf.size());
NSLog(@">>> Clipboard GET: %zu bytes text", text.size());
} else {
// 返回空剪贴板
uint8_t empty = TOKEN_CLIPBOARD_TEXT;
m_client->Send2Server((char*)&empty, 1);
NSLog(@">>> Clipboard GET: empty");
}
}
break;
case COMMAND_GET_FILE:
// Server requests file download: [cmd:1][targetDir\0][file1\0file2\0...\0]
// Use V2 protocol to upload files
@@ -303,7 +354,10 @@ void ScreenHandler::OnReceive(uint8_t* data, ULONG size)
ptr += fileGbk.length() + 1;
}
// TODO: If no file list, get from clipboard (ClipboardHandler not implemented yet)
// 如果没有文件列表,从剪贴板获取
if (files.empty()) {
files = ClipboardHandler::GetFiles();
}
if (!files.empty() && !targetDir.empty()) {
NSLog(@">>> COMMAND_GET_FILE: %zu files -> %s", files.size(), targetDir.c_str());

View File

@@ -27,6 +27,7 @@
#import "../common/FileManager.h"
#import "../common/FileTransferV2.h"
#import "../common/logger.h"
#import "ClipboardHandler.h"
// Global state
static std::atomic<bool> g_running(true);
@@ -740,6 +741,22 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
// C2C 准备接收通知
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
Mprintf("** [%p] C2C Prepare received ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (ulLength >= 13) {
uint32_t textLen;
memcpy(&textLen, szBuffer + 9, 4);
if (ulLength >= 13 + textLen && textLen > 0) {
if (!ClipboardHandler::IsAvailable()) {
Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user);
} else {
std::string utf8Text((const char*)szBuffer + 13, textLen);
if (ClipboardHandler::SetText(utf8Text)) {
Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen);
}
}
}
}
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
// V2 文件接收
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);