mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-06-30 11:01:50 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0681f6540d | |||
| d843901550 | |||
| b41630ebb4 | |||
| a815eecc73 | |||
| 7e0984fe9c | |||
| 9c28cd2ab2 | |||
| 3c4000bdbb | |||
| d8e9fa5bba | |||
| e026491b9f | |||
| 009699b375 | |||
| 3677588a3d | |||
| 3d280053a7 | |||
| fbde3f6a47 | |||
| 1c1a33fdce |
@@ -104,6 +104,7 @@ jobs:
|
|||||||
CUDA_PATH: /usr/local/cuda
|
CUDA_PATH: /usr/local/cuda
|
||||||
XMAKE_GLOBALDIR: /data
|
XMAKE_GLOBALDIR: /data
|
||||||
run: |
|
run: |
|
||||||
|
apt install -y libxft-dev
|
||||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
|
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
|
||||||
xmake b -vy --root crossdesk
|
xmake b -vy --root crossdesk
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
# run every day at midnight
|
# run every day at midnight
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
@@ -15,19 +16,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check inactive issues and close them
|
- name: Check inactive issues and close them
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v7
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
script: |
|
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,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
state: 'open',
|
state: 'open',
|
||||||
per_page: 100,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const now = new Date().getTime();
|
|
||||||
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
||||||
|
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
// skip pull requests (they are also returned by listForRepo)
|
// skip pull requests (they are also returned by listForRepo)
|
||||||
if (issue.pull_request) continue;
|
if (issue.pull_request) continue;
|
||||||
@@ -38,26 +41,14 @@ jobs:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch comments for this issue
|
// last activity time = the issue's own updated_at, which is
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
// refreshed on comments, labels, etc. This avoids relying on
|
||||||
owner: context.repo.owner,
|
// fetching comments and is accurate even when comments are edited.
|
||||||
repo: context.repo.repo,
|
const lastActivityTime = new Date(issue.updated_at).getTime();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// check inactivity
|
// check inactivity
|
||||||
if (now - lastActivityTime > inactivePeriod) {
|
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({
|
await github.rest.issues.createComment({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
@@ -76,5 +67,3 @@ jobs:
|
|||||||
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
|
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|||||||
+5
-1
@@ -128,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
|
|||||||
if (pid > 0) _exit(0);
|
if (pid > 0) _exit(0);
|
||||||
|
|
||||||
umask(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 file descriptors: keep stdout/stderr if from terminal, else
|
||||||
// redirect to /dev/null
|
// redirect to /dev/null
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ namespace crossdesk {
|
|||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
mouse = 0,
|
mouse = 0,
|
||||||
keyboard,
|
keyboard = 1,
|
||||||
audio_capture,
|
audio_capture = 2,
|
||||||
host_infomation,
|
host_infomation = 3,
|
||||||
display_id,
|
display_id = 4,
|
||||||
service_status,
|
service_status = 5,
|
||||||
service_command,
|
service_command = 6,
|
||||||
|
keyboard_state = 7,
|
||||||
} ControlType;
|
} ControlType;
|
||||||
typedef enum {
|
typedef enum {
|
||||||
move = 0,
|
move = 0,
|
||||||
@@ -55,6 +56,20 @@ typedef struct {
|
|||||||
KeyFlag flag;
|
KeyFlag flag;
|
||||||
} Key;
|
} 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 {
|
typedef struct {
|
||||||
char host_name[64];
|
char host_name[64];
|
||||||
size_t host_name_size;
|
size_t host_name_size;
|
||||||
@@ -80,6 +95,7 @@ struct RemoteAction {
|
|||||||
union {
|
union {
|
||||||
Mouse m;
|
Mouse m;
|
||||||
Key k;
|
Key k;
|
||||||
|
KeyboardState ks;
|
||||||
HostInfo i;
|
HostInfo i;
|
||||||
bool a;
|
bool a;
|
||||||
int d;
|
int d;
|
||||||
@@ -111,6 +127,20 @@ struct RemoteAction {
|
|||||||
{"extended", a.k.extended},
|
{"extended", a.k.extended},
|
||||||
{"flag", a.k.flag}};
|
{"flag", a.k.flag}};
|
||||||
break;
|
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:
|
case ControlType::audio_capture:
|
||||||
j["audio_capture"] = a.a;
|
j["audio_capture"] = a.a;
|
||||||
break;
|
break;
|
||||||
@@ -162,6 +192,33 @@ struct RemoteAction {
|
|||||||
out.k.extended = j.at("keyboard").value("extended", false);
|
out.k.extended = j.at("keyboard").value("extended", false);
|
||||||
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
||||||
break;
|
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:
|
case ControlType::audio_capture:
|
||||||
out.a = j.at("audio_capture").get<bool>();
|
out.a = j.at("audio_capture").get<bool>();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <dbus/dbus.h>
|
#include <dbus/dbus.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "linux_evdev_keycode.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "wayland_portal_shared.h"
|
#include "wayland_portal_shared.h"
|
||||||
|
|
||||||
@@ -579,33 +580,46 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
|||||||
uint32_t scan_code,
|
uint32_t scan_code,
|
||||||
bool extended) {
|
bool extended) {
|
||||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||||
(void)scan_code;
|
|
||||||
(void)extended;
|
|
||||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||||
if (key_it == vkCodeToX11KeySym.end()) {
|
|
||||||
|
const int evdev_keycode =
|
||||||
|
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||||
|
if (evdev_keycode >= 0 &&
|
||||||
|
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||||
const int keysym = key_it->second;
|
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
|
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||||
// implicit Shift interpretation for uppercase keysyms.
|
// implicit Shift interpretation for uppercase keysyms.
|
||||||
if (display_) {
|
if (display_) {
|
||||||
|
const int keysym = key_it->second;
|
||||||
const KeyCode x11_keycode =
|
const KeyCode x11_keycode =
|
||||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||||
if (x11_keycode > 8) {
|
if (x11_keycode > 8) {
|
||||||
const int evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||||
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int keysym = key_it->second;
|
||||||
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
||||||
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -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(install_windows_service, u8"安装", "Install", u8"Установить") \
|
||||||
X(windows_service_settings_label, u8"锁屏控制服务:", \
|
X(windows_service_settings_label, u8"锁屏控制服务:", \
|
||||||
"Lock Screen Service:", 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", \
|
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
|
||||||
u8"Больше не напоминать") \
|
u8"Больше не напоминать") \
|
||||||
X(windows_service_prompt_suppressed_message, \
|
X(windows_service_prompt_suppressed_message, \
|
||||||
@@ -159,13 +159,16 @@ struct TranslationRow {
|
|||||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||||
u8"Нет подключения к серверу") \
|
u8"Нет подключения к серверу") \
|
||||||
X(signal_tls_cert_error, 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"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
|
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
|
||||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||||
u8"P2P отключено") \
|
u8"P2P отключено") \
|
||||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||||
u8"Подключение P2P...") \
|
u8"Подключение P2P...") \
|
||||||
|
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
|
||||||
|
u8"Сбор кандидатов...") \
|
||||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||||
u8"Получение изображения...") \
|
u8"Получение изображения...") \
|
||||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||||
@@ -180,6 +183,14 @@ struct TranslationRow {
|
|||||||
X(access_website, u8"访问官网: ", \
|
X(access_website, u8"访问官网: ", \
|
||||||
"Access Website: ", u8"Официальный сайт: ") \
|
"Access Website: ", u8"Официальный сайт: ") \
|
||||||
X(update, u8"更新", "Update", 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"确认删除此连接", \
|
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||||
|
|||||||
@@ -1,9 +1,37 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "layout_relative.h"
|
#include "layout_relative.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "render.h"
|
#include "render.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
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() {
|
int Render::RecentConnectionsWindow() {
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
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_width = recent_connection_image_width * 0.15f;
|
||||||
float recent_connection_button_height =
|
float recent_connection_button_height =
|
||||||
recent_connection_image_height * 0.25f;
|
recent_connection_image_height * 0.25f;
|
||||||
float recent_connection_dummy_button_width =
|
float recent_connection_footer_height =
|
||||||
recent_connection_image_width - 2 * recent_connection_button_width;
|
recent_connection_button_height * 1.18f;
|
||||||
|
float recent_connection_name_width = recent_connection_image_width;
|
||||||
|
|
||||||
ImGui::SetCursorPos(
|
ImGui::SetCursorPos(
|
||||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
|
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
|
||||||
@@ -61,14 +90,16 @@ int Render::ShowRecentConnections() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
||||||
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
|
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.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(
|
ImGui::BeginChild(
|
||||||
"RecentConnectionsContainer",
|
"RecentConnectionsContainer",
|
||||||
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
|
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
|
||||||
ImGuiChildFlags_Borders,
|
ImGuiChildFlags_Borders, container_flags);
|
||||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
|
||||||
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
|
|
||||||
ImGuiWindowFlags_NoScrollWithMouse);
|
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
size_t recent_connections_count = recent_connections_.size();
|
size_t recent_connections_count = recent_connections_.size();
|
||||||
@@ -90,149 +121,311 @@ int Render::ShowRecentConnections() {
|
|||||||
// password length is 6
|
// password length is 6
|
||||||
// connection_info -> remote_id + 'Y' + host_name + '@' + password
|
// connection_info -> remote_id + 'Y' + host_name + '@' + password
|
||||||
// -> remote_id + 'N' + host_name
|
// -> 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_y = connection_info.find('Y');
|
||||||
size_t pos_at = connection_info.find('@');
|
size_t pos_at = connection_info.find('@');
|
||||||
|
|
||||||
if (pos_y == std::string::npos || pos_at == std::string::npos ||
|
if (pos_y == std::string::npos || pos_at == std::string::npos ||
|
||||||
pos_y >= pos_at) {
|
pos_y >= pos_at) {
|
||||||
LOG_ERROR("Invalid filename");
|
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;
|
||||||
}
|
}
|
||||||
|
} else if (connection_info.size() > 9 && 'N' == connection_info[9] &&
|
||||||
it.second.remote_id = connection_info.substr(0, pos_y);
|
connection_info.size() >= 10) {
|
||||||
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) {
|
|
||||||
size_t pos_n = connection_info.find('N');
|
size_t pos_n = connection_info.find('N');
|
||||||
size_t pos_at = connection_info.find('@');
|
|
||||||
|
|
||||||
if (pos_n == std::string::npos) {
|
if (pos_n == std::string::npos) {
|
||||||
LOG_ERROR("Invalid filename");
|
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 {
|
} 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);
|
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 image_pos =
|
||||||
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
|
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
|
||||||
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
|
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
|
||||||
|
|
||||||
ImGui::SetCursorPos(image_pos);
|
ImGui::SetCursorPos(image_pos);
|
||||||
|
ImVec2 image_screen_pos = ImGui::GetCursorScreenPos();
|
||||||
ImGui::Image(
|
ImGui::Image(
|
||||||
(ImTextureID)(intptr_t)it.second.texture,
|
(ImTextureID)(intptr_t)it.second.texture,
|
||||||
ImVec2(recent_connection_image_width, recent_connection_image_height));
|
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::BeginTooltip();
|
||||||
|
|
||||||
ImGui::SetWindowFontScale(0.5f);
|
ImGui::SetWindowFontScale(0.5f);
|
||||||
std::string display_host_name_with_presence =
|
|
||||||
it.second.remote_host_name + " " +
|
ImGui::Text("%s", display_name.c_str());
|
||||||
(online ? localization::online[localization_language_index_]
|
|
||||||
: localization::offline[localization_language_index_]);
|
if (!it.second.remote_host_name.empty() &&
|
||||||
ImGui::Text("%s", display_host_name_with_presence.c_str());
|
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::SetWindowFontScale(1.0f);
|
||||||
|
|
||||||
ImGui::EndTooltip();
|
ImGui::EndTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
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));
|
ImVec2 footer_pos =
|
||||||
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(image_pos.x, image_pos.y + recent_connection_image_height);
|
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));
|
ImVec2 footer_screen_pos =
|
||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
ImVec2(image_screen_pos.x,
|
||||||
ImVec4(0.1f, 0.4f, 0.8f, 1.0f));
|
image_screen_pos.y + recent_connection_image_height);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delete_connection_ && delete_connection_name_ == it.first) {
|
ImVec2 footer_screen_end =
|
||||||
if (!thumbnail_->DeleteThumbnail(it.first)) {
|
ImVec2(footer_screen_pos.x + recent_connection_name_width,
|
||||||
reload_recent_connections_ = true;
|
footer_screen_pos.y + recent_connection_footer_height);
|
||||||
delete_connection_ = false;
|
|
||||||
|
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
|
// toolbar / three buttons
|
||||||
{
|
if (card_hovered) {
|
||||||
ImVec2 connect_button_pos =
|
float toolbar_rounding = recent_connection_button_height * 0.22f;
|
||||||
ImVec2(image_pos.x + recent_connection_image_width -
|
|
||||||
recent_connection_button_width,
|
draw_list->AddRectFilled(
|
||||||
image_pos.y + recent_connection_image_height);
|
ImVec2(toolbar_screen_pos.x, toolbar_screen_pos.y + 1.0f),
|
||||||
ImGui::SetCursorPos(connect_button_pos);
|
ImVec2(toolbar_screen_end.x, toolbar_screen_end.y + 1.0f),
|
||||||
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
|
IM_COL32(0, 0, 0, 70), toolbar_rounding);
|
||||||
std::string connect_to_this_connection_button_name =
|
|
||||||
connect + "##ConnectionTo" + it.first;
|
draw_list->AddRectFilled(toolbar_screen_pos, toolbar_screen_end,
|
||||||
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
|
IM_COL32(20, 24, 30, 170), toolbar_rounding);
|
||||||
ImVec2(recent_connection_button_width,
|
|
||||||
recent_connection_button_height))) {
|
draw_list->AddRect(toolbar_screen_pos, toolbar_screen_end,
|
||||||
ConnectTo(it.second.remote_id, it.second.password.c_str(),
|
IM_COL32(255, 255, 255, 48), toolbar_rounding);
|
||||||
it.second.remember_password);
|
|
||||||
|
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();
|
ImGui::EndChild();
|
||||||
|
|
||||||
@@ -243,7 +436,7 @@ int Render::ShowRecentConnections() {
|
|||||||
ImVec2 line_end =
|
ImVec2 line_end =
|
||||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
|
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
|
||||||
image_screen_pos.y + recent_connection_image_height +
|
image_screen_pos.y + recent_connection_image_height +
|
||||||
recent_connection_button_height);
|
recent_connection_footer_height);
|
||||||
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
|
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
|
||||||
IM_COL32(0, 0, 0, 122), 1.0f);
|
IM_COL32(0, 0, 0, 122), 1.0f);
|
||||||
}
|
}
|
||||||
@@ -259,6 +452,9 @@ int Render::ShowRecentConnections() {
|
|||||||
if (show_confirm_delete_connection_) {
|
if (show_confirm_delete_connection_) {
|
||||||
ConfirmDeleteConnection();
|
ConfirmDeleteConnection();
|
||||||
}
|
}
|
||||||
|
if (show_edit_connection_alias_window_) {
|
||||||
|
EditRecentConnectionAliasWindow();
|
||||||
|
}
|
||||||
if (show_offline_warning_window_) {
|
if (show_offline_warning_window_) {
|
||||||
OfflineWarningWindow();
|
OfflineWarningWindow();
|
||||||
}
|
}
|
||||||
@@ -320,6 +516,89 @@ int Render::ConfirmDeleteConnection() {
|
|||||||
return 0;
|
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() {
|
int Render::OfflineWarningWindow() {
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
ImGui::SetNextWindowPos(
|
ImGui::SetNextWindowPos(
|
||||||
@@ -360,4 +639,4 @@ int Render::OfflineWarningWindow() {
|
|||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -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_width_ = title_bar_height_ * 10.0f;
|
||||||
props->control_window_max_height_ = title_bar_height_ * 7.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;
|
show_connection_status_window_ = true;
|
||||||
|
|
||||||
if (!props->peer_) {
|
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->file_feedback_label_.c_str(), true);
|
||||||
AddDataStream(props->peer_, props->clipboard_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_;
|
peer_to_init = props->peer_;
|
||||||
local_id = props->local_id_;
|
local_id = props->local_id_;
|
||||||
|
|||||||
+243
-11
@@ -13,6 +13,9 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -640,6 +643,113 @@ int Render::LoadSettingsFromCacheFile() {
|
|||||||
return 0;
|
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() {
|
int Render::ScreenCapturerInit() {
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (!EnsureMacScreenRecordingPermission()) {
|
if (!EnsureMacScreenRecordingPermission()) {
|
||||||
@@ -1192,10 +1302,16 @@ void Render::UpdateInteractions() {
|
|||||||
keyboard_capturer_is_started_ = true;
|
keyboard_capturer_is_started_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (keyboard_capturer_is_started_) {
|
||||||
|
SendKeyboardHeartbeat(false);
|
||||||
|
}
|
||||||
} else if (keyboard_capturer_is_started_) {
|
} else if (keyboard_capturer_is_started_) {
|
||||||
|
ForceReleasePressedKeys();
|
||||||
StopKeyboardCapturer();
|
StopKeyboardCapturer();
|
||||||
keyboard_capturer_is_started_ = false;
|
keyboard_capturer_is_started_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckRemoteKeyboardTimeouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
int Render::CreateMainWindow() {
|
int Render::CreateMainWindow() {
|
||||||
@@ -1258,6 +1374,13 @@ int Render::CreateMainWindow() {
|
|||||||
HICON tray_icon = LoadTrayIcon();
|
HICON tray_icon = LoadTrayIcon();
|
||||||
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
|
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
|
||||||
localization_language_index_);
|
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
|
#endif
|
||||||
|
|
||||||
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
|
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
|
||||||
@@ -1557,20 +1680,47 @@ int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::DestroyMainWindowContext() {
|
int Render::DestroyMainWindowContext() {
|
||||||
|
if (!main_ctx_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::SetCurrentContext(main_ctx_);
|
ImGui::SetCurrentContext(main_ctx_);
|
||||||
ImGui_ImplSDLRenderer3_Shutdown();
|
ImGui_ImplSDLRenderer3_Shutdown();
|
||||||
ImGui_ImplSDL3_Shutdown();
|
ImGui_ImplSDL3_Shutdown();
|
||||||
ImGui::DestroyContext(main_ctx_);
|
ImGui::DestroyContext(main_ctx_);
|
||||||
|
main_ctx_ = nullptr;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Render::DestroyStreamWindowContext() {
|
int Render::DestroyStreamWindowContext() {
|
||||||
|
if (!stream_ctx_) {
|
||||||
|
stream_window_inited_ = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
stream_window_inited_ = false;
|
stream_window_inited_ = false;
|
||||||
ImGui::SetCurrentContext(stream_ctx_);
|
ImGui::SetCurrentContext(stream_ctx_);
|
||||||
ImGui_ImplSDLRenderer3_Shutdown();
|
ImGui_ImplSDLRenderer3_Shutdown();
|
||||||
ImGui_ImplSDL3_Shutdown();
|
ImGui_ImplSDL3_Shutdown();
|
||||||
ImGui::DestroyContext(stream_ctx_);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1805,6 +1955,7 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); }
|
|||||||
|
|
||||||
void Render::InitializeSettings() {
|
void Render::InitializeSettings() {
|
||||||
LoadSettingsFromCacheFile();
|
LoadSettingsFromCacheFile();
|
||||||
|
LoadRecentConnectionAliases();
|
||||||
|
|
||||||
localization_language_index_ =
|
localization_language_index_ =
|
||||||
localization::detail::ClampLanguageIndex(language_button_value_);
|
localization::detail::ClampLanguageIndex(language_button_value_);
|
||||||
@@ -1838,9 +1989,12 @@ void Render::InitializeSDL() {
|
|||||||
screen_height_ = dm->h;
|
screen_height_ = dm->h;
|
||||||
}
|
}
|
||||||
|
|
||||||
STREAM_REFRESH_EVENT = SDL_RegisterEvents(1);
|
const uint32_t custom_event_base = SDL_RegisterEvents(2);
|
||||||
if (STREAM_REFRESH_EVENT == (uint32_t)-1) {
|
if (custom_event_base == static_cast<uint32_t>(-1)) {
|
||||||
LOG_ERROR("Failed to register custom SDL event");
|
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_);
|
LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
|
||||||
@@ -1914,6 +2068,10 @@ void Render::MainLoop() {
|
|||||||
TranslateMessage(&msg);
|
TranslateMessage(&msg);
|
||||||
DispatchMessage(&msg);
|
DispatchMessage(&msg);
|
||||||
}
|
}
|
||||||
|
#elif defined(__linux__) && !defined(__APPLE__)
|
||||||
|
if (tray_) {
|
||||||
|
tray_->ProcessEvents();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
UpdateLabels();
|
UpdateLabels();
|
||||||
@@ -1924,7 +2082,11 @@ void Render::MainLoop() {
|
|||||||
HandleServerWindow();
|
HandleServerWindow();
|
||||||
HandleWindowsServiceIntegration();
|
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_) {
|
if (stream_window_inited_) {
|
||||||
DrawStreamWindow();
|
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() {
|
void Render::UpdateLabels() {
|
||||||
if (!label_inited_ ||
|
if (!label_inited_ ||
|
||||||
localization_language_index_last_ != localization_language_index_) {
|
localization_language_index_last_ != localization_language_index_) {
|
||||||
@@ -1993,11 +2178,13 @@ void Render::HandleWindowsServiceIntegration() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool has_connected_remote =
|
const bool has_connected_remote = [&] {
|
||||||
std::any_of(connection_status_.begin(), connection_status_.end(),
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
[](const auto& entry) {
|
return std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||||
return entry.second == ConnectionStatus::Connected;
|
[](const auto& entry) {
|
||||||
});
|
return entry.second == ConnectionStatus::Connected;
|
||||||
|
});
|
||||||
|
}();
|
||||||
if (!has_connected_remote) {
|
if (!has_connected_remote) {
|
||||||
ResetLocalWindowsServiceState(false);
|
ResetLocalWindowsServiceState(false);
|
||||||
return;
|
return;
|
||||||
@@ -2273,6 +2460,7 @@ void Render::HandleServerWindow() {
|
|||||||
|
|
||||||
if (need_to_destroy_server_window_) {
|
if (need_to_destroy_server_window_) {
|
||||||
DestroyServerWindow();
|
DestroyServerWindow();
|
||||||
|
DestroyServerWindowContext();
|
||||||
need_to_destroy_server_window_ = false;
|
need_to_destroy_server_window_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2313,6 +2501,27 @@ void Render::Cleanup() {
|
|||||||
WaitForThumbnailSaveTasks();
|
WaitForThumbnailSaveTasks();
|
||||||
|
|
||||||
AudioDeviceDestroy();
|
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();
|
DestroyMainWindowContext();
|
||||||
DestroyMainWindow();
|
DestroyMainWindow();
|
||||||
SDL_Quit();
|
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) {
|
switch (event.type) {
|
||||||
case SDL_EVENT_QUIT:
|
case SDL_EVENT_QUIT:
|
||||||
if (stream_window_inited_) {
|
if (stream_window_inited_) {
|
||||||
@@ -2796,9 +3019,18 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||||
if (event.window.windowID != SDL_GetWindowID(stream_window_)) {
|
if (stream_window_ &&
|
||||||
exit_ = true;
|
event.window.windowID == SDL_GetWindowID(stream_window_)) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (main_window_ &&
|
||||||
|
event.window.windowID == SDL_GetWindowID(main_window_) &&
|
||||||
|
MinimizeMainWindowToTray()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
exit_ = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||||
|
|||||||
+61
-3
@@ -39,6 +39,10 @@
|
|||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
#include "win_tray.h"
|
#include "win_tray.h"
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
#include "mac_tray.h"
|
||||||
|
#elif defined(__linux__)
|
||||||
|
#include "linux_tray.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -180,7 +184,11 @@ class Render {
|
|||||||
SDL_Rect stream_render_rect_;
|
SDL_Rect stream_render_rect_;
|
||||||
SDL_Rect stream_render_rect_last_;
|
SDL_Rect stream_render_rect_last_;
|
||||||
ImVec2 control_window_pos_;
|
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;
|
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||||
int fps_ = 0;
|
int fps_ = 0;
|
||||||
int frame_count_ = 0;
|
int frame_count_ = 0;
|
||||||
@@ -278,6 +286,7 @@ class Render {
|
|||||||
int DrawStreamWindow();
|
int DrawStreamWindow();
|
||||||
int DrawServerWindow();
|
int DrawServerWindow();
|
||||||
int ConfirmDeleteConnection();
|
int ConfirmDeleteConnection();
|
||||||
|
int EditRecentConnectionAliasWindow();
|
||||||
int OfflineWarningWindow();
|
int OfflineWarningWindow();
|
||||||
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
|
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||||
void DrawConnectionStatusText(
|
void DrawConnectionStatusText(
|
||||||
@@ -343,11 +352,35 @@ class Render {
|
|||||||
static void FreeRemoteAction(RemoteAction& action);
|
static void FreeRemoteAction(RemoteAction& action);
|
||||||
|
|
||||||
private:
|
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,
|
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||||
bool extended = false);
|
bool extended = false);
|
||||||
static bool IsModifierVkKey(int key_code);
|
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 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 ProcessKeyboardEvent(const SDL_Event& event);
|
||||||
int ProcessMouseEvent(const SDL_Event& event);
|
int ProcessMouseEvent(const SDL_Event& event);
|
||||||
|
|
||||||
@@ -357,6 +390,12 @@ class Render {
|
|||||||
private:
|
private:
|
||||||
int SaveSettingsIntoCacheFile();
|
int SaveSettingsIntoCacheFile();
|
||||||
int LoadSettingsFromCacheFile();
|
int LoadSettingsFromCacheFile();
|
||||||
|
int LoadRecentConnectionAliases();
|
||||||
|
int SaveRecentConnectionAliases() const;
|
||||||
|
std::string GetRecentConnectionDisplayName(
|
||||||
|
const Thumbnail::RecentConnection& connection) const;
|
||||||
|
void BeginEditRecentConnectionAlias(
|
||||||
|
const Thumbnail::RecentConnection& connection);
|
||||||
|
|
||||||
int ScreenCapturerInit();
|
int ScreenCapturerInit();
|
||||||
int StartScreenCapturer();
|
int StartScreenCapturer();
|
||||||
@@ -383,6 +422,7 @@ class Render {
|
|||||||
int AudioDeviceInit();
|
int AudioDeviceInit();
|
||||||
int AudioDeviceDestroy();
|
int AudioDeviceDestroy();
|
||||||
void HandleWindowsServiceIntegration();
|
void HandleWindowsServiceIntegration();
|
||||||
|
bool MinimizeMainWindowToTray();
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||||
#if CROSSDESK_PORTABLE
|
#if CROSSDESK_PORTABLE
|
||||||
@@ -479,6 +519,10 @@ class Render {
|
|||||||
const int sdl_refresh_ms_ = 16; // ~60 FPS
|
const int sdl_refresh_ms_ = 16; // ~60 FPS
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
std::unique_ptr<WinTray> tray_;
|
std::unique_ptr<WinTray> tray_;
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
std::unique_ptr<MacTray> tray_;
|
||||||
|
#elif defined(__linux__)
|
||||||
|
std::unique_ptr<LinuxTray> tray_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// main window properties
|
// main window properties
|
||||||
@@ -551,11 +595,16 @@ class Render {
|
|||||||
std::string controlled_remote_id_ = "";
|
std::string controlled_remote_id_ = "";
|
||||||
std::string focused_remote_id_ = "";
|
std::string focused_remote_id_ = "";
|
||||||
std::string remote_client_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_;
|
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_Event last_mouse_event{};
|
||||||
SDL_AudioStream* output_stream_ = nullptr;
|
SDL_AudioStream* output_stream_ = nullptr;
|
||||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||||
|
uint32_t APP_EXIT_EVENT = 0;
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
std::atomic<bool> pending_windows_service_sas_{false};
|
std::atomic<bool> pending_windows_service_sas_{false};
|
||||||
bool local_service_status_received_ = false;
|
bool local_service_status_received_ = false;
|
||||||
@@ -655,10 +704,14 @@ class Render {
|
|||||||
bool is_server_mode_ = false;
|
bool is_server_mode_ = false;
|
||||||
bool reload_recent_connections_ = true;
|
bool reload_recent_connections_ = true;
|
||||||
bool show_confirm_delete_connection_ = false;
|
bool show_confirm_delete_connection_ = false;
|
||||||
|
bool show_edit_connection_alias_window_ = false;
|
||||||
bool show_offline_warning_window_ = false;
|
bool show_offline_warning_window_ = false;
|
||||||
bool delete_connection_ = false;
|
bool delete_connection_ = false;
|
||||||
bool is_tab_bar_hovered_ = false;
|
bool is_tab_bar_hovered_ = false;
|
||||||
std::string delete_connection_name_ = "";
|
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_ = "";
|
std::string offline_warning_text_ = "";
|
||||||
bool re_enter_remote_id_ = false;
|
bool re_enter_remote_id_ = false;
|
||||||
double copy_start_time_ = 0;
|
double copy_start_time_ = 0;
|
||||||
@@ -767,6 +820,11 @@ class Render {
|
|||||||
void WaitForThumbnailSaveTasks();
|
void WaitForThumbnailSaveTasks();
|
||||||
|
|
||||||
/* ------ server mode ------ */
|
/* ------ 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, ConnectionStatus> connection_status_;
|
||||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||||
std::string selected_server_remote_id_ = "";
|
std::string selected_server_remote_id_ = "";
|
||||||
|
|||||||
+329
-82
@@ -6,6 +6,9 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@@ -28,6 +31,8 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
constexpr uint32_t kKeyboardHeartbeatIntervalMs = 500;
|
||||||
|
constexpr uint32_t kRemoteKeyboardReleaseTimeoutMs = 2500;
|
||||||
|
|
||||||
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
|
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
|
||||||
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
|
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) {
|
void Render::TrackPressedKeyState(int key_code, bool is_down,
|
||||||
if (!IsWaylandSession() && !IsModifierVkKey(key_code)) {
|
uint32_t scan_code, bool extended) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||||
if (is_down) {
|
if (is_down) {
|
||||||
pressed_keyboard_keys_.insert(key_code);
|
pressed_keyboard_keys_[key_code] =
|
||||||
|
PressedKeyboardKey{key_code, scan_code, extended};
|
||||||
} else {
|
} else {
|
||||||
pressed_keyboard_keys_.erase(key_code);
|
pressed_keyboard_keys_.erase(key_code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Render::ForceReleasePressedKeys() {
|
void Render::ForceReleasePressedKeys() {
|
||||||
std::vector<int> pressed_keys;
|
std::vector<PressedKeyboardKey> pressed_keys;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||||
if (pressed_keyboard_keys_.empty()) {
|
pressed_keys.reserve(pressed_keyboard_keys_.size());
|
||||||
return;
|
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();
|
pressed_keyboard_keys_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int key_code : pressed_keys) {
|
for (const PressedKeyboardKey& key : pressed_keys) {
|
||||||
SendKeyCommand(key_code, false);
|
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,
|
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 (!target_id.empty()) {
|
||||||
if (client_properties_.find(target_id) != client_properties_.end()) {
|
if (client_properties_.find(target_id) != client_properties_.end()) {
|
||||||
auto props = client_properties_[target_id];
|
auto props = client_properties_[target_id];
|
||||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
if (props->connection_status_.load() == ConnectionStatus::Connected &&
|
||||||
props->peer_) {
|
props->peer_) {
|
||||||
std::string msg = remote_action.to_json();
|
std::string msg = remote_action.to_json();
|
||||||
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -506,6 +569,181 @@ int Render::ProcessKeyboardEvent(const SDL_Event& event) {
|
|||||||
return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
|
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) {
|
int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||||
controlled_remote_id_ = "";
|
controlled_remote_id_ = "";
|
||||||
RemoteAction remote_action{};
|
RemoteAction remote_action{};
|
||||||
@@ -693,10 +931,10 @@ void Render::SdlCaptureAudioIn(void* userdata, Uint8* stream, int len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (1) {
|
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_) {
|
for (const auto& it : render->client_properties_) {
|
||||||
auto props = it.second;
|
auto props = it.second;
|
||||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||||
if (props->peer_) {
|
if (props->peer_) {
|
||||||
SendAudioFrame(props->peer_, (const char*)stream, len,
|
SendAudioFrame(props->peer_, (const char*)stream, len,
|
||||||
render->audio_label_.c_str());
|
render->audio_label_.c_str());
|
||||||
@@ -1089,12 +1327,20 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// std::shared_lock lock(render->client_properties_mutex_);
|
|
||||||
if (remote_action.type == ControlType::host_infomation) {
|
if (remote_action.type == ControlType::host_infomation) {
|
||||||
if (render->client_properties_.find(remote_id) !=
|
bool is_client_mode = false;
|
||||||
render->client_properties_.end()) {
|
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
|
// client mode
|
||||||
auto props = render->client_properties_.find(remote_id)->second;
|
|
||||||
if (props && props->remote_host_name_.empty()) {
|
if (props && props->remote_host_name_.empty()) {
|
||||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||||
remote_action.i.host_name_size);
|
remote_action.i.host_name_size);
|
||||||
@@ -1110,17 +1356,22 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
FreeRemoteAction(remote_action);
|
FreeRemoteAction(remote_action);
|
||||||
} else {
|
} else {
|
||||||
// server mode
|
// server mode
|
||||||
render->connection_host_names_[remote_id] = std::string(
|
std::string host_name(remote_action.i.host_name,
|
||||||
remote_action.i.host_name, remote_action.i.host_name_size);
|
remote_action.i.host_name_size);
|
||||||
LOG_INFO("Remote hostname: [{}]",
|
{
|
||||||
render->connection_host_names_[remote_id]);
|
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);
|
FreeRemoteAction(remote_action);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// remote
|
// remote
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
if (render->local_service_status_received_ &&
|
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) {
|
if (remote_action.type == ControlType::mouse) {
|
||||||
int absolute_x = 0;
|
int absolute_x = 0;
|
||||||
int absolute_y = 0;
|
int absolute_y = 0;
|
||||||
@@ -1151,33 +1402,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
}
|
}
|
||||||
return;
|
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
|
#endif
|
||||||
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
||||||
@@ -1188,12 +1412,10 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
render->StartSpeakerCapturer();
|
render->StartSpeakerCapturer();
|
||||||
else if (!remote_action.a && render->start_speaker_capturer_)
|
else if (!remote_action.a && render->start_speaker_capturer_)
|
||||||
render->StopSpeakerCapturer();
|
render->StopSpeakerCapturer();
|
||||||
} else if (remote_action.type == ControlType::keyboard &&
|
} else if (remote_action.type == ControlType::keyboard) {
|
||||||
render->keyboard_capturer_) {
|
render->ApplyRemoteKeyboardEvent(remote_id, remote_action);
|
||||||
render->keyboard_capturer_->SendKeyboardCommand(
|
} else if (remote_action.type == ControlType::keyboard_state) {
|
||||||
(int)remote_action.k.key_value,
|
render->ApplyRemoteKeyboardState(remote_id, remote_action);
|
||||||
remote_action.k.flag == KeyFlag::key_down, remote_action.k.scan_code,
|
|
||||||
remote_action.k.extended);
|
|
||||||
} else if (remote_action.type == ControlType::display_id &&
|
} else if (remote_action.type == ControlType::display_id &&
|
||||||
render->screen_capturer_) {
|
render->screen_capturer_) {
|
||||||
const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
|
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;
|
if (!render) return;
|
||||||
|
|
||||||
std::string remote_id(user_id, user_id_size);
|
std::string remote_id(user_id, user_id_size);
|
||||||
// std::shared_lock lock(render->client_properties_mutex_);
|
std::shared_ptr<SubStreamWindowProperties> props;
|
||||||
auto it = render->client_properties_.find(remote_id);
|
{
|
||||||
auto props = (it != render->client_properties_.end()) ? it->second : nullptr;
|
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) {
|
if (props) {
|
||||||
render->is_client_mode_ = true;
|
render->is_client_mode_ = true;
|
||||||
render->show_connection_status_window_ = true;
|
render->show_connection_status_window_ = true;
|
||||||
props->connection_status_ = status;
|
props->connection_status_.store(status);
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
@@ -1345,6 +1572,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
|||||||
case ConnectionStatus::Disconnected:
|
case ConnectionStatus::Disconnected:
|
||||||
case ConnectionStatus::Failed:
|
case ConnectionStatus::Failed:
|
||||||
case ConnectionStatus::Closed: {
|
case ConnectionStatus::Closed: {
|
||||||
|
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
|
||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
props->enable_mouse_control_ = false;
|
props->enable_mouse_control_ = false;
|
||||||
render->ResetRemoteServiceStatus(*props);
|
render->ResetRemoteServiceStatus(*props);
|
||||||
@@ -1394,7 +1622,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
|||||||
} else {
|
} else {
|
||||||
render->is_client_mode_ = false;
|
render->is_client_mode_ = false;
|
||||||
render->show_connection_status_window_ = true;
|
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) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
@@ -1450,11 +1681,14 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
|||||||
render->start_speaker_capturer_ = true;
|
render->start_speaker_capturer_ = true;
|
||||||
render->remote_client_id_ = remote_id;
|
render->remote_client_id_ = remote_id;
|
||||||
render->start_mouse_controller_ = true;
|
render->start_mouse_controller_ = true;
|
||||||
if (std::all_of(render->connection_status_.begin(),
|
{
|
||||||
render->connection_status_.end(), [](const auto& kv) {
|
std::shared_lock lock(render->connection_status_mutex_);
|
||||||
return kv.first.find("web") != std::string::npos;
|
if (std::all_of(render->connection_status_.begin(),
|
||||||
})) {
|
render->connection_status_.end(), [](const auto& kv) {
|
||||||
render->show_cursor_ = true;
|
return kv.first.find("web") != std::string::npos;
|
||||||
|
})) {
|
||||||
|
render->show_cursor_ = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -1462,12 +1696,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
|||||||
case ConnectionStatus::Disconnected:
|
case ConnectionStatus::Disconnected:
|
||||||
case ConnectionStatus::Failed:
|
case ConnectionStatus::Failed:
|
||||||
case ConnectionStatus::Closed: {
|
case ConnectionStatus::Closed: {
|
||||||
if (std::all_of(render->connection_status_.begin(),
|
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
|
||||||
render->connection_status_.end(), [](const auto& kv) {
|
bool all_disconnected = false;
|
||||||
return kv.second == ConnectionStatus::Closed ||
|
{
|
||||||
kv.second == ConnectionStatus::Failed ||
|
std::shared_lock lock(render->connection_status_mutex_);
|
||||||
kv.second == ConnectionStatus::Disconnected;
|
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->need_to_destroy_server_window_ = true;
|
||||||
render->is_server_mode_ = false;
|
render->is_server_mode_ = false;
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
@@ -1494,18 +1735,24 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
|||||||
render->audio_capture_ = false;
|
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_) {
|
if (render->screen_capturer_) {
|
||||||
render->screen_capturer_->ResetToInitialMonitor();
|
render->screen_capturer_->ResetToInitialMonitor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::all_of(render->connection_status_.begin(),
|
{
|
||||||
render->connection_status_.end(), [](const auto& kv) {
|
std::shared_lock lock(render->connection_status_mutex_);
|
||||||
return kv.first.find("web") == std::string::npos;
|
if (std::all_of(render->connection_status_.begin(),
|
||||||
})) {
|
render->connection_status_.end(), [](const auto& kv) {
|
||||||
render->show_cursor_ = false;
|
return kv.first.find("web") == std::string::npos;
|
||||||
|
})) {
|
||||||
|
render->show_cursor_ = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
RemoteAction remote_action;
|
RemoteAction remote_action;
|
||||||
remote_action.type = ControlType::display_id;
|
remote_action.type = ControlType::display_id;
|
||||||
remote_action.d = i;
|
remote_action.d = i;
|
||||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||||
std::string msg = remote_action.to_json();
|
std::string msg = remote_action.to_json();
|
||||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||||
props->control_data_label_.c_str());
|
props->control_data_label_.c_str());
|
||||||
@@ -215,7 +215,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
|
|
||||||
auto send_service_command = [&](ServiceCommandFlag flag,
|
auto send_service_command = [&](ServiceCommandFlag flag,
|
||||||
const char* log_action) {
|
const char* log_action) {
|
||||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
if (props->connection_status_.load() == ConnectionStatus::Connected &&
|
||||||
props->peer_) {
|
props->peer_) {
|
||||||
RemoteAction remote_action;
|
RemoteAction remote_action;
|
||||||
remote_action.type = ControlType::service_command;
|
remote_action.type = ControlType::service_command;
|
||||||
|
|||||||
@@ -300,17 +300,12 @@ int Render::TitleBar(bool main_window) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (close_button_clicked) {
|
if (close_button_clicked) {
|
||||||
#if _WIN32
|
const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
|
||||||
if (enable_minimize_to_tray_) {
|
if (!minimized_to_tray) {
|
||||||
tray_->MinimizeToTray();
|
|
||||||
} else {
|
|
||||||
#endif
|
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
event.type = SDL_EVENT_QUIT;
|
event.type = SDL_EVENT_QUIT;
|
||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
#if _WIN32
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
|
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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_
|
||||||
@@ -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_
|
||||||
@@ -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__
|
||||||
@@ -31,8 +31,9 @@ bool Render::ConnectionStatusWindow(
|
|||||||
|
|
||||||
ImGui::SetWindowFontScale(0.5f);
|
ImGui::SetWindowFontScale(0.5f);
|
||||||
std::string text;
|
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_];
|
text = localization::p2p_connecting[localization_language_index_];
|
||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
@@ -48,7 +49,23 @@ bool Render::ConnectionStatusWindow(
|
|||||||
}
|
}
|
||||||
ret_flag = true;
|
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_];
|
text = localization::p2p_connected[localization_language_index_];
|
||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
@@ -58,7 +75,7 @@ bool Render::ConnectionStatusWindow(
|
|||||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||||
show_connection_status_window_ = false;
|
show_connection_status_window_ = false;
|
||||||
}
|
}
|
||||||
} else if (ConnectionStatus::Disconnected == props->connection_status_) {
|
} else if (ConnectionStatus::Disconnected == status) {
|
||||||
text = localization::p2p_disconnected[localization_language_index_];
|
text = localization::p2p_disconnected[localization_language_index_];
|
||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
@@ -68,7 +85,7 @@ bool Render::ConnectionStatusWindow(
|
|||||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||||
show_connection_status_window_ = false;
|
show_connection_status_window_ = false;
|
||||||
}
|
}
|
||||||
} else if (ConnectionStatus::Failed == props->connection_status_) {
|
} else if (ConnectionStatus::Failed == status) {
|
||||||
text = localization::p2p_failed[localization_language_index_];
|
text = localization::p2p_failed[localization_language_index_];
|
||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
@@ -78,7 +95,7 @@ bool Render::ConnectionStatusWindow(
|
|||||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||||
show_connection_status_window_ = false;
|
show_connection_status_window_ = false;
|
||||||
}
|
}
|
||||||
} else if (ConnectionStatus::Closed == props->connection_status_) {
|
} else if (ConnectionStatus::Closed == status) {
|
||||||
text = localization::p2p_closed[localization_language_index_];
|
text = localization::p2p_closed[localization_language_index_];
|
||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
@@ -88,7 +105,7 @@ bool Render::ConnectionStatusWindow(
|
|||||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||||
show_connection_status_window_ = false;
|
show_connection_status_window_ = false;
|
||||||
}
|
}
|
||||||
} else if (ConnectionStatus::IncorrectPassword == props->connection_status_) {
|
} else if (ConnectionStatus::IncorrectPassword == status) {
|
||||||
if (!password_validating_) {
|
if (!password_validating_) {
|
||||||
if (password_validating_time_ == 1) {
|
if (password_validating_time_ == 1) {
|
||||||
text = localization::input_password[localization_language_index_];
|
text = localization::input_password[localization_language_index_];
|
||||||
@@ -151,8 +168,7 @@ bool Render::ConnectionStatusWindow(
|
|||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
}
|
}
|
||||||
} else if (ConnectionStatus::NoSuchTransmissionId ==
|
} else if (ConnectionStatus::NoSuchTransmissionId == status) {
|
||||||
props->connection_status_) {
|
|
||||||
text = localization::no_such_id[localization_language_index_];
|
text = localization::no_such_id[localization_language_index_];
|
||||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||||
|
|||||||
@@ -356,10 +356,6 @@ int Render::SettingWindow() {
|
|||||||
ImGui::Separator();
|
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;
|
settings_items_offset += settings_items_padding;
|
||||||
ImGui::SetCursorPosY(settings_items_offset);
|
ImGui::SetCursorPosY(settings_items_offset);
|
||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
@@ -375,10 +371,6 @@ int Render::SettingWindow() {
|
|||||||
|
|
||||||
ImGui::Checkbox("##enable_minimize_to_tray_",
|
ImGui::Checkbox("##enable_minimize_to_tray_",
|
||||||
&enable_minimize_to_tray_);
|
&enable_minimize_to_tray_);
|
||||||
#ifndef _WIN32
|
|
||||||
ImGui::PopStyleColor();
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
@@ -626,14 +618,12 @@ int Render::SettingWindow() {
|
|||||||
}
|
}
|
||||||
enable_daemon_last_ = enable_daemon_;
|
enable_daemon_last_ = enable_daemon_;
|
||||||
|
|
||||||
#if _WIN32
|
|
||||||
if (enable_minimize_to_tray_) {
|
if (enable_minimize_to_tray_) {
|
||||||
config_center_->SetMinimizeToTray(true);
|
config_center_->SetMinimizeToTray(true);
|
||||||
} else {
|
} else {
|
||||||
config_center_->SetMinimizeToTray(false);
|
config_center_->SetMinimizeToTray(false);
|
||||||
}
|
}
|
||||||
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
||||||
#endif
|
|
||||||
|
|
||||||
// File transfer save path
|
// File transfer save path
|
||||||
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
|
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -149,14 +151,17 @@ int Render::RemoteClientInfoWindow() {
|
|||||||
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
||||||
|
|
||||||
std::vector<std::pair<std::string, std::string>> remote_entries;
|
std::vector<std::pair<std::string, std::string>> remote_entries;
|
||||||
remote_entries.reserve(connection_status_.size());
|
{
|
||||||
for (const auto& kv : connection_status_) {
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
const auto host_it = connection_host_names_.find(kv.first);
|
remote_entries.reserve(connection_status_.size());
|
||||||
const std::string display_name =
|
for (const auto& kv : connection_status_) {
|
||||||
(host_it != connection_host_names_.end() && !host_it->second.empty())
|
const auto host_it = connection_host_names_.find(kv.first);
|
||||||
? host_it->second
|
const std::string display_name =
|
||||||
: kv.first;
|
(host_it != connection_host_names_.end() && !host_it->second.empty())
|
||||||
remote_entries.emplace_back(kv.first, display_name);
|
? host_it->second
|
||||||
|
: kv.first;
|
||||||
|
remote_entries.emplace_back(kv.first, display_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto find_display_name_by_remote_id =
|
auto find_display_name_by_remote_id =
|
||||||
@@ -220,10 +225,14 @@ int Render::RemoteClientInfoWindow() {
|
|||||||
ImGui::SetWindowFontScale(font_scale);
|
ImGui::SetWindowFontScale(font_scale);
|
||||||
|
|
||||||
if (!selected_server_remote_id_.empty()) {
|
if (!selected_server_remote_id_.empty()) {
|
||||||
auto it = connection_status_.find(selected_server_remote_id_);
|
ConnectionStatus status = ConnectionStatus::Closed;
|
||||||
const ConnectionStatus status = (it == connection_status_.end())
|
{
|
||||||
? ConnectionStatus::Closed
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
: it->second;
|
auto it = connection_status_.find(selected_server_remote_id_);
|
||||||
|
if (it != connection_status_.end()) {
|
||||||
|
status = it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Text(
|
ImGui::Text(
|
||||||
"%s",
|
"%s",
|
||||||
@@ -376,4 +385,4 @@ int Render::RemoteClientInfoWindow() {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace crossdesk {
|
|||||||
void Render::DrawConnectionStatusText(
|
void Render::DrawConnectionStatusText(
|
||||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||||
std::string text;
|
std::string text;
|
||||||
switch (props->connection_status_) {
|
switch (props->connection_status_.load()) {
|
||||||
case ConnectionStatus::Disconnected:
|
case ConnectionStatus::Disconnected:
|
||||||
text = localization::p2p_disconnected[localization_language_index_];
|
text = localization::p2p_disconnected[localization_language_index_];
|
||||||
break;
|
break;
|
||||||
@@ -34,7 +34,7 @@ void Render::DrawConnectionStatusText(
|
|||||||
void Render::DrawReceivingScreenText(
|
void Render::DrawReceivingScreenText(
|
||||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||||
if (!props->connection_established_ ||
|
if (!props->connection_established_ ||
|
||||||
props->connection_status_ != ConnectionStatus::Connected) {
|
props->connection_status_.load() != ConnectionStatus::Connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ int ScreenCapturerDxgi::Resume(int monitor_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerDxgi::SwitchTo(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()) {
|
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
|
||||||
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
|
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
|
||||||
return -1;
|
return -1;
|
||||||
@@ -121,6 +122,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
|||||||
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
||||||
LOG_ERROR("DXGI: create duplication failed for monitor {}",
|
LOG_ERROR("DXGI: create duplication failed for monitor {}",
|
||||||
monitor_index_.load());
|
monitor_index_.load());
|
||||||
|
paused_ = false; // Reset paused_ on failure
|
||||||
return -2;
|
return -2;
|
||||||
}
|
}
|
||||||
paused_ = false;
|
paused_ = false;
|
||||||
@@ -130,6 +132,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerDxgi::ResetToInitialMonitor() {
|
int ScreenCapturerDxgi::ResetToInitialMonitor() {
|
||||||
|
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||||
if (display_info_list_.empty()) return -1;
|
if (display_info_list_.empty()) return -1;
|
||||||
int target = initial_monitor_index_;
|
int target = initial_monitor_index_;
|
||||||
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
|
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
|
||||||
@@ -245,6 +248,7 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||||
|
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||||
ReleaseDuplication();
|
ReleaseDuplication();
|
||||||
int current_monitor = monitor_index_.load();
|
int current_monitor = monitor_index_.load();
|
||||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||||
@@ -374,8 +378,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
|||||||
even_width, even_width, even_height);
|
even_width, even_width, even_height);
|
||||||
|
|
||||||
if (callback_) {
|
if (callback_) {
|
||||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
int idx = monitor_index_.load();
|
||||||
display_info_list_[monitor_index_].name.c_str());
|
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);
|
d3d_context_->Unmap(staging_.Get(), 0);
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
|||||||
std::thread thread_;
|
std::thread thread_;
|
||||||
int fps_ = 60;
|
int fps_ = 60;
|
||||||
cb_desktop_data callback_ = nullptr;
|
cb_desktop_data callback_ = nullptr;
|
||||||
|
std::mutex switch_mutex_;
|
||||||
|
|
||||||
unsigned char* nv12_frame_ = nullptr;
|
unsigned char* nv12_frame_ = nullptr;
|
||||||
int nv12_width_ = 0;
|
int nv12_width_ = 0;
|
||||||
|
|||||||
@@ -148,7 +148,14 @@ void ScreenCapturerGdi::CaptureLoop() {
|
|||||||
continue;
|
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 left = di.left;
|
||||||
int top = di.top;
|
int top = di.top;
|
||||||
int width = di.width & ~1;
|
int width = di.width & ~1;
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
|
|||||||
return 0;
|
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);
|
LOG_ERROR("Invalid monitor index: {}", monitor_index);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-10
@@ -225,9 +225,12 @@ int Thumbnail::LoadThumbnail(
|
|||||||
std::string remote_id;
|
std::string remote_id;
|
||||||
std::string cipher_password;
|
std::string cipher_password;
|
||||||
std::string remote_host_name;
|
std::string remote_host_name;
|
||||||
|
std::string password;
|
||||||
std::string original_image_name;
|
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_y = cipher_image_name.find('Y');
|
||||||
size_t pos_at = cipher_image_name.find('@');
|
size_t pos_at = cipher_image_name.find('@');
|
||||||
|
|
||||||
@@ -241,10 +244,11 @@ int Thumbnail::LoadThumbnail(
|
|||||||
remote_host_name =
|
remote_host_name =
|
||||||
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
|
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
|
||||||
cipher_password = cipher_image_name.substr(pos_at + 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 =
|
original_image_name = remote_id + 'Y' + remote_host_name + "@" +
|
||||||
remote_id + 'Y' + remote_host_name + "@" +
|
password;
|
||||||
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
|
||||||
} else {
|
} else {
|
||||||
size_t pos_n = cipher_image_name.find('N');
|
size_t pos_n = cipher_image_name.find('N');
|
||||||
// size_t pos_at = cipher_image_name.find('@');
|
// 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_id = cipher_image_name.substr(0, pos_n);
|
||||||
remote_host_name = cipher_image_name.substr(pos_n + 1);
|
remote_host_name = cipher_image_name.substr(pos_n + 1);
|
||||||
|
|
||||||
original_image_name =
|
original_image_name = remote_id + 'N' + remote_host_name;
|
||||||
remote_id + 'N' + remote_host_name + "@" +
|
|
||||||
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string image_path = save_path_ + cipher_image_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(
|
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,
|
LoadTextureFromFile(image_path.c_str(), renderer,
|
||||||
&(recent_connections[i].second.texture), width,
|
&(recent_connections.back().second.texture), width,
|
||||||
height);
|
height);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -436,4 +443,4 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
|
|||||||
|
|
||||||
return std::string(reinterpret_cast<char*>(plaintext), plaintext_len);
|
return std::string(reinterpret_cast<char*>(plaintext), plaintext_len);
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -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
@@ -35,7 +35,12 @@ function setup_platform_settings()
|
|||||||
add_links("pulse-simple", "pulse")
|
add_links("pulse-simple", "pulse")
|
||||||
add_requires("libyuv")
|
add_requires("libyuv")
|
||||||
add_syslinks("pthread", "dl")
|
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
|
if is_config("USE_DRM", true) then
|
||||||
add_links("drm")
|
add_links("drm")
|
||||||
@@ -75,7 +80,8 @@ function setup_platform_settings()
|
|||||||
add_links("SDL3")
|
add_links("SDL3")
|
||||||
add_ldflags("-Wl,-ld_classic")
|
add_ldflags("-Wl,-ld_classic")
|
||||||
add_cxflags("-Wno-unused-variable")
|
add_cxflags("-Wno-unused-variable")
|
||||||
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation",
|
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
|
||||||
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
|
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
|
||||||
|
"AudioToolbox")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+12
-2
@@ -44,6 +44,12 @@ function setup_targets()
|
|||||||
add_includedirs("src/device_controller")
|
add_includedirs("src/device_controller")
|
||||||
add_files("tests/macos_keyboard_modifier_state_test.cpp")
|
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")
|
target("windows_manifest_resource_test")
|
||||||
set_kind("binary")
|
set_kind("binary")
|
||||||
set_default(false)
|
set_default(false)
|
||||||
@@ -208,11 +214,15 @@ function setup_targets()
|
|||||||
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
||||||
"src/gui/windows", {public = true})
|
"src/gui/windows", {public = true})
|
||||||
if is_os("windows") then
|
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",
|
add_includedirs("src/gui/tray", "src/service/windows",
|
||||||
{public = true})
|
{public = true})
|
||||||
elseif is_os("macosx") then
|
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
|
end
|
||||||
|
|
||||||
if is_os("windows") then
|
if is_os("windows") then
|
||||||
|
|||||||
Reference in New Issue
Block a user