mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-03-28 12:27:21 +08:00
Compare commits
2 Commits
v1.2.0
...
a5a3bfc201
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5a3bfc201 | ||
|
|
dbb913e470 |
@@ -209,7 +209,6 @@ 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,7 +9,6 @@
|
|||||||
#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"
|
||||||
@@ -718,7 +717,6 @@ 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;
|
||||||
@@ -1269,36 +1267,6 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = SendReliableDataFrame(peer_, data, size,
|
|
||||||
clipboard_label_.c_str());
|
|
||||||
if (ret != 0) {
|
|
||||||
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
|
|
||||||
remote_id_display_, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
modules_inited_ = true;
|
modules_inited_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1430,8 +1398,6 @@ 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,7 +49,6 @@ 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;
|
||||||
@@ -516,7 +515,6 @@ 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,7 +6,6 @@
|
|||||||
#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"
|
||||||
@@ -328,14 +327,6 @@ 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);
|
||||||
|
|||||||
@@ -1,534 +0,0 @@
|
|||||||
|
|
||||||
#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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* @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
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
/*
|
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track changeCount to detect clipboard changes
|
|
||||||
// Use __block to allow modification inside the block
|
|
||||||
__block NSInteger lastChangeCount = [pasteboard changeCount];
|
|
||||||
|
|
||||||
LOG_INFO("Clipboard event monitoring started (macOS)");
|
|
||||||
|
|
||||||
// Use a timer to periodically check changeCount
|
|
||||||
// This is more reliable than NSPasteboardDidChangeNotification which may not be available
|
|
||||||
NSTimer* timer =
|
|
||||||
[NSTimer scheduledTimerWithTimeInterval:0.1
|
|
||||||
repeats:YES
|
|
||||||
block:^(NSTimer* timer) {
|
|
||||||
if (!g_monitoring.load()) {
|
|
||||||
[timer invalidate];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NSInteger currentChangeCount = [pasteboard changeCount];
|
|
||||||
if (currentChangeCount != lastChangeCount) {
|
|
||||||
lastChangeCount = currentChangeCount;
|
|
||||||
HandleClipboardChange();
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
while (g_monitoring.load()) {
|
|
||||||
@autoreleasepool {
|
|
||||||
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:0.1];
|
|
||||||
[runLoop runMode:NSDefaultRunLoopMode beforeDate:date];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
[timer invalidate];
|
|
||||||
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
|
|
||||||
@@ -298,6 +298,10 @@ bool FileReceiver::HandleChunk(const FileChunkHeader& header,
|
|||||||
LOG_INFO("FileReceiver: file received complete, file_id={}, size={}",
|
LOG_INFO("FileReceiver: file received complete, file_id={}, size={}",
|
||||||
header.file_id, ctx.received);
|
header.file_id, ctx.received);
|
||||||
|
|
||||||
|
if (on_file_complete_) {
|
||||||
|
on_file_complete_(saved_path);
|
||||||
|
}
|
||||||
|
|
||||||
contexts_.erase(header.file_id);
|
contexts_.erase(header.file_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class FileReceiver {
|
|||||||
// return true if parsed and processed successfully, false otherwise.
|
// return true if parsed and processed successfully, false otherwise.
|
||||||
bool OnData(const char* data, size_t size);
|
bool OnData(const char* data, size_t size);
|
||||||
|
|
||||||
|
void SetOnFileComplete(OnFileComplete cb) { on_file_complete_ = cb; }
|
||||||
|
|
||||||
void SetOnSendAck(OnSendAck cb) { on_send_ack_ = cb; }
|
void SetOnSendAck(OnSendAck cb) { on_send_ack_ = cb; }
|
||||||
|
|
||||||
const std::filesystem::path& OutputDir() const { return output_dir_; }
|
const std::filesystem::path& OutputDir() const { return output_dir_; }
|
||||||
@@ -106,6 +108,7 @@ class FileReceiver {
|
|||||||
private:
|
private:
|
||||||
std::filesystem::path output_dir_;
|
std::filesystem::path output_dir_;
|
||||||
std::unordered_map<uint32_t, FileContext> contexts_;
|
std::unordered_map<uint32_t, FileContext> contexts_;
|
||||||
|
OnFileComplete on_file_complete_ = nullptr;
|
||||||
OnSendAck on_send_ack_ = nullptr;
|
OnSendAck on_send_ack_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -173,9 +173,6 @@ 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