Compare commits

...

14 Commits

Author SHA1 Message Date
dijunkun 0681f6540d [fix] fix persistent scrollbar in recent connections area when empty, refs #85 2026-06-26 17:29:04 +08:00
dijunkun d843901550 [fix] fix intermittent crash on incoming connection by guarding shared connection status state across threads 2026-06-26 17:21:09 +08:00
dijunkun b41630ebb4 [fix] fix blank connection status popup by handling Gathering state 2026-06-26 16:23:15 +08:00
dijunkun a815eecc73 [fix] fix close-issue workflow env indentation, pagination, and activity time base 2026-06-26 16:14:09 +08:00
dijunkun 7e0984fe9c [chore] refine online status indicator with gradient glow and remove status text 2026-06-26 16:09:21 +08:00
dijunkun 9c28cd2ab2 [fix] fix crash when switching monitors due to race condition and missing bounds checks in screen capturer 2026-06-26 14:44:30 +08:00
dijunkun 3c4000bdbb [fix] add missing Xft development dependency for Linux tray build 2026-06-25 00:35:25 +08:00
dijunkun d8e9fa5bba [feat] support minimizing to the system tray on Linux when closing 2026-06-23 17:11:42 +08:00
dijunkun e026491b9f [feat] support minimizing to the system tray on macOS when closing, refs #87 2026-06-23 17:11:42 +08:00
dijunkun 009699b375 [chore] optimize online status indicator icon 2026-06-22 16:52:51 +08:00
dijunkun 3677588a3d [fix] align recent connection name footer 2026-06-18 10:53:31 +08:00
dijunkun 3d280053a7 [feat] add custom names for recent connection devices and improve panel display 2026-06-17 17:48:17 +08:00
dijunkun fbde3f6a47 [fix] prevent remote keyboard keys from getting stuck 2026-06-15 17:28:34 +08:00
dijunkun 1c1a33fdce [fix] use evdev keycodes for Wayland keyboard input 2026-06-12 11:25:00 +08:00
30 changed files with 2962 additions and 312 deletions
+1
View File
@@ -104,6 +104,7 @@ jobs:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
apt install -y libxft-dev
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
+14 -25
View File
@@ -4,6 +4,7 @@ on:
schedule:
# run every day at midnight
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
issues: write
@@ -15,19 +16,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check inactive issues and close them
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const { data: issues } = await github.rest.issues.listForRepo({
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
const now = Date.now();
// paginate through all open issues (listForRepo also returns PRs)
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
});
const now = new Date().getTime();
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
for (const issue of issues) {
// skip pull requests (they are also returned by listForRepo)
if (issue.pull_request) continue;
@@ -38,26 +41,14 @@ jobs:
continue;
}
// fetch comments for this issue
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
});
// determine the "last activity" time
let lastActivityTime;
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
lastActivityTime = new Date(lastComment.updated_at).getTime();
} else {
lastActivityTime = new Date(issue.created_at).getTime();
}
// last activity time = the issue's own updated_at, which is
// refreshed on comments, labels, etc. This avoids relying on
// fetching comments and is accurate even when comments are edited.
const lastActivityTime = new Date(issue.updated_at).getTime();
// check inactivity
if (now - lastActivityTime > inactivePeriod) {
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`);
console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
await github.rest.issues.createComment({
owner: context.repo.owner,
@@ -76,5 +67,3 @@ jobs:
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
}
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -1
View File
@@ -128,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
if (pid > 0) _exit(0);
umask(0);
chdir("/");
if (chdir("/") != 0) {
std::cerr << "Failed to change daemon working directory to /: "
<< std::strerror(errno) << std::endl;
return false;
}
// redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null
+63 -6
View File
@@ -21,12 +21,13 @@ namespace crossdesk {
typedef enum {
mouse = 0,
keyboard,
audio_capture,
host_infomation,
display_id,
service_status,
service_command,
keyboard = 1,
audio_capture = 2,
host_infomation = 3,
display_id = 4,
service_status = 5,
service_command = 6,
keyboard_state = 7,
} ControlType;
typedef enum {
move = 0,
@@ -55,6 +56,20 @@ typedef struct {
KeyFlag flag;
} Key;
inline constexpr size_t kMaxKeyboardStateKeys = 32;
typedef struct {
size_t key_value;
uint32_t scan_code;
bool extended;
} KeyboardStateKey;
typedef struct {
uint32_t seq;
size_t pressed_count;
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
typedef struct {
char host_name[64];
size_t host_name_size;
@@ -80,6 +95,7 @@ struct RemoteAction {
union {
Mouse m;
Key k;
KeyboardState ks;
HostInfo i;
bool a;
int d;
@@ -111,6 +127,20 @@ struct RemoteAction {
{"extended", a.k.extended},
{"flag", a.k.flag}};
break;
case ControlType::keyboard_state: {
json keys = json::array();
const size_t pressed_count =
a.ks.pressed_count < kMaxKeyboardStateKeys
? a.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
{"scan_code", a.ks.pressed_keys[idx].scan_code},
{"extended", a.ks.pressed_keys[idx].extended}});
}
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::audio_capture:
j["audio_capture"] = a.a;
break;
@@ -162,6 +192,33 @@ struct RemoteAction {
out.k.extended = j.at("keyboard").value("extended", false);
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break;
case ControlType::keyboard_state: {
const auto& keyboard_state_json = j.at("keyboard_state");
out.ks.seq = keyboard_state_json.value("seq", 0u);
out.ks.pressed_count = 0;
const auto keys_json =
keyboard_state_json.value("pressed_keys", json::array());
if (!keys_json.is_array()) {
break;
}
const size_t count =
keys_json.size() < kMaxKeyboardStateKeys
? keys_json.size()
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < count; ++idx) {
const auto& key_json = keys_json[idx];
out.ks.pressed_keys[idx].key_value =
key_json.at("key_value").get<size_t>();
out.ks.pressed_keys[idx].scan_code =
key_json.value("scan_code", static_cast<uint32_t>(0));
out.ks.pressed_keys[idx].extended =
key_json.value("extended", false);
}
out.ks.pressed_count = count;
break;
}
case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>();
break;
@@ -8,6 +8,7 @@
#include <dbus/dbus.h>
#endif
#include "linux_evdev_keycode.h"
#include "rd_log.h"
#include "wayland_portal_shared.h"
@@ -579,33 +580,46 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code,
bool extended) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
(void)scan_code;
(void)extended;
if (!dbus_connection_ || wayland_session_handle_.empty()) {
return -1;
}
const auto key_it = vkCodeToX11KeySym.find(key_code);
if (key_it == vkCodeToX11KeySym.end()) {
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
const int evdev_keycode =
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
if (evdev_keycode >= 0 &&
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
return 0;
}
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
const int keysym = key_it->second;
const auto key_it = vkCodeToX11KeySym.find(key_code);
if (key_it == vkCodeToX11KeySym.end()) {
if (evdev_keycode >= 0) {
LOG_ERROR(
"Failed to send Wayland keyboard keycode event, vk_code={}, "
"evdev_keycode={}, is_down={}",
key_code, evdev_keycode, is_down);
return -3;
}
return 0;
}
// Prefer keycode injection to preserve physical-key semantics and avoid
// implicit Shift interpretation for uppercase keysyms.
if (display_) {
const int keysym = key_it->second;
const KeyCode x11_keycode =
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
if (x11_keycode > 8) {
const int evdev_keycode = static_cast<int>(x11_keycode) - 8;
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
return 0;
}
}
}
const int keysym = key_it->second;
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
return 0;
+295
View File
@@ -0,0 +1,295 @@
#ifndef _LINUX_EVDEV_KEYCODE_H_
#define _LINUX_EVDEV_KEYCODE_H_
#include <linux/input-event-codes.h>
#include <cstdint>
namespace crossdesk {
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
bool extended) {
const uint32_t base_scan_code = scan_code & 0xFFu;
if (base_scan_code == 0) {
return -1;
}
if (extended) {
switch (base_scan_code) {
case 0x1C:
return KEY_KPENTER;
case 0x1D:
return KEY_RIGHTCTRL;
case 0x35:
return KEY_KPSLASH;
case 0x38:
return KEY_RIGHTALT;
case 0x47:
return KEY_HOME;
case 0x48:
return KEY_UP;
case 0x49:
return KEY_PAGEUP;
case 0x4B:
return KEY_LEFT;
case 0x4D:
return KEY_RIGHT;
case 0x4F:
return KEY_END;
case 0x50:
return KEY_DOWN;
case 0x51:
return KEY_PAGEDOWN;
case 0x52:
return KEY_INSERT;
case 0x53:
return KEY_DELETE;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
default:
return -1;
}
}
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
// with the low byte of the Windows scan code.
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
base_scan_code == 0x58) {
return static_cast<int>(base_scan_code);
}
return -1;
}
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
switch (key_code) {
case 0x08:
return KEY_BACKSPACE;
case 0x09:
return KEY_TAB;
case 0x0D:
return KEY_ENTER;
case 0x10:
case 0xA0:
return KEY_LEFTSHIFT;
case 0x11:
case 0xA2:
return KEY_LEFTCTRL;
case 0x12:
case 0xA4:
return KEY_LEFTALT;
case 0x13:
return KEY_PAUSE;
case 0x14:
return KEY_CAPSLOCK;
case 0x1B:
return KEY_ESC;
case 0x20:
return KEY_SPACE;
case 0x21:
return KEY_PAGEUP;
case 0x22:
return KEY_PAGEDOWN;
case 0x23:
return KEY_END;
case 0x24:
return KEY_HOME;
case 0x25:
return KEY_LEFT;
case 0x26:
return KEY_UP;
case 0x27:
return KEY_RIGHT;
case 0x28:
return KEY_DOWN;
case 0x2C:
return KEY_SYSRQ;
case 0x2D:
return KEY_INSERT;
case 0x2E:
return KEY_DELETE;
case 0x30:
return KEY_0;
case 0x31:
return KEY_1;
case 0x32:
return KEY_2;
case 0x33:
return KEY_3;
case 0x34:
return KEY_4;
case 0x35:
return KEY_5;
case 0x36:
return KEY_6;
case 0x37:
return KEY_7;
case 0x38:
return KEY_8;
case 0x39:
return KEY_9;
case 0x41:
return KEY_A;
case 0x42:
return KEY_B;
case 0x43:
return KEY_C;
case 0x44:
return KEY_D;
case 0x45:
return KEY_E;
case 0x46:
return KEY_F;
case 0x47:
return KEY_G;
case 0x48:
return KEY_H;
case 0x49:
return KEY_I;
case 0x4A:
return KEY_J;
case 0x4B:
return KEY_K;
case 0x4C:
return KEY_L;
case 0x4D:
return KEY_M;
case 0x4E:
return KEY_N;
case 0x4F:
return KEY_O;
case 0x50:
return KEY_P;
case 0x51:
return KEY_Q;
case 0x52:
return KEY_R;
case 0x53:
return KEY_S;
case 0x54:
return KEY_T;
case 0x55:
return KEY_U;
case 0x56:
return KEY_V;
case 0x57:
return KEY_W;
case 0x58:
return KEY_X;
case 0x59:
return KEY_Y;
case 0x5A:
return KEY_Z;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
case 0x60:
return KEY_KP0;
case 0x61:
return KEY_KP1;
case 0x62:
return KEY_KP2;
case 0x63:
return KEY_KP3;
case 0x64:
return KEY_KP4;
case 0x65:
return KEY_KP5;
case 0x66:
return KEY_KP6;
case 0x67:
return KEY_KP7;
case 0x68:
return KEY_KP8;
case 0x69:
return KEY_KP9;
case 0x6A:
return KEY_KPASTERISK;
case 0x6B:
return KEY_KPPLUS;
case 0x6D:
return KEY_KPMINUS;
case 0x6E:
return KEY_KPDOT;
case 0x6F:
return KEY_KPSLASH;
case 0x70:
return KEY_F1;
case 0x71:
return KEY_F2;
case 0x72:
return KEY_F3;
case 0x73:
return KEY_F4;
case 0x74:
return KEY_F5;
case 0x75:
return KEY_F6;
case 0x76:
return KEY_F7;
case 0x77:
return KEY_F8;
case 0x78:
return KEY_F9;
case 0x79:
return KEY_F10;
case 0x7A:
return KEY_F11;
case 0x7B:
return KEY_F12;
case 0x90:
return KEY_NUMLOCK;
case 0x91:
return KEY_SCROLLLOCK;
case 0xA1:
return KEY_RIGHTSHIFT;
case 0xA3:
return KEY_RIGHTCTRL;
case 0xA5:
return KEY_RIGHTALT;
case 0xBA:
return KEY_SEMICOLON;
case 0xBB:
return KEY_EQUAL;
case 0xBC:
return KEY_COMMA;
case 0xBD:
return KEY_MINUS;
case 0xBE:
return KEY_DOT;
case 0xBF:
return KEY_SLASH;
case 0xC0:
return KEY_GRAVE;
case 0xDB:
return KEY_LEFTBRACE;
case 0xDC:
return KEY_BACKSLASH;
case 0xDD:
return KEY_RIGHTBRACE;
case 0xDE:
return KEY_APOSTROPHE;
default:
return -1;
}
}
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
uint32_t scan_code,
bool extended) {
const int scan_keycode =
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
if (scan_keycode >= 0) {
return scan_keycode;
}
return LinuxEvdevKeycodeFromWindowsVk(key_code);
}
} // namespace crossdesk
#endif
@@ -87,7 +87,7 @@ struct TranslationRow {
X(install_windows_service, u8"安装", "Install", u8"Установить") \
X(windows_service_settings_label, u8"锁屏控制服务:", \
"Lock Screen Service:", u8"Служба блокировки экрана:") \
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
u8"Больше не напоминать") \
X(windows_service_prompt_suppressed_message, \
@@ -159,13 +159,16 @@ struct TranslationRow {
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
"Certificate verification failed. Reinstall the self-hosted root certificate.", \
"Certificate verification failed. Reinstall the self-hosted root " \
"certificate.", \
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
u8"Сбор кандидатов...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
@@ -180,6 +183,14 @@ struct TranslationRow {
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(connection_alias, u8"修改名称", "Edit Alias", \
u8"Изменить имя подключения") \
X(delete_connection, u8"删除连接", "Delete Connection", \
u8"Удалить подключение") \
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
u8"Подключиться") \
X(input_connection_alias, u8"请输入连接名称:", \
"Please input connection name:", u8"Введите имя подключения:") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
+389 -110
View File
@@ -1,9 +1,37 @@
#include <algorithm>
#include <cctype>
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
namespace {
std::string TrimConnectionAlias(const char* value) {
std::string alias = value ? value : "";
auto not_space = [](unsigned char ch) { return !std::isspace(ch); };
alias.erase(alias.begin(),
std::find_if(alias.begin(), alias.end(), not_space));
alias.erase(std::find_if(alias.rbegin(), alias.rend(), not_space).base(),
alias.end());
return alias;
}
void SetDarkTextTooltip(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.05f, 0.05f, 0.05f, 1.0f));
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", text);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
ImGui::PopStyleColor();
}
} // namespace
int Render::RecentConnectionsWindow() {
ImGuiIO& io = ImGui::GetIO();
@@ -51,8 +79,9 @@ int Render::ShowRecentConnections() {
float recent_connection_button_width = recent_connection_image_width * 0.15f;
float recent_connection_button_height =
recent_connection_image_height * 0.25f;
float recent_connection_dummy_button_width =
recent_connection_image_width - 2 * recent_connection_button_width;
float recent_connection_footer_height =
recent_connection_button_height * 1.18f;
float recent_connection_name_width = recent_connection_image_width;
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
@@ -61,14 +90,16 @@ int Render::ShowRecentConnections() {
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
const ImGuiWindowFlags container_flags =
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollWithMouse |
(recent_connections_.empty()
? ImGuiWindowFlags_None
: ImGuiWindowFlags_AlwaysHorizontalScrollbar);
ImGui::BeginChild(
"RecentConnectionsContainer",
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGuiChildFlags_Borders, container_flags);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
size_t recent_connections_count = recent_connections_.size();
@@ -90,149 +121,311 @@ int Render::ShowRecentConnections() {
// password length is 6
// connection_info -> remote_id + 'Y' + host_name + '@' + password
// -> remote_id + 'N' + host_name
if ('Y' == connection_info[9] && connection_info.size() >= 16) {
bool invalid_connection_info = false;
if (connection_info.size() > 9 && 'Y' == connection_info[9] &&
connection_info.size() >= 16) {
size_t pos_y = connection_info.find('Y');
size_t pos_at = connection_info.find('@');
if (pos_y == std::string::npos || pos_at == std::string::npos ||
pos_y >= pos_at) {
LOG_ERROR("Invalid filename");
continue;
invalid_connection_info = true;
} else {
it.second.remote_id = connection_info.substr(0, pos_y);
it.second.remote_host_name =
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
it.second.password = connection_info.substr(pos_at + 1);
it.second.remember_password = true;
}
it.second.remote_id = connection_info.substr(0, pos_y);
it.second.remote_host_name =
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
it.second.password = connection_info.substr(pos_at + 1);
it.second.remember_password = true;
} else if ('N' == connection_info[9] && connection_info.size() >= 10) {
} else if (connection_info.size() > 9 && 'N' == connection_info[9] &&
connection_info.size() >= 10) {
size_t pos_n = connection_info.find('N');
size_t pos_at = connection_info.find('@');
if (pos_n == std::string::npos) {
LOG_ERROR("Invalid filename");
continue;
invalid_connection_info = true;
} else {
it.second.remote_id = connection_info.substr(0, pos_n);
it.second.remote_host_name = connection_info.substr(pos_n + 1);
it.second.password = "";
it.second.remember_password = false;
}
it.second.remote_id = connection_info.substr(0, pos_n);
it.second.remote_host_name = connection_info.substr(pos_n + 1);
it.second.password = "";
it.second.remember_password = false;
} else {
it.second.remote_host_name = "unknown";
invalid_connection_info = true;
}
if (invalid_connection_info) {
it.second.remote_id = connection_info.substr(
0, std::min<size_t>(connection_info.size(), 9));
it.second.remote_host_name = "unknown";
it.second.password = "";
it.second.remember_password = false;
}
std::string display_name = GetRecentConnectionDisplayName(it.second);
bool online = device_presence_.IsOnline(it.second.remote_id);
ImVec2 image_screen_pos = ImVec2(
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
ImVec2 image_pos =
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
ImGui::SetCursorPos(image_pos);
ImVec2 image_screen_pos = ImGui::GetCursorScreenPos();
ImGui::Image(
(ImTextureID)(intptr_t)it.second.texture,
ImVec2(recent_connection_image_width, recent_connection_image_height));
if (ImGui::IsItemHovered()) {
const bool image_item_hovered = ImGui::IsItemHovered();
ImVec2 card_screen_min = image_screen_pos;
ImVec2 card_screen_max =
ImVec2(image_screen_pos.x + recent_connection_image_width,
image_screen_pos.y + recent_connection_image_height +
recent_connection_footer_height);
const bool card_hovered =
ImGui::IsMouseHoveringRect(card_screen_min, card_screen_max, true);
const float recent_connection_toolbar_width =
3.0f * recent_connection_button_width;
const float recent_connection_toolbar_padding =
recent_connection_image_width * 0.025f;
const ImVec2 toolbar_pos = ImVec2(
image_pos.x + recent_connection_image_width -
recent_connection_toolbar_width - recent_connection_toolbar_padding,
image_pos.y + recent_connection_toolbar_padding);
const ImVec2 toolbar_screen_pos = ImVec2(
image_screen_pos.x + recent_connection_image_width -
recent_connection_toolbar_width - recent_connection_toolbar_padding,
image_screen_pos.y + recent_connection_toolbar_padding);
const ImVec2 toolbar_screen_end =
ImVec2(toolbar_screen_pos.x + recent_connection_toolbar_width,
toolbar_screen_pos.y + recent_connection_button_height);
const bool toolbar_hovered =
card_hovered && ImGui::IsMouseHoveringRect(toolbar_screen_pos,
toolbar_screen_end, true);
const bool show_image_tooltip = image_item_hovered && !toolbar_hovered;
if (show_image_tooltip) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
std::string display_host_name_with_presence =
it.second.remote_host_name + " " +
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_]);
ImGui::Text("%s", display_host_name_with_presence.c_str());
ImGui::Text("%s", display_name.c_str());
if (!it.second.remote_host_name.empty() &&
it.second.remote_host_name != display_name) {
ImGui::Text("%s", it.second.remote_host_name.c_str());
}
ImGui::Text("%s: %s",
localization::remote_id[localization_language_index_].c_str(),
it.second.remote_id.c_str());
ImGui::Text("%s",
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_])
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 circle_pos =
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
image_screen_pos.y + recent_connection_image_height * 0.12f);
ImU32 fill_color =
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
ImU32 border_color = IM_COL32(255, 255, 255, 255);
float dot_radius = recent_connection_image_height * 0.06f;
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
100);
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
// remote id display button
// connection name footer
{
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0.2f));
ImVec2 dummy_button_pos =
ImVec2 footer_pos =
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
std::string dummy_button_name = "##DummyButton" + it.second.remote_id;
ImGui::SetCursorPos(dummy_button_pos);
ImGui::SetWindowFontScale(0.6f);
ImGui::Button(dummy_button_name.c_str(),
ImVec2(recent_connection_dummy_button_width,
recent_connection_button_height));
ImGui::SetWindowFontScale(1.0f);
ImGui::SetCursorPos(ImVec2(
dummy_button_pos.x + recent_connection_dummy_button_width * 0.05f,
dummy_button_pos.y + recent_connection_button_height * 0.05f));
ImGui::SetWindowFontScale(0.65f);
ImGui::Text("%s", it.second.remote_id.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
}
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.1f, 0.4f, 0.8f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 0.7f));
ImGui::SetWindowFontScale(0.5f);
// trash button
{
ImVec2 trash_can_button_pos =
ImVec2(image_pos.x + recent_connection_image_width -
2 * recent_connection_button_width,
image_pos.y + recent_connection_image_height);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" +
std::to_string(trash_can_button_pos.x);
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
ImVec2 footer_screen_pos =
ImVec2(image_screen_pos.x,
image_screen_pos.y + recent_connection_image_height);
if (delete_connection_ && delete_connection_name_ == it.first) {
if (!thumbnail_->DeleteThumbnail(it.first)) {
reload_recent_connections_ = true;
delete_connection_ = false;
ImVec2 footer_screen_end =
ImVec2(footer_screen_pos.x + recent_connection_name_width,
footer_screen_pos.y + recent_connection_footer_height);
float footer_rounding = recent_connection_footer_height * 0.16f;
draw_list->AddRectFilled(footer_screen_pos, footer_screen_end,
IM_COL32(0, 0, 0, 40), footer_rounding,
ImDrawFlags_RoundCornersBottom);
const float status_left_margin = recent_connection_footer_height * 0.22f;
const float status_gap = recent_connection_footer_height * 0.20f;
const float dot_radius = recent_connection_footer_height * 0.18f;
const ImVec2 dot_center(
footer_screen_pos.x + status_left_margin + dot_radius,
footer_screen_pos.y + recent_connection_footer_height * 0.5f);
const ImU32 dot_color =
online ? IM_COL32(34, 197, 94, 255) : IM_COL32(156, 163, 175, 255);
// Layered halo simulates a radial gradient glow for the online state.
if (online) {
const float halo_radius = dot_radius * 2.2f;
const int halo_layers = 8;
for (int i = halo_layers; i > 0; --i) {
const float t = static_cast<float>(i) / halo_layers;
const float r = dot_radius + (halo_radius - dot_radius) * t;
const int alpha = static_cast<int>(14.0f * (1.0f - t) + 4.0f);
draw_list->AddCircleFilled(
dot_center, r, IM_COL32(74, 222, 128, alpha));
}
}
draw_list->AddCircleFilled(dot_center, dot_radius, dot_color);
const float status_block_end_x = dot_center.x + dot_radius;
ImVec2 text_min =
ImVec2(status_block_end_x + status_gap, footer_screen_pos.y);
ImVec2 text_max =
ImVec2(footer_screen_end.x - recent_connection_name_width * 0.05f,
footer_screen_end.y);
ImGui::SetWindowFontScale(0.52f);
ImGui::RenderTextClipped(text_min, text_max, display_name.c_str(),
nullptr, nullptr, ImVec2(0.0f, 0.5f));
ImGui::SetWindowFontScale(1.0f);
}
// connect button
{
ImVec2 connect_button_pos =
ImVec2(image_pos.x + recent_connection_image_width -
recent_connection_button_width,
image_pos.y + recent_connection_image_height);
ImGui::SetCursorPos(connect_button_pos);
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first;
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
ConnectTo(it.second.remote_id, it.second.password.c_str(),
it.second.remember_password);
// toolbar / three buttons
if (card_hovered) {
float toolbar_rounding = recent_connection_button_height * 0.22f;
draw_list->AddRectFilled(
ImVec2(toolbar_screen_pos.x, toolbar_screen_pos.y + 1.0f),
ImVec2(toolbar_screen_end.x, toolbar_screen_end.y + 1.0f),
IM_COL32(0, 0, 0, 70), toolbar_rounding);
draw_list->AddRectFilled(toolbar_screen_pos, toolbar_screen_end,
IM_COL32(20, 24, 30, 170), toolbar_rounding);
draw_list->AddRect(toolbar_screen_pos, toolbar_screen_end,
IM_COL32(255, 255, 255, 48), toolbar_rounding);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toolbar_rounding);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(1.0f, 1.0f, 1.0f, 0.18f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.35f, 0.55f, 0.95f, 0.45f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.95f));
ImGui::SetWindowFontScale(0.5f);
// edit alias button
{
ImGui::SetCursorPos(toolbar_pos);
std::string edit = ICON_FA_PEN;
std::string recent_connection_edit_button_name =
edit + "##RecentConnectionAlias" + it.first;
if (ImGui::Button(recent_connection_edit_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
BeginEditRecentConnectionAlias(it.second);
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(
localization::connection_alias[localization_language_index_]
.c_str());
}
}
// trash button
{
ImVec2 trash_can_button_pos = ImVec2(
toolbar_pos.x + recent_connection_button_width, toolbar_pos.y);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" + it.first;
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(
localization::delete_connection[localization_language_index_]
.c_str());
}
}
// connect button
{
ImVec2 connect_button_pos = ImVec2(
toolbar_pos.x + 2 * recent_connection_button_width, toolbar_pos.y);
ImGui::SetCursorPos(connect_button_pos);
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first;
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
ConnectTo(it.second.remote_id, it.second.password.c_str(),
it.second.remember_password);
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(localization::connect_to_this_connection
[localization_language_index_]
.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(3);
}
if (count != recent_connections_count - 1) {
ImVec2 line_start =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y);
ImVec2 line_end =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y + recent_connection_image_height +
recent_connection_footer_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f);
}
if (delete_connection_ && delete_connection_name_ == it.first) {
if (!thumbnail_->DeleteThumbnail(it.first)) {
recent_connection_aliases_.erase(it.second.remote_id);
SaveRecentConnectionAliases();
reload_recent_connections_ = true;
delete_connection_ = false;
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::EndChild();
@@ -243,7 +436,7 @@ int Render::ShowRecentConnections() {
ImVec2 line_end =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y + recent_connection_image_height +
recent_connection_button_height);
recent_connection_footer_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f);
}
@@ -259,6 +452,9 @@ int Render::ShowRecentConnections() {
if (show_confirm_delete_connection_) {
ConfirmDeleteConnection();
}
if (show_edit_connection_alias_window_) {
EditRecentConnectionAliasWindow();
}
if (show_offline_warning_window_) {
OfflineWarningWindow();
}
@@ -320,6 +516,89 @@ int Render::ConfirmDeleteConnection() {
return 0;
}
int Render::EditRecentConnectionAliasWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("EditRecentConnectionAliasWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
std::string text =
localization::input_connection_alias[localization_language_index_];
ImGui::SetWindowFontScale(0.5f);
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetCursorPosX(window_width * 0.2f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::SetNextItemWidth(window_width * 0.6f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (focus_on_input_widget_) {
ImGui::SetKeyboardFocusHere();
focus_on_input_widget_ = false;
}
bool enter_pressed =
ImGui::InputText("##recent_connection_alias", edit_connection_alias_,
IM_ARRAYSIZE(edit_connection_alias_),
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::PopStyleVar();
ImGui::SetCursorPosX(window_width * 0.315f);
ImGui::SetCursorPosY(window_height * 0.75f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
enter_pressed) {
std::string alias = TrimConnectionAlias(edit_connection_alias_);
if (alias.empty()) {
recent_connection_aliases_.erase(edit_connection_alias_remote_id_);
} else {
recent_connection_aliases_[edit_connection_alias_remote_id_] = alias;
}
SaveRecentConnectionAliases();
show_edit_connection_alias_window_ = false;
focus_on_input_widget_ = true;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
edit_connection_alias_remote_id_.clear();
}
ImGui::SameLine();
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_edit_connection_alias_window_ = false;
focus_on_input_widget_ = true;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
edit_connection_alias_remote_id_.clear();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
int Render::OfflineWarningWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(
@@ -360,4 +639,4 @@ int Render::OfflineWarningWindow() {
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
+2 -2
View File
@@ -211,7 +211,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
props->control_window_max_width_ = title_bar_height_ * 10.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_ = ConnectionStatus::Connecting;
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
if (!props->peer_) {
@@ -231,7 +231,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
props->connection_status_ = ConnectionStatus::Connecting;
props->connection_status_.store(ConnectionStatus::Connecting);
peer_to_init = props->peer_;
local_id = props->local_id_;
+243 -11
View File
@@ -13,6 +13,9 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
@@ -640,6 +643,113 @@ int Render::LoadSettingsFromCacheFile() {
return 0;
}
int Render::LoadRecentConnectionAliases() {
recent_connection_aliases_.clear();
std::ifstream alias_file(cache_path_ + "/recent_connection_aliases.json");
if (!alias_file.good()) {
return 0;
}
try {
nlohmann::json alias_json;
alias_file >> alias_json;
const nlohmann::json* aliases = &alias_json;
if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) {
aliases = &alias_json["aliases"];
}
if (!aliases->is_object()) {
LOG_WARN("Invalid recent connection alias file");
return -1;
}
for (auto it = aliases->begin(); it != aliases->end(); ++it) {
if (!it.value().is_string()) {
continue;
}
std::string remote_id = it.key();
std::string alias = it.value().get<std::string>();
if (!remote_id.empty() && !alias.empty()) {
recent_connection_aliases_[remote_id] = alias;
}
}
} catch (const std::exception& e) {
LOG_WARN("Load recent connection aliases failed: {}", e.what());
return -1;
}
return 0;
}
int Render::SaveRecentConnectionAliases() const {
std::error_code ec;
std::filesystem::create_directories(cache_path_, ec);
if (ec) {
LOG_WARN("Create cache directory failed while saving aliases: {}",
ec.message());
return -1;
}
nlohmann::json alias_json;
alias_json["aliases"] = nlohmann::json::object();
for (const auto& [remote_id, alias] : recent_connection_aliases_) {
if (!remote_id.empty() && !alias.empty()) {
alias_json["aliases"][remote_id] = alias;
}
}
std::ofstream alias_file(cache_path_ + "/recent_connection_aliases.json",
std::ios::trunc);
if (!alias_file.good()) {
LOG_WARN("Open recent connection alias file failed");
return -1;
}
alias_file << alias_json.dump(2);
return 0;
}
std::string Render::GetRecentConnectionDisplayName(
const Thumbnail::RecentConnection& connection) const {
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
if (alias_it != recent_connection_aliases_.end() &&
!alias_it->second.empty()) {
return alias_it->second;
}
if (!connection.remote_host_name.empty() &&
connection.remote_host_name != "unknown") {
return connection.remote_host_name;
}
return connection.remote_id;
}
void Render::BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection& connection) {
edit_connection_alias_remote_id_ = connection.remote_id;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
std::string alias =
alias_it != recent_connection_aliases_.end()
? alias_it->second
: GetRecentConnectionDisplayName(connection);
if (!alias.empty()) {
strncpy(edit_connection_alias_, alias.c_str(),
sizeof(edit_connection_alias_) - 1);
edit_connection_alias_[sizeof(edit_connection_alias_) - 1] = '\0';
}
focus_on_input_widget_ = true;
show_edit_connection_alias_window_ = true;
}
int Render::ScreenCapturerInit() {
#ifdef __APPLE__
if (!EnsureMacScreenRecordingPermission()) {
@@ -1192,10 +1302,16 @@ void Render::UpdateInteractions() {
keyboard_capturer_is_started_ = true;
}
}
if (keyboard_capturer_is_started_) {
SendKeyboardHeartbeat(false);
}
} else if (keyboard_capturer_is_started_) {
ForceReleasePressedKeys();
StopKeyboardCapturer();
keyboard_capturer_is_started_ = false;
}
CheckRemoteKeyboardTimeouts();
}
int Render::CreateMainWindow() {
@@ -1258,6 +1374,13 @@ int Render::CreateMainWindow() {
HICON tray_icon = LoadTrayIcon();
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
localization_language_index_);
#elif defined(__APPLE__)
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
localization_language_index_);
#elif defined(__linux__) && !defined(__APPLE__)
tray_ = std::make_unique<LinuxTray>(main_window_, "CrossDesk",
localization_language_index_,
APP_EXIT_EVENT);
#endif
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
@@ -1557,20 +1680,47 @@ int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) {
}
int Render::DestroyMainWindowContext() {
if (!main_ctx_) {
return 0;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(main_ctx_);
main_ctx_ = nullptr;
return 0;
}
int Render::DestroyStreamWindowContext() {
if (!stream_ctx_) {
stream_window_inited_ = false;
return 0;
}
stream_window_inited_ = false;
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(stream_ctx_);
stream_ctx_ = nullptr;
return 0;
}
int Render::DestroyServerWindowContext() {
if (!server_ctx_) {
server_window_inited_ = false;
return 0;
}
server_window_inited_ = false;
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(server_ctx_);
server_ctx_ = nullptr;
return 0;
}
@@ -1805,6 +1955,7 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); }
void Render::InitializeSettings() {
LoadSettingsFromCacheFile();
LoadRecentConnectionAliases();
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
@@ -1838,9 +1989,12 @@ void Render::InitializeSDL() {
screen_height_ = dm->h;
}
STREAM_REFRESH_EVENT = SDL_RegisterEvents(1);
if (STREAM_REFRESH_EVENT == (uint32_t)-1) {
LOG_ERROR("Failed to register custom SDL event");
const uint32_t custom_event_base = SDL_RegisterEvents(2);
if (custom_event_base == static_cast<uint32_t>(-1)) {
LOG_ERROR("Failed to register custom SDL events");
} else {
STREAM_REFRESH_EVENT = custom_event_base;
APP_EXIT_EVENT = custom_event_base + 1;
}
LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
@@ -1914,6 +2068,10 @@ void Render::MainLoop() {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
#elif defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->ProcessEvents();
}
#endif
UpdateLabels();
@@ -1924,7 +2082,11 @@ void Render::MainLoop() {
HandleServerWindow();
HandleWindowsServiceIntegration();
DrawMainWindow();
const bool main_window_visible =
main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN);
if (main_window_visible) {
DrawMainWindow();
}
if (stream_window_inited_) {
DrawStreamWindow();
}
@@ -1937,6 +2099,29 @@ void Render::MainLoop() {
}
}
bool Render::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !main_window_) {
return false;
}
#if defined(_WIN32) || defined(__APPLE__)
if (!tray_) {
return false;
}
tray_->MinimizeToTray();
return true;
#elif defined(__linux__) && !defined(__APPLE__)
if (!tray_) {
return false;
}
return tray_->MinimizeToTray();
#else
return false;
#endif
}
void Render::UpdateLabels() {
if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) {
@@ -1993,11 +2178,13 @@ void Render::HandleWindowsServiceIntegration() {
return;
}
const bool has_connected_remote =
std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
const bool has_connected_remote = [&] {
std::shared_lock lock(connection_status_mutex_);
return std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
}();
if (!has_connected_remote) {
ResetLocalWindowsServiceState(false);
return;
@@ -2273,6 +2460,7 @@ void Render::HandleServerWindow() {
if (need_to_destroy_server_window_) {
DestroyServerWindow();
DestroyServerWindowContext();
need_to_destroy_server_window_ = false;
}
}
@@ -2313,6 +2501,27 @@ void Render::Cleanup() {
WaitForThumbnailSaveTasks();
AudioDeviceDestroy();
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
tray_.reset();
#endif
if (stream_window_created_) {
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
DestroyStreamWindow();
}
if (stream_ctx_) {
DestroyStreamWindowContext();
}
if (server_window_created_) {
DestroyServerWindow();
}
if (server_ctx_) {
DestroyServerWindowContext();
}
DestroyMainWindowContext();
DestroyMainWindow();
SDL_Quit();
@@ -2726,6 +2935,20 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
}
}
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
LOG_INFO("Quit program from system tray");
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
#if defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->RemoveTrayIcon();
}
#endif
exit_ = true;
return;
}
switch (event.type) {
case SDL_EVENT_QUIT:
if (stream_window_inited_) {
@@ -2796,9 +3019,18 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (event.window.windowID != SDL_GetWindowID(stream_window_)) {
exit_ = true;
if (stream_window_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
break;
}
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_) &&
MinimizeMainWindowToTray()) {
break;
}
exit_ = true;
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
+61 -3
View File
@@ -39,6 +39,10 @@
#if _WIN32
#include "win_tray.h"
#elif defined(__APPLE__)
#include "mac_tray.h"
#elif defined(__linux__)
#include "linux_tray.h"
#endif
namespace crossdesk {
@@ -180,7 +184,11 @@ class Render {
SDL_Rect stream_render_rect_;
SDL_Rect stream_render_rect_last_;
ImVec2 control_window_pos_;
ConnectionStatus connection_status_ = ConnectionStatus::Closed;
// Written from the minirtc/libnice callback thread (OnConnectionStatusCb)
// and the SDL main thread (remote_peer_panel connect button); read from
// the SDL main render thread (stream/control windows) and the SDL audio
// thread (SdlCaptureAudioIn). Atomic so those reads/writes are defined.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0;
int frame_count_ = 0;
@@ -278,6 +286,7 @@ class Render {
int DrawStreamWindow();
int DrawServerWindow();
int ConfirmDeleteConnection();
int EditRecentConnectionAliasWindow();
int OfflineWarningWindow();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText(
@@ -343,11 +352,35 @@ class Render {
static void FreeRemoteAction(RemoteAction& action);
private:
struct PressedKeyboardKey {
int key_code = 0;
uint32_t scan_code = 0;
bool extended = false;
};
struct RemoteKeyboardState {
std::unordered_map<int, PressedKeyboardKey> pressed_keys;
uint32_t last_seq = 0;
uint32_t last_seen_tick = 0;
bool keyboard_state_seen = false;
};
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
bool extended = false);
static bool IsModifierVkKey(int key_code);
void TrackPressedKeyState(int key_code, bool is_down);
void TrackPressedKeyState(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void ForceReleasePressedKeys();
void SendKeyboardHeartbeat(bool force);
void ApplyRemoteKeyboardEvent(const std::string& remote_id,
const RemoteAction& remote_action);
void ApplyRemoteKeyboardState(const std::string& remote_id,
const RemoteAction& remote_action);
bool InjectRemoteKeyboardKey(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason);
void CheckRemoteKeyboardTimeouts();
int ProcessKeyboardEvent(const SDL_Event& event);
int ProcessMouseEvent(const SDL_Event& event);
@@ -357,6 +390,12 @@ class Render {
private:
int SaveSettingsIntoCacheFile();
int LoadSettingsFromCacheFile();
int LoadRecentConnectionAliases();
int SaveRecentConnectionAliases() const;
std::string GetRecentConnectionDisplayName(
const Thumbnail::RecentConnection& connection) const;
void BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection& connection);
int ScreenCapturerInit();
int StartScreenCapturer();
@@ -383,6 +422,7 @@ class Render {
int AudioDeviceInit();
int AudioDeviceDestroy();
void HandleWindowsServiceIntegration();
bool MinimizeMainWindowToTray();
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#if CROSSDESK_PORTABLE
@@ -479,6 +519,10 @@ class Render {
const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32
std::unique_ptr<WinTray> tray_;
#elif defined(__APPLE__)
std::unique_ptr<MacTray> tray_;
#elif defined(__linux__)
std::unique_ptr<LinuxTray> tray_;
#endif
// main window properties
@@ -551,11 +595,16 @@ class Render {
std::string controlled_remote_id_ = "";
std::string focused_remote_id_ = "";
std::string remote_client_id_ = "";
std::unordered_set<int> pressed_keyboard_keys_;
std::unordered_map<int, PressedKeyboardKey> pressed_keyboard_keys_;
std::mutex pressed_keyboard_keys_mutex_;
uint32_t keyboard_state_seq_ = 0;
uint32_t last_keyboard_heartbeat_tick_ = 0;
std::unordered_map<std::string, RemoteKeyboardState> remote_keyboard_states_;
std::mutex remote_keyboard_states_mutex_;
SDL_Event last_mouse_event{};
SDL_AudioStream* output_stream_ = nullptr;
uint32_t STREAM_REFRESH_EVENT = 0;
uint32_t APP_EXIT_EVENT = 0;
#if _WIN32
std::atomic<bool> pending_windows_service_sas_{false};
bool local_service_status_received_ = false;
@@ -655,10 +704,14 @@ class Render {
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool show_edit_connection_alias_window_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_ = "";
std::unordered_map<std::string, std::string> recent_connection_aliases_;
std::string edit_connection_alias_remote_id_ = "";
char edit_connection_alias_[128] = "";
std::string offline_warning_text_ = "";
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
@@ -767,6 +820,11 @@ class Render {
void WaitForThumbnailSaveTasks();
/* ------ server mode ------ */
// connection_status_ / connection_host_names_ are read on the main
// render thread (DrawServerWindow, HandleWindowsServiceIntegration) and
// written from minirtc/libnice callback threads (OnConnectionStatusCb,
// OnReceiveDataBufferCb). Guard every access with this shared mutex.
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_ = "";
+329 -82
View File
@@ -6,6 +6,9 @@
#include <filesystem>
#include <fstream>
#include <limits>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>
#include <unordered_set>
@@ -28,6 +31,8 @@
namespace crossdesk {
namespace {
constexpr uint32_t kKeyboardHeartbeatIntervalMs = 500;
constexpr uint32_t kRemoteKeyboardReleaseTimeoutMs = 2500;
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
@@ -415,34 +420,92 @@ bool Render::IsModifierVkKey(int key_code) {
}
}
void Render::TrackPressedKeyState(int key_code, bool is_down) {
if (!IsWaylandSession() && !IsModifierVkKey(key_code)) {
return;
}
void Render::TrackPressedKeyState(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
if (is_down) {
pressed_keyboard_keys_.insert(key_code);
pressed_keyboard_keys_[key_code] =
PressedKeyboardKey{key_code, scan_code, extended};
} else {
pressed_keyboard_keys_.erase(key_code);
}
}
void Render::ForceReleasePressedKeys() {
std::vector<int> pressed_keys;
std::vector<PressedKeyboardKey> pressed_keys;
{
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
if (pressed_keyboard_keys_.empty()) {
return;
pressed_keys.reserve(pressed_keyboard_keys_.size());
for (const auto& [_, key] : pressed_keyboard_keys_) {
pressed_keys.push_back(key);
}
pressed_keys.assign(pressed_keyboard_keys_.begin(),
pressed_keyboard_keys_.end());
pressed_keyboard_keys_.clear();
}
for (int key_code : pressed_keys) {
SendKeyCommand(key_code, false);
for (const PressedKeyboardKey& key : pressed_keys) {
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
}
SendKeyboardHeartbeat(true);
}
void Render::SendKeyboardHeartbeat(bool force) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && now - last_keyboard_heartbeat_tick_ <
kKeyboardHeartbeatIntervalMs) {
return;
}
RemoteAction remote_action{};
remote_action.type = ControlType::keyboard_state;
remote_action.ks.seq = ++keyboard_state_seq_;
{
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
size_t idx = 0;
for (const auto& [_, key] : pressed_keyboard_keys_) {
if (idx >= kMaxKeyboardStateKeys) {
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
pressed_keyboard_keys_.size());
break;
}
remote_action.ks.pressed_keys[idx].key_value =
static_cast<size_t>(key.key_code);
remote_action.ks.pressed_keys[idx].scan_code = key.scan_code;
remote_action.ks.pressed_keys[idx].extended = key.extended;
++idx;
}
remote_action.ks.pressed_count = idx;
}
const std::string target_id = controlled_remote_id_.empty()
? focused_remote_id_
: controlled_remote_id_;
if (target_id.empty()) {
last_keyboard_heartbeat_tick_ = now;
return;
}
auto props_it = client_properties_.find(target_id);
if (props_it == client_properties_.end()) {
last_keyboard_heartbeat_tick_ = now;
return;
}
const auto props = props_it->second;
if (props->connection_status_.load() != ConnectionStatus::Connected ||
!props->peer_) {
last_keyboard_heartbeat_tick_ = now;
return;
}
const std::string msg = remote_action.to_json();
const int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->keyboard_label_.c_str());
if (ret != 0) {
LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id,
ret);
}
last_keyboard_heartbeat_tick_ = now;
}
int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
@@ -471,7 +534,7 @@ int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
if (!target_id.empty()) {
if (client_properties_.find(target_id) != client_properties_.end()) {
auto props = client_properties_[target_id];
if (props->connection_status_ == ConnectionStatus::Connected &&
if (props->connection_status_.load() == ConnectionStatus::Connected &&
props->peer_) {
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
@@ -484,7 +547,7 @@ int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
}
}
TrackPressedKeyState(key_code, is_down);
TrackPressedKeyState(key_code, is_down, scan_code, extended);
return 0;
}
@@ -506,6 +569,181 @@ int Render::ProcessKeyboardEvent(const SDL_Event& event) {
return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
}
bool Render::InjectRemoteKeyboardKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
#if _WIN32
if (local_service_status_received_ &&
IsSecureDesktopInteractionRequired(local_interactive_stage_)) {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, scan_code, extended, 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.key_value = static_cast<size_t>(key_code);
action.k.scan_code = scan_code;
action.k.extended = extended;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, action)) {
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return true;
}
LogSecureDesktopInputBlocked(&last_local_secure_input_block_log_tick_,
"local",
local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
"response={}",
key_code, is_down, response);
return false;
}
return true;
}
#endif
if (!keyboard_capturer_) {
return false;
}
return keyboard_capturer_->SendKeyboardCommand(key_code, is_down, scan_code,
extended) == 0;
}
void Render::ApplyRemoteKeyboardEvent(const std::string& remote_id,
const RemoteAction& remote_action) {
const int key_code = static_cast<int>(remote_action.k.key_value);
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
const uint32_t scan_code = remote_action.k.scan_code;
const bool extended = remote_action.k.extended;
const bool injected =
InjectRemoteKeyboardKey(key_code, is_down, scan_code, extended);
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto& state = remote_keyboard_states_[remote_id];
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
if (is_down) {
if (injected) {
state.pressed_keys[key_code] =
PressedKeyboardKey{key_code, scan_code, extended};
}
} else if (injected) {
state.pressed_keys.erase(key_code);
}
}
void Render::ApplyRemoteKeyboardState(const std::string& remote_id,
const RemoteAction& remote_action) {
std::vector<PressedKeyboardKey> keys_to_release;
std::vector<PressedKeyboardKey> keys_to_press;
{
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto& state = remote_keyboard_states_[remote_id];
if (remote_action.ks.seq != 0 && state.last_seq != 0 &&
static_cast<int32_t>(remote_action.ks.seq - state.last_seq) <= 0) {
return;
}
state.last_seq = remote_action.ks.seq;
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
state.keyboard_state_seen = true;
std::unordered_map<int, PressedKeyboardKey> desired_keys;
const size_t pressed_count =
remote_action.ks.pressed_count < kMaxKeyboardStateKeys
? remote_action.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
const auto& key = remote_action.ks.pressed_keys[idx];
const int key_code = static_cast<int>(key.key_value);
desired_keys[key_code] =
PressedKeyboardKey{key_code, key.scan_code, key.extended};
}
for (const auto& [key_code, key] : state.pressed_keys) {
if (desired_keys.find(key_code) == desired_keys.end()) {
keys_to_release.push_back(key);
}
}
for (const auto& [key_code, key] : desired_keys) {
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
keys_to_press.push_back(key);
}
}
}
for (const PressedKeyboardKey& key : keys_to_release) {
if (InjectRemoteKeyboardKey(key.key_code, false, key.scan_code,
key.extended)) {
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto state_it = remote_keyboard_states_.find(remote_id);
if (state_it != remote_keyboard_states_.end()) {
state_it->second.pressed_keys.erase(key.key_code);
}
}
}
for (const PressedKeyboardKey& key : keys_to_press) {
if (InjectRemoteKeyboardKey(key.key_code, true, key.scan_code,
key.extended)) {
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto& state = remote_keyboard_states_[remote_id];
state.pressed_keys[key.key_code] = key;
}
}
}
void Render::ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason) {
std::vector<PressedKeyboardKey> keys_to_release;
{
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto state_it = remote_keyboard_states_.find(remote_id);
if (state_it == remote_keyboard_states_.end()) {
return;
}
keys_to_release.reserve(state_it->second.pressed_keys.size());
for (const auto& [_, key] : state_it->second.pressed_keys) {
keys_to_release.push_back(key);
}
remote_keyboard_states_.erase(state_it);
}
if (!keys_to_release.empty()) {
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
keys_to_release.size(), remote_id, reason ? reason : "unknown");
}
for (const PressedKeyboardKey& key : keys_to_release) {
InjectRemoteKeyboardKey(key.key_code, false, key.scan_code, key.extended);
}
}
void Render::CheckRemoteKeyboardTimeouts() {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
std::vector<std::string> timed_out_remotes;
{
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
for (const auto& [remote_id, state] : remote_keyboard_states_) {
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
state.last_seen_tick != 0 &&
now - state.last_seen_tick > kRemoteKeyboardReleaseTimeoutMs) {
timed_out_remotes.push_back(remote_id);
}
}
}
for (const std::string& remote_id : timed_out_remotes) {
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
}
}
int Render::ProcessMouseEvent(const SDL_Event& event) {
controlled_remote_id_ = "";
RemoteAction remote_action{};
@@ -693,10 +931,10 @@ void Render::SdlCaptureAudioIn(void* userdata, Uint8* stream, int len) {
}
if (1) {
// std::shared_lock lock(render->client_properties_mutex_);
std::shared_lock lock(render->client_properties_mutex_);
for (const auto& it : render->client_properties_) {
auto props = it.second;
if (props->connection_status_ == ConnectionStatus::Connected) {
if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) {
SendAudioFrame(props->peer_, (const char*)stream, len,
render->audio_label_.c_str());
@@ -1089,12 +1327,20 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
return;
}
// 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()) {
bool is_client_mode = false;
std::shared_ptr<SubStreamWindowProperties> props;
{
std::shared_lock lock(render->client_properties_mutex_);
auto props_it = render->client_properties_.find(remote_id);
if (props_it != render->client_properties_.end()) {
is_client_mode = true;
props = props_it->second;
}
}
if (is_client_mode) {
// 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);
@@ -1110,17 +1356,22 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
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]);
std::string host_name(remote_action.i.host_name,
remote_action.i.host_name_size);
{
std::unique_lock lock(render->connection_status_mutex_);
render->connection_host_names_[remote_id] = host_name;
}
LOG_INFO("Remote hostname: [{}]", host_name);
FreeRemoteAction(remote_action);
}
} else {
// remote
#if _WIN32
if (render->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) {
IsSecureDesktopInteractionRequired(render->local_interactive_stage_) &&
remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
if (remote_action.type == ControlType::mouse) {
int absolute_x = 0;
int absolute_y = 0;
@@ -1151,33 +1402,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
}
return;
}
if (remote_action.type == ControlType::keyboard) {
const int key_code = static_cast<int>(remote_action.k.key_value);
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, remote_action.k.scan_code,
remote_action.k.extended, 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, remote_action)) {
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return;
}
LogSecureDesktopInputBlocked(
&render->last_local_secure_input_block_log_tick_, "local",
render->local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, "
"is_down={}, response={}",
key_code, is_down, response);
}
return;
}
}
#endif
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
@@ -1188,12 +1412,10 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
render->StartSpeakerCapturer();
else if (!remote_action.a && render->start_speaker_capturer_)
render->StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard &&
render->keyboard_capturer_) {
render->keyboard_capturer_->SendKeyboardCommand(
(int)remote_action.k.key_value,
remote_action.k.flag == KeyFlag::key_down, remote_action.k.scan_code,
remote_action.k.extended);
} else if (remote_action.type == ControlType::keyboard) {
render->ApplyRemoteKeyboardEvent(remote_id, remote_action);
} else if (remote_action.type == ControlType::keyboard_state) {
render->ApplyRemoteKeyboardState(remote_id, remote_action);
} else if (remote_action.type == ControlType::display_id &&
render->screen_capturer_) {
const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
@@ -1272,14 +1494,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
if (!render) return;
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(render->client_properties_mutex_);
auto it = render->client_properties_.find(remote_id);
auto props = (it != render->client_properties_.end()) ? it->second : nullptr;
std::shared_ptr<SubStreamWindowProperties> props;
{
std::shared_lock lock(render->client_properties_mutex_);
auto it = render->client_properties_.find(remote_id);
if (it != render->client_properties_.end()) {
props = it->second;
}
}
if (props) {
render->is_client_mode_ = true;
render->show_connection_status_window_ = true;
props->connection_status_ = status;
props->connection_status_.store(status);
switch (status) {
case ConnectionStatus::Connected: {
@@ -1345,6 +1572,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
props->connection_established_ = false;
props->enable_mouse_control_ = false;
render->ResetRemoteServiceStatus(*props);
@@ -1394,7 +1622,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
} else {
render->is_client_mode_ = false;
render->show_connection_status_window_ = true;
render->connection_status_[remote_id] = status;
{
std::unique_lock lock(render->connection_status_mutex_);
render->connection_status_[remote_id] = status;
}
switch (status) {
case ConnectionStatus::Connected: {
@@ -1450,11 +1681,14 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->start_speaker_capturer_ = true;
render->remote_client_id_ = remote_id;
render->start_mouse_controller_ = true;
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
render->show_cursor_ = true;
{
std::shared_lock lock(render->connection_status_mutex_);
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
render->show_cursor_ = true;
}
}
break;
@@ -1462,12 +1696,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
})) {
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
bool all_disconnected = false;
{
std::shared_lock lock(render->connection_status_mutex_);
all_disconnected = std::all_of(
render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
});
}
if (all_disconnected) {
render->need_to_destroy_server_window_ = true;
render->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
@@ -1494,18 +1735,24 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->audio_capture_ = false;
}
render->connection_status_.erase(remote_id);
render->connection_host_names_.erase(remote_id);
{
std::unique_lock lock(render->connection_status_mutex_);
render->connection_status_.erase(remote_id);
render->connection_host_names_.erase(remote_id);
}
if (render->screen_capturer_) {
render->screen_capturer_->ResetToInitialMonitor();
}
}
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") == std::string::npos;
})) {
render->show_cursor_ = false;
{
std::shared_lock lock(render->connection_status_mutex_);
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") == std::string::npos;
})) {
render->show_cursor_ = false;
}
}
break;
+2 -2
View File
@@ -191,7 +191,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
RemoteAction remote_action;
remote_action.type = ControlType::display_id;
remote_action.d = i;
if (props->connection_status_ == ConnectionStatus::Connected) {
if (props->connection_status_.load() == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
@@ -215,7 +215,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
auto send_service_command = [&](ServiceCommandFlag flag,
const char* log_action) {
if (props->connection_status_ == ConnectionStatus::Connected &&
if (props->connection_status_.load() == ConnectionStatus::Connected &&
props->peer_) {
RemoteAction remote_action;
remote_action.type = ControlType::service_command;
+2 -7
View File
@@ -300,17 +300,12 @@ int Render::TitleBar(bool main_window) {
}
if (close_button_clicked) {
#if _WIN32
if (enable_minimize_to_tray_) {
tray_->MinimizeToTray();
} else {
#endif
const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
if (!minimized_to_tray) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#if _WIN32
}
#endif
}
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LINUX_TRAY_H_
#define _LINUX_TRAY_H_
#if defined(__linux__) && !defined(__APPLE__)
#include <cstdint>
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct LinuxTrayImpl;
class LinuxTray {
public:
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
~LinuxTray();
bool MinimizeToTray();
void RemoveTrayIcon();
void ProcessEvents();
private:
std::unique_ptr<LinuxTrayImpl> impl_;
};
} // namespace crossdesk
#endif // defined(__linux__) && !defined(__APPLE__)
#endif // _LINUX_TRAY_H_
+34
View File
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct MacTrayImpl;
class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
~MacTray();
void MinimizeToTray();
void RemoveTrayIcon();
private:
std::unique_ptr<MacTrayImpl> impl_;
};
} // namespace crossdesk
#endif // _MAC_TRAY_H_
+249
View File
@@ -0,0 +1,249 @@
#include "mac_tray.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
#include "localization.h"
#include <utility>
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)exitApplication:(id)sender;
@end
namespace crossdesk {
struct MacTrayImpl {
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
int language_index_value)
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
~MacTrayImpl() {
RemoveTrayIcon();
target = nil;
}
void MinimizeToTray() {
EnsureStatusItem();
if (app_window) {
SDL_HideWindow(app_window);
}
}
void RemoveTrayIcon() {
if (!status_item) {
return;
}
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
status_item = nil;
}
void ShowWindow() {
if (!app_window) {
return;
}
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
void ShowMenu() {
EnsureStatusItem();
if (!status_item) {
return;
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
action:@selector(exitApplication:)
keyEquivalent:@""];
[exit_item setTarget:target];
[menu addItem:exit_item];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
const NSRect bounds = [button bounds];
[menu popUpMenuPositioningItem:nil
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
inView:button];
}
void RequestExit() {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
private:
void EnsureStatusItem() {
if (status_item) {
return;
}
status_item = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSSquareStatusItemLength];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
[button setToolTip:NSStringFromUtf8(tooltip)];
NSImage *crossdesk_icon = LoadCrossDeskIcon();
if (crossdesk_icon) {
NSImage *status_icon = [crossdesk_icon copy];
[status_icon setSize:NSMakeSize(18.0, 18.0)];
[status_icon setTemplate:NO];
[button setImage:status_icon];
[button setImagePosition:NSImageOnly];
} else {
[button setTitle:@"CD"];
}
[button setTarget:target];
[button setAction:@selector(statusItemClicked:)];
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
}
NSString *NSStringFromUtf8(const std::string &text) {
return [NSString stringWithUTF8String:text.c_str()];
}
NSImage *LoadCrossDeskIcon() {
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
if (!icon) {
icon = LoadIconFromBundleResource(@"crossedesk");
}
if (!icon) {
icon = LoadIconFromDevelopmentPath();
}
if (!icon) {
icon = [NSApp applicationIconImage];
}
return icon;
}
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
NSString *icon_path =
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
return LoadIconFromPath(icon_path);
}
NSImage *LoadIconFromDevelopmentPath() {
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
NSString *current_directory =
[[NSFileManager defaultManager] currentDirectoryPath];
[candidate_paths
addObject:[current_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
const char *base_path = SDL_GetBasePath();
if (base_path && base_path[0] != '\0') {
NSString *base_directory = NSStringFromUtf8(base_path);
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"../../../../icons/macos/crossdesk.icns"]];
}
for (NSString *candidate_path in candidate_paths) {
NSImage *icon = LoadIconFromPath(
[candidate_path stringByStandardizingPath]);
if (icon) {
return icon;
}
}
return nil;
}
NSImage *LoadIconFromPath(NSString *icon_path) {
if (![icon_path length]) {
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
return nil;
}
return [[NSImage alloc] initWithContentsOfFile:icon_path];
}
::SDL_Window *app_window = nullptr;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
CrossDeskMacTrayTarget *target = nil;
};
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
int language_index)
: impl_(
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
} // namespace crossdesk
@implementation CrossDeskMacTrayTarget {
crossdesk::MacTrayImpl *owner_;
}
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)statusItemClicked:(id)sender {
(void)sender;
if (!owner_) {
return;
}
NSEvent *event = [NSApp currentEvent];
if (event && [event type] == NSEventTypeRightMouseUp) {
owner_->ShowMenu();
return;
}
owner_->ShowWindow();
}
- (void)exitApplication:(id)sender {
(void)sender;
if (owner_) {
owner_->RequestExit();
}
}
@end
#endif // __APPLE__
+24 -8
View File
@@ -31,8 +31,9 @@ bool Render::ConnectionStatusWindow(
ImGui::SetWindowFontScale(0.5f);
std::string text;
const ConnectionStatus status = props->connection_status_.load();
if (ConnectionStatus::Connecting == props->connection_status_) {
if (ConnectionStatus::Connecting == status) {
text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -48,7 +49,23 @@ bool Render::ConnectionStatusWindow(
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == props->connection_status_) {
} else if (ConnectionStatus::Gathering == status) {
text = localization::p2p_gathering[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == status) {
text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -58,7 +75,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::Disconnected == props->connection_status_) {
} else if (ConnectionStatus::Disconnected == status) {
text = localization::p2p_disconnected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -68,7 +85,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::Failed == props->connection_status_) {
} else if (ConnectionStatus::Failed == status) {
text = localization::p2p_failed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -78,7 +95,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::Closed == props->connection_status_) {
} else if (ConnectionStatus::Closed == status) {
text = localization::p2p_closed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -88,7 +105,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
}
} else if (ConnectionStatus::IncorrectPassword == props->connection_status_) {
} else if (ConnectionStatus::IncorrectPassword == status) {
if (!password_validating_) {
if (password_validating_time_ == 1) {
text = localization::input_password[localization_language_index_];
@@ -151,8 +168,7 @@ bool Render::ConnectionStatusWindow(
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
}
} else if (ConnectionStatus::NoSuchTransmissionId ==
props->connection_status_) {
} else if (ConnectionStatus::NoSuchTransmissionId == status) {
text = localization::no_such_id[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
-10
View File
@@ -356,10 +356,6 @@ int Render::SettingWindow() {
ImGui::Separator();
{
#ifndef _WIN32
ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
@@ -375,10 +371,6 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_);
#ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
#endif
}
ImGui::Separator();
@@ -626,14 +618,12 @@ int Render::SettingWindow() {
}
enable_daemon_last_ = enable_daemon_;
#if _WIN32
if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true);
} else {
config_center_->SetMinimizeToTray(false);
}
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif
// File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
+22 -13
View File
@@ -1,6 +1,8 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <memory>
#include <shared_mutex>
#include <string>
#include <vector>
@@ -149,14 +151,17 @@ int Render::RemoteClientInfoWindow() {
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::pair<std::string, std::string>> remote_entries;
remote_entries.reserve(connection_status_.size());
for (const auto& kv : connection_status_) {
const auto host_it = connection_host_names_.find(kv.first);
const std::string display_name =
(host_it != connection_host_names_.end() && !host_it->second.empty())
? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
{
std::shared_lock lock(connection_status_mutex_);
remote_entries.reserve(connection_status_.size());
for (const auto& kv : connection_status_) {
const auto host_it = connection_host_names_.find(kv.first);
const std::string display_name =
(host_it != connection_host_names_.end() && !host_it->second.empty())
? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
}
}
auto find_display_name_by_remote_id =
@@ -220,10 +225,14 @@ int Render::RemoteClientInfoWindow() {
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;
ConnectionStatus status = ConnectionStatus::Closed;
{
std::shared_lock lock(connection_status_mutex_);
auto it = connection_status_.find(selected_server_remote_id_);
if (it != connection_status_.end()) {
status = it->second;
}
}
ImGui::Text(
"%s",
@@ -376,4 +385,4 @@ int Render::RemoteClientInfoWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
+2 -2
View File
@@ -7,7 +7,7 @@ namespace crossdesk {
void Render::DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props) {
std::string text;
switch (props->connection_status_) {
switch (props->connection_status_.load()) {
case ConnectionStatus::Disconnected:
text = localization::p2p_disconnected[localization_language_index_];
break;
@@ -34,7 +34,7 @@ void Render::DrawConnectionStatusText(
void Render::DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props) {
if (!props->connection_established_ ||
props->connection_status_ != ConnectionStatus::Connected) {
props->connection_status_.load() != ConnectionStatus::Connected) {
return;
}
@@ -111,6 +111,7 @@ int ScreenCapturerDxgi::Resume(int monitor_index) {
}
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1;
@@ -121,6 +122,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
paused_ = false; // Reset paused_ on failure
return -2;
}
paused_ = false;
@@ -130,6 +132,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
}
int ScreenCapturerDxgi::ResetToInitialMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
@@ -245,6 +248,7 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
}
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
ReleaseDuplication();
int current_monitor = monitor_index_.load();
if (CreateDuplicationForMonitor(current_monitor)) {
@@ -374,8 +378,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
even_width, even_width, even_height);
if (callback_) {
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[monitor_index_].name.c_str());
int idx = monitor_index_.load();
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[idx].name.c_str());
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
}
}
d3d_context_->Unmap(staging_.Get(), 0);
@@ -72,6 +72,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
std::mutex switch_mutex_;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
@@ -148,7 +148,14 @@ void ScreenCapturerGdi::CaptureLoop() {
continue;
}
const auto& di = display_info_list_[monitor_index_];
int idx = monitor_index_.load();
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
const auto& di = display_info_list_[idx];
int left = di.left;
int top = di.top;
int width = di.width & ~1;
@@ -306,7 +306,7 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
return 0;
}
if (monitor_index >= display_info_list_.size()) {
if (monitor_index < 0 || monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid monitor index: {}", monitor_index);
return -1;
}
+17 -10
View File
@@ -225,9 +225,12 @@ int Thumbnail::LoadThumbnail(
std::string remote_id;
std::string cipher_password;
std::string remote_host_name;
std::string password;
std::string original_image_name;
bool remember_password = false;
if ('Y' == cipher_image_name[9] && cipher_image_name.size() >= 16) {
if (cipher_image_name.size() > 9 && 'Y' == cipher_image_name[9] &&
cipher_image_name.size() >= 16) {
size_t pos_y = cipher_image_name.find('Y');
size_t pos_at = cipher_image_name.find('@');
@@ -241,10 +244,11 @@ int Thumbnail::LoadThumbnail(
remote_host_name =
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
cipher_password = cipher_image_name.substr(pos_at + 1);
password = AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
remember_password = true;
original_image_name =
remote_id + 'Y' + remote_host_name + "@" +
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
original_image_name = remote_id + 'Y' + remote_host_name + "@" +
password;
} else {
size_t pos_n = cipher_image_name.find('N');
// size_t pos_at = cipher_image_name.find('@');
@@ -257,16 +261,19 @@ int Thumbnail::LoadThumbnail(
remote_id = cipher_image_name.substr(0, pos_n);
remote_host_name = cipher_image_name.substr(pos_n + 1);
original_image_name =
remote_id + 'N' + remote_host_name + "@" +
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
original_image_name = remote_id + 'N' + remote_host_name;
}
std::string image_path = save_path_ + cipher_image_name;
Thumbnail::RecentConnection recent_connection;
recent_connection.remote_id = remote_id;
recent_connection.remote_host_name = remote_host_name;
recent_connection.password = password;
recent_connection.remember_password = remember_password;
recent_connections.emplace_back(
std::make_pair(original_image_name, Thumbnail::RecentConnection()));
std::make_pair(original_image_name, recent_connection));
LoadTextureFromFile(image_path.c_str(), renderer,
&(recent_connections[i].second.texture), width,
&(recent_connections.back().second.texture), width,
height);
}
return 0;
@@ -436,4 +443,4 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
return std::string(reinterpret_cast<char*>(plaintext), plaintext_len);
}
} // namespace crossdesk
} // namespace crossdesk
+71
View File
@@ -0,0 +1,71 @@
#include "device_controller.h"
#include <iostream>
#include <string>
namespace {
bool ExpectEqual(const char* name, size_t actual, size_t expected) {
if (actual == expected) {
return true;
}
std::cerr << name << " mismatch\n"
<< " expected: " << expected << "\n"
<< " actual: " << actual << "\n";
return false;
}
bool ExpectTrue(const char* name, bool value) {
if (value) {
return true;
}
std::cerr << name << " expected true\n";
return false;
}
} // namespace
int main() {
bool ok = true;
ok &= ExpectEqual("mouse type", crossdesk::ControlType::mouse, 0);
ok &= ExpectEqual("keyboard type", crossdesk::ControlType::keyboard, 1);
ok &= ExpectEqual("audio_capture type", crossdesk::ControlType::audio_capture,
2);
ok &= ExpectEqual("host_infomation type",
crossdesk::ControlType::host_infomation, 3);
ok &= ExpectEqual("display_id type", crossdesk::ControlType::display_id, 4);
ok &= ExpectEqual("service_status type",
crossdesk::ControlType::service_status, 5);
ok &= ExpectEqual("service_command type",
crossdesk::ControlType::service_command, 6);
ok &= ExpectEqual("keyboard_state type",
crossdesk::ControlType::keyboard_state, 7);
crossdesk::RemoteAction action{};
action.type = crossdesk::ControlType::keyboard_state;
action.ks.seq = 42;
action.ks.pressed_count = 2;
action.ks.pressed_keys[0] = {65, 30, false};
action.ks.pressed_keys[1] = {0xA3, 29, true};
const std::string json = action.to_json();
crossdesk::RemoteAction parsed{};
ok &= ExpectTrue("parse keyboard_state", parsed.from_json(json));
ok &= ExpectEqual("parsed type", parsed.type,
crossdesk::ControlType::keyboard_state);
ok &= ExpectEqual("parsed seq", parsed.ks.seq, 42);
ok &= ExpectEqual("parsed pressed_count", parsed.ks.pressed_count, 2);
ok &= ExpectEqual("parsed key 0", parsed.ks.pressed_keys[0].key_value, 65);
ok &= ExpectEqual("parsed scan 0", parsed.ks.pressed_keys[0].scan_code, 30);
ok &= ExpectTrue("parsed extended 0",
!parsed.ks.pressed_keys[0].extended);
ok &= ExpectEqual("parsed key 1", parsed.ks.pressed_keys[1].key_value,
0xA3);
ok &= ExpectEqual("parsed scan 1", parsed.ks.pressed_keys[1].scan_code, 29);
ok &= ExpectTrue("parsed extended 1", parsed.ks.pressed_keys[1].extended);
return ok ? 0 : 1;
}
+10 -4
View File
@@ -35,7 +35,12 @@ function setup_platform_settings()
add_links("pulse-simple", "pulse")
add_requires("libyuv")
add_syslinks("pthread", "dl")
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes")
add_links("SDL3", "asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
"Xrandr", "Xfixes")
add_existing_include_dirs({
"/usr/include/freetype2",
"/usr/local/include/freetype2"
}, {system = true})
if is_config("USE_DRM", true) then
add_links("drm")
@@ -75,7 +80,8 @@ function setup_platform_settings()
add_links("SDL3")
add_ldflags("-Wl,-ld_classic")
add_cxflags("-Wno-unused-variable")
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation",
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
"AudioToolbox")
end
end
end
+12 -2
View File
@@ -44,6 +44,12 @@ function setup_targets()
add_includedirs("src/device_controller")
add_files("tests/macos_keyboard_modifier_state_test.cpp")
target("keyboard_state_protocol_test")
set_kind("binary")
set_default(false)
add_includedirs("src/device_controller", "src/common")
add_files("tests/keyboard_state_protocol_test.cpp")
target("windows_manifest_resource_test")
set_kind("binary")
set_default(false)
@@ -208,11 +214,15 @@ function setup_targets()
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
"src/gui/windows", {public = true})
if is_os("windows") then
add_files("src/gui/tray/*.cpp")
add_files("src/gui/tray/win_tray.cpp")
add_includedirs("src/gui/tray", "src/service/windows",
{public = true})
elseif is_os("macosx") then
add_files("src/gui/windows/*.mm")
add_files("src/gui/windows/*.mm", "src/gui/tray/*.mm")
add_includedirs("src/gui/tray", {public = true})
elseif is_os("linux") then
add_files("src/gui/tray/linux_tray.cpp")
add_includedirs("src/gui/tray", {public = true})
end
if is_os("windows") then