Compare commits

..

2 Commits

Author SHA1 Message Date
dijunkun
a5a3bfc201 [fix] correct file send rate calculation by using data channel feedback 2025-12-28 19:58:08 +08:00
dijunkun
dbb913e470 [feat] support multi-file transfer 2025-12-28 18:19:26 +08:00
19 changed files with 3013 additions and 5051 deletions

View File

@@ -1,25 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-01-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_UTIL_MAC_H_
#define _WINDOW_UTIL_MAC_H_
struct SDL_Window;
namespace crossdesk {
// Best-effort: keep an SDL window above normal windows on macOS.
// No-op on non-macOS builds.
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
// Best-effort: exclude an SDL window from the Window menu and window cycling.
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
// affects the Window menu and Cmd-` window cycling.
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
} // namespace crossdesk
#endif

View File

@@ -1,64 +0,0 @@
#include "window_util_mac.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
namespace crossdesk {
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
if (!window) {
return nil;
}
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
return nil;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
void* cocoa_window_ptr =
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
if (!cocoa_window_ptr) {
return nil;
}
return (__bridge NSWindow*)cocoa_window_ptr;
#endif
}
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)always_on_top;
return;
}
// Keep above normal windows.
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
[ns_window setLevel:level];
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
[ns_window setCollectionBehavior:behavior];
}
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)excluded;
return;
}
[ns_window setExcludedFromWindowsMenu:excluded];
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
behavior |= NSWindowCollectionBehaviorTransient;
[ns_window setCollectionBehavior:behavior];
}
} // namespace crossdesk
#endif // __APPLE__

File diff suppressed because it is too large Load Diff

View File

@@ -197,12 +197,6 @@ static std::vector<std::string> completed = {
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
static std::vector<std::string> failed = {
reinterpret_cast<const char*>(u8"失败"), "Failed"};
static std::vector<std::string> controller = {
reinterpret_cast<const char*>(u8"控制端:"), "Controller:"};
static std::vector<std::string> file_transfer = {
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
static std::vector<std::string> connection_status = {
reinterpret_cast<const char*>(u8"连接状态:"), "Connection Status:"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {

View File

@@ -207,10 +207,8 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->control_data_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->clipboard_label_.c_str(), true);
props->connection_status_ = ConnectionStatus::Connecting;

File diff suppressed because it is too large Load Diff

View File

@@ -42,50 +42,13 @@
namespace crossdesk {
class Render {
public:
struct FileTransferState {
std::atomic<bool> file_sending_ = false;
std::atomic<uint64_t> file_sent_bytes_ = 0;
std::atomic<uint64_t> file_total_bytes_ = 0;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
std::string remote_id;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
};
struct SubStreamWindowProperties {
Params params_;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "data";
std::string data_label_ = "control_data";
std::string file_label_ = "file";
std::string control_data_label_ = "control_data";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
std::string local_id_ = "";
std::string remote_id_ = "";
bool exit_ = false;
@@ -123,13 +86,8 @@ class Render {
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
// Double-buffered NV12 frame storage. Written by decode callback thread,
// consumed by SDL main thread.
std::mutex video_frame_mutex_;
std::shared_ptr<std::vector<unsigned char>> front_frame_;
std::shared_ptr<std::vector<unsigned char>> back_frame_;
bool render_rect_dirty_ = false;
bool stream_cleanup_pending_ = false;
unsigned char* dst_buffer_ = nullptr;
size_t dst_buffer_capacity_ = 0;
float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0;
@@ -169,10 +127,38 @@ class Render {
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_;
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
using FileTransferInfo = FileTransferState::FileTransferInfo;
FileTransferState file_transfer_;
// File transfer progress
std::atomic<bool> file_sending_ = false;
std::atomic<uint64_t> file_sent_bytes_ = 0;
std::atomic<uint64_t> file_total_bytes_ = 0;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
};
public:
@@ -193,7 +179,6 @@ class Render {
void UpdateInteractions();
void HandleRecentConnections();
void HandleStreamWindow();
void HandleServerWindow();
void Cleanup();
void CleanupFactories();
void CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props);
@@ -203,24 +188,12 @@ class Render {
void UpdateRenderRect();
void ProcessSdlEvent(const SDL_Event& event);
void ProcessFileDropEvent(const SDL_Event& event);
void ProcessSelectedFile(
const std::string& path,
const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id = "");
std::shared_ptr<SubStreamWindowProperties>
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
private:
int CreateStreamRenderWindow();
int TitleBar(bool main_window);
int MainWindow();
int UpdateNotificationWindow();
int StreamWindow();
int ServerWindow();
int RemoteClientInfoWindow();
int LocalWindow();
int RemoteWindow();
int RecentConnectionsWindow();
@@ -237,7 +210,6 @@ class Render {
void Hyperlink(const std::string& label, const std::string& url,
const float window_width);
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
std::string OpenFileDialog(std::string title);
private:
int ConnectTo(const std::string& remote_id, const char* password,
@@ -246,15 +218,11 @@ class Render {
int DestroyMainWindow();
int CreateStreamWindow();
int DestroyStreamWindow();
int CreateServerWindow();
int DestroyServerWindow();
int SetupFontAndStyle(ImFont** system_chinese_font_out);
int SetupFontAndStyle(bool main_window);
int DestroyMainWindowContext();
int DestroyStreamWindowContext();
int DestroyServerWindowContext();
int DrawMainWindow();
int DrawStreamWindow();
int DrawServerWindow();
int ConfirmDeleteConnection();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText(
@@ -290,11 +258,11 @@ class Render {
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
size_t user_id_size, void* user_data);
static void OnNetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data);
static void NetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data);
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
const SDL_Point* area, void* data);
@@ -335,8 +303,7 @@ class Render {
// File transfer helper functions
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
const std::filesystem::path& file_path,
const std::string& file_label,
const std::string& remote_id = "");
const std::string& file_label);
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
int AudioDeviceInit();
@@ -414,7 +381,6 @@ class Render {
ImGuiContext* main_ctx_ = nullptr;
ImFont* main_windows_system_chinese_font_ = nullptr;
ImFont* stream_windows_system_chinese_font_ = nullptr;
ImFont* server_windows_system_chinese_font_ = nullptr;
bool exit_ = false;
const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32
@@ -437,9 +403,6 @@ class Render {
bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false;
bool foucs_on_stream_window_ = false;
bool main_window_minimized_ = false;
uint32_t last_main_minimize_request_tick_ = 0;
uint32_t last_stream_minimize_request_tick_ = 0;
bool audio_capture_ = false;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
@@ -489,7 +452,7 @@ class Render {
bool just_created_ = false;
std::string controlled_remote_id_ = "";
std::string focused_remote_id_ = "";
std::string remote_client_id_ = "";
bool need_to_send_host_info_ = false;
SDL_Event last_mouse_event;
SDL_AudioStream* output_stream_;
uint32_t STREAM_REFRESH_EVENT = 0;
@@ -516,42 +479,6 @@ class Render {
float stream_window_dpi_scaling_w_ = 1.0f;
float stream_window_dpi_scaling_h_ = 1.0f;
// server window render
SDL_Window* server_window_ = nullptr;
SDL_Renderer* server_renderer_ = nullptr;
ImGuiContext* server_ctx_ = nullptr;
// server window properties
bool need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
int server_window_height_default_ = 150;
float server_window_width_ = 250;
float server_window_height_ = 150;
float server_window_title_bar_height_ = 30.0f;
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
int server_window_normal_width_ = 250;
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f;
// server window collapsed mode
bool server_window_collapsed_ = false;
bool server_window_collapsed_dragging_ = false;
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
int server_window_collapsed_drag_start_win_x_ = 0;
int server_window_collapsed_drag_start_win_y_ = 0;
// server window drag normal mode
bool server_window_dragging_ = false;
float server_window_drag_start_mouse_x_ = 0.0f;
float server_window_drag_start_mouse_y_ = 0.0f;
int server_window_drag_start_win_x_ = 0;
int server_window_drag_start_win_y_ = 0;
bool label_inited_ = false;
bool connect_button_pressed_ = false;
bool password_validating_ = false;
@@ -568,7 +495,6 @@ class Render {
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool delete_connection_ = false;
@@ -589,16 +515,11 @@ 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<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
file_id_to_props_;
std::shared_mutex file_id_to_props_mutex_;
// Map file_id to FileTransferState for global file transfer (props == null)
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
std::shared_mutex file_id_to_transfer_state_mutex_;
SDL_AudioDeviceID input_dev_;
SDL_AudioDeviceID output_dev_;
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
@@ -612,8 +533,8 @@ class Render {
uint64_t last_frame_time_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
double new_version_icon_render_start_time_ = 0.0;
uint64_t new_version_icon_last_trigger_time_ = 0;
uint64_t new_version_icon_render_start_time_ = 0;
#ifdef __APPLE__
bool show_request_permission_window_ = true;
#endif
@@ -673,10 +594,6 @@ class Render {
/* ------ server mode ------ */
std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_ = "";
std::string selected_server_remote_hostname_ = "";
FileTransferState file_transfer_;
};
} // namespace crossdesk
#endif

View File

@@ -4,10 +4,8 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <unordered_map>
#include "clipboard.h"
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
@@ -238,31 +236,31 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
render->client_properties_.find(remote_id)->second.get();
if (props->connection_established_) {
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
if (!props->dst_buffer_) {
props->dst_buffer_capacity_ = video_frame->size;
props->dst_buffer_ = new unsigned char[video_frame->size];
}
if (!props->back_frame_) {
props->back_frame_ =
std::make_shared<std::vector<unsigned char>>(video_frame->size);
}
if (props->back_frame_->size() != video_frame->size) {
props->back_frame_->resize(video_frame->size);
}
if (props->dst_buffer_capacity_ < video_frame->size) {
delete props->dst_buffer_;
props->dst_buffer_capacity_ = video_frame->size;
props->dst_buffer_ = new unsigned char[video_frame->size];
}
std::memcpy(props->back_frame_->data(), video_frame->data,
video_frame->size);
memcpy(props->dst_buffer_, video_frame->data, video_frame->size);
bool need_to_update_render_rect = false;
if (props->video_width_ != props->video_width_last_ ||
props->video_height_ != props->video_height_last_) {
need_to_update_render_rect = true;
props->video_width_last_ = props->video_width_;
props->video_height_last_ = props->video_height_;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
props->front_frame_.swap(props->back_frame_);
if (need_to_update_render_rect) {
render->UpdateRenderRect();
}
SDL_Event event;
@@ -321,20 +319,7 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
receiver.SetOnSendAck([render,
remote_user_id](const FileTransferAck& ack) -> int {
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
if (props) {
PeerPtr* peer = props->peer_;
return SendReliableDataFrame(
peer, reinterpret_cast<const char*>(&ack),
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
}
}
receiver.SetOnSendAck([render](const FileTransferAck& ack) -> int {
return SendReliableDataFrame(
render->peer_, reinterpret_cast<const char*>(&ack),
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
@@ -342,14 +327,6 @@ 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);
@@ -375,100 +352,42 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
}
}
Render::FileTransferState* state = nullptr;
if (!props) {
{
std::shared_lock lock(render->file_id_to_transfer_state_mutex_);
auto it = render->file_id_to_transfer_state_.find(ack.file_id);
if (it != render->file_id_to_transfer_state_.end()) {
state = it->second;
}
}
if (!state) {
LOG_WARN("FileTransferAck: no props/state found for file_id={}",
ack.file_id);
return;
}
} else {
state = &props->file_transfer_;
LOG_WARN("FileTransferAck: no props found for file_id={}", ack.file_id);
return;
}
// Update progress based on ACK
state->file_sent_bytes_ = ack.acked_offset;
state->file_total_bytes_ = ack.total_size;
props->file_sent_bytes_ = ack.acked_offset;
props->file_total_bytes_ = ack.total_size;
uint32_t rate_bps = 0;
{
if (props) {
uint32_t data_channel_bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
uint32_t data_channel_bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
if (data_channel_bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
if (data_channel_bitrate > 0 && props->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
uint32_t current_rate = state->file_send_rate_bps_.load();
if (current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps =
static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
}
} else {
rate_bps = state->file_send_rate_bps_.load();
uint32_t current_rate = props->file_send_rate_bps_.load();
if (current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
}
} else {
// Global transfer: no per-connection bitrate available.
// Estimate send rate from ACKed bytes delta over time.
const uint32_t current_rate = state->file_send_rate_bps_.load();
uint32_t estimated_rate_bps = 0;
const auto now = std::chrono::steady_clock::now();
uint64_t last_bytes = 0;
std::chrono::steady_clock::time_point last_time;
{
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
last_bytes = state->file_send_last_bytes_;
last_time = state->file_send_last_update_time_;
}
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
const uint64_t delta_bytes = ack.acked_offset - last_bytes;
const double delta_seconds =
std::chrono::duration<double>(now - last_time).count();
if (delta_seconds > 0.0 && delta_bytes > 0) {
const double bps =
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
if (bps > 0.0) {
const double capped = (std::min)(
bps,
static_cast<double>((std::numeric_limits<uint32_t>::max)()));
estimated_rate_bps = static_cast<uint32_t>(capped);
}
}
}
if (estimated_rate_bps > 0 && current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps = static_cast<uint32_t>(current_rate * 0.7 +
estimated_rate_bps * 0.3);
} else if (estimated_rate_bps > 0) {
rate_bps = estimated_rate_bps;
} else {
rate_bps = current_rate;
}
rate_bps = props->file_send_rate_bps_.load();
}
state->file_send_rate_bps_ = rate_bps;
state->file_send_last_bytes_ = ack.acked_offset;
props->file_send_rate_bps_ = rate_bps;
props->file_send_last_bytes_ = ack.acked_offset;
auto now = std::chrono::steady_clock::now();
state->file_send_last_update_time_ = now;
props->file_send_last_update_time_ = now;
}
// Update file transfer list: update progress and rate
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : state->file_transfer_list_) {
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) {
if (info.file_id == ack.file_id) {
info.sent_bytes = ack.acked_offset;
info.file_size = ack.total_size;
@@ -482,8 +401,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
if ((ack.flags & 0x01) != 0) {
// Transfer completed - receiver has finished receiving the file
// Reopen window if it was closed by user
state->file_transfer_window_visible_ = true;
state->file_sending_ = false; // Mark sending as finished
props->file_transfer_window_visible_ = true;
props->file_sending_ = false; // Mark sending as finished
LOG_INFO(
"File transfer completed via ACK, file_id={}, total_size={}, "
"acked_offset={}",
@@ -491,11 +410,11 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
// Update file transfer list: mark as completed
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : state->file_transfer_list_) {
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) {
if (info.file_id == ack.file_id) {
info.status =
Render::FileTransferState::FileTransferStatus::Completed;
SubStreamWindowProperties::FileTransferStatus::Completed;
info.sent_bytes = ack.total_size;
break;
}
@@ -504,15 +423,9 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
// Unregister file_id mapping after completion
{
if (props) {
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
} else {
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_transfer_state_mutex_);
render->file_id_to_transfer_state_.erase(ack.file_id);
}
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
}
// Process next file in queue
@@ -534,39 +447,24 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(render->client_properties_mutex_);
if (remote_action.type == ControlType::host_infomation) {
if (render->client_properties_.find(remote_id) !=
render->client_properties_.end()) {
// client mode
auto props = render->client_properties_.find(remote_id)->second;
if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
FreeRemoteAction(remote_action);
} else {
// server mode
render->connection_host_names_[remote_id] = std::string(
remote_action.i.host_name, remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]",
render->connection_host_names_[remote_id]);
if (render->client_properties_.find(remote_id) !=
render->client_properties_.end()) {
// local
auto props = render->client_properties_.find(remote_id)->second;
if (remote_action.type == ControlType::host_infomation &&
props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) {
render->display_info_list_.push_back(
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
FreeRemoteAction(remote_action);
}
FreeRemoteAction(remote_action);
} else {
// remote
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
@@ -613,10 +511,6 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
render->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalFingerprintMismatch == status) {
render->signal_connected_ = false;
LOG_ERROR("[{}] signal server fingerprint mismatch", client_id);
render->config_center_->ClearDefaultCertFingerprint();
}
} else {
if (client_id.rfind("C-", 0) != 0) {
@@ -644,9 +538,6 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalFingerprintMismatch == status) {
props->signal_connected_ = false;
LOG_ERROR("[{}] signal server fingerprint mismatch", remote_id);
}
}
}
@@ -668,49 +559,6 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
switch (status) {
case ConnectionStatus::Connected: {
{
RemoteAction remote_action;
remote_action.i.display_num = render->display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
render->display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(render->display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
render->display_info_list_[i].name.c_str(),
render->display_info_list_[i].name.length());
remote_action.i
.display_list[i][render->display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = render->display_info_list_[i].left;
remote_action.i.top[i] = render->display_info_list_[i].top;
remote_action.i.right[i] = render->display_info_list_[i].right;
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
render->control_data_label_.c_str());
FreeRemoteAction(remote_action);
}
if (!render->need_to_create_stream_window_ &&
!render->client_properties_.empty()) {
render->need_to_create_stream_window_ = true;
@@ -727,22 +575,12 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
case ConnectionStatus::Closed: {
props->connection_established_ = false;
props->mouse_control_button_pressed_ = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
if (props->dst_buffer_ && props->stream_texture_) {
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_);
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
props->texture_width_);
}
SDL_Event event;
event.type = render->STREAM_REFRESH_EVENT;
event.user.data1 = props.get();
SDL_PushEvent(&event);
render->CleanSubStreamWindowProperties(props);
break;
}
@@ -775,54 +613,9 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
switch (status) {
case ConnectionStatus::Connected: {
{
RemoteAction remote_action;
remote_action.i.display_num = render->display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
render->display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(render->display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
render->display_info_list_[i].name.c_str(),
render->display_info_list_[i].name.length());
remote_action.i
.display_list[i][render->display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = render->display_info_list_[i].left;
remote_action.i.top[i] = render->display_info_list_[i].top;
remote_action.i.right[i] = render->display_info_list_[i].right;
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(),
render->control_data_label_.c_str());
FreeRemoteAction(remote_action);
}
render->need_to_create_server_window_ = true;
render->is_server_mode_ = true;
render->need_to_send_host_info_ = true;
render->start_screen_capturer_ = true;
render->start_speaker_capturer_ = true;
render->remote_client_id_ = remote_id;
#ifdef CROSSDESK_DEBUG
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
@@ -845,13 +638,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
})) {
render->need_to_destroy_server_window_ = true;
render->is_server_mode_ = false;
render->start_screen_capturer_ = false;
render->start_speaker_capturer_ = false;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
render->remote_client_id_ = "";
render->need_to_send_host_info_ = false;
if (props) props->connection_established_ = false;
if (render->audio_capture_) {
render->StopSpeakerCapturer();
@@ -876,11 +667,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
}
}
void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data) {
void Render::NetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render) {
return;

View File

@@ -15,6 +15,18 @@
namespace crossdesk {
std::string OpenFileDialog(std::string title) {
const char* path = tinyfd_openFileDialog(title.c_str(),
"", // default path
0, // number of filters
nullptr, // filters
nullptr, // filter description
0 // no multiple selection
);
return path ? path : "";
}
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
@@ -41,97 +53,6 @@ int LossRateDisplay(float loss_rate) {
return 0;
}
std::string Render::OpenFileDialog(std::string title) {
const char* path = tinyfd_openFileDialog(title.c_str(),
"", // default path
0, // number of filters
nullptr, // filters
nullptr, // filter description
0 // no multiple selection
);
return path ? path : "";
}
void Render::ProcessSelectedFile(
const std::string& path,
const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id) {
if (path.empty()) {
return;
}
FileTransferState* file_transfer_state =
props ? &props->file_transfer_ : &file_transfer_;
LOG_INFO("Selected file: {}", path.c_str());
std::filesystem::path file_path = std::filesystem::u8path(path);
// Get file size
std::error_code ec;
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
file_size = 0;
}
// Add file to transfer list
{
std::lock_guard<std::mutex> lock(
file_transfer_state->file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
info.file_name = file_path.filename().u8string();
info.file_path = file_path; // Store full path for precise matching
info.file_size = file_size;
info.status = FileTransferState::FileTransferStatus::Queued;
info.sent_bytes = 0;
info.file_id = 0;
info.rate_bps = 0;
file_transfer_state->file_transfer_list_.push_back(info);
}
file_transfer_state->file_transfer_window_visible_ = true;
if (file_transfer_state->file_sending_.load()) {
// Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
queued_file.remote_id = remote_id;
file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
}
LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
} else {
StartFileTransfer(props, file_path, file_label, remote_id);
if (file_transfer_state->file_sending_.load()) {
} else {
// Failed to start (race condition: another file started between
// check and call) Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(
file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
queued_file.remote_id = remote_id;
file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
}
LOG_INFO(
"File added to queue after race condition: {} ({} files in "
"queue)",
file_path.filename().string().c_str(), queue_size);
}
}
}
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float button_width = title_bar_height_ * 0.8f;
float button_height = title_bar_height_ * 0.8f;
@@ -174,8 +95,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
remote_action.d = i;
if (props->connection_status_ == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
@@ -255,8 +176,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
remote_action.type = ControlType::audio_capture;
remote_action.a = props->audio_capture_button_pressed_;
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
@@ -283,7 +204,71 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::string title =
localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
ProcessSelectedFile(path, props, file_label_);
if (!path.empty()) {
LOG_INFO("Selected file: {}", path.c_str());
std::filesystem::path file_path = std::filesystem::path(path);
std::string file_label = file_label_;
// Get file size
std::error_code ec;
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
file_size = 0;
}
// Add file to transfer list
{
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
SubStreamWindowProperties::FileTransferInfo info;
info.file_name = file_path.filename().string();
info.file_path = file_path; // Store full path for precise matching
info.file_size = file_size;
info.status = SubStreamWindowProperties::FileTransferStatus::Queued;
info.sent_bytes = 0;
info.file_id = 0;
info.rate_bps = 0;
props->file_transfer_list_.push_back(info);
}
props->file_transfer_window_visible_ = true;
if (props->file_sending_.load()) {
// Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
props->file_send_queue_.push(queued_file);
queue_size = props->file_send_queue_.size();
}
LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
} else {
StartFileTransfer(props, file_path, file_label);
if (props->file_sending_.load()) {
} else {
// Failed to start (race condition: another file started between
// check and call) Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
props->file_send_queue_.push(queued_file);
queue_size = props->file_send_queue_.size();
}
LOG_INFO(
"File added to queue after race condition: {} ({} files in "
"queue)",
file_path.filename().string().c_str(), queue_size);
}
}
}
}
ImGui::SameLine();

View File

@@ -3,8 +3,7 @@
#include "rd_log.h"
#include "render.h"
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000
namespace crossdesk {
@@ -16,28 +15,14 @@ int Render::TitleBar(bool main_window) {
float title_bar_button_width = title_bar_button_width_;
float title_bar_button_height = title_bar_button_height_;
if (main_window) {
// When the main window is minimized, Dear ImGui may report DisplaySize as
// (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
// stream/server windows (which reuse these metrics) will lose their title
// bars and appear collapsed.
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
title_bar_width = io.DisplaySize.x;
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
title_bar_height_ = title_bar_height;
title_bar_button_width_ = title_bar_button_width;
title_bar_button_height_ = title_bar_button_height;
} else {
// Keep using last known good values.
title_bar_width = title_bar_width_;
title_bar_height = title_bar_height_;
title_bar_height_padding = title_bar_height_;
title_bar_button_width = title_bar_button_width_;
title_bar_button_height = title_bar_button_height_;
}
title_bar_width = io.DisplaySize.x;
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
title_bar_height_ = title_bar_height;
title_bar_button_width_ = title_bar_button_width;
title_bar_button_height_ = title_bar_button_height;
} else {
title_bar_width = io.DisplaySize.x;
title_bar_height = title_bar_button_height_;
@@ -107,11 +92,13 @@ int Render::TitleBar(bool main_window) {
std::string about_str = localization::about[localization_language_index_];
if (update_available_) {
const double now_time = ImGui::GetTime();
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
kNewVersionIconBlinkIntervalSec) {
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
@@ -119,9 +106,9 @@ int Render::TitleBar(bool main_window) {
// render for 1 second
if (show_new_version_icon_) {
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION;
if (now_time - new_version_icon_render_start_time_ >=
kNewVersionIconBlinkOnTimeSec) {
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
show_new_version_icon_ = false;
}
} else {
@@ -150,11 +137,13 @@ int Render::TitleBar(bool main_window) {
}
if (update_available_ && show_new_version_icon_in_menu_) {
const double now_time = ImGui::GetTime();
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
kNewVersionIconBlinkIntervalSec) {
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
@@ -163,13 +152,14 @@ int Render::TitleBar(bool main_window) {
// render for 1 second
if (show_new_version_icon_) {
ImGui::SetWindowFontScale(0.6f);
ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
bar_pos_y - title_bar_button_width * 0.24f));
ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
ImGui::SetCursorPos(
ImVec2(bar_pos_x + title_bar_button_width * 0.15f,
bar_pos_y - title_bar_button_width * 0.325f));
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
ImGui::SetWindowFontScale(1.0f);
if (now_time - new_version_icon_render_start_time_ >=
kNewVersionIconBlinkOnTimeSec) {
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
show_new_version_icon_ = false;
}
}
@@ -197,11 +187,6 @@ int Render::TitleBar(bool main_window) {
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(window_minimize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
if (main_window) {
last_main_minimize_request_tick_ = SDL_GetTicks();
} else {
last_stream_minimize_request_tick_ = SDL_GetTicks();
}
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
}
draw_list->AddLine(

View File

@@ -30,14 +30,12 @@ int BitrateDisplay(int bitrate) {
int Render::FileTransferWindow(
std::shared_ptr<SubStreamWindowProperties>& props) {
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
// Only show window if there are files in transfer list or currently
// transferring
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
file_list = state->file_transfer_list_;
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
file_list = props->file_transfer_list_;
}
// Sort file list: Sending first, then Completed, then Queued, then Failed
@@ -68,7 +66,7 @@ int Render::FileTransferWindow(
// It will be reopened automatically when:
// 1. A file transfer completes (in render_callback.cpp)
// 2. A new file starts sending from queue (in render.cpp)
if (!state->file_transfer_window_visible_) {
if (!props->file_transfer_window_visible_) {
return 0;
}
@@ -89,11 +87,6 @@ int Render::FileTransferWindow(
ImVec2(file_transfer_window_width, file_transfer_window_height),
ImGuiCond_Always);
// Set Chinese font for proper display
if (stream_windows_system_chinese_font_) {
ImGui::PushFont(stream_windows_system_chinese_font_);
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f));
@@ -117,7 +110,7 @@ int Render::FileTransferWindow(
// Close button handling
if (!window_opened) {
state->file_transfer_window_visible_ = false;
props->file_transfer_window_visible_ = false;
ImGui::End();
return 0;
}
@@ -128,9 +121,9 @@ int Render::FileTransferWindow(
} else {
// Use a scrollable child window for the file list
ImGui::SetWindowFontScale(0.5f);
ImGui::BeginChild(
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::BeginChild("FileList",
ImVec2(0, file_transfer_window_height * 0.75f),
ImGuiChildFlags_Border);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
@@ -227,11 +220,6 @@ int Render::FileTransferWindow(
ImGui::SetWindowFontScale(0.5f);
ImGui::End();
ImGui::SetWindowFontScale(1.0f);
// Pop Chinese font if it was pushed
if (stream_windows_system_chinese_font_) {
ImGui::PopFont();
}
} else {
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2);

View File

@@ -1,368 +0,0 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
namespace {
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
void BitrateDisplay(uint32_t bitrate) {
const int num_of_digits = CountDigits(static_cast<int>(bitrate));
if (num_of_digits <= 3) {
ImGui::Text("%u bps", bitrate);
} else if (num_of_digits > 3 && num_of_digits <= 6) {
ImGui::Text("%u kbps", bitrate / 1000);
} else {
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
}
}
std::string FormatBytes(uint64_t bytes) {
char buf[64];
if (bytes < 1024ULL) {
std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
} else if (bytes < 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
} else {
std::snprintf(buf, sizeof(buf), "%.2f GB",
bytes / (1024.0 * 1024.0 * 1024.0));
}
return std::string(buf);
}
} // namespace
int Render::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::Begin("##server_window", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
server_window_title_bar_height_ = title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(
"ServerTitleBar",
ImVec2(server_window_width_, server_window_title_bar_height_),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
float server_title_bar_button_width = server_window_title_bar_height_;
float server_title_bar_button_height = server_window_title_bar_height_;
// Collapse/expand toggle button (FontAwesome icon).
{
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetWindowFontScale(0.5f);
const char* icon =
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
std::string toggle_label = std::string(icon) + "##server_toggle";
if (ImGui::Button(toggle_label.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
if (server_window_) {
int w = 0;
int h = 0;
int x = 0;
int y = 0;
SDL_GetWindowSize(server_window_, &w, &h);
SDL_GetWindowPosition(server_window_, &x, &y);
if (server_window_collapsed_) {
const int normal_h = server_window_normal_height_;
SDL_SetWindowSize(server_window_, w, normal_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = false;
} else {
const int collapsed_h = (int)server_window_title_bar_height_;
// Collapse upward: keep top edge stable.
SDL_SetWindowSize(server_window_, w, collapsed_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = true;
}
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
}
ImGui::EndChild();
ImGui::PopStyleVar();
ImGui::PopStyleColor();
RemoteClientInfoWindow();
ImGui::End();
return 0;
}
int Render::RemoteClientInfoWindow() {
float remote_client_info_window_width = server_window_width_ * 0.8f;
float remote_client_info_window_height =
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
ImGui::BeginChild(
"RemoteClientInfoWindow",
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::string> remote_hostnames;
remote_hostnames.reserve(connection_host_names_.size());
for (const auto& kv : connection_host_names_) {
remote_hostnames.push_back(kv.second);
}
auto find_remote_id_by_hostname =
[this](const std::string& hostname) -> std::string {
for (const auto& kv : connection_host_names_) {
if (kv.second == hostname) {
return kv.first;
}
}
return {};
};
if (!selected_server_remote_hostname_.empty()) {
if (std::find(remote_hostnames.begin(), remote_hostnames.end(),
selected_server_remote_hostname_) == remote_hostnames.end()) {
selected_server_remote_hostname_.clear();
selected_server_remote_id_.clear();
}
}
if (selected_server_remote_hostname_.empty() && !remote_hostnames.empty()) {
selected_server_remote_hostname_ = remote_hostnames.front();
selected_server_remote_id_ =
find_remote_id_by_hostname(selected_server_remote_hostname_);
}
ImGui::SetWindowFontScale(font_scale);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::controller[localization_language_index_].c_str());
ImGui::SameLine();
const char* selected_preview = "-";
if (!selected_server_remote_hostname_.empty()) {
selected_preview = selected_server_remote_hostname_.c_str();
} else if (!remote_client_id_.empty()) {
selected_preview = remote_client_id_.c_str();
}
ImGui::SetNextItemWidth(remote_client_info_window_width *
(localization_language_index_ == 0 ? 0.68f : 0.62f));
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
ImGui::AlignTextToFramePadding();
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
for (int i = 0; i < static_cast<int>(remote_hostnames.size()); i++) {
const bool selected =
(remote_hostnames[i] == selected_server_remote_hostname_);
if (ImGui::Selectable(remote_hostnames[i].c_str(), selected)) {
selected_server_remote_hostname_ = remote_hostnames[i];
selected_server_remote_id_ =
find_remote_id_by_hostname(selected_server_remote_hostname_);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Separator();
ImGui::SetWindowFontScale(font_scale);
if (!selected_server_remote_id_.empty()) {
auto it = connection_status_.find(selected_server_remote_id_);
const ConnectionStatus status = (it == connection_status_.end())
? ConnectionStatus::Closed
: it->second;
ImGui::Text(
"%s",
localization::connection_status[localization_language_index_].c_str());
ImGui::SameLine();
switch (status) {
case ConnectionStatus::Connected:
ImGui::Text(
"%s",
localization::p2p_connected[localization_language_index_].c_str());
break;
case ConnectionStatus::Connecting:
ImGui::Text(
"%s",
localization::p2p_connecting[localization_language_index_].c_str());
break;
case ConnectionStatus::Disconnected:
ImGui::Text("%s",
localization::p2p_disconnected[localization_language_index_]
.c_str());
break;
case ConnectionStatus::Failed:
ImGui::Text(
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
case ConnectionStatus::Closed:
ImGui::Text(
"%s",
localization::p2p_closed[localization_language_index_].c_str());
break;
default:
ImGui::Text(
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
}
}
ImGui::Separator();
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s", localization::file_transfer[localization_language_index_].c_str());
ImGui::SameLine();
if (ImGui::Button(
localization::select_file[localization_language_index_].c_str())) {
std::string title = localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
LOG_INFO("Selected file path: {}", path.c_str());
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
}
if (file_transfer_.file_transfer_window_visible_) {
ImGui::SameLine();
const bool is_sending = file_transfer_.file_sending_.load();
if (is_sending) {
// Simple animation: cycle icon every 0.5s while sending.
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
ICON_FA_ANGLES_UP};
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
(static_cast<int>(sizeof(kFileTransferIcons) /
sizeof(kFileTransferIcons[0])));
ImGui::Text("%s", kFileTransferIcons[icon_index]);
} else {
// Completed.
ImGui::Text("%s", ICON_FA_CHECK);
}
if (ImGui::IsItemHovered()) {
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
float progress = 0.0f;
if (total_bytes > 0) {
progress =
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
}
std::string current_file_name;
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
if (current_file_id != 0) {
std::lock_guard<std::mutex> lock(
file_transfer_.file_transfer_list_mutex_);
for (const auto& info : file_transfer_.file_transfer_list_) {
if (info.file_id == current_file_id) {
current_file_name = info.file_name;
break;
}
}
}
ImGui::BeginTooltip();
if (server_windows_system_chinese_font_) {
ImGui::PushFont(server_windows_system_chinese_font_);
}
ImGui::SetWindowFontScale(0.5f);
if (!current_file_name.empty()) {
ImGui::Text("%s", current_file_name.c_str());
}
if (total_bytes > 0) {
const std::string sent_str = FormatBytes(sent_bytes);
const std::string total_str = FormatBytes(total_bytes);
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
}
const float text_height = ImGui::GetTextLineHeight();
char overlay[32];
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
BitrateDisplay(rate_bps);
ImGui::SetWindowFontScale(1.0f);
if (server_windows_system_chinese_font_) {
ImGui::PopFont();
}
ImGui::EndTooltip();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
ImGui::SameLine();
float close_connection_button_width = server_window_width_ * 0.1f;
float close_connection_button_height = remote_client_info_window_height;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f);
ImGui::SetWindowFontScale(font_scale);
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
close_connection_button_height))) {
if (peer_ && !selected_server_remote_id_.empty()) {
LeaveConnection(peer_, selected_server_remote_id_.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -298,6 +298,10 @@ bool FileReceiver::HandleChunk(const FileChunkHeader& header,
LOG_INFO("FileReceiver: file received complete, file_id={}, size={}",
header.file_id, ctx.received);
if (on_file_complete_) {
on_file_complete_(saved_path);
}
contexts_.erase(header.file_id);
}

View File

@@ -93,6 +93,8 @@ class FileReceiver {
// return true if parsed and processed successfully, false otherwise.
bool OnData(const char* data, size_t size);
void SetOnFileComplete(OnFileComplete cb) { on_file_complete_ = cb; }
void SetOnSendAck(OnSendAck cb) { on_send_ack_ = cb; }
const std::filesystem::path& OutputDir() const { return output_dir_; }
@@ -106,6 +108,7 @@ class FileReceiver {
private:
std::filesystem::path output_dir_;
std::unordered_map<uint32_t, FileContext> contexts_;
OnFileComplete on_file_complete_ = nullptr;
OnSendAck on_send_ack_ = nullptr;
};

View File

@@ -70,9 +70,6 @@ target("common")
set_kind("object")
add_deps("rd_log")
add_files("src/common/*.cpp")
if is_os("macosx") then
add_files("src/common/*.mm")
end
add_includedirs("src/common", {public = true})
target("path_manager")
@@ -176,9 +173,6 @@ 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")