diff --git a/src/gui/panels/remote_peer_panel.cpp b/src/gui/panels/remote_peer_panel.cpp index 95a2ddc..804927d 100644 --- a/src/gui/panels/remote_peer_panel.cpp +++ b/src/gui/panels/remote_peer_panel.cpp @@ -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->file_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; diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 91544a6..03abd43 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -9,6 +9,7 @@ #include #include "OPPOSans_Regular.h" +#include "clipboard.h" #include "device_controller_factory.h" #include "fa_regular_400.h" #include "fa_solid_900.h" @@ -717,6 +718,7 @@ int Render::CreateConnectionPeer() { AddDataStream(peer_, data_label_.c_str(), false); AddDataStream(peer_, file_label_.c_str(), true); AddDataStream(peer_, file_feedback_label_.c_str(), true); + AddDataStream(peer_, clipboard_label_.c_str(), true); return 0; } else { return -1; @@ -1267,6 +1269,26 @@ void Render::InitializeModules() { keyboard_capturer_ = (KeyboardCapturer*)device_controller_factory_->Create( DeviceControllerFactory::Device::Keyboard); 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; } } @@ -1398,6 +1420,8 @@ void Render::HandleStreamWindow() { } void Render::Cleanup() { + Clipboard::StopMonitoring(); + if (screen_capturer_) { screen_capturer_->Destroy(); delete screen_capturer_; diff --git a/src/gui/render.h b/src/gui/render.h index f1a5698..abf05df 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -49,6 +49,7 @@ class Render { std::string data_label_ = "control_data"; std::string file_label_ = "file"; std::string file_feedback_label_ = "file_feedback"; + std::string clipboard_label_ = "clipboard"; std::string local_id_ = ""; std::string remote_id_ = ""; bool exit_ = false; @@ -515,6 +516,7 @@ class Render { std::string control_data_label_ = "control_data"; std::string file_label_ = "file"; std::string file_feedback_label_ = "file_feedback"; + std::string clipboard_label_ = "clipboard"; Params params_; // Map file_id to props for tracking file transfer progress via ACK std::unordered_map> diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index 0a0c5bb..5390509 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -6,6 +6,7 @@ #include #include +#include "clipboard.h" #include "device_controller.h" #include "file_transfer.h" #include "localization.h" @@ -327,6 +328,14 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size, receiver.OnData(data, size); 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_) { if (size < sizeof(FileTransferAck)) { LOG_ERROR("FileTransferAck: buffer too small, size={}", size); diff --git a/src/tools/clipboard.cpp b/src/tools/clipboard.cpp new file mode 100644 index 0000000..d7258b0 --- /dev/null +++ b/src/tools/clipboard.cpp @@ -0,0 +1,534 @@ + +#include "clipboard.h" + +#include +#include +#include +#include +#include + +#include "rd_log.h" + +#ifdef _WIN32 +#include +#elif __linux__ +#include +#include +#include + +#include +#include +#endif + +namespace crossdesk { + +std::atomic 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(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(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(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(text.c_str()), + static_cast(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(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(text.c_str()), + static_cast(text.length())); + se.property = req->property; + } else { + se.property = None; + } + + XSendEvent(display, req->requestor, False, 0, + reinterpret_cast(&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 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 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 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 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 diff --git a/src/tools/clipboard.h b/src/tools/clipboard.h new file mode 100644 index 0000000..a1bd7c2 --- /dev/null +++ b/src/tools/clipboard.h @@ -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 +#include + +namespace crossdesk { + +class Clipboard { + public: + using OnClipboardChanged = std::function; + + 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 \ No newline at end of file diff --git a/src/tools/clipboard_mac.mm b/src/tools/clipboard_mac.mm new file mode 100644 index 0000000..3914692 --- /dev/null +++ b/src/tools/clipboard_mac.mm @@ -0,0 +1,113 @@ +/* + * @Author: DI JUNKUN + * @Date: 2025-12-18 + * Copyright (c) 2025 by DI JUNKUN, All Rights Reserved. + */ + +#include "clipboard.h" + +#include +#include +#include +#include +#include +#include + +#include "rd_log.h" + +namespace crossdesk { + +extern std::atomic 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 diff --git a/xmake.lua b/xmake.lua index 9af381b..2cb95f4 100644 --- a/xmake.lua +++ b/xmake.lua @@ -173,6 +173,9 @@ target("tools") set_kind("object") add_deps("rd_log") add_files("src/tools/*.cpp") + if is_os("macosx") then + add_files("src/tools/*.mm") + end add_includedirs("src/tools", {public = true}) target("gui")