mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-03-23 16:47:32 +08:00
[feat] support clipboard sharing, refs #35
This commit is contained in:
@@ -209,6 +209,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
|||||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||||
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||||
|
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
|
||||||
|
|
||||||
props->connection_status_ = ConnectionStatus::Connecting;
|
props->connection_status_ = ConnectionStatus::Connecting;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "OPPOSans_Regular.h"
|
#include "OPPOSans_Regular.h"
|
||||||
|
#include "clipboard.h"
|
||||||
#include "device_controller_factory.h"
|
#include "device_controller_factory.h"
|
||||||
#include "fa_regular_400.h"
|
#include "fa_regular_400.h"
|
||||||
#include "fa_solid_900.h"
|
#include "fa_solid_900.h"
|
||||||
@@ -717,6 +718,7 @@ int Render::CreateConnectionPeer() {
|
|||||||
AddDataStream(peer_, data_label_.c_str(), false);
|
AddDataStream(peer_, data_label_.c_str(), false);
|
||||||
AddDataStream(peer_, file_label_.c_str(), true);
|
AddDataStream(peer_, file_label_.c_str(), true);
|
||||||
AddDataStream(peer_, file_feedback_label_.c_str(), true);
|
AddDataStream(peer_, file_feedback_label_.c_str(), true);
|
||||||
|
AddDataStream(peer_, clipboard_label_.c_str(), true);
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -1267,6 +1269,26 @@ void Render::InitializeModules() {
|
|||||||
keyboard_capturer_ = (KeyboardCapturer*)device_controller_factory_->Create(
|
keyboard_capturer_ = (KeyboardCapturer*)device_controller_factory_->Create(
|
||||||
DeviceControllerFactory::Device::Keyboard);
|
DeviceControllerFactory::Device::Keyboard);
|
||||||
CreateConnectionPeer();
|
CreateConnectionPeer();
|
||||||
|
|
||||||
|
// start clipboard monitoring with callback to send data to peers
|
||||||
|
Clipboard::StartMonitoring(
|
||||||
|
100, [this](const char* data, size_t size) -> int {
|
||||||
|
// send clipboard data to all connected peers
|
||||||
|
std::shared_lock lock(client_properties_mutex_);
|
||||||
|
int ret = -1;
|
||||||
|
for (const auto& [remote_id, props] : client_properties_) {
|
||||||
|
if (props && props->peer_ && props->connection_established_) {
|
||||||
|
ret = SendReliableDataFrame(props->peer_, data, size,
|
||||||
|
props->clipboard_label_.c_str());
|
||||||
|
if (ret != 0) {
|
||||||
|
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
|
||||||
|
remote_id.c_str(), ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
modules_inited_ = true;
|
modules_inited_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1398,6 +1420,8 @@ void Render::HandleStreamWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Render::Cleanup() {
|
void Render::Cleanup() {
|
||||||
|
Clipboard::StopMonitoring();
|
||||||
|
|
||||||
if (screen_capturer_) {
|
if (screen_capturer_) {
|
||||||
screen_capturer_->Destroy();
|
screen_capturer_->Destroy();
|
||||||
delete screen_capturer_;
|
delete screen_capturer_;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class Render {
|
|||||||
std::string data_label_ = "control_data";
|
std::string data_label_ = "control_data";
|
||||||
std::string file_label_ = "file";
|
std::string file_label_ = "file";
|
||||||
std::string file_feedback_label_ = "file_feedback";
|
std::string file_feedback_label_ = "file_feedback";
|
||||||
|
std::string clipboard_label_ = "clipboard";
|
||||||
std::string local_id_ = "";
|
std::string local_id_ = "";
|
||||||
std::string remote_id_ = "";
|
std::string remote_id_ = "";
|
||||||
bool exit_ = false;
|
bool exit_ = false;
|
||||||
@@ -515,6 +516,7 @@ class Render {
|
|||||||
std::string control_data_label_ = "control_data";
|
std::string control_data_label_ = "control_data";
|
||||||
std::string file_label_ = "file";
|
std::string file_label_ = "file";
|
||||||
std::string file_feedback_label_ = "file_feedback";
|
std::string file_feedback_label_ = "file_feedback";
|
||||||
|
std::string clipboard_label_ = "clipboard";
|
||||||
Params params_;
|
Params params_;
|
||||||
// Map file_id to props for tracking file transfer progress via ACK
|
// Map file_id to props for tracking file transfer progress via ACK
|
||||||
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
|
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "clipboard.h"
|
||||||
#include "device_controller.h"
|
#include "device_controller.h"
|
||||||
#include "file_transfer.h"
|
#include "file_transfer.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
@@ -327,6 +328,14 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
|
|
||||||
receiver.OnData(data, size);
|
receiver.OnData(data, size);
|
||||||
return;
|
return;
|
||||||
|
} else if (source_id == render->clipboard_label_) {
|
||||||
|
if (size > 0) {
|
||||||
|
std::string clipboard_text(data, size);
|
||||||
|
if (!Clipboard::SetText(clipboard_text)) {
|
||||||
|
LOG_ERROR("Failed to set clipboard content from remote");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
} else if (source_id == render->file_feedback_label_) {
|
} else if (source_id == render->file_feedback_label_) {
|
||||||
if (size < sizeof(FileTransferAck)) {
|
if (size < sizeof(FileTransferAck)) {
|
||||||
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
|
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
|
||||||
|
|||||||
534
src/tools/clipboard.cpp
Normal file
534
src/tools/clipboard.cpp
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
|
||||||
|
#include "clipboard.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#elif __linux__
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/extensions/Xfixes.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
std::atomic<bool> g_monitoring{false};
|
||||||
|
std::thread g_monitor_thread;
|
||||||
|
std::mutex g_monitor_mutex;
|
||||||
|
std::string g_last_clipboard_text;
|
||||||
|
int g_check_interval_ms = 100;
|
||||||
|
Clipboard::OnClipboardChanged g_on_clipboard_changed;
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
HWND g_clipboard_wnd = nullptr;
|
||||||
|
const char* g_clipboard_class_name = "CrossDeskClipboardMonitor";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
Display* g_x11_display = nullptr;
|
||||||
|
Atom g_clipboard_atom = None;
|
||||||
|
Atom g_xfixes_selection_notify = None;
|
||||||
|
#endif
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::string Clipboard::GetText() {
|
||||||
|
if (!OpenClipboard(nullptr)) {
|
||||||
|
LOG_ERROR("Clipboard::GetText: failed to open clipboard");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
HANDLE hData = GetClipboardData(CF_UNICODETEXT);
|
||||||
|
if (hData != nullptr) {
|
||||||
|
wchar_t* pszText = static_cast<wchar_t*>(GlobalLock(hData));
|
||||||
|
if (pszText != nullptr) {
|
||||||
|
int size_needed = WideCharToMultiByte(CP_UTF8, 0, pszText, -1, nullptr, 0,
|
||||||
|
nullptr, nullptr);
|
||||||
|
if (size_needed > 0) {
|
||||||
|
// -1 because WideCharToMultiByte contains '\0'
|
||||||
|
result.resize(size_needed - 1);
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, pszText, -1, &result[0], size_needed,
|
||||||
|
nullptr, nullptr);
|
||||||
|
}
|
||||||
|
GlobalUnlock(hData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseClipboard();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::SetText(const std::string& text) {
|
||||||
|
if (!OpenClipboard(nullptr)) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to open clipboard");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EmptyClipboard()) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to empty clipboard");
|
||||||
|
CloseClipboard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert UTF-8 string to wide char
|
||||||
|
int size_needed =
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
|
||||||
|
if (size_needed <= 0) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to convert to wide char");
|
||||||
|
CloseClipboard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size_needed * sizeof(wchar_t));
|
||||||
|
if (hMem == nullptr) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to allocate memory");
|
||||||
|
CloseClipboard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wchar_t* pszText = static_cast<wchar_t*>(GlobalLock(hMem));
|
||||||
|
if (pszText == nullptr) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to lock memory");
|
||||||
|
GlobalFree(hMem);
|
||||||
|
CloseClipboard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, pszText, size_needed);
|
||||||
|
GlobalUnlock(hMem);
|
||||||
|
|
||||||
|
if (SetClipboardData(CF_UNICODETEXT, hMem) == nullptr) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to set clipboard data");
|
||||||
|
GlobalFree(hMem);
|
||||||
|
CloseClipboard();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseClipboard();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::HasText() {
|
||||||
|
if (!OpenClipboard(nullptr)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_text = IsClipboardFormatAvailable(CF_UNICODETEXT) ||
|
||||||
|
IsClipboardFormatAvailable(CF_TEXT);
|
||||||
|
CloseClipboard();
|
||||||
|
return has_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif __APPLE__
|
||||||
|
// macOS implementation is in clipboard_mac.mm
|
||||||
|
#elif __linux__
|
||||||
|
|
||||||
|
std::string Clipboard::GetText() {
|
||||||
|
Display* display = XOpenDisplay(nullptr);
|
||||||
|
if (display == nullptr) {
|
||||||
|
LOG_ERROR("Clipboard::GetText: failed to open X display");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
Window owner = XGetSelectionOwner(display, XA_PRIMARY);
|
||||||
|
if (owner == None) {
|
||||||
|
// Try using CLIPBOARD
|
||||||
|
owner =
|
||||||
|
XGetSelectionOwner(display, XInternAtom(display, "CLIPBOARD", False));
|
||||||
|
if (owner == None) {
|
||||||
|
XCloseDisplay(display);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Atom selection = XA_PRIMARY;
|
||||||
|
Atom target = XInternAtom(display, "UTF8_STRING", False);
|
||||||
|
if (target == None) {
|
||||||
|
target = XA_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
XEvent event;
|
||||||
|
Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, 0,
|
||||||
|
1, 1, 0, 0, 0);
|
||||||
|
XSelectInput(display, window, PropertyChangeMask);
|
||||||
|
|
||||||
|
XConvertSelection(display, selection, target, XA_PRIMARY, window,
|
||||||
|
CurrentTime);
|
||||||
|
|
||||||
|
// Wait for selection conversion to complete
|
||||||
|
bool done = false;
|
||||||
|
while (!done) {
|
||||||
|
XNextEvent(display, &event);
|
||||||
|
if (event.type == SelectionNotify) {
|
||||||
|
if (event.xselection.property == None) {
|
||||||
|
// Try using CLIPBOARD
|
||||||
|
if (selection == XA_PRIMARY) {
|
||||||
|
selection = XInternAtom(display, "CLIPBOARD", False);
|
||||||
|
XConvertSelection(display, selection, target, XA_PRIMARY, window,
|
||||||
|
CurrentTime);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Atom actual_type;
|
||||||
|
int actual_format;
|
||||||
|
unsigned long nitems;
|
||||||
|
unsigned long bytes_after;
|
||||||
|
unsigned char* data = nullptr;
|
||||||
|
|
||||||
|
if (XGetWindowProperty(display, window, XA_PRIMARY, 0, LONG_MAX / 4,
|
||||||
|
False, AnyPropertyType, &actual_type,
|
||||||
|
&actual_format, &nitems, &bytes_after,
|
||||||
|
&data) == Success) {
|
||||||
|
if (data != nullptr) {
|
||||||
|
result = std::string(reinterpret_cast<char*>(data), nitems);
|
||||||
|
XFree(data);
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XDestroyWindow(display, window);
|
||||||
|
XCloseDisplay(display);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::SetText(const std::string& text) {
|
||||||
|
Display* display = XOpenDisplay(nullptr);
|
||||||
|
if (display == nullptr) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to open X display");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, 0,
|
||||||
|
1, 1, 0, 0, 0);
|
||||||
|
Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||||
|
Atom utf8_string = XInternAtom(display, "UTF8_STRING", False);
|
||||||
|
Atom targets = XInternAtom(display, "TARGETS", False);
|
||||||
|
Atom xa_string = XA_STRING;
|
||||||
|
|
||||||
|
XSetSelectionOwner(display, clipboard, window, CurrentTime);
|
||||||
|
if (XGetSelectionOwner(display, clipboard) != window) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to set selection owner");
|
||||||
|
XDestroyWindow(display, window);
|
||||||
|
XCloseDisplay(display);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XChangeProperty(display, window, XA_PRIMARY, utf8_string, 8, PropModeReplace,
|
||||||
|
reinterpret_cast<const unsigned char*>(text.c_str()),
|
||||||
|
static_cast<int>(text.length()));
|
||||||
|
|
||||||
|
XEvent event;
|
||||||
|
while (true) {
|
||||||
|
XNextEvent(display, &event);
|
||||||
|
if (event.type == SelectionRequest) {
|
||||||
|
XSelectionRequestEvent* req = &event.xselectionrequest;
|
||||||
|
XSelectionEvent se;
|
||||||
|
se.type = SelectionNotify;
|
||||||
|
se.display = req->display;
|
||||||
|
se.requestor = req->requestor;
|
||||||
|
se.selection = req->selection;
|
||||||
|
se.time = req->time;
|
||||||
|
se.target = req->target;
|
||||||
|
se.property = req->property;
|
||||||
|
|
||||||
|
if (req->target == targets) {
|
||||||
|
// Return supported formats
|
||||||
|
Atom supported[] = {utf8_string, xa_string, targets};
|
||||||
|
XChangeProperty(display, req->requestor, req->property, XA_ATOM, 32,
|
||||||
|
PropModeReplace,
|
||||||
|
reinterpret_cast<unsigned char*>(supported), 3);
|
||||||
|
se.property = req->property;
|
||||||
|
} else if (req->target == utf8_string || req->target == xa_string) {
|
||||||
|
// Return text data
|
||||||
|
XChangeProperty(display, req->requestor, req->property, req->target, 8,
|
||||||
|
PropModeReplace,
|
||||||
|
reinterpret_cast<const unsigned char*>(text.c_str()),
|
||||||
|
static_cast<int>(text.length()));
|
||||||
|
se.property = req->property;
|
||||||
|
} else {
|
||||||
|
se.property = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
XSendEvent(display, req->requestor, False, 0,
|
||||||
|
reinterpret_cast<XEvent*>(&se));
|
||||||
|
XSync(display, False);
|
||||||
|
} else if (event.type == SelectionClear) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XDestroyWindow(display, window);
|
||||||
|
XCloseDisplay(display);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::HasText() {
|
||||||
|
Display* display = XOpenDisplay(nullptr);
|
||||||
|
if (display == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||||
|
Window owner = XGetSelectionOwner(display, clipboard);
|
||||||
|
if (owner == None) {
|
||||||
|
owner = XGetSelectionOwner(display, XA_PRIMARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
XCloseDisplay(display);
|
||||||
|
return owner != None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
std::string Clipboard::GetText() {
|
||||||
|
LOG_ERROR("Clipboard::GetText: unsupported platform");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::SetText(const std::string& text) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: unsupported platform");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::HasText() {
|
||||||
|
LOG_ERROR("Clipboard::HasText: unsupported platform");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void HandleClipboardChange() {
|
||||||
|
if (!Clipboard::HasText()) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||||
|
if (!g_last_clipboard_text.empty()) {
|
||||||
|
g_last_clipboard_text.clear();
|
||||||
|
LOG_INFO("Clipboard content cleared");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string current_text = Clipboard::GetText();
|
||||||
|
|
||||||
|
// Check if the content has changed
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||||
|
if (current_text != g_last_clipboard_text) {
|
||||||
|
g_last_clipboard_text = current_text;
|
||||||
|
if (!current_text.empty()) {
|
||||||
|
if (g_on_clipboard_changed) {
|
||||||
|
int ret = g_on_clipboard_changed(current_text.c_str(),
|
||||||
|
current_text.length());
|
||||||
|
if (ret != 0) {
|
||||||
|
LOG_WARN("Clipboard callback returned error: {}", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
LRESULT CALLBACK ClipboardWndProc(HWND hwnd, UINT uMsg, WPARAM wParam,
|
||||||
|
LPARAM lParam) {
|
||||||
|
if (uMsg == WM_CLIPBOARDUPDATE) {
|
||||||
|
HandleClipboardChange();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MonitorThreadFunc() {
|
||||||
|
// Create a hidden window to receive clipboard messages
|
||||||
|
WNDCLASSA wc = {0};
|
||||||
|
wc.lpfnWndProc = ClipboardWndProc;
|
||||||
|
wc.hInstance = GetModuleHandle(nullptr);
|
||||||
|
wc.lpszClassName = g_clipboard_class_name;
|
||||||
|
RegisterClassA(&wc);
|
||||||
|
|
||||||
|
g_clipboard_wnd = CreateWindowA(g_clipboard_class_name, nullptr, 0, 0, 0, 0,
|
||||||
|
0, HWND_MESSAGE, nullptr, nullptr, nullptr);
|
||||||
|
if (!g_clipboard_wnd) {
|
||||||
|
LOG_ERROR("Failed to create clipboard monitor window");
|
||||||
|
g_monitoring.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register clipboard format listener
|
||||||
|
if (!AddClipboardFormatListener(g_clipboard_wnd)) {
|
||||||
|
LOG_ERROR("Failed to add clipboard format listener");
|
||||||
|
DestroyWindow(g_clipboard_wnd);
|
||||||
|
g_clipboard_wnd = nullptr;
|
||||||
|
g_monitoring.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Clipboard event monitoring started (Windows)");
|
||||||
|
|
||||||
|
MSG msg;
|
||||||
|
while (g_monitoring.load()) {
|
||||||
|
BOOL ret = GetMessage(&msg, nullptr, 0, 0);
|
||||||
|
if (ret == 0 || ret == -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveClipboardFormatListener(g_clipboard_wnd);
|
||||||
|
if (g_clipboard_wnd) {
|
||||||
|
DestroyWindow(g_clipboard_wnd);
|
||||||
|
g_clipboard_wnd = nullptr;
|
||||||
|
}
|
||||||
|
UnregisterClassA(g_clipboard_class_name, GetModuleHandle(nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif __APPLE__
|
||||||
|
// macOS use notification mechanism, need external functions
|
||||||
|
extern void StartMacOSClipboardMonitoring();
|
||||||
|
extern void StopMacOSClipboardMonitoring();
|
||||||
|
|
||||||
|
static void MonitorThreadFunc() { StartMacOSClipboardMonitoring(); }
|
||||||
|
|
||||||
|
#elif __linux__
|
||||||
|
static void MonitorThreadFunc() {
|
||||||
|
g_x11_display = XOpenDisplay(nullptr);
|
||||||
|
if (!g_x11_display) {
|
||||||
|
LOG_ERROR("Failed to open X display for clipboard monitoring");
|
||||||
|
g_monitoring.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if XFixes extension is available
|
||||||
|
int event_base, error_base;
|
||||||
|
if (!XFixesQueryExtension(g_x11_display, &event_base, &error_base)) {
|
||||||
|
LOG_WARN("XFixes extension not available, falling back to polling");
|
||||||
|
XCloseDisplay(g_x11_display);
|
||||||
|
g_x11_display = nullptr;
|
||||||
|
// fallback to polling mode
|
||||||
|
while (g_monitoring.load()) {
|
||||||
|
std::this_thread::sleep_for(
|
||||||
|
std::chrono::milliseconds(g_check_interval_ms));
|
||||||
|
if (!g_monitoring.load()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
HandleClipboardChange();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_clipboard_atom = XInternAtom(g_x11_display, "CLIPBOARD", False);
|
||||||
|
g_xfixes_selection_notify =
|
||||||
|
XInternAtom(g_x11_display, "XFIXES_SELECTION_NOTIFY", False);
|
||||||
|
|
||||||
|
// Create event window
|
||||||
|
Window root = DefaultRootWindow(g_x11_display);
|
||||||
|
Window event_window =
|
||||||
|
XCreateSimpleWindow(g_x11_display, root, 0, 0, 1, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
// Select events to monitor
|
||||||
|
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom,
|
||||||
|
XFixesSetSelectionOwnerNotifyMask |
|
||||||
|
XFixesSelectionWindowDestroyNotifyMask |
|
||||||
|
XFixesSelectionClientCloseNotifyMask);
|
||||||
|
|
||||||
|
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
|
||||||
|
|
||||||
|
XEvent event;
|
||||||
|
while (g_monitoring.load()) {
|
||||||
|
XNextEvent(g_x11_display, &event);
|
||||||
|
if (event.type == event_base + XFixesSelectionNotify) {
|
||||||
|
HandleClipboardChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);
|
||||||
|
XDestroyWindow(g_x11_display, event_window);
|
||||||
|
XCloseDisplay(g_x11_display);
|
||||||
|
g_x11_display = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
// Fallback to polling mode for other platforms
|
||||||
|
static void MonitorThreadFunc() {
|
||||||
|
while (g_monitoring.load()) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(g_check_interval_ms));
|
||||||
|
if (!g_monitoring.load()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
HandleClipboardChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void Clipboard::StartMonitoring(int check_interval_ms,
|
||||||
|
OnClipboardChanged on_changed) {
|
||||||
|
if (g_monitoring.load()) {
|
||||||
|
LOG_WARN("Clipboard monitoring is already running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_check_interval_ms = check_interval_ms > 0 ? check_interval_ms : 100;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||||
|
g_on_clipboard_changed = on_changed;
|
||||||
|
if (HasText()) {
|
||||||
|
g_last_clipboard_text = GetText();
|
||||||
|
} else {
|
||||||
|
g_last_clipboard_text.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_monitoring.store(true);
|
||||||
|
|
||||||
|
g_monitor_thread = std::thread(MonitorThreadFunc);
|
||||||
|
LOG_INFO("Clipboard event monitoring started");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clipboard::StopMonitoring() {
|
||||||
|
if (!g_monitoring.load()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_monitoring.store(false);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (g_clipboard_wnd) {
|
||||||
|
PostMessage(g_clipboard_wnd, WM_QUIT, 0, 0);
|
||||||
|
}
|
||||||
|
#elif __APPLE__
|
||||||
|
StopMacOSClipboardMonitoring();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (g_monitor_thread.joinable()) {
|
||||||
|
g_monitor_thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||||
|
g_last_clipboard_text.clear();
|
||||||
|
g_on_clipboard_changed = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Clipboard monitoring stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::IsMonitoring() { return g_monitoring.load(); }
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
38
src/tools/clipboard.h
Normal file
38
src/tools/clipboard.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2025-12-28
|
||||||
|
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _CLIPBOARD_H_
|
||||||
|
#define _CLIPBOARD_H_
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
class Clipboard {
|
||||||
|
public:
|
||||||
|
using OnClipboardChanged = std::function<int(const char* data, size_t size)>;
|
||||||
|
|
||||||
|
Clipboard() = default;
|
||||||
|
~Clipboard() = default;
|
||||||
|
|
||||||
|
static std::string GetText();
|
||||||
|
|
||||||
|
static bool SetText(const std::string& text);
|
||||||
|
|
||||||
|
static bool HasText();
|
||||||
|
|
||||||
|
static void StartMonitoring(int check_interval_ms = 100,
|
||||||
|
OnClipboardChanged on_changed = nullptr);
|
||||||
|
|
||||||
|
static void StopMonitoring();
|
||||||
|
|
||||||
|
static bool IsMonitoring();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
113
src/tools/clipboard_mac.mm
Normal file
113
src/tools/clipboard_mac.mm
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2025-12-18
|
||||||
|
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "clipboard.h"
|
||||||
|
|
||||||
|
#include <AppKit/AppKit.h>
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
extern std::atomic<bool> g_monitoring;
|
||||||
|
extern std::mutex g_monitor_mutex;
|
||||||
|
extern std::string g_last_clipboard_text;
|
||||||
|
extern Clipboard::OnClipboardChanged g_on_clipboard_changed;
|
||||||
|
|
||||||
|
static CFRunLoopRef g_monitor_runloop = nullptr;
|
||||||
|
|
||||||
|
std::string Clipboard::GetText() {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||||
|
NSString* string = [pasteboard stringForType:NSPasteboardTypeString];
|
||||||
|
if (string == nil) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return std::string([string UTF8String]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::SetText(const std::string& text) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||||
|
[pasteboard clearContents];
|
||||||
|
NSString* string = [NSString stringWithUTF8String:text.c_str()];
|
||||||
|
if (string == nil) {
|
||||||
|
LOG_ERROR("Clipboard::SetText: failed to create NSString");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
BOOL success = [pasteboard setString:string forType:NSPasteboardTypeString];
|
||||||
|
return success == YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Clipboard::HasText() {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||||
|
NSArray* types = [pasteboard types];
|
||||||
|
return [types containsObject:NSPasteboardTypeString];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern void HandleClipboardChange();
|
||||||
|
|
||||||
|
void StartMacOSClipboardMonitoring() {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||||
|
|
||||||
|
// Store RunLoop reference for waking up
|
||||||
|
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
|
||||||
|
g_monitor_runloop = [runLoop getCFRunLoop];
|
||||||
|
if (g_monitor_runloop) {
|
||||||
|
CFRetain(g_monitor_runloop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register for clipboard change notifications
|
||||||
|
id observer =
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserverForName:NSPasteboardDidChangeNotification
|
||||||
|
object:pasteboard
|
||||||
|
queue:nil
|
||||||
|
usingBlock:^(NSNotification* notification) {
|
||||||
|
if (!g_monitoring.load()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HandleClipboardChange();
|
||||||
|
}];
|
||||||
|
|
||||||
|
LOG_INFO("Clipboard event monitoring started (macOS)");
|
||||||
|
|
||||||
|
while (g_monitoring.load()) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:0.1];
|
||||||
|
[runLoop runMode:NSDefaultRunLoopMode beforeDate:date];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:observer
|
||||||
|
name:NSPasteboardDidChangeNotification
|
||||||
|
object:pasteboard];
|
||||||
|
if (g_monitor_runloop) {
|
||||||
|
CFRelease(g_monitor_runloop);
|
||||||
|
g_monitor_runloop = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopMacOSClipboardMonitoring() {
|
||||||
|
// Wake up the RunLoop immediately so it can check g_monitoring and exit
|
||||||
|
// This ensures the RunLoop exits promptly instead of waiting up to 0.1 seconds
|
||||||
|
if (g_monitor_runloop) {
|
||||||
|
CFRunLoopWakeUp(g_monitor_runloop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -173,6 +173,9 @@ target("tools")
|
|||||||
set_kind("object")
|
set_kind("object")
|
||||||
add_deps("rd_log")
|
add_deps("rd_log")
|
||||||
add_files("src/tools/*.cpp")
|
add_files("src/tools/*.cpp")
|
||||||
|
if is_os("macosx") then
|
||||||
|
add_files("src/tools/*.mm")
|
||||||
|
end
|
||||||
add_includedirs("src/tools", {public = true})
|
add_includedirs("src/tools", {public = true})
|
||||||
|
|
||||||
target("gui")
|
target("gui")
|
||||||
|
|||||||
Reference in New Issue
Block a user