mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-07-20 22:08:47 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eeb6a2a1ae | |||
| 1f86c43458 | |||
| c487d3a62c | |||
| aee5be5ee6 | |||
| 6ff2783105 | |||
| d6d30b6842 | |||
| 5db309243b | |||
| 0681f6540d | |||
| d843901550 | |||
| b41630ebb4 | |||
| a815eecc73 | |||
| 7e0984fe9c | |||
| 9c28cd2ab2 | |||
| 3c4000bdbb | |||
| d8e9fa5bba | |||
| e026491b9f | |||
| 009699b375 | |||
| 3677588a3d | |||
| 3d280053a7 | |||
| fbde3f6a47 | |||
| 1c1a33fdce | |||
| fea238722d | |||
| 178d958c08 | |||
| f9633f366b | |||
| 7a81f3e767 | |||
| bbbbbf7927 |
@@ -104,6 +104,7 @@ jobs:
|
||||
CUDA_PATH: /usr/local/cuda
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
apt install -y libxft-dev
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
schedule:
|
||||
# run every day at midnight
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -15,19 +16,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check inactive issues and close them
|
||||
uses: actions/github-script@v6
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const { data: issues } = await github.rest.issues.listForRepo({
|
||||
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
const now = Date.now();
|
||||
|
||||
// paginate through all open issues (listForRepo also returns PRs)
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const now = new Date().getTime();
|
||||
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
for (const issue of issues) {
|
||||
// skip pull requests (they are also returned by listForRepo)
|
||||
if (issue.pull_request) continue;
|
||||
@@ -38,26 +41,14 @@ jobs:
|
||||
continue;
|
||||
}
|
||||
|
||||
// fetch comments for this issue
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// determine the "last activity" time
|
||||
let lastActivityTime;
|
||||
if (comments.length > 0) {
|
||||
const lastComment = comments[comments.length - 1];
|
||||
lastActivityTime = new Date(lastComment.updated_at).getTime();
|
||||
} else {
|
||||
lastActivityTime = new Date(issue.created_at).getTime();
|
||||
}
|
||||
// last activity time = the issue's own updated_at, which is
|
||||
// refreshed on comments, labels, etc. This avoids relying on
|
||||
// fetching comments and is accurate even when comments are edited.
|
||||
const lastActivityTime = new Date(issue.updated_at).getTime();
|
||||
|
||||
// check inactivity
|
||||
if (now - lastActivityTime > inactivePeriod) {
|
||||
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`);
|
||||
console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
@@ -76,5 +67,3 @@ jobs:
|
||||
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
|
||||
}
|
||||
}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# GUI 目录结构与职责说明
|
||||
|
||||
本文档说明 `src/gui` 的目录结构、核心类型、依赖方向和代码归属规则,用于避免 `Render`、`GuiApplication` 或 `GuiRuntime` 再次演变成职责混杂的大类。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
src/gui/
|
||||
├── render.h # 对外稳定入口 Render
|
||||
├── render.cpp # 创建 GuiApplication 并转发 Run
|
||||
├── application/ # SDL/ImGui 应用外壳
|
||||
│ ├── gui_application.h # GuiApplication 声明
|
||||
│ ├── gui_application.cpp # 初始化、主循环和清理
|
||||
│ ├── application_state.h # 窗口、交互和 UI 状态
|
||||
│ ├── sdl_event_dispatch.cpp # SDL 窗口及应用事件分发
|
||||
│ ├── sdl_events.cpp # 键盘和鼠标事件转换
|
||||
│ ├── window_lifecycle.cpp # 原生窗口及 ImGui 上下文生命周期
|
||||
│ └── window_rendering.cpp # 三类窗口的渲染流程
|
||||
├── runtime/ # 非界面的 GUI 运行时
|
||||
│ ├── gui_runtime.h/.cpp # 运行时协调接口与公共实现
|
||||
│ ├── connection_runtime.cpp # 连接、在线探测、超时及会话清理
|
||||
│ ├── windows_service_runtime.cpp # Windows 服务和安全桌面集成
|
||||
│ ├── mac_permission_runtime.mm # macOS 权限检查和系统设置调用
|
||||
│ ├── gui_state.h # ApplicationState 与 RuntimeState 组合点
|
||||
│ ├── runtime_state.h # 配置、连接、平台和通信状态
|
||||
│ ├── remote_session.h # 单个远端会话 RemoteSession
|
||||
│ ├── device_presence_cache.h # 设备在线状态缓存
|
||||
│ ├── peer_event_handler.h/.cpp # 信令和连接状态回调
|
||||
│ ├── peer_media_callbacks.cpp # 视频和音频回调
|
||||
│ ├── peer_data_callbacks.cpp # 控制、剪贴板和文件数据回调
|
||||
│ └── remote_action_codec.h/.cpp # RemoteAction 编解码
|
||||
├── features/ # 可独立演进的功能模块
|
||||
│ ├── clipboard/ # 本地与远端剪贴板同步
|
||||
│ ├── devices/ # 媒体及输入设备生命周期
|
||||
│ ├── file_transfer/ # 文件队列、发送、确认和进度
|
||||
│ ├── input/ # 键盘状态、命令和超时处理
|
||||
│ └── settings/ # 配置缓存和最近连接别名
|
||||
├── views/ # ImGui 视图实现
|
||||
│ ├── panels/ # 主窗口内嵌面板
|
||||
│ ├── toolbars/ # 标题栏、状态栏和控制栏
|
||||
│ └── windows/ # 独立窗口和模态对话框
|
||||
├── platform/ # 原生桌面平台适配
|
||||
│ └── tray/ # Windows、macOS 和 Linux 托盘
|
||||
└── assets/ # 字体、图标、布局和本地化资源
|
||||
```
|
||||
|
||||
## 分层关系
|
||||
|
||||
```text
|
||||
Render
|
||||
└── GuiApplication
|
||||
├── SDL/ImGui 生命周期
|
||||
├── views/panels / views/toolbars / views/windows
|
||||
└── GuiRuntime
|
||||
├── SessionDeviceManager
|
||||
├── KeyboardController
|
||||
├── ClipboardController
|
||||
├── FileTransferManager
|
||||
├── SettingsManager
|
||||
└── PeerEventHandler
|
||||
```
|
||||
|
||||
依赖方向应保持从上向下:
|
||||
|
||||
```text
|
||||
公开入口 → 应用层 → 运行时协调层 → 功能模块 → 底层库
|
||||
↘ 视图层
|
||||
```
|
||||
|
||||
底层功能模块不应反向依赖具体面板、工具栏或窗口。
|
||||
|
||||
## 核心类型
|
||||
|
||||
### Render
|
||||
|
||||
`Render` 是应用其他部分可见的稳定入口,只负责:
|
||||
|
||||
- 管理 `GuiApplication` 生命周期;
|
||||
- 将 `Run()` 转发给 `GuiApplication`;
|
||||
- 隔离 GUI 内部类型,避免实现细节扩散到其他模块。
|
||||
|
||||
不要向 `Render` 添加窗口状态、连接状态或业务方法。
|
||||
|
||||
### GuiApplication
|
||||
|
||||
`GuiApplication` 是 SDL/ImGui 应用外壳,负责:
|
||||
|
||||
- 初始化日志、配置、SDL 和功能模块;
|
||||
- 创建、销毁原生窗口及 ImGui 上下文;
|
||||
- 执行主事件循环;
|
||||
- 分发 SDL 事件;
|
||||
- 调用面板、工具栏和窗口的绘制方法;
|
||||
- 在程序退出时按顺序清理资源。
|
||||
|
||||
它不应实现连接协议、文件传输、剪贴板或设备控制细节。
|
||||
|
||||
### GuiRuntime
|
||||
|
||||
`GuiRuntime` 是 GUI 进程的非界面协调层,负责:
|
||||
|
||||
- 初始化 MiniRTC Peer 及回调参数;
|
||||
- 协调连接、远端会话和在线状态;
|
||||
- 持有各功能 Manager/Controller;
|
||||
- 处理跨功能模块的调用顺序;
|
||||
- 提供平台集成功能所需的运行时上下文。
|
||||
|
||||
`GuiRuntime` 应保持为协调器。新的独立功能应优先创建 Manager 或 Controller,不应直接继续堆积到 `GuiRuntime`。
|
||||
|
||||
### RemoteSession
|
||||
|
||||
`RemoteSession` 表示一个连接中或已连接的远端端点,其生命周期覆盖:
|
||||
|
||||
- MiniRTC Peer 和连接状态;
|
||||
- 视频帧、纹理和渲染区域;
|
||||
- 远端显示器和控制栏状态;
|
||||
- 音频、鼠标和键盘控制状态;
|
||||
- 文件传输状态;
|
||||
- Windows 服务及安全桌面状态。
|
||||
|
||||
`remote_sessions_` 是按远端 ID 索引的会话表。会话查找、连接和清理统一由运行时处理。
|
||||
|
||||
## 状态划分
|
||||
|
||||
状态定义按生命周期和使用范围划分:
|
||||
|
||||
| 文件 | 状态范围 |
|
||||
| --- | --- |
|
||||
| `application/application_state.h` | SDL 窗口、渲染上下文、交互标志和 UI 显示状态 |
|
||||
| `runtime/runtime_state.h` | 配置、Peer、连接表、在线探测和平台集成状态 |
|
||||
| `runtime/remote_session.h` | 单个远端会话独占的连接、媒体、控制和文件状态 |
|
||||
| `runtime/gui_state.h` | 仅组合 `ApplicationState` 与 `RuntimeState` |
|
||||
|
||||
添加状态前应先判断其生命周期:
|
||||
|
||||
- 只属于某个远端连接:放入 `RemoteSession`;
|
||||
- 只属于窗口或 UI:放入 `ApplicationState` 对应子状态;
|
||||
- 属于整个 GUI 运行期:放入 `RuntimeState` 对应子状态;
|
||||
- 只属于某个功能模块:优先作为 Manager/Controller 的私有成员。
|
||||
|
||||
## 主要运行流程
|
||||
|
||||
### 启动
|
||||
|
||||
```text
|
||||
Render::Run
|
||||
→ GuiApplication::Run
|
||||
→ 初始化路径、日志和配置
|
||||
→ 初始化 SDL 和功能模块
|
||||
→ GuiRuntime::CreateConnectionPeer
|
||||
→ 创建主窗口
|
||||
→ GuiApplication::MainLoop
|
||||
```
|
||||
|
||||
### 建立远端连接
|
||||
|
||||
```text
|
||||
remote_peer_panel
|
||||
→ GuiRuntime::ConnectTo
|
||||
→ 在线状态探测
|
||||
→ 创建或复用 RemoteSession
|
||||
→ MiniRTC JoinConnection
|
||||
→ PeerEventHandler 接收连接状态
|
||||
→ 创建串流窗口并开始渲染
|
||||
```
|
||||
|
||||
### 接收远端数据
|
||||
|
||||
```text
|
||||
MiniRTC callback
|
||||
→ PeerEventHandler
|
||||
├── peer_media_callbacks:视频、音频
|
||||
├── peer_data_callbacks:控制、文件、剪贴板
|
||||
└── peer_event_handler:信令、连接状态、网络统计
|
||||
→ 对应 Manager/Controller
|
||||
→ 更新 RemoteSession 或运行时状态
|
||||
```
|
||||
|
||||
### 退出和清理
|
||||
|
||||
```text
|
||||
SDL quit / tray exit
|
||||
→ GuiApplication::Cleanup
|
||||
→ CloseAllRemoteSessions
|
||||
→ 停止设备及后台任务
|
||||
→ 销毁 Peer
|
||||
→ 销毁 ImGui 上下文和 SDL 窗口
|
||||
→ SDL_Quit
|
||||
```
|
||||
|
||||
## 新代码归属规则
|
||||
|
||||
新增代码时按以下规则选择目录:
|
||||
|
||||
| 功能 | 目录 |
|
||||
| --- | --- |
|
||||
| SDL 初始化、事件循环、窗口生命周期 | `application/` |
|
||||
| 连接、会话、Peer 回调和平台运行时 | `runtime/` |
|
||||
| 视频、音频、鼠标、键盘设备生命周期 | `features/devices/` |
|
||||
| 键盘协议和按键状态 | `features/input/` |
|
||||
| 剪贴板同步 | `features/clipboard/` |
|
||||
| 文件传输 | `features/file_transfer/` |
|
||||
| 配置持久化 | `features/settings/` |
|
||||
| 主窗口内嵌区域 | `views/panels/` |
|
||||
| 标题栏、状态栏、控制栏 | `views/toolbars/` |
|
||||
| 独立窗口和对话框 | `views/windows/` |
|
||||
| 系统托盘 | `platform/tray/` |
|
||||
| 字体、图标、布局和本地化数据 | `assets/` |
|
||||
+5
-1
@@ -128,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
|
||||
if (pid > 0) _exit(0);
|
||||
|
||||
umask(0);
|
||||
chdir("/");
|
||||
if (chdir("/") != 0) {
|
||||
std::cerr << "Failed to change daemon working directory to /: "
|
||||
<< std::strerror(errno) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// redirect file descriptors: keep stdout/stderr if from terminal, else
|
||||
// redirect to /dev/null
|
||||
|
||||
@@ -5,6 +5,15 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsValidTurnModeValue(long value) {
|
||||
return value >= static_cast<long>(ConfigCenter::TURN_MODE::DISABLED) &&
|
||||
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCenter::ConfigCenter(const std::string& config_path)
|
||||
: config_path_(config_path) {
|
||||
ini_.SetUnicode(true);
|
||||
@@ -20,6 +29,8 @@ int ConfigCenter::Load() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool persist_turn_mode_migration = false;
|
||||
|
||||
const long language_value =
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
|
||||
@@ -42,7 +53,25 @@ int ConfigCenter::Load() {
|
||||
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
|
||||
hardware_video_codec_);
|
||||
|
||||
enable_turn_ = ini_.GetBoolValue(section_, "enable_turn", enable_turn_);
|
||||
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
|
||||
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
|
||||
const long parsed_turn_mode = ini_.GetLongValue(
|
||||
section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
if (IsValidTurnModeValue(parsed_turn_mode)) {
|
||||
turn_mode_ = static_cast<TURN_MODE>(parsed_turn_mode);
|
||||
} else {
|
||||
LOG_WARN("Invalid TURN mode [{}], using auto UDP/TCP",
|
||||
parsed_turn_mode);
|
||||
turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
}
|
||||
} else {
|
||||
const bool legacy_enable_turn = ini_.GetBoolValue(
|
||||
section_, "enable_turn", turn_mode_ != TURN_MODE::DISABLED);
|
||||
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
|
||||
: TURN_MODE::DISABLED;
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
persist_turn_mode_migration = true;
|
||||
}
|
||||
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
enable_self_hosted_ =
|
||||
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
@@ -92,6 +121,11 @@ int ConfigCenter::Load() {
|
||||
file_transfer_save_path_ = "";
|
||||
}
|
||||
|
||||
if (persist_turn_mode_migration &&
|
||||
ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -104,7 +138,9 @@ int ConfigCenter::Save() {
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
ini_.SetBoolValue(section_, "enable_turn", enable_turn_);
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
@@ -191,9 +227,15 @@ int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurn(bool enable_turn) {
|
||||
enable_turn_ = enable_turn;
|
||||
ini_.SetBoolValue(section_, "enable_turn", enable_turn_);
|
||||
int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
|
||||
if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
turn_mode_ = turn_mode;
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
@@ -201,6 +243,16 @@ int ConfigCenter::SetTurn(bool enable_turn) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurn(bool enable_turn) {
|
||||
if (!enable_turn) {
|
||||
return SetTurnMode(TURN_MODE::DISABLED);
|
||||
}
|
||||
if (turn_mode_ == TURN_MODE::DISABLED) {
|
||||
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
|
||||
}
|
||||
return SetTurnMode(turn_mode_);
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSrtp(bool enable_srtp) {
|
||||
enable_srtp_ = enable_srtp;
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
@@ -362,7 +414,13 @@ bool ConfigCenter::IsHardwareVideoCodec() const {
|
||||
return hardware_video_codec_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableTurn() const { return enable_turn_; }
|
||||
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
|
||||
return turn_mode_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableTurn() const {
|
||||
return turn_mode_ != TURN_MODE::DISABLED;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ class ConfigCenter {
|
||||
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
|
||||
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
AUTO_UDP_TCP = 1,
|
||||
FORCE_UDP = 2,
|
||||
FORCE_TCP = 3
|
||||
};
|
||||
|
||||
public:
|
||||
explicit ConfigCenter(const std::string& config_path = "config.ini");
|
||||
@@ -30,6 +36,7 @@ class ConfigCenter {
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
int SetTurn(bool enable_turn);
|
||||
int SetSrtp(bool enable_srtp);
|
||||
int SetServerHost(const std::string& signal_server_host);
|
||||
@@ -49,6 +56,7 @@ class ConfigCenter {
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
bool IsEnableTurn() const;
|
||||
bool IsEnableSrtp() const;
|
||||
std::string GetSignalServerHost() const;
|
||||
@@ -77,7 +85,7 @@ class ConfigCenter {
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
bool enable_turn_ = true;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
bool enable_srtp_ = false;
|
||||
std::string signal_server_host_ = "";
|
||||
std::string signal_server_host_default_ = "api.crossdesk.cn";
|
||||
|
||||
@@ -21,12 +21,13 @@ namespace crossdesk {
|
||||
|
||||
typedef enum {
|
||||
mouse = 0,
|
||||
keyboard,
|
||||
audio_capture,
|
||||
host_infomation,
|
||||
display_id,
|
||||
service_status,
|
||||
service_command,
|
||||
keyboard = 1,
|
||||
audio_capture = 2,
|
||||
host_infomation = 3,
|
||||
display_id = 4,
|
||||
service_status = 5,
|
||||
service_command = 6,
|
||||
keyboard_state = 7,
|
||||
} ControlType;
|
||||
typedef enum {
|
||||
move = 0,
|
||||
@@ -55,6 +56,20 @@ typedef struct {
|
||||
KeyFlag flag;
|
||||
} Key;
|
||||
|
||||
inline constexpr size_t kMaxKeyboardStateKeys = 32;
|
||||
|
||||
typedef struct {
|
||||
size_t key_value;
|
||||
uint32_t scan_code;
|
||||
bool extended;
|
||||
} KeyboardStateKey;
|
||||
|
||||
typedef struct {
|
||||
uint32_t seq;
|
||||
size_t pressed_count;
|
||||
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
|
||||
} KeyboardState;
|
||||
|
||||
typedef struct {
|
||||
char host_name[64];
|
||||
size_t host_name_size;
|
||||
@@ -80,6 +95,7 @@ struct RemoteAction {
|
||||
union {
|
||||
Mouse m;
|
||||
Key k;
|
||||
KeyboardState ks;
|
||||
HostInfo i;
|
||||
bool a;
|
||||
int d;
|
||||
@@ -111,6 +127,20 @@ struct RemoteAction {
|
||||
{"extended", a.k.extended},
|
||||
{"flag", a.k.flag}};
|
||||
break;
|
||||
case ControlType::keyboard_state: {
|
||||
json keys = json::array();
|
||||
const size_t pressed_count =
|
||||
a.ks.pressed_count < kMaxKeyboardStateKeys
|
||||
? a.ks.pressed_count
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < pressed_count; ++idx) {
|
||||
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
|
||||
{"scan_code", a.ks.pressed_keys[idx].scan_code},
|
||||
{"extended", a.ks.pressed_keys[idx].extended}});
|
||||
}
|
||||
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
j["audio_capture"] = a.a;
|
||||
break;
|
||||
@@ -162,6 +192,33 @@ struct RemoteAction {
|
||||
out.k.extended = j.at("keyboard").value("extended", false);
|
||||
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
||||
break;
|
||||
case ControlType::keyboard_state: {
|
||||
const auto& keyboard_state_json = j.at("keyboard_state");
|
||||
out.ks.seq = keyboard_state_json.value("seq", 0u);
|
||||
out.ks.pressed_count = 0;
|
||||
|
||||
const auto keys_json =
|
||||
keyboard_state_json.value("pressed_keys", json::array());
|
||||
if (!keys_json.is_array()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t count =
|
||||
keys_json.size() < kMaxKeyboardStateKeys
|
||||
? keys_json.size()
|
||||
: kMaxKeyboardStateKeys;
|
||||
for (size_t idx = 0; idx < count; ++idx) {
|
||||
const auto& key_json = keys_json[idx];
|
||||
out.ks.pressed_keys[idx].key_value =
|
||||
key_json.at("key_value").get<size_t>();
|
||||
out.ks.pressed_keys[idx].scan_code =
|
||||
key_json.value("scan_code", static_cast<uint32_t>(0));
|
||||
out.ks.pressed_keys[idx].extended =
|
||||
key_json.value("extended", false);
|
||||
}
|
||||
out.ks.pressed_count = count;
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
out.a = j.at("audio_capture").get<bool>();
|
||||
break;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <dbus/dbus.h>
|
||||
#endif
|
||||
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "rd_log.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
@@ -579,33 +580,46 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode >= 0 &&
|
||||
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
const int keysym = key_it->second;
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
if (evdev_keycode >= 0) {
|
||||
LOG_ERROR(
|
||||
"Failed to send Wayland keyboard keycode event, vk_code={}, "
|
||||
"evdev_keycode={}, is_down={}",
|
||||
key_code, evdev_keycode, is_down);
|
||||
return -3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
const int keysym = key_it->second;
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
if (x11_keycode > 8) {
|
||||
const int evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int keysym = key_it->second;
|
||||
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
||||
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
||||
return 0;
|
||||
|
||||
@@ -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
|
||||
@@ -1,11 +1,36 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDoubleClickInterval = std::chrono::milliseconds(500);
|
||||
constexpr int kDoubleClickMaxDistance = 8;
|
||||
constexpr int kMaxClickState = 3;
|
||||
|
||||
bool IsWithinClickDistance(int x1, int y1, int x2, int y2) {
|
||||
return std::abs(x1 - x2) <= kDoubleClickMaxDistance &&
|
||||
std::abs(y1 - y2) <= kDoubleClickMaxDistance;
|
||||
}
|
||||
|
||||
void SetClickState(CGEventRef event, int click_state) {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGEventSetIntegerValueField(
|
||||
event, kCGMouseEventClickState,
|
||||
std::max(1, std::min(click_state, kMaxClickState)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MouseController::MouseController() {}
|
||||
|
||||
@@ -19,6 +44,36 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
|
||||
int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::BeginClick(ClickTracker& tracker, int x, int y) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool continues_previous_click =
|
||||
tracker.has_last_down &&
|
||||
now - tracker.last_down_time <= kDoubleClickInterval &&
|
||||
IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y);
|
||||
|
||||
tracker.click_state = continues_previous_click
|
||||
? std::min(tracker.click_state + 1, kMaxClickState)
|
||||
: 1;
|
||||
tracker.active_click_state = tracker.click_state;
|
||||
tracker.has_last_down = true;
|
||||
tracker.last_down_time = now;
|
||||
tracker.last_down_x = x;
|
||||
tracker.last_down_y = y;
|
||||
|
||||
return tracker.active_click_state;
|
||||
}
|
||||
|
||||
int MouseController::EndClick(ClickTracker& tracker, int x, int y) {
|
||||
const int click_state = tracker.active_click_state;
|
||||
if (!IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y)) {
|
||||
tracker.has_last_down = false;
|
||||
tracker.click_state = 0;
|
||||
tracker.active_click_state = 1;
|
||||
}
|
||||
|
||||
return click_state;
|
||||
}
|
||||
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
if (remote_action.type != ControlType::mouse) {
|
||||
@@ -41,58 +96,69 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
|
||||
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
int mouse_pos_x =
|
||||
normalized_x * display_info.width + display_info.left;
|
||||
int mouse_pos_y =
|
||||
normalized_y * display_info.height + display_info.top;
|
||||
int mouse_pos_x = normalized_x * display_info.width + display_info.left;
|
||||
int mouse_pos_y = normalized_y * display_info.height + display_info.top;
|
||||
|
||||
CGEventRef mouse_event = nullptr;
|
||||
CGEventType mouse_type;
|
||||
CGMouseButton mouse_button;
|
||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||
int click_state = 1;
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
mouse_type = kCGEventLeftMouseDown;
|
||||
left_dragging_ = true;
|
||||
click_state = BeginClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
mouse_type = kCGEventLeftMouseUp;
|
||||
left_dragging_ = false;
|
||||
click_state = EndClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
mouse_type = kCGEventRightMouseDown;
|
||||
right_dragging_ = true;
|
||||
click_state = BeginClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
mouse_type = kCGEventRightMouseUp;
|
||||
right_dragging_ = false;
|
||||
click_state = EndClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
mouse_type = kCGEventOtherMouseDown;
|
||||
click_state = BeginClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
mouse_type = kCGEventOtherMouseUp;
|
||||
click_state = EndClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
SetClickState(mouse_event, click_state);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
|
||||
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
|
||||
2, remote_action.m.s, 0);
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
|
||||
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
|
||||
2, 0, remote_action.m.s);
|
||||
break;
|
||||
default:
|
||||
if (left_dragging_) {
|
||||
@@ -106,8 +172,8 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
}
|
||||
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
mouse_button);
|
||||
mouse_event =
|
||||
CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, mouse_button);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifndef _MOUSE_CONTROLLER_H_
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
@@ -24,9 +25,24 @@ class MouseController : public DeviceController {
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
|
||||
private:
|
||||
struct ClickTracker {
|
||||
bool has_last_down = false;
|
||||
std::chrono::steady_clock::time_point last_down_time{};
|
||||
int last_down_x = 0;
|
||||
int last_down_y = 0;
|
||||
int click_state = 0;
|
||||
int active_click_state = 1;
|
||||
};
|
||||
|
||||
int BeginClick(ClickTracker& tracker, int x, int y);
|
||||
int EndClick(ClickTracker& tracker, int x, int y);
|
||||
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
bool left_dragging_ = false;
|
||||
bool right_dragging_ = false;
|
||||
ClickTracker left_click_tracker_;
|
||||
ClickTracker right_click_tracker_;
|
||||
ClickTracker middle_click_tracker_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,228 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "platform/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/tray/mac_tray.h"
|
||||
#elif defined(__linux__)
|
||||
#include "platform/tray/linux_tray.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct MainWindowState {
|
||||
float title_bar_width_ = 640;
|
||||
float title_bar_height_ = 30;
|
||||
float title_bar_button_width_ = 30;
|
||||
float title_bar_button_height_ = 30;
|
||||
|
||||
SDL_Window *main_window_ = nullptr;
|
||||
SDL_Renderer *main_renderer_ = nullptr;
|
||||
ImGuiContext *main_ctx_ = nullptr;
|
||||
ImFont *main_windows_system_chinese_font_ = nullptr;
|
||||
ImFont *stream_windows_system_chinese_font_ = nullptr;
|
||||
ImFont *server_windows_system_chinese_font_ = nullptr;
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16;
|
||||
#if _WIN32
|
||||
std::unique_ptr<WinTray> tray_;
|
||||
#elif defined(__APPLE__)
|
||||
std::unique_ptr<MacTray> tray_;
|
||||
#elif defined(__linux__)
|
||||
std::unique_ptr<LinuxTray> tray_;
|
||||
#endif
|
||||
|
||||
bool main_window_minimized_ = false;
|
||||
uint32_t last_main_minimize_request_tick_ = 0;
|
||||
uint32_t last_stream_minimize_request_tick_ = 0;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
float main_window_dpi_scaling_w_ = 1.0f;
|
||||
float main_window_dpi_scaling_h_ = 1.0f;
|
||||
float dpi_scale_ = 1.0f;
|
||||
float main_window_width_default_ = 640;
|
||||
float main_window_height_default_ = 480;
|
||||
float main_window_width_ = 640;
|
||||
float main_window_height_ = 480;
|
||||
float main_window_width_last_ = 640;
|
||||
float main_window_height_last_ = 480;
|
||||
float local_window_width_ = 320;
|
||||
float local_window_height_ = 235;
|
||||
float remote_window_width_ = 320;
|
||||
float remote_window_height_ = 235;
|
||||
float local_child_window_width_ = 266;
|
||||
float local_child_window_height_ = 180;
|
||||
float remote_child_window_width_ = 266;
|
||||
float remote_child_window_height_ = 180;
|
||||
float main_window_text_y_padding_ = 10;
|
||||
float main_child_window_x_padding_ = 27;
|
||||
float main_child_window_y_padding_ = 45;
|
||||
float status_bar_height_ = 22;
|
||||
float connection_status_window_width_ = 200;
|
||||
float connection_status_window_height_ = 150;
|
||||
float notification_window_width_ = 200;
|
||||
float notification_window_height_ = 80;
|
||||
float about_window_width_ = 300;
|
||||
float about_window_height_ = 170;
|
||||
float update_notification_window_width_ = 400;
|
||||
float update_notification_window_height_ = 320;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
uint32_t APP_EXIT_EVENT = 0;
|
||||
};
|
||||
|
||||
// Application-global interaction flags that coordinate SDL events with media
|
||||
// and remote-control devices.
|
||||
struct InteractionState {
|
||||
bool start_mouse_controller_ = false;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool start_screen_capturer_ = false;
|
||||
bool screen_capturer_is_started_ = false;
|
||||
bool start_speaker_capturer_ = false;
|
||||
bool speaker_capturer_is_started_ = false;
|
||||
bool start_keyboard_capturer_ = false;
|
||||
bool show_cursor_ = false;
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool keyboard_capturer_uses_sdl_events_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool focus_on_stream_window_ = false;
|
||||
bool audio_capture_ = false;
|
||||
int screen_width_ = 1280;
|
||||
int screen_height_ = 720;
|
||||
int selected_display_ = 0;
|
||||
std::string connect_button_label_ = "Connect";
|
||||
char input_password_tmp_[7] = "";
|
||||
char input_password_[7] = "";
|
||||
std::string random_password_;
|
||||
char new_password_[7] = "";
|
||||
char remote_id_display_[12] = "";
|
||||
unsigned char audio_buffer_[720]{};
|
||||
int audio_len_ = 0;
|
||||
bool audio_buffer_fresh_ = false;
|
||||
bool need_to_rejoin_ = false;
|
||||
std::chrono::steady_clock::time_point last_rejoin_check_time_ =
|
||||
std::chrono::steady_clock::now();
|
||||
bool just_created_ = false;
|
||||
std::string controlled_remote_id_;
|
||||
std::string focused_remote_id_;
|
||||
std::string remote_client_id_;
|
||||
SDL_Event last_mouse_event{};
|
||||
};
|
||||
|
||||
struct UpdateState {
|
||||
nlohmann::json latest_version_info_ = nlohmann::json{};
|
||||
bool update_available_ = false;
|
||||
std::string latest_version_;
|
||||
std::string release_notes_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
};
|
||||
|
||||
struct StreamWindowState {
|
||||
SDL_Window *stream_window_ = nullptr;
|
||||
SDL_Renderer *stream_renderer_ = nullptr;
|
||||
ImGuiContext *stream_ctx_ = nullptr;
|
||||
bool need_to_create_stream_window_ = false;
|
||||
bool stream_window_created_ = false;
|
||||
bool stream_window_inited_ = false;
|
||||
bool window_maximized_ = false;
|
||||
bool stream_window_grabbed_ = false;
|
||||
bool control_mouse_ = false;
|
||||
int stream_window_width_default_ = 1280;
|
||||
int stream_window_height_default_ = 720;
|
||||
float stream_window_width_ = 1280;
|
||||
float stream_window_height_ = 720;
|
||||
SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int stream_window_width_real_ = 1280;
|
||||
int stream_window_height_real_ = 720;
|
||||
float stream_window_dpi_scaling_w_ = 1.0f;
|
||||
float stream_window_dpi_scaling_h_ = 1.0f;
|
||||
};
|
||||
|
||||
struct ServerWindowState {
|
||||
SDL_Window *server_window_ = nullptr;
|
||||
SDL_Renderer *server_renderer_ = nullptr;
|
||||
ImGuiContext *server_ctx_ = nullptr;
|
||||
bool need_to_create_server_window_ = false;
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
float window_rounding_ = 6.0f;
|
||||
float window_rounding_default_ = 6.0f;
|
||||
bool server_window_collapsed_ = false;
|
||||
bool server_window_collapsed_dragging_ = false;
|
||||
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_collapsed_drag_start_win_x_ = 0;
|
||||
int server_window_collapsed_drag_start_win_y_ = 0;
|
||||
bool server_window_dragging_ = false;
|
||||
float server_window_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_drag_start_win_x_ = 0;
|
||||
int server_window_drag_start_win_y_ = 0;
|
||||
};
|
||||
|
||||
struct UiState {
|
||||
bool label_inited_ = false;
|
||||
bool connect_button_pressed_ = false;
|
||||
bool password_validating_ = false;
|
||||
uint32_t password_validating_time_ = 0;
|
||||
bool show_settings_window_ = false;
|
||||
bool show_self_hosted_server_config_window_ = false;
|
||||
bool rejoin_ = false;
|
||||
bool local_id_copied_ = false;
|
||||
bool show_password_ = true;
|
||||
bool show_about_window_ = false;
|
||||
bool show_connection_status_window_ = false;
|
||||
bool show_reset_password_window_ = false;
|
||||
bool show_update_notification_window_ = false;
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool focus_on_input_widget_ = true;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool show_edit_connection_alias_window_ = false;
|
||||
bool show_offline_warning_window_ = false;
|
||||
bool delete_connection_ = false;
|
||||
bool is_tab_bar_hovered_ = false;
|
||||
std::string delete_connection_name_;
|
||||
std::string edit_connection_alias_remote_id_;
|
||||
char edit_connection_alias_[128] = "";
|
||||
std::string offline_warning_text_;
|
||||
bool re_enter_remote_id_ = false;
|
||||
double copy_start_time_ = 0;
|
||||
};
|
||||
|
||||
struct ApplicationState : MainWindowState,
|
||||
InteractionState,
|
||||
UpdateState,
|
||||
StreamWindowState,
|
||||
ServerWindowState,
|
||||
UiState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
@@ -0,0 +1,295 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
GuiApplication::GuiApplication() = default;
|
||||
|
||||
GuiApplication::~GuiApplication() = default;
|
||||
|
||||
int GuiApplication::Run() {
|
||||
path_manager_ = std::make_unique<PathManager>("CrossDesk");
|
||||
if (path_manager_) {
|
||||
exec_log_path_ = path_manager_->GetLogPath().string();
|
||||
dll_log_path_ = path_manager_->GetLogPath().string();
|
||||
cache_path_ = path_manager_->GetCachePath().string();
|
||||
config_center_ =
|
||||
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
||||
strncpy(signal_server_ip_self_,
|
||||
config_center_->GetSignalServerHost().c_str(),
|
||||
sizeof(signal_server_ip_self_) - 1);
|
||||
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
|
||||
int signal_port_init = config_center_->GetSignalServerPort();
|
||||
if (signal_port_init > 0) {
|
||||
strncpy(signal_server_port_self_,
|
||||
std::to_string(signal_port_init).c_str(),
|
||||
sizeof(signal_server_port_self_) - 1);
|
||||
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
|
||||
} else {
|
||||
signal_server_port_self_[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Failed to create PathManager" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
InitializeLogger();
|
||||
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
|
||||
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty()) {
|
||||
std::string version;
|
||||
if (latest_version_info_.contains("latest_version") &&
|
||||
latest_version_info_["latest_version"].is_string()) {
|
||||
version = latest_version_info_["latest_version"].get<std::string>();
|
||||
} else if (latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
version = latest_version_info_["version"].get<std::string>();
|
||||
}
|
||||
|
||||
if (!version.empty()) {
|
||||
latest_version_ = 'v' + version;
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
}
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ =
|
||||
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
LOG_INFO("Update check: current={}, latest={}, available={}",
|
||||
CROSSDESK_VERSION, latest_version_, update_available_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
LOG_WARN("Update check skipped: version.json is empty or missing "
|
||||
"latest_version");
|
||||
}
|
||||
|
||||
InitializeSettings();
|
||||
InitializeSDL();
|
||||
InitializeModules();
|
||||
InitializeMainWindow();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
CheckPortableWindowsService();
|
||||
#endif
|
||||
|
||||
const int scaled_video_width_ = 160;
|
||||
const int scaled_video_height_ = 90;
|
||||
|
||||
MainLoop();
|
||||
|
||||
Cleanup();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void GuiApplication::InitializeLogger() { InitLogger(exec_log_path_); }
|
||||
|
||||
void GuiApplication::InitializeSettings() {
|
||||
settings_.Load();
|
||||
settings_.LoadRecentConnectionAliases();
|
||||
|
||||
localization_language_index_ =
|
||||
localization::detail::ClampLanguageIndex(language_button_value_);
|
||||
language_button_value_ = localization_language_index_;
|
||||
|
||||
if (localization_language_index_ == 0) {
|
||||
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
|
||||
} else if (localization_language_index_ == 1) {
|
||||
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::InitializeSDL() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (!getenv("SDL_AUDIODRIVER")) {
|
||||
// Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency.
|
||||
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
|
||||
LOG_ERROR("Error: {}", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
const SDL_DisplayMode *dm = SDL_GetCurrentDisplayMode(0);
|
||||
if (dm) {
|
||||
screen_width_ = dm->w;
|
||||
screen_height_ = dm->h;
|
||||
}
|
||||
|
||||
const uint32_t custom_event_base = SDL_RegisterEvents(3);
|
||||
if (custom_event_base == static_cast<uint32_t>(-1)) {
|
||||
LOG_ERROR("Failed to register custom SDL events");
|
||||
} else {
|
||||
STREAM_REFRESH_EVENT = custom_event_base;
|
||||
APP_EXIT_EVENT = custom_event_base + 1;
|
||||
clipboard_.SetEventType(custom_event_base + 2);
|
||||
}
|
||||
|
||||
LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
|
||||
}
|
||||
|
||||
void GuiApplication::InitializeModules() {
|
||||
if (!modules_inited_) {
|
||||
devices_.Initialize();
|
||||
CreateConnectionPeer();
|
||||
|
||||
modules_inited_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::InitializeMainWindow() {
|
||||
CreateMainWindow();
|
||||
clipboard_.Initialize();
|
||||
if (SDL_WINDOW_HIDDEN & SDL_GetWindowFlags(main_window_)) {
|
||||
SDL_ShowWindow(main_window_);
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::MainLoop() {
|
||||
while (!exit_) {
|
||||
if (!peer_) {
|
||||
CreateConnectionPeer();
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
if (SDL_WaitEventTimeout(&event, sdl_refresh_ms_)) {
|
||||
ProcessSdlEvent(event);
|
||||
}
|
||||
// Also drain the pending value after a timeout so a full SDL event queue
|
||||
// cannot indefinitely delay a clipboard update received from the network.
|
||||
clipboard_.ApplyPendingRemoteText();
|
||||
|
||||
#if _WIN32
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
if (tray_) {
|
||||
tray_->ProcessEvents();
|
||||
}
|
||||
#endif
|
||||
|
||||
UpdateLabels();
|
||||
HandleRecentConnections();
|
||||
HandleConnectionStatusChange();
|
||||
HandlePendingPresenceProbe();
|
||||
HandleConnectionTimeouts();
|
||||
HandleStreamWindow();
|
||||
HandleServerWindow();
|
||||
HandleWindowsServiceIntegration();
|
||||
|
||||
const bool main_window_visible =
|
||||
main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN);
|
||||
if (main_window_visible) {
|
||||
DrawMainWindow();
|
||||
}
|
||||
if (stream_window_inited_) {
|
||||
DrawStreamWindow();
|
||||
}
|
||||
|
||||
if (is_server_mode_) {
|
||||
DrawServerWindow();
|
||||
}
|
||||
|
||||
devices_.UpdateInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::HandleStreamWindow() {
|
||||
if (need_to_create_stream_window_) {
|
||||
CreateStreamWindow();
|
||||
need_to_create_stream_window_ = false;
|
||||
}
|
||||
|
||||
if (stream_window_inited_) {
|
||||
if (!stream_window_grabbed_ && control_mouse_) {
|
||||
SDL_SetWindowMouseGrab(stream_window_, true);
|
||||
stream_window_grabbed_ = true;
|
||||
} else if (stream_window_grabbed_ && !control_mouse_) {
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
stream_window_grabbed_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::HandleServerWindow() {
|
||||
if (need_to_create_server_window_) {
|
||||
CreateServerWindow();
|
||||
need_to_create_server_window_ = false;
|
||||
}
|
||||
|
||||
if (need_to_destroy_server_window_) {
|
||||
DestroyServerWindow();
|
||||
DestroyServerWindowContext();
|
||||
need_to_destroy_server_window_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::Cleanup() {
|
||||
clipboard_.Shutdown();
|
||||
|
||||
devices_.DestroyDevices();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
|
||||
WaitForThumbnailSaveTasks();
|
||||
|
||||
devices_.DestroyAudioOutput();
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
||||
tray_.reset();
|
||||
#endif
|
||||
|
||||
if (stream_window_created_) {
|
||||
if (stream_window_) {
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
}
|
||||
DestroyStreamWindow();
|
||||
}
|
||||
if (stream_ctx_) {
|
||||
DestroyStreamWindowContext();
|
||||
}
|
||||
|
||||
if (server_window_created_) {
|
||||
DestroyServerWindow();
|
||||
}
|
||||
if (server_ctx_) {
|
||||
DestroyServerWindowContext();
|
||||
}
|
||||
|
||||
DestroyMainWindowContext();
|
||||
DestroyMainWindow();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,112 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
#include "imgui_impl_sdlrenderer3.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// SDL/ImGui application shell. Feature state and protocol behavior live in
|
||||
// GuiRuntime and its composed controllers.
|
||||
class GuiApplication final : private GuiRuntime {
|
||||
public:
|
||||
GuiApplication();
|
||||
~GuiApplication();
|
||||
|
||||
int Run();
|
||||
|
||||
private:
|
||||
// Native window integration.
|
||||
static SDL_HitTestResult HitTestCallback(SDL_Window *window,
|
||||
const SDL_Point *area, void *data);
|
||||
|
||||
// Application lifecycle.
|
||||
void InitializeLogger();
|
||||
void InitializeSettings();
|
||||
void InitializeSDL();
|
||||
void InitializeModules();
|
||||
void InitializeMainWindow();
|
||||
void MainLoop();
|
||||
void HandleStreamWindow();
|
||||
void HandleServerWindow();
|
||||
void Cleanup();
|
||||
|
||||
// Window lifecycle and rendering.
|
||||
int CreateMainWindow();
|
||||
int DestroyMainWindow();
|
||||
int CreateStreamWindow();
|
||||
int DestroyStreamWindow();
|
||||
int CreateServerWindow();
|
||||
int DestroyServerWindow();
|
||||
int SetupFontAndStyle(ImFont **system_chinese_font_out);
|
||||
int DestroyMainWindowContext();
|
||||
int DestroyStreamWindowContext();
|
||||
int DestroyServerWindowContext();
|
||||
int DrawMainWindow();
|
||||
int DrawStreamWindow();
|
||||
int DrawServerWindow();
|
||||
|
||||
// Views and dialogs.
|
||||
int TitleBar(bool main_window);
|
||||
int MainWindow();
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
int SettingWindow();
|
||||
int SelfHostedServerWindow();
|
||||
int ControlWindow(std::shared_ptr<RemoteSession> &props);
|
||||
int ControlBar(std::shared_ptr<RemoteSession> &props);
|
||||
int AboutWindow();
|
||||
int StatusBar();
|
||||
bool
|
||||
ConnectionStatusWindow(std::shared_ptr<RemoteSession> &props);
|
||||
int ShowRecentConnections();
|
||||
int ConfirmDeleteConnection();
|
||||
int EditRecentConnectionAliasWindow();
|
||||
int OfflineWarningWindow();
|
||||
int NetTrafficStats(std::shared_ptr<RemoteSession> &props);
|
||||
int FileTransferWindow(std::shared_ptr<RemoteSession> &props);
|
||||
void
|
||||
DrawConnectionStatusText(std::shared_ptr<RemoteSession> &props);
|
||||
void
|
||||
DrawReceivingScreenText(std::shared_ptr<RemoteSession> &props);
|
||||
bool OpenUrl(const std::string &url);
|
||||
void Hyperlink(const std::string &label, const std::string &url,
|
||||
float window_width);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
bool MinimizeMainWindowToTray();
|
||||
|
||||
// SDL event dispatch and input translation.
|
||||
void UpdateRenderRect();
|
||||
void ProcessSdlEvent(const SDL_Event &event);
|
||||
int ProcessKeyboardEvent(const SDL_Event &event);
|
||||
int ProcessMouseEvent(const SDL_Event &event);
|
||||
void CloseTab(decltype(remote_sessions_)::iterator &it);
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
void CheckPortableWindowsService();
|
||||
int PortableServiceInstallWindow();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
int RequestPermissionWindow();
|
||||
bool DrawToggleSwitch(const char *id, bool active, bool enabled);
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_H_
|
||||
@@ -0,0 +1,320 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
|
||||
if (main_ctx_) {
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
} else {
|
||||
LOG_ERROR("Main context is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream_window_inited_) {
|
||||
if (stream_ctx_) {
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
} else {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (server_window_inited_) {
|
||||
if (server_ctx_) {
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
} else {
|
||||
LOG_ERROR("Server context is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (clipboard_.event_type() != 0 && event.type == clipboard_.event_type()) {
|
||||
clipboard_.ApplyPendingRemoteText();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_QUIT:
|
||||
if (stream_window_inited_) {
|
||||
LOG_INFO("Destroy stream window");
|
||||
SDL_SetWindowMouseGrab(stream_window_, false);
|
||||
DestroyStreamWindow();
|
||||
DestroyStreamWindowContext();
|
||||
|
||||
{
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &[host_name, props] : remote_sessions_) {
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
thumbnail_->SaveToThumbnail(
|
||||
(char *)frame_snapshot->data(), video_width, video_height,
|
||||
host_name, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
}
|
||||
|
||||
if (props->peer_) {
|
||||
std::string client_id = (host_name == client_id_)
|
||||
? "C-" + std::string(client_id_)
|
||||
: client_id_;
|
||||
LOG_INFO("[{}] Leave connection [{}]", client_id, host_name);
|
||||
LeaveConnection(props->peer_, host_name.c_str());
|
||||
LOG_INFO("Destroy peer [{}]", client_id);
|
||||
DestroyPeer(&props->peer_);
|
||||
}
|
||||
|
||||
props->streaming_ = false;
|
||||
props->remember_password_ = false;
|
||||
props->connection_established_ = false;
|
||||
props->audio_capture_button_pressed_ = false;
|
||||
|
||||
memset(&props->net_traffic_stats_, 0,
|
||||
sizeof(props->net_traffic_stats_));
|
||||
SDL_SetWindowFullscreen(main_window_, false);
|
||||
SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT);
|
||||
memset(audio_buffer_, 0, 720);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
remote_sessions_.clear();
|
||||
}
|
||||
|
||||
rejoin_ = false;
|
||||
is_client_mode_ = false;
|
||||
reload_recent_connections_ = true;
|
||||
fullscreen_button_pressed_ = false;
|
||||
start_keyboard_capturer_ = false;
|
||||
just_created_ = false;
|
||||
recent_connection_image_save_time_ = SDL_GetTicks();
|
||||
} else {
|
||||
LOG_INFO("Quit program");
|
||||
exit_ = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
|
||||
if (stream_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(stream_window_)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (main_window_ &&
|
||||
event.window.windowID == SDL_GetWindowID(main_window_) &&
|
||||
MinimizeMainWindowToTray()) {
|
||||
break;
|
||||
}
|
||||
|
||||
exit_ = true;
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
if (stream_window_created_ &&
|
||||
event.window.windowID == SDL_GetWindowID(stream_window_)) {
|
||||
UpdateRenderRect();
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
if (stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
focus_on_stream_window_ = true;
|
||||
} else if (main_window_ &&
|
||||
SDL_GetWindowID(main_window_) == event.window.windowID) {
|
||||
foucs_on_main_window_ = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
if (stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.window.windowID) {
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
focus_on_stream_window_ = false;
|
||||
} else if (main_window_ &&
|
||||
SDL_GetWindowID(main_window_) == event.window.windowID) {
|
||||
foucs_on_main_window_ = false;
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE:
|
||||
transfers_.HandleDropEvent(event);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_CLIPBOARD_UPDATE:
|
||||
clipboard_.HandleLocalUpdate();
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
case SDL_EVENT_MOUSE_WHEEL: {
|
||||
Uint32 mouse_window_id = 0;
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
mouse_window_id = event.motion.windowID;
|
||||
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
mouse_window_id = event.button.windowID;
|
||||
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
mouse_window_id = event.wheel.windowID;
|
||||
}
|
||||
|
||||
if (focus_on_stream_window_ && stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == mouse_window_id) {
|
||||
ProcessMouseEvent(event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ &&
|
||||
focus_on_stream_window_ && stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.key.windowID) {
|
||||
ProcessKeyboardEvent(event);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (event.type == STREAM_REFRESH_EVENT) {
|
||||
auto *props = static_cast<RemoteSession *>(event.user.data1);
|
||||
if (!props) {
|
||||
break;
|
||||
}
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
bool render_rect_dirty = false;
|
||||
bool cleanup_pending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
cleanup_pending = props->stream_cleanup_pending_;
|
||||
if (!cleanup_pending) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
render_rect_dirty = props->render_rect_dirty_;
|
||||
}
|
||||
|
||||
if (cleanup_pending) {
|
||||
if (props->stream_texture_) {
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (video_width <= 0 || video_height <= 0) {
|
||||
break;
|
||||
}
|
||||
if (!frame_snapshot || frame_snapshot->empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (props->stream_texture_) {
|
||||
if (video_width != props->texture_width_ ||
|
||||
video_height != props->texture_height_) {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
// props->texture_height_);
|
||||
|
||||
SDL_PropertiesID nvProps = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
props->texture_width_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
props->texture_height_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_NV12);
|
||||
SDL_SetNumberProperty(nvProps,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
SDL_COLORSPACE_BT601_LIMITED);
|
||||
props->stream_texture_ =
|
||||
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
} else {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
// props->texture_height_);
|
||||
|
||||
SDL_PropertiesID nvProps = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
props->texture_width_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
props->texture_height_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_NV12);
|
||||
SDL_SetNumberProperty(nvProps,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
SDL_COLORSPACE_BT601_LIMITED);
|
||||
props->stream_texture_ =
|
||||
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
|
||||
props->texture_width_);
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,415 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent &event) {
|
||||
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||
return 0x90;
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_KP_0:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2D;
|
||||
}
|
||||
return 0x60;
|
||||
case SDL_SCANCODE_KP_1:
|
||||
if (!numlock_enabled) {
|
||||
return 0x23;
|
||||
}
|
||||
return 0x61;
|
||||
case SDL_SCANCODE_KP_2:
|
||||
if (!numlock_enabled) {
|
||||
return 0x28;
|
||||
}
|
||||
return 0x62;
|
||||
case SDL_SCANCODE_KP_3:
|
||||
if (!numlock_enabled) {
|
||||
return 0x22;
|
||||
}
|
||||
return 0x63;
|
||||
case SDL_SCANCODE_KP_4:
|
||||
if (!numlock_enabled) {
|
||||
return 0x25;
|
||||
}
|
||||
return 0x64;
|
||||
case SDL_SCANCODE_KP_5:
|
||||
return 0x65;
|
||||
case SDL_SCANCODE_KP_6:
|
||||
if (!numlock_enabled) {
|
||||
return 0x27;
|
||||
}
|
||||
return 0x66;
|
||||
case SDL_SCANCODE_KP_7:
|
||||
if (!numlock_enabled) {
|
||||
return 0x24;
|
||||
}
|
||||
return 0x67;
|
||||
case SDL_SCANCODE_KP_8:
|
||||
if (!numlock_enabled) {
|
||||
return 0x26;
|
||||
}
|
||||
return 0x68;
|
||||
case SDL_SCANCODE_KP_9:
|
||||
if (!numlock_enabled) {
|
||||
return 0x21;
|
||||
}
|
||||
return 0x69;
|
||||
case SDL_SCANCODE_KP_PERIOD:
|
||||
case SDL_SCANCODE_KP_COMMA:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2E;
|
||||
}
|
||||
return 0x6E;
|
||||
case SDL_SCANCODE_KP_DIVIDE:
|
||||
return 0x6F;
|
||||
case SDL_SCANCODE_KP_MULTIPLY:
|
||||
return 0x6A;
|
||||
case SDL_SCANCODE_KP_MINUS:
|
||||
return 0x6D;
|
||||
case SDL_SCANCODE_KP_PLUS:
|
||||
return 0x6B;
|
||||
case SDL_SCANCODE_KP_EQUALS:
|
||||
return 0xBB;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent &event) {
|
||||
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
|
||||
if (keypad_key_code >= 0) {
|
||||
return keypad_key_code;
|
||||
}
|
||||
|
||||
const int key = static_cast<int>(event.key);
|
||||
if (key >= 'a' && key <= 'z') {
|
||||
return key - 'a' + 0x41;
|
||||
}
|
||||
if (key >= 'A' && key <= 'Z') {
|
||||
return key;
|
||||
}
|
||||
if (key >= '0' && key <= '9') {
|
||||
return key;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case ';':
|
||||
return 0xBA;
|
||||
case '\'':
|
||||
return 0xDE;
|
||||
case '`':
|
||||
return 0xC0;
|
||||
case ',':
|
||||
return 0xBC;
|
||||
case '.':
|
||||
return 0xBE;
|
||||
case '/':
|
||||
return 0xBF;
|
||||
case '\\':
|
||||
return 0xDC;
|
||||
case '[':
|
||||
return 0xDB;
|
||||
case ']':
|
||||
return 0xDD;
|
||||
case '-':
|
||||
return 0xBD;
|
||||
case '=':
|
||||
return 0xBB;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
return 0x1B;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_SPACE:
|
||||
return 0x20;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
return 0x08;
|
||||
case SDL_SCANCODE_TAB:
|
||||
return 0x09;
|
||||
case SDL_SCANCODE_PRINTSCREEN:
|
||||
return 0x2C;
|
||||
case SDL_SCANCODE_SCROLLLOCK:
|
||||
return 0x91;
|
||||
case SDL_SCANCODE_PAUSE:
|
||||
return 0x13;
|
||||
case SDL_SCANCODE_INSERT:
|
||||
return 0x2D;
|
||||
case SDL_SCANCODE_DELETE:
|
||||
return 0x2E;
|
||||
case SDL_SCANCODE_HOME:
|
||||
return 0x24;
|
||||
case SDL_SCANCODE_END:
|
||||
return 0x23;
|
||||
case SDL_SCANCODE_PAGEUP:
|
||||
return 0x21;
|
||||
case SDL_SCANCODE_PAGEDOWN:
|
||||
return 0x22;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
return 0x25;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
return 0x27;
|
||||
case SDL_SCANCODE_UP:
|
||||
return 0x26;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
return 0x28;
|
||||
case SDL_SCANCODE_F1:
|
||||
return 0x70;
|
||||
case SDL_SCANCODE_F2:
|
||||
return 0x71;
|
||||
case SDL_SCANCODE_F3:
|
||||
return 0x72;
|
||||
case SDL_SCANCODE_F4:
|
||||
return 0x73;
|
||||
case SDL_SCANCODE_F5:
|
||||
return 0x74;
|
||||
case SDL_SCANCODE_F6:
|
||||
return 0x75;
|
||||
case SDL_SCANCODE_F7:
|
||||
return 0x76;
|
||||
case SDL_SCANCODE_F8:
|
||||
return 0x77;
|
||||
case SDL_SCANCODE_F9:
|
||||
return 0x78;
|
||||
case SDL_SCANCODE_F10:
|
||||
return 0x79;
|
||||
case SDL_SCANCODE_F11:
|
||||
return 0x7A;
|
||||
case SDL_SCANCODE_F12:
|
||||
return 0x7B;
|
||||
case SDL_SCANCODE_CAPSLOCK:
|
||||
return 0x14;
|
||||
case SDL_SCANCODE_LSHIFT:
|
||||
return 0xA0;
|
||||
case SDL_SCANCODE_RSHIFT:
|
||||
return 0xA1;
|
||||
case SDL_SCANCODE_LCTRL:
|
||||
return 0xA2;
|
||||
case SDL_SCANCODE_RCTRL:
|
||||
return 0xA3;
|
||||
case SDL_SCANCODE_LALT:
|
||||
return 0xA4;
|
||||
case SDL_SCANCODE_RALT:
|
||||
return 0xA5;
|
||||
case SDL_SCANCODE_LGUI:
|
||||
return 0x5B;
|
||||
case SDL_SCANCODE_RGUI:
|
||||
return 0x5C;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
||||
if (key_code < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return keyboard_.SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
|
||||
}
|
||||
|
||||
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
|
||||
controlled_remote_id_ = "";
|
||||
RemoteAction remote_action{};
|
||||
float cursor_x = last_mouse_event.motion.x;
|
||||
float cursor_y = last_mouse_event.motion.y;
|
||||
|
||||
auto normalize_cursor_to_window_space = [&](float *x, float *y) {
|
||||
if (!x || !y || !stream_window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
int window_width = 0;
|
||||
int window_height = 0;
|
||||
int pixel_width = 0;
|
||||
int pixel_height = 0;
|
||||
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
|
||||
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
|
||||
|
||||
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
|
||||
pixel_height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((window_width != pixel_width || window_height != pixel_height) &&
|
||||
(*x > static_cast<float>(window_width) + 1.0f ||
|
||||
*y > static_cast<float>(window_height) + 1.0f)) {
|
||||
const float scale_x =
|
||||
static_cast<float>(window_width) / static_cast<float>(pixel_width);
|
||||
const float scale_y =
|
||||
static_cast<float>(window_height) / static_cast<float>(pixel_height);
|
||||
*x *= scale_x;
|
||||
*y *= scale_y;
|
||||
|
||||
static bool logged_pixel_to_window_conversion = false;
|
||||
if (!logged_pixel_to_window_conversion) {
|
||||
LOG_INFO(
|
||||
"Mouse coordinate space converted from pixels to window units: "
|
||||
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
|
||||
window_width, window_height, pixel_width, pixel_height, scale_x,
|
||||
scale_y);
|
||||
logged_pixel_to_window_conversion = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
cursor_x = event.motion.x;
|
||||
cursor_y = event.motion.y;
|
||||
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
|
||||
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
cursor_x = event.button.x;
|
||||
cursor_y = event.button.y;
|
||||
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
|
||||
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
cursor_x = last_mouse_event.motion.x;
|
||||
cursor_y = last_mouse_event.motion.y;
|
||||
}
|
||||
|
||||
const bool is_pointer_position_event =
|
||||
(event.type == SDL_EVENT_MOUSE_MOTION ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
|
||||
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (!props->control_mouse_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool file_transfer_window_hovered =
|
||||
props->file_transfer_.file_transfer_window_hovered_;
|
||||
const bool overlay_hovered =
|
||||
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
|
||||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
|
||||
|
||||
const SDL_FRect render_rect = props->stream_render_rect_f_;
|
||||
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_pointer_position_event && cursor_x >= render_rect.x &&
|
||||
cursor_x <= render_rect.x + render_rect.w &&
|
||||
cursor_y >= render_rect.y &&
|
||||
cursor_y <= render_rect.y + render_rect.h) {
|
||||
controlled_remote_id_ = it.first;
|
||||
last_mouse_event.motion.x = cursor_x;
|
||||
last_mouse_event.motion.y = cursor_y;
|
||||
last_mouse_event.button.x = cursor_x;
|
||||
last_mouse_event.button.y = cursor_y;
|
||||
|
||||
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
|
||||
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
if (SDL_BUTTON_LEFT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::left_down;
|
||||
} else if (SDL_BUTTON_RIGHT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::right_down;
|
||||
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::middle_down;
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
if (SDL_BUTTON_LEFT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::left_up;
|
||||
} else if (SDL_BUTTON_RIGHT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::right_up;
|
||||
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::middle_up;
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
remote_action.m.flag = MouseFlag::move;
|
||||
}
|
||||
|
||||
if (overlay_hovered) {
|
||||
break;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
|
||||
last_mouse_event.button.x >= render_rect.x &&
|
||||
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
|
||||
last_mouse_event.button.y >= render_rect.y &&
|
||||
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
|
||||
float scroll_x = event.wheel.x;
|
||||
float scroll_y = event.wheel.y;
|
||||
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
scroll_x = -scroll_x;
|
||||
scroll_y = -scroll_y;
|
||||
}
|
||||
|
||||
remote_action.type = ControlType::mouse;
|
||||
|
||||
auto roundUp = [](float value) -> int {
|
||||
if (value > 0) {
|
||||
return static_cast<int>(std::ceil(value));
|
||||
} else if (value < 0) {
|
||||
return static_cast<int>(std::floor(value));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
|
||||
remote_action.m.flag = MouseFlag::wheel_vertical;
|
||||
remote_action.m.s = roundUp(scroll_y);
|
||||
} else {
|
||||
remote_action.m.flag = MouseFlag::wheel_horizontal;
|
||||
remote_action.m.s = roundUp(scroll_x);
|
||||
}
|
||||
|
||||
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
|
||||
(std::max)(render_rect.w, 1.0f);
|
||||
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
|
||||
(std::max)(render_rect.h, 1.0f);
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (overlay_hovered) {
|
||||
continue;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,687 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <libyuv.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "fa_regular_400.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "window_util_mac.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
const ImWchar *GetMultilingualGlyphRanges() {
|
||||
static std::vector<ImWchar> glyph_ranges;
|
||||
if (glyph_ranges.empty()) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImFontGlyphRangesBuilder builder;
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
|
||||
|
||||
ImVector<ImWchar> built_ranges;
|
||||
builder.BuildRanges(&built_ranges);
|
||||
glyph_ranges.assign(built_ranges.Data,
|
||||
built_ranges.Data + built_ranges.Size);
|
||||
}
|
||||
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
|
||||
}
|
||||
|
||||
bool CanReadFontFile(const char *font_path) {
|
||||
if (!font_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream font_file(font_path, std::ios::binary);
|
||||
return font_file.good();
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
HICON LoadTrayIcon() {
|
||||
HMODULE module = GetModuleHandleW(nullptr);
|
||||
HICON icon = reinterpret_cast<HICON>(
|
||||
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
return LoadIconW(nullptr, IDI_APPLICATION);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
inline bool X11GetDisplayAndWindow(SDL_Window *window, Display **display_out,
|
||||
::Window *x11_window_out) {
|
||||
if (!window || !display_out || !x11_window_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
|
||||
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
|
||||
// SDL build does not expose X11 window properties.
|
||||
return false;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
Display *display = (Display *)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
|
||||
const Sint64 x11_window_num =
|
||||
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
const ::Window x11_window = (::Window)x11_window_num;
|
||||
|
||||
if (!display || !x11_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*display_out = display;
|
||||
*x11_window_out = x11_window;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void X11SendNetWmState(Display *display, ::Window x11_window,
|
||||
long action, Atom state1, Atom state2 = 0) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
|
||||
XEvent event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.xclient.type = ClientMessage;
|
||||
event.xclient.serial = 0;
|
||||
event.xclient.send_event = True;
|
||||
event.xclient.message_type = wm_state;
|
||||
event.xclient.window = x11_window;
|
||||
event.xclient.format = 32;
|
||||
event.xclient.data.l[0] = action;
|
||||
event.xclient.data.l[1] = (long)state1;
|
||||
event.xclient.data.l[2] = (long)state2;
|
||||
event.xclient.data.l[3] = 1; // normal source indication
|
||||
event.xclient.data.l[4] = 0;
|
||||
|
||||
XSendEvent(display, DefaultRootWindow(display), False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &event);
|
||||
}
|
||||
|
||||
inline void X11SetWindowTypeUtility(Display *display, ::Window x11_window) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_window_type =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom wm_window_type_utility =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
|
||||
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char *)&wm_window_type_utility, 1);
|
||||
}
|
||||
|
||||
inline void X11SetWindowAlwaysOnTop(SDL_Window *window) {
|
||||
Display *display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
|
||||
const Atom state_stays_on_top =
|
||||
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
|
||||
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
inline void X11SetWindowSkipTaskbar(SDL_Window *window) {
|
||||
Display *display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom skip_taskbar =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
|
||||
const Atom skip_pager =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
|
||||
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
|
||||
|
||||
// Hint the WM that this is an auxiliary/utility window.
|
||||
X11SetWindowTypeUtility(display, x11_window);
|
||||
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window,
|
||||
const SDL_Point *area,
|
||||
void *data) {
|
||||
GuiApplication *application = (GuiApplication *)data;
|
||||
if (!application) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
if (application->fullscreen_button_pressed_) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
// Server window: OS-level dragging for the title bar, but keep the left-side
|
||||
// collapse/expand button clickable.
|
||||
if (application->server_window_ && window == application->server_window_) {
|
||||
const float title_h = application->server_window_title_bar_height_;
|
||||
const float button_w = title_h;
|
||||
if (area->y >= 0 && area->y < title_h) {
|
||||
if (area->x >= 0 && area->x < button_w) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int window_width, window_height;
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
|
||||
// check if curosor is in tab bar
|
||||
if (application->stream_window_inited_ && application->stream_window_created_ &&
|
||||
!application->fullscreen_button_pressed_ && application->stream_ctx_) {
|
||||
ImGuiContext *prev_ctx = ImGui::GetCurrentContext();
|
||||
ImGui::SetCurrentContext(application->stream_ctx_);
|
||||
|
||||
ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar");
|
||||
if (tab_bar_window && tab_bar_window->Active) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float scale_x = io.DisplayFramebufferScale.x;
|
||||
float scale_y = io.DisplayFramebufferScale.y;
|
||||
|
||||
float tab_bar_x = tab_bar_window->Pos.x * scale_x;
|
||||
float tab_bar_y = tab_bar_window->Pos.y * scale_y;
|
||||
float tab_bar_width = tab_bar_window->Size.x * scale_x;
|
||||
float tab_bar_height = tab_bar_window->Size.y * scale_y;
|
||||
|
||||
ImGui::SetCurrentContext(prev_ctx);
|
||||
|
||||
if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width &&
|
||||
area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
} else {
|
||||
ImGui::SetCurrentContext(prev_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
float mouse_grab_padding = application->title_bar_button_width_ * 0.16f;
|
||||
if (area->y < application->title_bar_button_width_ &&
|
||||
area->y > mouse_grab_padding &&
|
||||
area->x < window_width - application->title_bar_button_width_ * 3.0f &&
|
||||
area->x > mouse_grab_padding) {
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
|
||||
// if (!application->streaming_) {
|
||||
// return SDL_HITTEST_NORMAL;
|
||||
// }
|
||||
|
||||
if (area->y < mouse_grab_padding) {
|
||||
if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_TOPLEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_TOPRIGHT;
|
||||
} else {
|
||||
return SDL_HITTEST_RESIZE_TOP;
|
||||
}
|
||||
} else if (area->y > window_height - mouse_grab_padding) {
|
||||
if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_BOTTOMLEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
|
||||
} else {
|
||||
return SDL_HITTEST_RESIZE_BOTTOM;
|
||||
}
|
||||
} else if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_LEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_RIGHT;
|
||||
}
|
||||
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateMainWindow() {
|
||||
main_ctx_ = ImGui::CreateContext();
|
||||
if (!main_ctx_) {
|
||||
LOG_ERROR("Main context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Main Window", (int)main_window_width_,
|
||||
(int)main_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
|
||||
&main_window_, &main_renderer_)) {
|
||||
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
|
||||
if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) {
|
||||
dpi_scale_ = dpi_scale;
|
||||
|
||||
main_window_width_ = (int)(main_window_width_default_ * dpi_scale_);
|
||||
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
|
||||
server_window_normal_width_ =
|
||||
(int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_normal_height_ =
|
||||
(int)(server_window_height_default_ * dpi_scale_);
|
||||
window_rounding_ = window_rounding_default_ * dpi_scale_;
|
||||
|
||||
SDL_SetWindowSize(main_window_, (int)main_window_width_,
|
||||
(int)main_window_height_);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(main_window_, false);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
SetupFontAndStyle(&main_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
#if _WIN32
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(main_window_);
|
||||
HWND main_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
HICON tray_icon = LoadTrayIcon();
|
||||
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__APPLE__)
|
||||
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
tray_ = std::make_unique<LinuxTray>(
|
||||
main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT);
|
||||
#endif
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(main_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyMainWindow() {
|
||||
if (main_ctx_) {
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
}
|
||||
|
||||
if (main_renderer_) {
|
||||
SDL_DestroyRenderer(main_renderer_);
|
||||
}
|
||||
|
||||
if (main_window_) {
|
||||
SDL_DestroyWindow(main_window_);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateStreamWindow() {
|
||||
if (stream_window_created_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
|
||||
stream_ctx_ = ImGui::CreateContext();
|
||||
if (!stream_ctx_) {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Stream Window", (int)stream_window_width_,
|
||||
(int)stream_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_TRANSPARENT,
|
||||
&stream_window_, &stream_renderer_)) {
|
||||
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
stream_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
|
||||
SDL_SetWindowResizable(stream_window_, true);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
SetupFontAndStyle(&stream_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(stream_renderer_);
|
||||
|
||||
// change props->stream_render_rect_
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
|
||||
event.window.windowID = SDL_GetWindowID(stream_window_);
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
stream_window_created_ = true;
|
||||
just_created_ = true;
|
||||
|
||||
stream_window_inited_ = true;
|
||||
LOG_INFO("Stream window inited");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyStreamWindow() {
|
||||
stream_window_width_ = (float)stream_window_width_default_;
|
||||
stream_window_height_ = (float)stream_window_height_default_;
|
||||
|
||||
if (stream_ctx_) {
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
}
|
||||
|
||||
if (stream_renderer_) {
|
||||
SDL_DestroyRenderer(stream_renderer_);
|
||||
stream_renderer_ = nullptr;
|
||||
}
|
||||
|
||||
if (stream_window_) {
|
||||
SDL_DestroyWindow(stream_window_);
|
||||
stream_window_ = nullptr;
|
||||
}
|
||||
|
||||
stream_window_created_ = false;
|
||||
focus_on_stream_window_ = false;
|
||||
stream_window_grabbed_ = false;
|
||||
control_mouse_ = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateServerWindow() {
|
||||
if (server_window_created_) {
|
||||
return 0;
|
||||
}
|
||||
server_ctx_ = ImGui::CreateContext();
|
||||
if (!server_ctx_) {
|
||||
LOG_ERROR("Server context is null");
|
||||
return -1;
|
||||
}
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Server Window", (int)server_window_width_,
|
||||
(int)server_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_TRANSPARENT,
|
||||
&server_window_, &server_renderer_)) {
|
||||
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
// Hide server window from the taskbar by making it a tool window.
|
||||
{
|
||||
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
|
||||
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
if (server_hwnd) {
|
||||
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
|
||||
ex_style |= WS_EX_TOOLWINDOW;
|
||||
ex_style &= ~WS_EX_APPWINDOW;
|
||||
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
|
||||
|
||||
// Keep the server window above normal windows.
|
||||
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
// Best-effort keep above other windows on X11.
|
||||
X11SetWindowAlwaysOnTop(server_window_);
|
||||
// Best-effort hide from taskbar on X11.
|
||||
X11SetWindowSkipTaskbar(server_window_);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Best-effort keep above other windows on macOS.
|
||||
MacSetWindowAlwaysOnTop(server_window_, true);
|
||||
// Best-effort exclude from Window menu / window cycling.
|
||||
MacSetWindowExcludedFromWindowMenu(server_window_, true);
|
||||
#endif
|
||||
|
||||
// Set window position to bottom-right corner
|
||||
SDL_Rect display_bounds;
|
||||
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
|
||||
&display_bounds)) {
|
||||
int window_x =
|
||||
display_bounds.x + display_bounds.w - (int)server_window_width_;
|
||||
int window_y =
|
||||
display_bounds.y + display_bounds.h - (int)server_window_height_;
|
||||
SDL_SetWindowPosition(server_window_, window_x, window_y);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(server_window_, false);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(server_window_, HitTestCallback, this);
|
||||
|
||||
SetupFontAndStyle(&server_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(server_renderer_);
|
||||
|
||||
server_window_created_ = true;
|
||||
server_window_inited_ = true;
|
||||
|
||||
LOG_INFO("Server window inited");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyServerWindow() {
|
||||
if (server_ctx_) {
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
}
|
||||
|
||||
if (server_renderer_) {
|
||||
SDL_DestroyRenderer(server_renderer_);
|
||||
server_renderer_ = nullptr;
|
||||
}
|
||||
|
||||
if (server_window_) {
|
||||
SDL_DestroyWindow(server_window_);
|
||||
server_window_ = nullptr;
|
||||
}
|
||||
|
||||
server_window_created_ = false;
|
||||
server_window_inited_ = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) {
|
||||
float font_size = 32.0f;
|
||||
|
||||
// Setup Dear ImGui style
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
io.IniFilename = NULL; // disable imgui.ini
|
||||
|
||||
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
|
||||
ImFontConfig config;
|
||||
config.FontDataOwnedByAtlas = false;
|
||||
config.MergeMode = false;
|
||||
|
||||
if (system_chinese_font_out) {
|
||||
*system_chinese_font_out = nullptr;
|
||||
}
|
||||
|
||||
ImFont *ui_font = nullptr;
|
||||
const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges();
|
||||
|
||||
#if defined(_WIN32)
|
||||
const char *base_font_paths[] = {
|
||||
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
|
||||
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc", nullptr};
|
||||
#elif defined(__APPLE__)
|
||||
const char *base_font_paths[] = {
|
||||
"/System/Library/Fonts/PingFang.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/System/Library/Fonts/SFNS.ttf", nullptr};
|
||||
#else
|
||||
const char *base_font_paths[] = {
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
nullptr};
|
||||
#endif
|
||||
|
||||
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
|
||||
if (!CanReadFontFile(base_font_paths[i])) {
|
||||
continue;
|
||||
}
|
||||
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
|
||||
&config, multilingual_ranges);
|
||||
if (ui_font != nullptr) {
|
||||
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
|
||||
}
|
||||
}
|
||||
if (!ui_font) {
|
||||
ui_font = io.Fonts->AddFontDefault(&config);
|
||||
}
|
||||
|
||||
if (!ui_font) {
|
||||
LOG_WARN("Failed to initialize base UI font");
|
||||
ImGui::StyleColorsLight();
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImFontConfig icon_config = config;
|
||||
icon_config.MergeMode = true;
|
||||
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
|
||||
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
|
||||
font_size, &icon_config, icon_ranges);
|
||||
|
||||
io.FontDefault = ui_font;
|
||||
if (system_chinese_font_out) {
|
||||
*system_chinese_font_out = ui_font;
|
||||
}
|
||||
|
||||
ImGui::StyleColorsLight();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyMainWindowContext() {
|
||||
if (!main_ctx_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(main_ctx_);
|
||||
main_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyStreamWindowContext() {
|
||||
if (!stream_ctx_) {
|
||||
stream_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(stream_ctx_);
|
||||
stream_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::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;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int GuiApplication::DrawMainWindow() {
|
||||
if (!main_ctx_) {
|
||||
LOG_ERROR("Main context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("MainRender", nullptr,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoDocking);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
TitleBar(true);
|
||||
|
||||
MainWindow();
|
||||
|
||||
UpdateNotificationWindow();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
PortableServiceInstallWindow();
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (show_request_permission_window_) {
|
||||
RequestPermissionWindow();
|
||||
}
|
||||
#endif
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Rendering
|
||||
(void)io;
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(main_renderer_);
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
|
||||
SDL_RenderPresent(main_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DrawStreamWindow() {
|
||||
if (!stream_ctx_) {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
StreamWindow();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float stream_title_window_height =
|
||||
fullscreen_button_pressed_ ? 0 : title_bar_height_;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
// Set minimum window size to 0 to allow exact height control
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("StreamTitleWindow", nullptr,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoDocking);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (!fullscreen_button_pressed_) {
|
||||
TitleBar(false);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Rendering
|
||||
(void)io;
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
|
||||
SDL_RenderClear(stream_renderer_);
|
||||
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->tab_selected_) {
|
||||
SDL_FRect render_rect_f = {
|
||||
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
|
||||
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
|
||||
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
|
||||
&render_rect_f);
|
||||
}
|
||||
}
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_);
|
||||
SDL_RenderPresent(stream_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DrawServerWindow() {
|
||||
if (!server_ctx_) {
|
||||
LOG_ERROR("Server context is null");
|
||||
return -1;
|
||||
}
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
server_window_width_ = io.DisplaySize.x;
|
||||
server_window_height_ = io.DisplaySize.y;
|
||||
|
||||
ServerWindow();
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(server_renderer_);
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
|
||||
SDL_RenderPresent(server_renderer_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GuiApplication::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 GuiApplication::UpdateRenderRect() {
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &[_, props] : remote_sessions_) {
|
||||
if (!props->reset_control_bar_pos_) {
|
||||
props->mouse_diff_control_bar_pos_x_ = 0;
|
||||
props->mouse_diff_control_bar_pos_y_ = 0;
|
||||
}
|
||||
|
||||
if (!just_created_) {
|
||||
props->reset_control_bar_pos_ = true;
|
||||
}
|
||||
|
||||
int stream_window_width, stream_window_height;
|
||||
SDL_GetWindowSize(stream_window_, &stream_window_width,
|
||||
&stream_window_height);
|
||||
stream_window_width_ = (float)stream_window_width;
|
||||
stream_window_height_ = (float)stream_window_height;
|
||||
|
||||
float video_ratio =
|
||||
(float)props->video_width_ / (float)props->video_height_;
|
||||
float video_ratio_reverse =
|
||||
(float)props->video_height_ / (float)props->video_width_;
|
||||
|
||||
float render_area_width = props->render_window_width_;
|
||||
float render_area_height = props->render_window_height_;
|
||||
|
||||
props->stream_render_rect_last_ = props->stream_render_rect_;
|
||||
|
||||
SDL_FRect rect_f{props->render_window_x_, props->render_window_y_,
|
||||
render_area_width, render_area_height};
|
||||
if (render_area_width < render_area_height * video_ratio) {
|
||||
rect_f.x = props->render_window_x_;
|
||||
rect_f.y = std::abs(render_area_height -
|
||||
render_area_width * video_ratio_reverse) /
|
||||
2.0f +
|
||||
props->render_window_y_;
|
||||
rect_f.w = render_area_width;
|
||||
rect_f.h = render_area_width * video_ratio_reverse;
|
||||
} else if (render_area_width > render_area_height * video_ratio) {
|
||||
rect_f.x =
|
||||
std::abs(render_area_width - render_area_height * video_ratio) /
|
||||
2.0f +
|
||||
props->render_window_x_;
|
||||
rect_f.y = props->render_window_y_;
|
||||
rect_f.w = render_area_height * video_ratio;
|
||||
rect_f.h = render_area_height;
|
||||
} else {
|
||||
rect_f.x = props->render_window_x_;
|
||||
rect_f.y = props->render_window_y_;
|
||||
rect_f.w = render_area_width;
|
||||
rect_f.h = render_area_height;
|
||||
}
|
||||
|
||||
props->stream_render_rect_f_ = rect_f;
|
||||
props->stream_render_rect_ = {static_cast<int>(std::lround(rect_f.x)),
|
||||
static_cast<int>(std::lround(rect_f.y)),
|
||||
static_cast<int>(std::lround(rect_f.w)),
|
||||
static_cast<int>(std::lround(rect_f.h))};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -7,8 +7,6 @@
|
||||
#ifndef _LAYOUT_STYLE_H_
|
||||
#define _LAYOUT_STYLE_H_
|
||||
|
||||
#include "render.h"
|
||||
|
||||
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
|
||||
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
|
||||
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#ifndef _LAYOUT_STYLE_H_
|
||||
#define _LAYOUT_STYLE_H_
|
||||
|
||||
#include "render.h"
|
||||
|
||||
#define TITLE_BAR_HEIGHT 0.0625f
|
||||
#define TITLE_BAR_BUTTON_WIDTH 0.0625f
|
||||
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f
|
||||
|
||||
@@ -28,6 +28,7 @@ struct TranslationRow {
|
||||
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||
u8"Удаленный рабочий стол") \
|
||||
X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
|
||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||
@@ -158,11 +159,17 @@ struct TranslationRow {
|
||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||
u8"Нет подключения к серверу") \
|
||||
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
|
||||
"Certificate verification failed. Reinstall the self-hosted root " \
|
||||
"certificate.", \
|
||||
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
|
||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||
u8"P2P отключено") \
|
||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||
u8"Подключение P2P...") \
|
||||
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
|
||||
u8"Сбор кандидатов...") \
|
||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||
u8"Получение изображения...") \
|
||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||
@@ -177,6 +184,14 @@ struct TranslationRow {
|
||||
X(access_website, u8"访问官网: ", \
|
||||
"Access Website: ", u8"Официальный сайт: ") \
|
||||
X(update, u8"更新", "Update", u8"Обновить") \
|
||||
X(connection_alias, u8"修改名称", "Edit Alias", \
|
||||
u8"Изменить имя подключения") \
|
||||
X(delete_connection, u8"删除连接", "Delete Connection", \
|
||||
u8"Удалить подключение") \
|
||||
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
|
||||
u8"Подключиться") \
|
||||
X(input_connection_alias, u8"请输入连接名称:", \
|
||||
"Please input connection name:", u8"Введите имя подключения:") \
|
||||
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "features/clipboard/clipboard_controller.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <shared_mutex>
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
// Keep a reliable clipboard message within MiniRTC/KCP's single-message
|
||||
// fragmentation window (MTU is configured to 1200 bytes).
|
||||
constexpr size_t kMaxClipboardTextBytes = 128 * 1024;
|
||||
|
||||
} // namespace
|
||||
|
||||
ClipboardController::ClipboardController(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
void ClipboardController::SetEventType(uint32_t event_type) {
|
||||
event_type_ = event_type;
|
||||
}
|
||||
|
||||
uint32_t ClipboardController::event_type() const { return event_type_; }
|
||||
|
||||
void ClipboardController::Initialize() {
|
||||
last_text_.clear();
|
||||
|
||||
char *clipboard_text = SDL_GetClipboardText();
|
||||
if (clipboard_text) {
|
||||
last_text_.assign(clipboard_text);
|
||||
SDL_free(clipboard_text);
|
||||
}
|
||||
|
||||
if (event_type_ == 0) {
|
||||
LOG_ERROR("Clipboard synchronization disabled: SDL event is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
events_enabled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ClipboardController::Shutdown() {
|
||||
events_enabled_.store(false, std::memory_order_release);
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
pending_remote_text_.reset();
|
||||
}
|
||||
|
||||
void ClipboardController::QueueRemoteText(const char *data, size_t size) {
|
||||
if (!events_enabled_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
if (!data || size == 0) {
|
||||
return;
|
||||
}
|
||||
if (size > kMaxClipboardTextBytes) {
|
||||
LOG_WARN("Ignore oversized remote clipboard text: {} bytes", size);
|
||||
return;
|
||||
}
|
||||
if (std::memchr(data, '\0', size) != nullptr) {
|
||||
LOG_WARN("Ignore remote clipboard text containing an embedded NUL byte");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
if (!events_enabled_.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
// Only the newest clipboard value matters. Replacing it also bounds the
|
||||
// amount of memory a busy or malicious peer can queue.
|
||||
pending_remote_text_ = std::string(data, size);
|
||||
}
|
||||
|
||||
SDL_Event event{};
|
||||
event.type = event_type_;
|
||||
if (!SDL_PushEvent(&event)) {
|
||||
// MainLoop also drains the pending value after its wait timeout.
|
||||
LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
void ClipboardController::ApplyPendingRemoteText() {
|
||||
if (!events_enabled_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<std::string> pending_text;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_mutex_);
|
||||
pending_text.swap(pending_remote_text_);
|
||||
}
|
||||
if (!pending_text || *pending_text == last_text_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// SDL clipboard functions must run on the thread that initialized SDL.
|
||||
if (!SDL_SetClipboardText(pending_text->c_str())) {
|
||||
LOG_ERROR("Failed to set remote clipboard text: {}", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
// SDL will normally emit SDL_EVENT_CLIPBOARD_UPDATE for this write. Update
|
||||
// the baseline first so that event cannot echo the text back to the sender.
|
||||
last_text_ = std::move(*pending_text);
|
||||
}
|
||||
|
||||
void ClipboardController::HandleLocalUpdate() {
|
||||
if (!events_enabled_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
if (!SDL_HasClipboardText()) {
|
||||
last_text_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
char *clipboard_text = SDL_GetClipboardText();
|
||||
if (!clipboard_text) {
|
||||
LOG_WARN("Failed to read local clipboard text: {}", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string text(clipboard_text);
|
||||
SDL_free(clipboard_text);
|
||||
if (text == last_text_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the value before sending. Duplicate SDL notifications and a
|
||||
// remote write of the same value must not create a clipboard feedback loop.
|
||||
last_text_ = text;
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
if (text.size() > kMaxClipboardTextBytes) {
|
||||
LOG_WARN("Ignore oversized local clipboard text: {} bytes", text.size());
|
||||
return;
|
||||
}
|
||||
|
||||
SendToPeers(text);
|
||||
}
|
||||
|
||||
int ClipboardController::SendToPeers(const std::string &text) {
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
|
||||
if (!props || !props->peer_ || !props->connection_established_ ||
|
||||
!props->enable_mouse_control_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int ret =
|
||||
SendReliableDataFrame(props->peer_, text.data(), text.size(),
|
||||
props->clipboard_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
|
||||
remote_id.c_str(), ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (owner_.peer_) {
|
||||
const int ret =
|
||||
SendReliableDataFrame(owner_.peer_, text.data(), text.size(),
|
||||
owner_.clipboard_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
|
||||
owner_.remote_id_display_, ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
|
||||
#define CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
|
||||
// Owns clipboard synchronization state and keeps feedback-loop prevention out
|
||||
// of the main UI coordinator.
|
||||
class ClipboardController {
|
||||
public:
|
||||
explicit ClipboardController(GuiRuntime &owner);
|
||||
|
||||
void SetEventType(uint32_t event_type);
|
||||
uint32_t event_type() const;
|
||||
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
void QueueRemoteText(const char *data, size_t size);
|
||||
void ApplyPendingRemoteText();
|
||||
void HandleLocalUpdate();
|
||||
|
||||
private:
|
||||
int SendToPeers(const std::string &text);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
uint32_t event_type_ = 0;
|
||||
std::atomic<bool> events_enabled_{false};
|
||||
std::mutex pending_mutex_;
|
||||
std::optional<std::string> pending_remote_text_;
|
||||
std::string last_text_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
|
||||
@@ -0,0 +1,484 @@
|
||||
#include "features/devices/session_device_manager.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
void SessionDeviceManager::Initialize() {
|
||||
InitializeAudioOutput();
|
||||
screen_capturer_factory_ = new ScreenCapturerFactory();
|
||||
speaker_capturer_factory_ = new SpeakerCapturerFactory();
|
||||
device_controller_factory_ = new DeviceControllerFactory();
|
||||
keyboard_capturer_ =
|
||||
static_cast<KeyboardCapturer *>(device_controller_factory_->Create(
|
||||
DeviceControllerFactory::Device::Keyboard));
|
||||
}
|
||||
|
||||
int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!screen_capturer_) {
|
||||
screen_capturer_ =
|
||||
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
||||
}
|
||||
|
||||
last_frame_time_ = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
LOG_INFO("Init screen capturer with {} fps", fps);
|
||||
|
||||
const int init_ret = screen_capturer_->Init(
|
||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||
const char *display_name) {
|
||||
const auto now_time = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
const auto duration = now_time - last_frame_time_;
|
||||
if (duration * fps < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string stream_id = display_name ? display_name : "";
|
||||
const bool resumed_after_gap =
|
||||
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
|
||||
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
||||
last_video_frame_stream_id_ != stream_id;
|
||||
if (resumed_after_gap || stream_changed) {
|
||||
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
||||
LOG_INFO("Request video key frame before sending captured frame, "
|
||||
"stream='{}', gap_ms={}, stream_changed={}",
|
||||
stream_id, duration, stream_changed);
|
||||
}
|
||||
}
|
||||
|
||||
XVideoFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
SendVideoFrame(owner_.peer_, &frame, stream_id.c_str());
|
||||
last_video_frame_stream_id_ = stream_id;
|
||||
last_frame_time_ = now_time;
|
||||
});
|
||||
|
||||
if (init_ret == 0) {
|
||||
LOG_INFO("Init screen capturer success");
|
||||
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
||||
if (!latest_display_info.empty()) {
|
||||
display_info_list_ = latest_display_info;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Init screen capturer failed");
|
||||
screen_capturer_->Destroy();
|
||||
delete screen_capturer_;
|
||||
screen_capturer_ = nullptr;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartScreenCapturer() {
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!screen_capturer_) {
|
||||
LOG_INFO("Screen capturer instance missing, recreating before start");
|
||||
if (InitializeScreenCapturer() != 0) {
|
||||
LOG_ERROR("Recreate screen capturer failed");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_);
|
||||
const int ret = screen_capturer_->Start(owner_.show_cursor_);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Start screen capturer failed: {}", ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopScreenCapturer() {
|
||||
if (screen_capturer_) {
|
||||
LOG_INFO("Stop screen capturer");
|
||||
screen_capturer_->Stop();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartSpeakerCapturer() {
|
||||
if (!speaker_capturer_) {
|
||||
speaker_capturer_ =
|
||||
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
|
||||
const int init_ret = speaker_capturer_->Init(
|
||||
[this](unsigned char *data, size_t size, const char *audio_name) {
|
||||
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
|
||||
size, owner_.audio_label_.c_str());
|
||||
});
|
||||
|
||||
if (init_ret != 0) {
|
||||
speaker_capturer_->Destroy();
|
||||
delete speaker_capturer_;
|
||||
speaker_capturer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!speaker_capturer_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int ret = speaker_capturer_->Start();
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Start speaker capturer failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
owner_.start_speaker_capturer_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopSpeakerCapturer() {
|
||||
if (speaker_capturer_) {
|
||||
speaker_capturer_->Stop();
|
||||
owner_.start_speaker_capturer_ = false;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartMouseController() {
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacAccessibilityPermission()) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!device_controller_factory_) {
|
||||
LOG_INFO("Device controller factory is nullptr");
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
if (!screen_capturer_) {
|
||||
return 1;
|
||||
}
|
||||
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
||||
if (latest_display_info.empty() ||
|
||||
latest_display_info[0].handle == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (screen_capturer_) {
|
||||
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
||||
if (!latest_display_info.empty()) {
|
||||
display_info_list_ = latest_display_info;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
mouse_controller_ =
|
||||
static_cast<MouseController *>(device_controller_factory_->Create(
|
||||
DeviceControllerFactory::Device::Mouse));
|
||||
if (!mouse_controller_) {
|
||||
LOG_ERROR("Create mouse controller failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int init_ret = mouse_controller_->Init(display_info_list_);
|
||||
if (init_ret != 0) {
|
||||
LOG_INFO("Destroy mouse controller");
|
||||
mouse_controller_->Destroy();
|
||||
delete mouse_controller_;
|
||||
mouse_controller_ = nullptr;
|
||||
}
|
||||
return init_ret;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopMouseController() {
|
||||
if (mouse_controller_) {
|
||||
mouse_controller_->Destroy();
|
||||
delete mouse_controller_;
|
||||
mouse_controller_ = nullptr;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = false;
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacAccessibilityPermission()) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
LOG_INFO("Start keyboard capturer with SDL Wayland backend");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!keyboard_capturer_) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
LOG_WARN(
|
||||
"keyboard capturer is nullptr, falling back to SDL keyboard events");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int hook_ret = keyboard_capturer_->Hook(
|
||||
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
void *user_ptr) {
|
||||
if (user_ptr) {
|
||||
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
|
||||
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
},
|
||||
&owner_);
|
||||
if (hook_ret != 0) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
LOG_WARN(
|
||||
"Start keyboard capturer failed, falling back to SDL keyboard events");
|
||||
} else {
|
||||
LOG_INFO("Start keyboard capturer with native hook");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopKeyboardCapturer() {
|
||||
if (owner_.keyboard_capturer_uses_sdl_events_) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = false;
|
||||
LOG_INFO("Stop keyboard capturer with SDL keyboard backend");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (keyboard_capturer_) {
|
||||
keyboard_capturer_->Unhook();
|
||||
LOG_INFO("Stop keyboard capturer");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::InitializeAudioOutput() {
|
||||
SDL_AudioSpec desired_out{};
|
||||
desired_out.freq = 48000;
|
||||
desired_out.format = SDL_AUDIO_S16;
|
||||
desired_out.channels = 1;
|
||||
|
||||
auto open_stream = [&]() {
|
||||
output_stream_ = SDL_OpenAudioDeviceStream(
|
||||
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr);
|
||||
return output_stream_ != nullptr;
|
||||
};
|
||||
|
||||
if (!open_stream()) {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
LOG_WARN("Failed to open output stream with driver [{}]: {}",
|
||||
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER")
|
||||
: "(default)",
|
||||
SDL_GetError());
|
||||
|
||||
setenv("SDL_AUDIODRIVER", "dummy", 1);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
if (!open_stream()) {
|
||||
LOG_ERROR("Failed to open output stream with dummy driver: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
LOG_WARN("Audio output disabled, using SDL dummy audio driver");
|
||||
#else
|
||||
LOG_ERROR("Failed to open output stream: {}", SDL_GetError());
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::DestroyAudioOutput() {
|
||||
if (output_stream_) {
|
||||
SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
|
||||
SDL_DestroyAudioStream(output_stream_);
|
||||
output_stream_ = nullptr;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::PushAudio(const char *data, size_t size) {
|
||||
if (!output_stream_) {
|
||||
return;
|
||||
}
|
||||
const int pushed = SDL_PutAudioStreamData(
|
||||
output_stream_, reinterpret_cast<const Uint8 *>(data),
|
||||
static_cast<int>(size));
|
||||
if (pushed < 0) {
|
||||
LOG_ERROR("Failed to push audio data: {}", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
void SessionDeviceManager::UpdateInteractions() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
const bool is_wayland_session = IsWaylandSession();
|
||||
const bool stop_wayland_mouse_before_screen =
|
||||
is_wayland_session && !owner_.start_screen_capturer_ &&
|
||||
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
|
||||
owner_.mouse_controller_is_started_;
|
||||
if (stop_wayland_mouse_before_screen) {
|
||||
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
|
||||
"cleanly release the shared portal session");
|
||||
StopMouseController();
|
||||
owner_.mouse_controller_is_started_ = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) {
|
||||
if (StartScreenCapturer() == 0) {
|
||||
owner_.screen_capturer_is_started_ = true;
|
||||
}
|
||||
} else if (!owner_.start_screen_capturer_ &&
|
||||
owner_.screen_capturer_is_started_) {
|
||||
StopScreenCapturer();
|
||||
owner_.screen_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) {
|
||||
if (StartSpeakerCapturer() == 0) {
|
||||
owner_.speaker_capturer_is_started_ = true;
|
||||
}
|
||||
} else if (!owner_.start_speaker_capturer_ &&
|
||||
owner_.speaker_capturer_is_started_) {
|
||||
StopSpeakerCapturer();
|
||||
owner_.speaker_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
|
||||
if (StartMouseController() == 0) {
|
||||
owner_.mouse_controller_is_started_ = true;
|
||||
}
|
||||
} else if (!owner_.start_mouse_controller_ &&
|
||||
owner_.mouse_controller_is_started_) {
|
||||
StopMouseController();
|
||||
owner_.mouse_controller_is_started_ = false;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
|
||||
mouse_controller_) {
|
||||
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
||||
if (!latest_display_info.empty()) {
|
||||
display_info_list_ = latest_display_info;
|
||||
mouse_controller_->UpdateDisplayInfoList(display_info_list_);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) {
|
||||
if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
|
||||
owner_.keyboard_capturer_is_started_ = true;
|
||||
}
|
||||
if (owner_.keyboard_capturer_is_started_) {
|
||||
owner_.keyboard_.SendHeartbeat(false);
|
||||
}
|
||||
} else if (owner_.keyboard_capturer_is_started_) {
|
||||
owner_.keyboard_.ForceReleasePressedKeys();
|
||||
StopKeyboardCapturer();
|
||||
owner_.keyboard_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
owner_.keyboard_.CheckRemoteTimeouts();
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
return keyboard_capturer_ && keyboard_capturer_->SendKeyboardCommand(
|
||||
key_code, is_down, scan_code, extended) == 0;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
||||
int selected_display) {
|
||||
if (mouse_controller_) {
|
||||
mouse_controller_->SendMouseCommand(action, selected_display);
|
||||
}
|
||||
}
|
||||
|
||||
int SessionDeviceManager::SwitchDisplay(int display_id) {
|
||||
return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::ResetToInitialDisplay() {
|
||||
if (screen_capturer_) {
|
||||
screen_capturer_->ResetToInitialMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<DisplayInfo> &
|
||||
SessionDeviceManager::display_info_list() const {
|
||||
return display_info_list_;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DestroyDevices() {
|
||||
if (mouse_controller_) {
|
||||
mouse_controller_->Destroy();
|
||||
delete mouse_controller_;
|
||||
mouse_controller_ = nullptr;
|
||||
}
|
||||
if (screen_capturer_) {
|
||||
screen_capturer_->Destroy();
|
||||
delete screen_capturer_;
|
||||
screen_capturer_ = nullptr;
|
||||
}
|
||||
if (speaker_capturer_) {
|
||||
speaker_capturer_->Destroy();
|
||||
delete speaker_capturer_;
|
||||
speaker_capturer_ = nullptr;
|
||||
}
|
||||
if (keyboard_capturer_) {
|
||||
delete keyboard_capturer_;
|
||||
keyboard_capturer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DestroyFactories() {
|
||||
delete screen_capturer_factory_;
|
||||
screen_capturer_factory_ = nullptr;
|
||||
delete speaker_capturer_factory_;
|
||||
speaker_capturer_factory_ = nullptr;
|
||||
delete device_controller_factory_;
|
||||
device_controller_factory_ = nullptr;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,72 @@
|
||||
#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "device_controller_factory.h"
|
||||
#include "display_info.h"
|
||||
#include "screen_capturer_factory.h"
|
||||
#include "speaker_capturer_factory.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
|
||||
// Owns media capture/playback and remote input devices for the active GUI
|
||||
// session. GuiRuntime supplies application intent; this class handles device
|
||||
// creation, updates and teardown.
|
||||
class SessionDeviceManager {
|
||||
public:
|
||||
explicit SessionDeviceManager(GuiRuntime &owner);
|
||||
|
||||
void Initialize();
|
||||
void UpdateInteractions();
|
||||
void DestroyDevices();
|
||||
void DestroyFactories();
|
||||
|
||||
int InitializeScreenCapturer();
|
||||
int StartScreenCapturer();
|
||||
int StopScreenCapturer();
|
||||
int StartSpeakerCapturer();
|
||||
int StopSpeakerCapturer();
|
||||
int StartMouseController();
|
||||
int StopMouseController();
|
||||
int StartKeyboardCapturer();
|
||||
int StopKeyboardCapturer();
|
||||
|
||||
int InitializeAudioOutput();
|
||||
int DestroyAudioOutput();
|
||||
void PushAudio(const char *data, size_t size);
|
||||
|
||||
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
void SendMouseCommand(const RemoteAction &action, int selected_display);
|
||||
int SwitchDisplay(int display_id);
|
||||
void ResetToInitialDisplay();
|
||||
|
||||
const std::vector<DisplayInfo> &display_info_list() const;
|
||||
|
||||
private:
|
||||
GuiRuntime &owner_;
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||
ScreenCapturer *screen_capturer_ = nullptr;
|
||||
SpeakerCapturerFactory *speaker_capturer_factory_ = nullptr;
|
||||
SpeakerCapturer *speaker_capturer_ = nullptr;
|
||||
DeviceControllerFactory *device_controller_factory_ = nullptr;
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
@@ -0,0 +1,379 @@
|
||||
#include "features/file_transfer/file_transfer_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
|
||||
return global_state_;
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::state_for(
|
||||
const std::shared_ptr<RemoteSession> &props) {
|
||||
return props ? props->file_transfer_ : global_state_;
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessSelectedFile(
|
||||
const std::string &path,
|
||||
const std::shared_ptr<RemoteSession> &props,
|
||||
const std::string &file_label, const std::string &remote_id) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState &state = state_for(props);
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
const std::filesystem::path file_path = std::filesystem::u8path(path);
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("Selected path is not a regular file: {}", path);
|
||||
return;
|
||||
}
|
||||
const uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().u8string();
|
||||
info.file_path = file_path;
|
||||
info.file_size = file_size;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
state.file_transfer_list_.push_back(std::move(info));
|
||||
}
|
||||
state.file_transfer_window_visible_ = true;
|
||||
|
||||
auto enqueue = [&]() {
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
|
||||
state.file_send_queue_.push(
|
||||
FileTransferState::QueuedFile{file_path, file_label, remote_id});
|
||||
queue_size = state.file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
};
|
||||
|
||||
if (state.file_sending_.load()) {
|
||||
enqueue();
|
||||
return;
|
||||
}
|
||||
|
||||
Start(props, file_path, file_label, remote_id);
|
||||
if (!state.file_sending_.load()) {
|
||||
enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::Start(
|
||||
std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path, const std::string &file_label,
|
||||
const std::string &remote_id) {
|
||||
const bool is_global = !props;
|
||||
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
|
||||
if (!peer) {
|
||||
LOG_ERROR("StartFileTransfer: invalid peer");
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState &initial_state = state_for(props);
|
||||
bool expected = false;
|
||||
if (!initial_state.file_sending_.compare_exchange_strong(expected, true)) {
|
||||
LOG_WARN("StartFileTransfer called while another file is active: {}",
|
||||
file_path.filename().string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto props_weak = std::weak_ptr<RemoteSession>(props);
|
||||
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
|
||||
is_global]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
FileTransferState *state = nullptr;
|
||||
if (props_locked) {
|
||||
state = &props_locked->file_transfer_;
|
||||
} else if (is_global) {
|
||||
state = &global_state_;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const uint64_t total_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
state->file_sending_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
state->file_sent_bytes_ = 0;
|
||||
state->file_total_bytes_ = total_size;
|
||||
state->file_send_rate_bps_ = 0;
|
||||
state->file_transfer_window_visible_ = true;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
state->file_send_start_time_ = std::chrono::steady_clock::now();
|
||||
state->file_send_last_update_time_ = state->file_send_start_time_;
|
||||
state->file_send_last_bytes_ = 0;
|
||||
}
|
||||
|
||||
FileSender sender;
|
||||
const uint32_t file_id = FileSender::NextFileId();
|
||||
if (props_locked) {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
|
||||
file_id_to_props_[file_id] = props_weak;
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
|
||||
file_id_to_state_[file_id] = state;
|
||||
}
|
||||
state->current_file_id_ = file_id;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto &info : state->file_transfer_list_) {
|
||||
if (info.file_path == file_path &&
|
||||
info.status == FileTransferState::FileTransferStatus::Queued) {
|
||||
info.status = FileTransferState::FileTransferStatus::Sending;
|
||||
info.file_id = file_id;
|
||||
info.file_size = total_size;
|
||||
info.sent_bytes = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, file_label, remote_id](const char *buffer, size_t size) {
|
||||
if (remote_id.empty()) {
|
||||
return SendReliableDataFrame(peer, buffer, size,
|
||||
file_label.c_str());
|
||||
}
|
||||
return SendReliableDataFrameToPeer(
|
||||
peer, buffer, size, file_label.c_str(), remote_id.c_str(),
|
||||
remote_id.size());
|
||||
},
|
||||
64 * 1024, file_id);
|
||||
|
||||
if (ret == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->file_sending_ = false;
|
||||
state->file_transfer_window_visible_ = false;
|
||||
state->file_sent_bytes_ = 0;
|
||||
state->file_total_bytes_ = 0;
|
||||
state->file_send_rate_bps_ = 0;
|
||||
state->current_file_id_ = 0;
|
||||
Unregister(file_id, props_locked != nullptr);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto &info : state->file_transfer_list_) {
|
||||
if (info.file_id == file_id) {
|
||||
info.status = FileTransferState::FileTransferStatus::Failed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
ProcessQueue(props_locked);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessQueue(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
FileTransferState &state = state_for(props);
|
||||
if (state.file_sending_.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
|
||||
if (state.file_send_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
queued_file = state.file_send_queue_.front();
|
||||
state.file_send_queue_.pop();
|
||||
}
|
||||
Start(props, queued_file.file_path, queued_file.file_label,
|
||||
queued_file.remote_id);
|
||||
}
|
||||
|
||||
void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
|
||||
if (per_peer) {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
|
||||
file_id_to_props_.erase(file_id);
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
|
||||
file_id_to_state_.erase(file_id);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::HandleAck(const char *data, size_t size) {
|
||||
if (size < sizeof(FileTransferAck)) {
|
||||
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferAck ack{};
|
||||
std::memcpy(&ack, data, sizeof(ack));
|
||||
if (ack.magic != kFileAckMagic) {
|
||||
LOG_ERROR("FileTransferAck: invalid magic, got 0x{:08X}", ack.magic);
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<RemoteSession> props;
|
||||
{
|
||||
std::shared_lock lock(file_id_to_props_mutex_);
|
||||
const auto it = file_id_to_props_.find(ack.file_id);
|
||||
if (it != file_id_to_props_.end()) {
|
||||
props = it->second.lock();
|
||||
}
|
||||
}
|
||||
|
||||
FileTransferState *state = props ? &props->file_transfer_ : nullptr;
|
||||
if (!state) {
|
||||
std::shared_lock lock(file_id_to_state_mutex_);
|
||||
const auto it = file_id_to_state_.find(ack.file_id);
|
||||
if (it != file_id_to_state_.end()) {
|
||||
state = it->second;
|
||||
}
|
||||
}
|
||||
if (!state) {
|
||||
LOG_WARN("FileTransferAck: no state found for file_id={}", ack.file_id);
|
||||
return;
|
||||
}
|
||||
|
||||
state->file_sent_bytes_ = ack.acked_offset;
|
||||
state->file_total_bytes_ = ack.total_size;
|
||||
uint32_t rate_bps = 0;
|
||||
if (props) {
|
||||
const uint32_t bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
if (bitrate > 0 && state->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
}
|
||||
} else {
|
||||
rate_bps = state->file_send_rate_bps_.load();
|
||||
}
|
||||
} else {
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
uint32_t estimated_rate = 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
uint64_t last_bytes = 0;
|
||||
std::chrono::steady_clock::time_point last_time;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
last_bytes = state->file_send_last_bytes_;
|
||||
last_time = state->file_send_last_update_time_;
|
||||
}
|
||||
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
|
||||
const auto delta_bytes = ack.acked_offset - last_bytes;
|
||||
const double seconds =
|
||||
std::chrono::duration<double>(now - last_time).count();
|
||||
if (seconds > 0.0 && delta_bytes > 0) {
|
||||
const double bits_per_second = delta_bytes * 8.0 / seconds;
|
||||
estimated_rate = static_cast<uint32_t>((std::min)(
|
||||
bits_per_second,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
}
|
||||
}
|
||||
rate_bps =
|
||||
estimated_rate > 0 && current_rate > 0
|
||||
? static_cast<uint32_t>(current_rate * 0.7 + estimated_rate * 0.3)
|
||||
: estimated_rate > 0 ? estimated_rate
|
||||
: current_rate;
|
||||
}
|
||||
|
||||
state->file_send_rate_bps_ = rate_bps;
|
||||
state->file_send_last_bytes_ = ack.acked_offset;
|
||||
state->file_send_last_update_time_ = std::chrono::steady_clock::now();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto &info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.sent_bytes = ack.acked_offset;
|
||||
info.file_size = ack.total_size;
|
||||
info.rate_bps = rate_bps;
|
||||
if ((ack.flags & 0x01) != 0) {
|
||||
info.status = FileTransferState::FileTransferStatus::Completed;
|
||||
info.sent_bytes = ack.total_size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((ack.flags & 0x01) == 0) {
|
||||
return;
|
||||
}
|
||||
LOG_INFO("File transfer completed: file_id={}, bytes={}", ack.file_id,
|
||||
ack.total_size);
|
||||
state->file_transfer_window_visible_ = true;
|
||||
state->file_sending_ = false;
|
||||
Unregister(ack.file_id, props != nullptr);
|
||||
ProcessQueue(props);
|
||||
}
|
||||
|
||||
void FileTransferManager::HandleDropEvent(const SDL_Event &event) {
|
||||
if (event.type != SDL_EVENT_DROP_FILE ||
|
||||
!((owner_.stream_window_ &&
|
||||
SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) ||
|
||||
(owner_.server_window_ &&
|
||||
SDL_GetWindowID(owner_.server_window_) == event.window.windowID))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner_.stream_window_ &&
|
||||
SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) {
|
||||
if (!owner_.stream_window_inited_ || !event.drop.data) {
|
||||
return;
|
||||
}
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
|
||||
if (props && props->tab_selected_ && props->peer_) {
|
||||
ProcessSelectedFile(static_cast<const char *>(event.drop.data), props,
|
||||
props->file_label_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner_.server_window_inited_ && event.drop.data) {
|
||||
const auto file_path = std::filesystem::u8path(event.drop.data);
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
|
||||
event.drop.data, std::filesystem::file_size(file_path, ec));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "runtime/gui_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
|
||||
class FileTransferManager {
|
||||
public:
|
||||
using FileTransferState = gui_detail::FileTransferState;
|
||||
using RemoteSession = gui_detail::RemoteSession;
|
||||
|
||||
explicit FileTransferManager(GuiRuntime &owner);
|
||||
|
||||
FileTransferState &global_state();
|
||||
FileTransferState &
|
||||
state_for(const std::shared_ptr<RemoteSession> &props);
|
||||
|
||||
void
|
||||
ProcessSelectedFile(const std::string &path,
|
||||
const std::shared_ptr<RemoteSession> &props,
|
||||
const std::string &file_label,
|
||||
const std::string &remote_id = "");
|
||||
void HandleDropEvent(const SDL_Event &event);
|
||||
void HandleAck(const char *data, size_t size);
|
||||
|
||||
private:
|
||||
void Start(std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path,
|
||||
const std::string &file_label, const std::string &remote_id = "");
|
||||
void ProcessQueue(std::shared_ptr<RemoteSession> props);
|
||||
void Unregister(uint32_t file_id, bool per_peer);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
FileTransferState global_state_;
|
||||
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
|
||||
file_id_to_props_;
|
||||
std::shared_mutex file_id_to_props_mutex_;
|
||||
std::unordered_map<uint32_t, FileTransferState *> file_id_to_state_;
|
||||
std::shared_mutex file_id_to_state_mutex_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
@@ -0,0 +1,362 @@
|
||||
#include "features/input/keyboard_controller.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "windows_key_metadata.h"
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kHeartbeatIntervalMs = 500;
|
||||
constexpr uint32_t kRemoteReleaseTimeoutMs = 2500;
|
||||
|
||||
int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if _WIN32
|
||||
if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) {
|
||||
return key_code;
|
||||
}
|
||||
|
||||
UINT scan_code_with_prefix = static_cast<UINT>(scan_code & 0xFF);
|
||||
if (extended) {
|
||||
scan_code_with_prefix |= 0xE000;
|
||||
}
|
||||
const UINT normalized_vk =
|
||||
MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX);
|
||||
return normalized_vk != 0 ? static_cast<int>(normalized_vk) : key_code;
|
||||
#else
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return key_code;
|
||||
#endif
|
||||
}
|
||||
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
bool *extended_out) {
|
||||
if (!scan_code_out || !extended_out) {
|
||||
return;
|
||||
}
|
||||
#if _WIN32
|
||||
const UINT scan_code =
|
||||
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
if (scan_code != 0) {
|
||||
*scan_code_out = static_cast<uint32_t>(scan_code & 0xFF);
|
||||
*extended_out = (scan_code & 0xFF00) != 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||
return;
|
||||
}
|
||||
*last_tick = now;
|
||||
LOG_WARN(
|
||||
"local secure-desktop input blocked, stage={}, normal SendInput path "
|
||||
"cannot drive the Windows password UI",
|
||||
stage ? stage : "");
|
||||
}
|
||||
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
const RemoteAction &action) {
|
||||
return response.is_object() &&
|
||||
response.value("error", std::string()) == "send_input_failed" &&
|
||||
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
||||
action.type == ControlType::keyboard &&
|
||||
action.k.flag == KeyFlag::key_up;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
if (is_down) {
|
||||
pressed_keys_[key_code] = PressedKey{key_code, scan_code, extended};
|
||||
} else {
|
||||
pressed_keys_.erase(key_code);
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ForceReleasePressedKeys() {
|
||||
std::vector<PressedKey> pressed_keys;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
pressed_keys.reserve(pressed_keys_.size());
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
pressed_keys.push_back(key);
|
||||
}
|
||||
pressed_keys_.clear();
|
||||
}
|
||||
|
||||
for (const PressedKey &key : pressed_keys) {
|
||||
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
SendHeartbeat(true);
|
||||
}
|
||||
|
||||
void KeyboardController::SendHeartbeat(bool force) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::keyboard_state;
|
||||
action.ks.seq = ++state_sequence_;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
size_t index = 0;
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
if (index >= kMaxKeyboardStateKeys) {
|
||||
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
||||
pressed_keys_.size());
|
||||
break;
|
||||
}
|
||||
action.ks.pressed_keys[index].key_value =
|
||||
static_cast<size_t>(key.key_code);
|
||||
action.ks.pressed_keys[index].scan_code = key.scan_code;
|
||||
action.ks.pressed_keys[index].extended = key.extended;
|
||||
++index;
|
||||
}
|
||||
action.ks.pressed_count = index;
|
||||
}
|
||||
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
const auto props_it = owner_.remote_sessions_.find(target_id);
|
||||
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
|
||||
props_it->second->connection_status_.load() !=
|
||||
ConnectionStatus::Connected ||
|
||||
!props_it->second->peer_) {
|
||||
last_heartbeat_tick_ = now;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string message = action.to_json();
|
||||
const int result = SendReliableDataFrame(
|
||||
props_it->second->peer_, message.c_str(), message.size(),
|
||||
props_it->second->keyboard_label_.c_str());
|
||||
if (result != 0) {
|
||||
LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id,
|
||||
result);
|
||||
}
|
||||
last_heartbeat_tick_ = now;
|
||||
}
|
||||
|
||||
int KeyboardController::SendKeyCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
if (scan_code == 0) {
|
||||
PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
}
|
||||
#if _WIN32
|
||||
key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended);
|
||||
#endif
|
||||
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::keyboard;
|
||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
||||
action.k.key_value = key_code;
|
||||
action.k.scan_code = scan_code;
|
||||
action.k.extended = extended;
|
||||
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
const auto props_it = owner_.remote_sessions_.find(target_id);
|
||||
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
|
||||
props_it->second->connection_status_.load() ==
|
||||
ConnectionStatus::Connected &&
|
||||
props_it->second->peer_) {
|
||||
const std::string message = action.to_json();
|
||||
const int result = SendReliableDataFrame(
|
||||
props_it->second->peer_, message.c_str(), message.size(),
|
||||
props_it->second->keyboard_label_.c_str());
|
||||
if (result != 0) {
|
||||
LOG_WARN("Send keyboard command failed, remote_id={}, ret={}", target_id,
|
||||
result);
|
||||
}
|
||||
}
|
||||
|
||||
TrackPressedKey(key_code, is_down, scan_code, extended);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
#if _WIN32
|
||||
if (owner_.local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, scan_code, extended, 1000);
|
||||
const 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(
|
||||
&owner_.last_local_secure_input_block_log_tick_,
|
||||
owner_.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
|
||||
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
const int key_code = static_cast<int>(action.k.key_value);
|
||||
const bool is_down = action.k.flag == KeyFlag::key_down;
|
||||
const bool injected =
|
||||
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
||||
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (is_down && injected) {
|
||||
state.pressed_keys[key_code] =
|
||||
PressedKey{key_code, action.k.scan_code, action.k.extended};
|
||||
} else if (!is_down && injected) {
|
||||
state.pressed_keys.erase(key_code);
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
std::vector<PressedKey> keys_to_press;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
||||
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.last_seq = action.ks.seq;
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
state.keyboard_state_seen = true;
|
||||
|
||||
std::unordered_map<int, PressedKey> desired_keys;
|
||||
const size_t count =
|
||||
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
const auto &key = action.ks.pressed_keys[index];
|
||||
const int key_code = static_cast<int>(key.key_value);
|
||||
desired_keys[key_code] =
|
||||
PressedKey{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 PressedKey &key : keys_to_release) {
|
||||
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
const auto state_it = remote_states_.find(remote_id);
|
||||
if (state_it != remote_states_.end()) {
|
||||
state_it->second.pressed_keys.erase(key.key_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const PressedKey &key : keys_to_press) {
|
||||
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
remote_states_[remote_id].pressed_keys[key.key_code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
const auto state_it = remote_states_.find(remote_id);
|
||||
if (state_it == remote_states_.end()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[_, key] : state_it->second.pressed_keys) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
remote_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 PressedKey &key : keys_to_release) {
|
||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::CheckRemoteTimeouts() {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
for (const auto &[remote_id, state] : remote_states_) {
|
||||
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
||||
state.last_seen_tick != 0 &&
|
||||
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
|
||||
timed_out_remotes.push_back(remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const std::string &remote_id : timed_out_remotes) {
|
||||
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
|
||||
#define CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
|
||||
// Synchronizes local and remote keyboard state, including heartbeat recovery
|
||||
// for keys whose key-up event was lost during a connection interruption.
|
||||
class KeyboardController {
|
||||
public:
|
||||
explicit KeyboardController(GuiRuntime &owner);
|
||||
|
||||
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
void ForceReleasePressedKeys();
|
||||
void SendHeartbeat(bool force);
|
||||
void ApplyRemoteEvent(const std::string &remote_id,
|
||||
const RemoteAction &remote_action);
|
||||
void ApplyRemoteState(const std::string &remote_id,
|
||||
const RemoteAction &remote_action);
|
||||
void ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason);
|
||||
void CheckRemoteTimeouts();
|
||||
|
||||
private:
|
||||
struct PressedKey {
|
||||
int key_code = 0;
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
};
|
||||
|
||||
struct RemoteState {
|
||||
std::unordered_map<int, PressedKey> pressed_keys;
|
||||
uint32_t last_seq = 0;
|
||||
uint32_t last_seen_tick = 0;
|
||||
bool keyboard_state_seen = false;
|
||||
};
|
||||
|
||||
void TrackPressedKey(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
std::unordered_map<int, PressedKey> pressed_keys_;
|
||||
std::mutex pressed_keys_mutex_;
|
||||
uint32_t state_sequence_ = 0;
|
||||
uint32_t last_heartbeat_tick_ = 0;
|
||||
std::unordered_map<std::string, RemoteState> remote_states_;
|
||||
std::mutex remote_states_mutex_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
|
||||
@@ -0,0 +1,345 @@
|
||||
#include "features/settings/settings_manager.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
template <size_t Size>
|
||||
void CopyString(char (&destination)[Size], const char *source) {
|
||||
static_assert(Size > 0);
|
||||
std::memset(destination, 0, Size);
|
||||
if (source) {
|
||||
std::strncpy(destination, source, Size - 1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
int SettingsManager::Save() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
return SaveLocked();
|
||||
}
|
||||
|
||||
int SettingsManager::SaveLocked() {
|
||||
std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
std::ios::binary);
|
||||
if (!cache_v2_file) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CopyString(cache_v2_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
|
||||
|
||||
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
|
||||
sizeof(cache_v2_));
|
||||
|
||||
// Keep writing the legacy cache while older installations may still read it.
|
||||
std::ofstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
||||
std::ios::binary);
|
||||
if (cache_v1_file) {
|
||||
CopyString(cache_v1_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
cache_v1_file.write(reinterpret_cast<const char *>(&cache_v1_),
|
||||
sizeof(cache_v1_));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SettingsManager::ReadV2Locked() {
|
||||
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
std::ios::binary);
|
||||
if (!cache_v2_file) {
|
||||
return false;
|
||||
}
|
||||
cache_v2_file.read(reinterpret_cast<char *>(&cache_v2_), sizeof(cache_v2_));
|
||||
if (cache_v2_file.gcount() !=
|
||||
static_cast<std::streamsize>(sizeof(cache_v2_))) {
|
||||
return false;
|
||||
}
|
||||
cache_v2_
|
||||
.client_id_with_password[sizeof(cache_v2_.client_id_with_password) - 1] =
|
||||
'\0';
|
||||
cache_v2_.self_hosted_id[sizeof(cache_v2_.self_hosted_id) - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
int SettingsManager::Load() {
|
||||
std::unique_lock<std::mutex> lock(cache_mutex_);
|
||||
|
||||
if (ReadV2Locked()) {
|
||||
CopyString(owner_.client_id_with_password_,
|
||||
cache_v2_.client_id_with_password);
|
||||
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
|
||||
std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key));
|
||||
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv));
|
||||
LOG_INFO("Load settings from v2 cache file");
|
||||
} else {
|
||||
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
||||
std::ios::binary);
|
||||
if (!cache_v1_file) {
|
||||
lock.unlock();
|
||||
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
std::memset(owner_.aes128_key_, 0, sizeof(owner_.aes128_key_));
|
||||
std::memset(owner_.aes128_iv_, 0, sizeof(owner_.aes128_iv_));
|
||||
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||
|
||||
owner_.thumbnail_ =
|
||||
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/");
|
||||
owner_.thumbnail_->GetKeyAndIv(owner_.aes128_key_, owner_.aes128_iv_);
|
||||
owner_.thumbnail_->DeleteAllFilesInDirectory();
|
||||
|
||||
Save();
|
||||
return -1;
|
||||
}
|
||||
|
||||
cache_v1_file.read(reinterpret_cast<char *>(&cache_v1_), sizeof(cache_v1_));
|
||||
cache_v1_
|
||||
.client_id_with_password[sizeof(cache_v1_.client_id_with_password) -
|
||||
1] = '\0';
|
||||
|
||||
CopyString(cache_v2_.client_id_with_password,
|
||||
cache_v1_.client_id_with_password);
|
||||
std::memcpy(cache_v2_.key, cache_v1_.key, sizeof(cache_v1_.key));
|
||||
std::memcpy(cache_v2_.iv, cache_v1_.iv, sizeof(cache_v1_.iv));
|
||||
std::memset(cache_v2_.self_hosted_id, 0, sizeof(cache_v2_.self_hosted_id));
|
||||
|
||||
CopyString(owner_.client_id_with_password_,
|
||||
cache_v1_.client_id_with_password);
|
||||
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||
std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key));
|
||||
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
|
||||
|
||||
SaveLocked();
|
||||
LOG_INFO("Migrated settings from v1 to v2 cache file");
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
|
||||
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
|
||||
if (at_pos != nullptr) {
|
||||
const std::string id(owner_.client_id_with_password_,
|
||||
at_pos - owner_.client_id_with_password_);
|
||||
const std::string password(at_pos + 1);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, password.c_str());
|
||||
}
|
||||
|
||||
owner_.thumbnail_ =
|
||||
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
|
||||
owner_.aes128_key_, owner_.aes128_iv_);
|
||||
|
||||
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
|
||||
static_cast<int>(owner_.config_center_->GetLanguage()));
|
||||
owner_.video_quality_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoQuality());
|
||||
owner_.video_frame_rate_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
|
||||
owner_.video_encode_format_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
|
||||
owner_.enable_hardware_video_codec_ =
|
||||
owner_.config_center_->IsHardwareVideoCodec();
|
||||
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
|
||||
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
|
||||
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
|
||||
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
|
||||
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
|
||||
owner_.enable_minimize_to_tray_ = owner_.config_center_->IsMinimizeToTray();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
owner_.portable_service_prompt_suppressed_ =
|
||||
owner_.config_center_->IsPortableServicePromptSuppressed();
|
||||
owner_.portable_service_do_not_remind_ =
|
||||
owner_.portable_service_prompt_suppressed_;
|
||||
#endif
|
||||
|
||||
const std::string saved_path =
|
||||
owner_.config_center_->GetFileTransferSavePath();
|
||||
CopyString(owner_.file_transfer_save_path_buf_, saved_path.c_str());
|
||||
owner_.file_transfer_save_path_last_ = saved_path;
|
||||
|
||||
owner_.language_button_value_last_ = owner_.language_button_value_;
|
||||
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
|
||||
owner_.video_encode_format_button_value_last_ =
|
||||
owner_.video_encode_format_button_value_;
|
||||
owner_.enable_hardware_video_codec_last_ =
|
||||
owner_.enable_hardware_video_codec_;
|
||||
owner_.enable_turn_last_ = owner_.enable_turn_;
|
||||
owner_.enable_srtp_last_ = owner_.enable_srtp_;
|
||||
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
|
||||
owner_.enable_autostart_last_ = owner_.enable_autostart_;
|
||||
owner_.enable_minimize_to_tray_last_ = owner_.enable_minimize_to_tray_;
|
||||
|
||||
LOG_INFO("Load settings from cache file");
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (!ReadV2Locked() || cache_v2_.self_hosted_id[0] == '\0') {
|
||||
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||
return false;
|
||||
}
|
||||
|
||||
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
|
||||
const char *at_pos = std::strchr(owner_.self_hosted_id_, '@');
|
||||
if (at_pos == nullptr) {
|
||||
CopyString(owner_.client_id_, owner_.self_hosted_id_);
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
} else {
|
||||
const std::string id(owner_.self_hosted_id_,
|
||||
at_pos - owner_.self_hosted_id_);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, at_pos + 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
|
||||
if (!client_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (!ReadV2Locked()) {
|
||||
cache_v2_ = {};
|
||||
}
|
||||
|
||||
CopyString(cache_v2_.self_hosted_id, client_id);
|
||||
CopyString(cache_v2_.client_id_with_password,
|
||||
owner_.client_id_with_password_);
|
||||
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||
|
||||
std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
std::ios::binary);
|
||||
if (cache_v2_file) {
|
||||
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
|
||||
sizeof(cache_v2_));
|
||||
}
|
||||
}
|
||||
|
||||
int SettingsManager::LoadRecentConnectionAliases() {
|
||||
recent_connection_aliases_.clear();
|
||||
|
||||
std::ifstream alias_file(owner_.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()) {
|
||||
const std::string remote_id = it.key();
|
||||
const 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 SettingsManager::SaveRecentConnectionAliases() const {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(owner_.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(
|
||||
owner_.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 SettingsManager::RecentConnectionDisplayName(
|
||||
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 SettingsManager::BeginEditRecentConnectionAlias(
|
||||
const Thumbnail::RecentConnection &connection) {
|
||||
owner_.edit_connection_alias_remote_id_ = connection.remote_id;
|
||||
std::memset(owner_.edit_connection_alias_, 0,
|
||||
sizeof(owner_.edit_connection_alias_));
|
||||
|
||||
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
|
||||
const std::string alias = alias_it != recent_connection_aliases_.end()
|
||||
? alias_it->second
|
||||
: RecentConnectionDisplayName(connection);
|
||||
CopyString(owner_.edit_connection_alias_, alias.c_str());
|
||||
|
||||
owner_.focus_on_input_widget_ = true;
|
||||
owner_.show_edit_connection_alias_window_ = true;
|
||||
}
|
||||
|
||||
void SettingsManager::SetRecentConnectionAlias(const std::string &remote_id,
|
||||
const std::string &alias) {
|
||||
if (!remote_id.empty()) {
|
||||
recent_connection_aliases_[remote_id] = alias;
|
||||
}
|
||||
}
|
||||
|
||||
void SettingsManager::EraseRecentConnectionAlias(const std::string &remote_id) {
|
||||
recent_connection_aliases_.erase(remote_id);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,78 @@
|
||||
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "thumbnail.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
|
||||
// Owns persistent GUI settings and recent-connection aliases. GuiRuntime keeps
|
||||
// only the runtime/UI state that consumes these values.
|
||||
class SettingsManager {
|
||||
public:
|
||||
explicit SettingsManager(GuiRuntime &owner);
|
||||
|
||||
int Save();
|
||||
int Load();
|
||||
|
||||
int LoadRecentConnectionAliases();
|
||||
int SaveRecentConnectionAliases() const;
|
||||
std::string RecentConnectionDisplayName(
|
||||
const Thumbnail::RecentConnection &connection) const;
|
||||
void
|
||||
BeginEditRecentConnectionAlias(const Thumbnail::RecentConnection &connection);
|
||||
void SetRecentConnectionAlias(const std::string &remote_id,
|
||||
const std::string &alias);
|
||||
void EraseRecentConnectionAlias(const std::string &remote_id);
|
||||
|
||||
// Loads the cached self-hosted identity into the owner's active connection
|
||||
// fields. Returns true only when a non-empty identity was restored.
|
||||
bool LoadCachedSelfHostedIdentity();
|
||||
void PersistSelfHostedIdentity(const char *client_id);
|
||||
|
||||
private:
|
||||
struct CacheV1 {
|
||||
char client_id_with_password[17];
|
||||
int language;
|
||||
int video_quality;
|
||||
int video_frame_rate;
|
||||
int video_encode_format;
|
||||
bool enable_hardware_video_codec;
|
||||
bool enable_turn;
|
||||
bool enable_srtp;
|
||||
unsigned char key[16];
|
||||
unsigned char iv[16];
|
||||
};
|
||||
|
||||
struct CacheV2 {
|
||||
char client_id_with_password[17];
|
||||
int language;
|
||||
int video_quality;
|
||||
int video_frame_rate;
|
||||
int video_encode_format;
|
||||
bool enable_hardware_video_codec;
|
||||
bool enable_turn;
|
||||
bool enable_srtp;
|
||||
unsigned char key[16];
|
||||
unsigned char iv[16];
|
||||
char self_hosted_id[17];
|
||||
};
|
||||
|
||||
int SaveLocked();
|
||||
bool ReadV2Locked();
|
||||
|
||||
GuiRuntime &owner_;
|
||||
CacheV1 cache_v1_{};
|
||||
CacheV2 cache_v2_{};
|
||||
mutable std::mutex cache_mutex_;
|
||||
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
@@ -1,363 +0,0 @@
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::RecentConnectionsWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float recent_connection_window_width = io.DisplaySize.x;
|
||||
float recent_connection_window_height =
|
||||
io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f),
|
||||
ImGuiCond_Always);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsWindow",
|
||||
ImVec2(recent_connection_window_width, recent_connection_window_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::recent_connections[localization_language_index_].c_str());
|
||||
|
||||
ShowRecentConnections();
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::ShowRecentConnections() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float recent_connection_panel_width = io.DisplaySize.x * 0.912f;
|
||||
float recent_connection_panel_height = io.DisplaySize.y * 0.29f;
|
||||
float recent_connection_image_height = recent_connection_panel_height * 0.6f;
|
||||
float recent_connection_image_width = recent_connection_image_height * 16 / 9;
|
||||
float recent_connection_sub_container_width =
|
||||
recent_connection_image_width * 1.2f;
|
||||
float recent_connection_sub_container_height =
|
||||
recent_connection_image_height * 1.4f;
|
||||
float recent_connection_button_width = recent_connection_image_width * 0.15f;
|
||||
float recent_connection_button_height =
|
||||
recent_connection_image_height * 0.25f;
|
||||
float recent_connection_dummy_button_width =
|
||||
recent_connection_image_width - 2 * recent_connection_button_width;
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
|
||||
|
||||
std::map<std::string, ImVec2> sub_containers_pos;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
||||
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsContainer",
|
||||
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
size_t recent_connections_count = recent_connections_.size();
|
||||
int count = 0;
|
||||
for (auto& it : recent_connections_) {
|
||||
sub_containers_pos[it.first] = ImGui::GetCursorPos();
|
||||
std::string recent_connection_sub_window_name =
|
||||
"RecentConnectionsSubContainer" + it.first;
|
||||
// recent connections sub container
|
||||
ImGui::BeginChild(recent_connection_sub_window_name.c_str(),
|
||||
ImVec2(recent_connection_sub_container_width,
|
||||
recent_connection_sub_container_height),
|
||||
ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
std::string connection_info = it.first;
|
||||
|
||||
// remote id length is 9
|
||||
// password length is 6
|
||||
// connection_info -> remote_id + 'Y' + host_name + '@' + password
|
||||
// -> remote_id + 'N' + host_name
|
||||
if ('Y' == connection_info[9] && connection_info.size() >= 16) {
|
||||
size_t pos_y = connection_info.find('Y');
|
||||
size_t pos_at = connection_info.find('@');
|
||||
|
||||
if (pos_y == std::string::npos || pos_at == std::string::npos ||
|
||||
pos_y >= pos_at) {
|
||||
LOG_ERROR("Invalid filename");
|
||||
continue;
|
||||
}
|
||||
|
||||
it.second.remote_id = connection_info.substr(0, pos_y);
|
||||
it.second.remote_host_name =
|
||||
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
|
||||
it.second.password = connection_info.substr(pos_at + 1);
|
||||
it.second.remember_password = true;
|
||||
} else if ('N' == connection_info[9] && connection_info.size() >= 10) {
|
||||
size_t pos_n = connection_info.find('N');
|
||||
size_t pos_at = connection_info.find('@');
|
||||
|
||||
if (pos_n == std::string::npos) {
|
||||
LOG_ERROR("Invalid filename");
|
||||
continue;
|
||||
}
|
||||
|
||||
it.second.remote_id = connection_info.substr(0, pos_n);
|
||||
it.second.remote_host_name = connection_info.substr(pos_n + 1);
|
||||
it.second.password = "";
|
||||
it.second.remember_password = false;
|
||||
} else {
|
||||
it.second.remote_host_name = "unknown";
|
||||
}
|
||||
|
||||
bool online = device_presence_.IsOnline(it.second.remote_id);
|
||||
|
||||
ImVec2 image_screen_pos = ImVec2(
|
||||
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
|
||||
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
|
||||
ImVec2 image_pos =
|
||||
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
|
||||
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
|
||||
ImGui::SetCursorPos(image_pos);
|
||||
ImGui::Image(
|
||||
(ImTextureID)(intptr_t)it.second.texture,
|
||||
ImVec2(recent_connection_image_width, recent_connection_image_height));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string display_host_name_with_presence =
|
||||
it.second.remote_host_name + " " +
|
||||
(online ? localization::online[localization_language_index_]
|
||||
: localization::offline[localization_language_index_]);
|
||||
ImGui::Text("%s", display_host_name_with_presence.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
ImVec2 circle_pos =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
|
||||
image_screen_pos.y + recent_connection_image_height * 0.12f);
|
||||
ImU32 fill_color =
|
||||
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
|
||||
ImU32 border_color = IM_COL32(255, 255, 255, 255);
|
||||
float dot_radius = recent_connection_image_height * 0.06f;
|
||||
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
|
||||
100);
|
||||
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
|
||||
|
||||
// remote id display button
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0.2f));
|
||||
|
||||
ImVec2 dummy_button_pos =
|
||||
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
|
||||
std::string dummy_button_name = "##DummyButton" + it.second.remote_id;
|
||||
ImGui::SetCursorPos(dummy_button_pos);
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
ImGui::Button(dummy_button_name.c_str(),
|
||||
ImVec2(recent_connection_dummy_button_width,
|
||||
recent_connection_button_height));
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetCursorPos(ImVec2(
|
||||
dummy_button_pos.x + recent_connection_dummy_button_width * 0.05f,
|
||||
dummy_button_pos.y + recent_connection_button_height * 0.05f));
|
||||
ImGui::SetWindowFontScale(0.65f);
|
||||
ImGui::Text("%s", it.second.remote_id.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(0.1f, 0.4f, 0.8f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 0.7f));
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
// trash button
|
||||
{
|
||||
ImVec2 trash_can_button_pos =
|
||||
ImVec2(image_pos.x + recent_connection_image_width -
|
||||
2 * recent_connection_button_width,
|
||||
image_pos.y + recent_connection_image_height);
|
||||
ImGui::SetCursorPos(trash_can_button_pos);
|
||||
std::string trash_can = ICON_FA_TRASH_CAN;
|
||||
std::string recent_connection_delete_button_name =
|
||||
trash_can + "##RecentConnectionDelete" +
|
||||
std::to_string(trash_can_button_pos.x);
|
||||
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
show_confirm_delete_connection_ = true;
|
||||
delete_connection_name_ = it.first;
|
||||
}
|
||||
|
||||
if (delete_connection_ && delete_connection_name_ == it.first) {
|
||||
if (!thumbnail_->DeleteThumbnail(it.first)) {
|
||||
reload_recent_connections_ = true;
|
||||
delete_connection_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connect button
|
||||
{
|
||||
ImVec2 connect_button_pos =
|
||||
ImVec2(image_pos.x + recent_connection_image_width -
|
||||
recent_connection_button_width,
|
||||
image_pos.y + recent_connection_image_height);
|
||||
ImGui::SetCursorPos(connect_button_pos);
|
||||
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
|
||||
std::string connect_to_this_connection_button_name =
|
||||
connect + "##ConnectionTo" + it.first;
|
||||
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
ConnectTo(it.second.remote_id, it.second.password.c_str(),
|
||||
it.second.remember_password);
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
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_button_height);
|
||||
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
|
||||
IM_COL32(0, 0, 0, 122), 1.0f);
|
||||
}
|
||||
|
||||
count++;
|
||||
ImGui::SameLine(0, count != recent_connections_count
|
||||
? (recent_connection_image_width * 0.165f)
|
||||
: 0.0f);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (show_confirm_delete_connection_) {
|
||||
ConfirmDeleteConnection();
|
||||
}
|
||||
if (show_offline_warning_window_) {
|
||||
OfflineWarningWindow();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::ConfirmDeleteConnection() {
|
||||
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("ConfirmDeleteConnectionWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto connection_status_window_width = ImGui::GetWindowSize().x;
|
||||
auto connection_status_window_height = ImGui::GetWindowSize().y;
|
||||
|
||||
std::string text =
|
||||
localization::confirm_delete_connection[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.33f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
|
||||
// ok
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
|
||||
delete_connection_ = true;
|
||||
show_confirm_delete_connection_ = false;
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
// cancel
|
||||
if (ImGui::Button(
|
||||
localization::cancel[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
delete_connection_ = false;
|
||||
show_confirm_delete_connection_ = false;
|
||||
}
|
||||
|
||||
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
|
||||
ImGui::SetCursorPosX((connection_status_window_width - text_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.2f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::OfflineWarningWindow() {
|
||||
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("OfflineWarningWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto window_width = ImGui::GetWindowSize().x;
|
||||
auto window_height = ImGui::GetWindowSize().y;
|
||||
|
||||
ImGui::SetCursorPosX(window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(window_height * 0.67f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_offline_warning_window_ = false;
|
||||
}
|
||||
|
||||
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
|
||||
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(window_height * 0.2f);
|
||||
ImGui::Text("%s", offline_warning_text_.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
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__
|
||||
+5
-3073
File diff suppressed because it is too large
Load Diff
+14
-770
@@ -1,784 +1,28 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef CROSSDESK_GUI_RENDER_H_
|
||||
#define CROSSDESK_GUI_RENDER_H_
|
||||
|
||||
#ifndef _MAIN_WINDOW_H_
|
||||
#define _MAIN_WINDOW_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "config_center.h"
|
||||
#include "device_controller_factory.h"
|
||||
#include "device_presence.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
#include "imgui_impl_sdlrenderer3.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "minirtc.h"
|
||||
#include "path_manager.h"
|
||||
#include "screen_capturer_factory.h"
|
||||
#include "speaker_capturer_factory.h"
|
||||
#include "thumbnail.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "win_tray.h"
|
||||
#endif
|
||||
#include <memory>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiApplication;
|
||||
|
||||
// Stable application-facing facade. The SDL application and feature
|
||||
// controllers remain private implementation details.
|
||||
class Render {
|
||||
public:
|
||||
enum class RemoteUnlockState {
|
||||
none,
|
||||
service_unavailable,
|
||||
lock_screen,
|
||||
credential_ui,
|
||||
secure_desktop,
|
||||
};
|
||||
|
||||
struct FileTransferState {
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
bool file_transfer_window_hovered_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
std::string remote_id;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
};
|
||||
|
||||
struct SubStreamWindowProperties {
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
std::string keyboard_label_ = "keyboard";
|
||||
std::string file_label_ = "file";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
std::string local_id_ = "";
|
||||
std::string remote_id_ = "";
|
||||
bool exit_ = false;
|
||||
bool signal_connected_ = false;
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
bool connection_established_ = false;
|
||||
bool rejoin_ = false;
|
||||
bool net_traffic_stats_button_pressed_ = false;
|
||||
bool enable_mouse_control_ = true;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool audio_capture_button_pressed_ = true;
|
||||
bool control_mouse_ = true;
|
||||
bool streaming_ = false;
|
||||
bool is_control_bar_in_left_ = true;
|
||||
bool control_bar_hovered_ = false;
|
||||
bool display_selectable_hovered_ = false;
|
||||
bool shortcut_selectable_hovered_ = false;
|
||||
bool control_bar_expand_ = true;
|
||||
bool reset_control_bar_pos_ = false;
|
||||
bool control_window_width_is_changing_ = false;
|
||||
bool control_window_height_is_changing_ = false;
|
||||
bool p2p_mode_ = true;
|
||||
bool remember_password_ = false;
|
||||
char remote_password_[7] = "";
|
||||
float sub_stream_window_width_ = 1280;
|
||||
float sub_stream_window_height_ = 720;
|
||||
float control_window_min_width_ = 20;
|
||||
float control_window_max_width_ = 300;
|
||||
float control_window_min_height_ = 38;
|
||||
float control_window_max_height_ = 180;
|
||||
float control_window_width_ = 300;
|
||||
float control_window_height_ = 38;
|
||||
float control_bar_pos_x_ = 0;
|
||||
float control_bar_pos_y_ = 30;
|
||||
float mouse_diff_control_bar_pos_x_ = 0;
|
||||
float mouse_diff_control_bar_pos_y_ = 0;
|
||||
double control_bar_button_pressed_time_ = 0;
|
||||
double net_traffic_stats_button_pressed_time_ = 0;
|
||||
// Double-buffered NV12 frame storage. Written by decode callback thread,
|
||||
// consumed by SDL main thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
float mouse_pos_y_ = 0;
|
||||
float mouse_pos_x_last_ = 0;
|
||||
float mouse_pos_y_last_ = 0;
|
||||
int texture_width_ = 1280;
|
||||
int texture_height_ = 720;
|
||||
int video_width_ = 0;
|
||||
int video_height_ = 0;
|
||||
int video_width_last_ = 0;
|
||||
int video_height_last_ = 0;
|
||||
int selected_display_ = 0;
|
||||
size_t video_size_ = 0;
|
||||
bool tab_selected_ = false;
|
||||
bool tab_opened_ = true;
|
||||
std::optional<float> pos_x_before_docked_;
|
||||
std::optional<float> pos_y_before_docked_;
|
||||
float render_window_x_ = 0;
|
||||
float render_window_y_ = 0;
|
||||
float render_window_width_ = 0;
|
||||
float render_window_height_ = 0;
|
||||
std::string fullscreen_button_label_ = "Fullscreen";
|
||||
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
|
||||
std::string mouse_control_button_label_ = "Mouse Control";
|
||||
std::string audio_capture_button_label_ = "Audio Capture";
|
||||
std::string remote_host_name_ = "";
|
||||
bool remote_service_status_received_ = false;
|
||||
bool remote_service_available_ = false;
|
||||
std::string remote_interactive_stage_ = "";
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
SDL_Texture* stream_texture_ = nullptr;
|
||||
uint8_t* argb_buffer_ = nullptr;
|
||||
int argb_buffer_size_ = 0;
|
||||
SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
SDL_Rect stream_render_rect_;
|
||||
SDL_Rect stream_render_rect_last_;
|
||||
ImVec2 control_window_pos_;
|
||||
ConnectionStatus connection_status_ = ConnectionStatus::Closed;
|
||||
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||
int fps_ = 0;
|
||||
int frame_count_ = 0;
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_;
|
||||
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
using FileTransferInfo = FileTransferState::FileTransferInfo;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
|
||||
public:
|
||||
Render();
|
||||
~Render();
|
||||
|
||||
public:
|
||||
Render(const Render &) = delete;
|
||||
Render &operator=(const Render &) = delete;
|
||||
|
||||
int Run();
|
||||
|
||||
private:
|
||||
void InitializeLogger();
|
||||
void InitializeSettings();
|
||||
void InitializeSDL();
|
||||
void InitializeModules();
|
||||
void InitializeMainWindow();
|
||||
void MainLoop();
|
||||
void UpdateLabels();
|
||||
void UpdateInteractions();
|
||||
void HandleRecentConnections();
|
||||
void HandleConnectionStatusChange();
|
||||
void HandlePendingPresenceProbe();
|
||||
void HandleStreamWindow();
|
||||
void HandleServerWindow();
|
||||
void Cleanup();
|
||||
void CleanupFactories();
|
||||
void CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
void CleanupPeers();
|
||||
void CleanSubStreamWindowProperties(
|
||||
std::shared_ptr<SubStreamWindowProperties> props);
|
||||
void UpdateRenderRect();
|
||||
void ProcessSdlEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessFileDropEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "");
|
||||
|
||||
std::shared_ptr<SubStreamWindowProperties>
|
||||
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
|
||||
|
||||
private:
|
||||
int CreateStreamRenderWindow();
|
||||
int TitleBar(bool main_window);
|
||||
int MainWindow();
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
int SettingWindow();
|
||||
int SelfHostedServerWindow();
|
||||
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
int AboutWindow();
|
||||
int StatusBar();
|
||||
bool ConnectionStatusWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
int ShowRecentConnections();
|
||||
bool OpenUrl(const std::string& url);
|
||||
void Hyperlink(const std::string& label, const std::string& url,
|
||||
const float window_width);
|
||||
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password, bool bypass_presence_check = false);
|
||||
int RequestSingleDevicePresence(const std::string& remote_id,
|
||||
const char* password, bool remember_password);
|
||||
int CreateMainWindow();
|
||||
int DestroyMainWindow();
|
||||
int CreateStreamWindow();
|
||||
int DestroyStreamWindow();
|
||||
int CreateServerWindow();
|
||||
int DestroyServerWindow();
|
||||
int SetupFontAndStyle(ImFont** system_chinese_font_out);
|
||||
int DestroyMainWindowContext();
|
||||
int DestroyStreamWindowContext();
|
||||
int DestroyServerWindowContext();
|
||||
int DrawMainWindow();
|
||||
int DrawStreamWindow();
|
||||
int DrawServerWindow();
|
||||
int ConfirmDeleteConnection();
|
||||
int OfflineWarningWindow();
|
||||
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawConnectionStatusText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawReceivingScreenText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void ResetRemoteServiceStatus(SubStreamWindowProperties& props);
|
||||
void ApplyRemoteServiceStatus(SubStreamWindowProperties& props,
|
||||
const ServiceStatus& status);
|
||||
RemoteUnlockState GetRemoteUnlockState(
|
||||
const SubStreamWindowProperties& props) const;
|
||||
#ifdef __APPLE__
|
||||
int RequestPermissionWindow();
|
||||
bool CheckScreenRecordingPermission();
|
||||
bool CheckAccessibilityPermission();
|
||||
void OpenScreenRecordingPreferences();
|
||||
void OpenAccessibilityPreferences();
|
||||
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
|
||||
void RefreshMacPermissionStatus(bool force);
|
||||
bool EnsureMacScreenRecordingPermission();
|
||||
bool EnsureMacAccessibilityPermission();
|
||||
#endif
|
||||
|
||||
public:
|
||||
static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data);
|
||||
|
||||
static void OnReceiveAudioBufferCb(const char* data, size_t size,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data);
|
||||
|
||||
static void OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data);
|
||||
|
||||
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data);
|
||||
|
||||
static void OnSignalMessageCb(const char* message, size_t size,
|
||||
void* user_data);
|
||||
|
||||
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data);
|
||||
|
||||
static void OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
|
||||
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
|
||||
const SDL_Point* area, void* data);
|
||||
|
||||
static std::vector<char> SerializeRemoteAction(const RemoteAction& action);
|
||||
|
||||
static bool DeserializeRemoteAction(const char* data, size_t size,
|
||||
RemoteAction& out);
|
||||
|
||||
static void FreeRemoteAction(RemoteAction& action);
|
||||
|
||||
private:
|
||||
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
static bool IsModifierVkKey(int key_code);
|
||||
void TrackPressedKeyState(int key_code, bool is_down);
|
||||
void ForceReleasePressedKeys();
|
||||
int ProcessKeyboardEvent(const SDL_Event& event);
|
||||
int ProcessMouseEvent(const SDL_Event& event);
|
||||
|
||||
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
|
||||
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
|
||||
|
||||
private:
|
||||
int SaveSettingsIntoCacheFile();
|
||||
int LoadSettingsFromCacheFile();
|
||||
|
||||
int ScreenCapturerInit();
|
||||
int StartScreenCapturer();
|
||||
int StopScreenCapturer();
|
||||
|
||||
int StartSpeakerCapturer();
|
||||
int StopSpeakerCapturer();
|
||||
|
||||
int StartMouseController();
|
||||
int StopMouseController();
|
||||
|
||||
int StartKeyboardCapturer();
|
||||
int StopKeyboardCapturer();
|
||||
|
||||
int CreateConnectionPeer();
|
||||
|
||||
// File transfer helper functions
|
||||
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
const std::filesystem::path& file_path,
|
||||
const std::string& file_label,
|
||||
const std::string& remote_id = "");
|
||||
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
|
||||
int AudioDeviceInit();
|
||||
int AudioDeviceDestroy();
|
||||
void HandleWindowsServiceIntegration();
|
||||
#if _WIN32
|
||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||
#if CROSSDESK_PORTABLE
|
||||
enum class PortableServiceInstallState {
|
||||
idle,
|
||||
installing,
|
||||
succeeded,
|
||||
failed,
|
||||
std::unique_ptr<GuiApplication> application_;
|
||||
};
|
||||
|
||||
void CheckPortableWindowsService();
|
||||
int PortableServiceInstallWindow();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
private:
|
||||
struct CDCache {
|
||||
char client_id_with_password[17];
|
||||
int language;
|
||||
int video_quality;
|
||||
int video_frame_rate;
|
||||
int video_encode_format;
|
||||
bool enable_hardware_video_codec;
|
||||
bool enable_turn;
|
||||
bool enable_srtp;
|
||||
|
||||
unsigned char key[16];
|
||||
unsigned char iv[16];
|
||||
};
|
||||
|
||||
struct CDCacheV2 {
|
||||
char client_id_with_password[17];
|
||||
int language;
|
||||
int video_quality;
|
||||
int video_frame_rate;
|
||||
int video_encode_format;
|
||||
bool enable_hardware_video_codec;
|
||||
bool enable_turn;
|
||||
bool enable_srtp;
|
||||
|
||||
unsigned char key[16];
|
||||
unsigned char iv[16];
|
||||
|
||||
char self_hosted_id[17];
|
||||
};
|
||||
|
||||
private:
|
||||
CDCache cd_cache_;
|
||||
CDCacheV2 cd_cache_v2_;
|
||||
std::mutex cd_cache_mutex_;
|
||||
std::unique_ptr<ConfigCenter> config_center_;
|
||||
ConfigCenter::LANGUAGE localization_language_ =
|
||||
ConfigCenter::LANGUAGE::CHINESE;
|
||||
std::unique_ptr<PathManager> path_manager_;
|
||||
std::string exec_log_path_;
|
||||
std::string dll_log_path_;
|
||||
std::string cache_path_;
|
||||
int localization_language_index_ = -1;
|
||||
int localization_language_index_last_ = -1;
|
||||
bool modules_inited_ = false;
|
||||
/* ------ all windows property start ------ */
|
||||
float title_bar_width_ = 640;
|
||||
float title_bar_height_ = 30;
|
||||
float title_bar_button_width_ = 30;
|
||||
float title_bar_button_height_ = 30;
|
||||
/* ------ all windows property end ------ */
|
||||
|
||||
/* ------ main window property start ------ */
|
||||
// thumbnail
|
||||
unsigned char aes128_key_[16];
|
||||
unsigned char aes128_iv_[16];
|
||||
std::shared_ptr<Thumbnail> thumbnail_;
|
||||
|
||||
// recent connections
|
||||
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
|
||||
recent_connections_;
|
||||
std::vector<std::string> recent_connection_ids_;
|
||||
int recent_connection_image_width_ = 160;
|
||||
int recent_connection_image_height_ = 90;
|
||||
uint32_t recent_connection_image_save_time_ = 0;
|
||||
DevicePresence device_presence_;
|
||||
bool need_to_send_recent_connections_ = true;
|
||||
|
||||
// main window render
|
||||
SDL_Window* main_window_ = nullptr;
|
||||
SDL_Renderer* main_renderer_ = nullptr;
|
||||
ImGuiContext* main_ctx_ = nullptr;
|
||||
ImFont* main_windows_system_chinese_font_ = nullptr;
|
||||
ImFont* stream_windows_system_chinese_font_ = nullptr;
|
||||
ImFont* server_windows_system_chinese_font_ = nullptr;
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16; // ~60 FPS
|
||||
#if _WIN32
|
||||
std::unique_ptr<WinTray> tray_;
|
||||
#endif
|
||||
|
||||
// main window properties
|
||||
nlohmann::json latest_version_info_ = nlohmann::json{};
|
||||
bool update_available_ = false;
|
||||
std::string latest_version_ = "";
|
||||
std::string release_notes_ = "";
|
||||
bool start_mouse_controller_ = false;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool start_screen_capturer_ = false;
|
||||
bool screen_capturer_is_started_ = false;
|
||||
bool start_speaker_capturer_ = false;
|
||||
bool speaker_capturer_is_started_ = false;
|
||||
bool start_keyboard_capturer_ = false;
|
||||
bool show_cursor_ = false;
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool keyboard_capturer_uses_sdl_events_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool focus_on_stream_window_ = false;
|
||||
bool main_window_minimized_ = false;
|
||||
uint32_t last_main_minimize_request_tick_ = 0;
|
||||
uint32_t last_stream_minimize_request_tick_ = 0;
|
||||
bool audio_capture_ = false;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
float main_window_dpi_scaling_w_ = 1.0f;
|
||||
float main_window_dpi_scaling_h_ = 1.0f;
|
||||
float dpi_scale_ = 1.0f;
|
||||
float main_window_width_default_ = 640;
|
||||
float main_window_height_default_ = 480;
|
||||
float main_window_width_ = 640;
|
||||
float main_window_height_ = 480;
|
||||
float main_window_width_last_ = 640;
|
||||
float main_window_height_last_ = 480;
|
||||
float local_window_width_ = 320;
|
||||
float local_window_height_ = 235;
|
||||
float remote_window_width_ = 320;
|
||||
float remote_window_height_ = 235;
|
||||
float local_child_window_width_ = 266;
|
||||
float local_child_window_height_ = 180;
|
||||
float remote_child_window_width_ = 266;
|
||||
float remote_child_window_height_ = 180;
|
||||
float main_window_text_y_padding_ = 10;
|
||||
float main_child_window_x_padding_ = 27;
|
||||
float main_child_window_y_padding_ = 45;
|
||||
float status_bar_height_ = 22;
|
||||
float connection_status_window_width_ = 200;
|
||||
float connection_status_window_height_ = 150;
|
||||
float notification_window_width_ = 200;
|
||||
float notification_window_height_ = 80;
|
||||
float about_window_width_ = 300;
|
||||
float about_window_height_ = 170;
|
||||
float update_notification_window_width_ = 400;
|
||||
float update_notification_window_height_ = 320;
|
||||
int screen_width_ = 1280;
|
||||
int screen_height_ = 720;
|
||||
int selected_display_ = 0;
|
||||
std::string connect_button_label_ = "Connect";
|
||||
char input_password_tmp_[7] = "";
|
||||
char input_password_[7] = "";
|
||||
std::string random_password_ = "";
|
||||
char new_password_[7] = "";
|
||||
char remote_id_display_[12] = "";
|
||||
unsigned char audio_buffer_[720];
|
||||
int audio_len_ = 0;
|
||||
bool audio_buffer_fresh_ = false;
|
||||
bool need_to_rejoin_ = false;
|
||||
std::chrono::steady_clock::time_point last_rejoin_check_time_;
|
||||
bool just_created_ = false;
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
std::string remote_client_id_ = "";
|
||||
std::unordered_set<int> pressed_keyboard_keys_;
|
||||
std::mutex pressed_keyboard_keys_mutex_;
|
||||
SDL_Event last_mouse_event{};
|
||||
SDL_AudioStream* output_stream_ = nullptr;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
#if _WIN32
|
||||
std::atomic<bool> pending_windows_service_sas_{false};
|
||||
bool local_service_status_received_ = false;
|
||||
bool local_service_available_ = false;
|
||||
std::string local_interactive_stage_;
|
||||
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
||||
uint32_t last_windows_service_status_tick_ = 0;
|
||||
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
#if CROSSDESK_PORTABLE
|
||||
bool portable_service_prompt_checked_ = false;
|
||||
bool show_portable_service_install_window_ = false;
|
||||
bool show_portable_service_prompt_suppressed_window_ = false;
|
||||
bool portable_service_do_not_remind_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// stream window render
|
||||
SDL_Window* stream_window_ = nullptr;
|
||||
SDL_Renderer* stream_renderer_ = nullptr;
|
||||
ImGuiContext* stream_ctx_ = nullptr;
|
||||
|
||||
// stream window properties
|
||||
bool need_to_create_stream_window_ = false;
|
||||
bool stream_window_created_ = false;
|
||||
bool stream_window_inited_ = false;
|
||||
bool window_maximized_ = false;
|
||||
bool stream_window_grabbed_ = false;
|
||||
bool control_mouse_ = false;
|
||||
int stream_window_width_default_ = 1280;
|
||||
int stream_window_height_default_ = 720;
|
||||
float stream_window_width_ = 1280;
|
||||
float stream_window_height_ = 720;
|
||||
SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int stream_window_width_real_ = 1280;
|
||||
int stream_window_height_real_ = 720;
|
||||
float stream_window_dpi_scaling_w_ = 1.0f;
|
||||
float stream_window_dpi_scaling_h_ = 1.0f;
|
||||
|
||||
// server window render
|
||||
SDL_Window* server_window_ = nullptr;
|
||||
SDL_Renderer* server_renderer_ = nullptr;
|
||||
ImGuiContext* server_ctx_ = nullptr;
|
||||
|
||||
// server window properties
|
||||
bool need_to_create_server_window_ = false;
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
float window_rounding_ = 6.0f;
|
||||
float window_rounding_default_ = 6.0f;
|
||||
|
||||
// server window collapsed mode
|
||||
bool server_window_collapsed_ = false;
|
||||
bool server_window_collapsed_dragging_ = false;
|
||||
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_collapsed_drag_start_win_x_ = 0;
|
||||
int server_window_collapsed_drag_start_win_y_ = 0;
|
||||
|
||||
// server window drag normal mode
|
||||
bool server_window_dragging_ = false;
|
||||
float server_window_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_drag_start_win_x_ = 0;
|
||||
int server_window_drag_start_win_y_ = 0;
|
||||
|
||||
bool label_inited_ = false;
|
||||
bool connect_button_pressed_ = false;
|
||||
bool password_validating_ = false;
|
||||
uint32_t password_validating_time_ = 0;
|
||||
bool show_settings_window_ = false;
|
||||
bool show_self_hosted_server_config_window_ = false;
|
||||
bool rejoin_ = false;
|
||||
bool local_id_copied_ = false;
|
||||
bool show_password_ = true;
|
||||
bool show_about_window_ = false;
|
||||
bool show_connection_status_window_ = false;
|
||||
bool show_reset_password_window_ = false;
|
||||
bool show_update_notification_window_ = false;
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool focus_on_input_widget_ = true;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool show_offline_warning_window_ = false;
|
||||
bool delete_connection_ = false;
|
||||
bool is_tab_bar_hovered_ = false;
|
||||
std::string delete_connection_name_ = "";
|
||||
std::string offline_warning_text_ = "";
|
||||
bool re_enter_remote_id_ = false;
|
||||
double copy_start_time_ = 0;
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
std::string signal_status_str_ = "";
|
||||
bool signal_connected_ = false;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
PeerPtr* peer_reserved_ = nullptr;
|
||||
std::string video_primary_label_ = "primary_display";
|
||||
std::string video_secondary_label_ = "secondary_display";
|
||||
std::string audio_label_ = "audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
std::string keyboard_label_ = "keyboard";
|
||||
std::string info_label_ = "info";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_label_ = "file";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
Params params_;
|
||||
// Map file_id to props for tracking file transfer progress via ACK
|
||||
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
|
||||
file_id_to_props_;
|
||||
std::shared_mutex file_id_to_props_mutex_;
|
||||
|
||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||
SDL_AudioDeviceID input_dev_ = 0;
|
||||
SDL_AudioDeviceID output_dev_ = 0;
|
||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||
ScreenCapturer* screen_capturer_ = nullptr;
|
||||
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
|
||||
SpeakerCapturer* speaker_capturer_ = nullptr;
|
||||
DeviceControllerFactory* device_controller_factory_ = nullptr;
|
||||
MouseController* mouse_controller_ = nullptr;
|
||||
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
#ifdef __APPLE__
|
||||
bool show_request_permission_window_ = true;
|
||||
bool mac_permission_status_initialized_ = false;
|
||||
uint32_t mac_permission_last_check_tick_ = 0;
|
||||
bool mac_screen_recording_permission_granted_ = false;
|
||||
bool mac_accessibility_permission_granted_ = false;
|
||||
bool mac_screen_recording_permission_requested_ = false;
|
||||
bool mac_accessibility_permission_requested_ = false;
|
||||
#endif
|
||||
char client_id_[10] = "";
|
||||
char client_id_display_[12] = "";
|
||||
char client_id_with_password_[17] = "";
|
||||
char password_saved_[7] = "";
|
||||
char self_hosted_id_[17] = "";
|
||||
char self_hosted_user_id_[17] = "";
|
||||
int language_button_value_ = 0;
|
||||
int video_quality_button_value_ = 2;
|
||||
int video_frame_rate_button_value_ = 1;
|
||||
int video_encode_format_button_value_ = 0;
|
||||
bool enable_hardware_video_codec_ = true;
|
||||
bool enable_turn_ = true;
|
||||
bool enable_srtp_ = false;
|
||||
char signal_server_ip_[256] = "api.crossdesk.cn";
|
||||
char signal_server_port_[6] = "9099";
|
||||
char coturn_server_port_[6] = "3478";
|
||||
bool enable_self_hosted_ = false;
|
||||
int language_button_value_last_ = 0;
|
||||
int video_quality_button_value_last_ = 0;
|
||||
int video_frame_rate_button_value_last_ = 0;
|
||||
int video_encode_format_button_value_last_ = 0;
|
||||
bool enable_hardware_video_codec_last_ = false;
|
||||
bool enable_turn_last_ = true;
|
||||
bool enable_srtp_last_ = false;
|
||||
bool enable_self_hosted_last_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_autostart_last_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool enable_daemon_last_ = false;
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_minimize_to_tray_last_ = false;
|
||||
char file_transfer_save_path_buf_[512] = "";
|
||||
std::string file_transfer_save_path_last_ = "";
|
||||
char signal_server_ip_self_[256] = "";
|
||||
char signal_server_port_self_[6] = "";
|
||||
char coturn_server_port_self_[6] = "";
|
||||
bool settings_window_pos_reset_ = true;
|
||||
bool self_hosted_server_config_window_pos_reset_ = true;
|
||||
std::string selected_current_file_path_ = "";
|
||||
bool show_file_browser_ = true;
|
||||
/* ------ main window property end ------ */
|
||||
|
||||
/* ------ sub stream window property start ------ */
|
||||
std::unordered_map<std::string, std::shared_ptr<SubStreamWindowProperties>>
|
||||
client_properties_;
|
||||
std::shared_mutex client_properties_mutex_;
|
||||
void CloseTab(decltype(client_properties_)::iterator& it);
|
||||
/* ------ stream window property end ------ */
|
||||
|
||||
/* ------ async thumbnail save tasks ------ */
|
||||
std::vector<std::thread> thumbnail_save_threads_;
|
||||
std::mutex thumbnail_save_threads_mutex_;
|
||||
void WaitForThumbnailSaveTasks();
|
||||
|
||||
/* ------ server mode ------ */
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::string selected_server_remote_id_ = "";
|
||||
std::string selected_server_remote_hostname_ = "";
|
||||
std::mutex pending_presence_probe_mutex_;
|
||||
bool pending_presence_probe_ = false;
|
||||
bool pending_presence_result_ready_ = false;
|
||||
bool pending_presence_online_ = false;
|
||||
std::string pending_presence_remote_id_ = "";
|
||||
std::string pending_presence_password_ = "";
|
||||
bool pending_presence_remember_password_ = false;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDER_H_
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,439 @@
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
|
||||
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20);
|
||||
bool IsConnectionAttemptPending(ConnectionStatus status) {
|
||||
return status == ConnectionStatus::Connecting ||
|
||||
status == ConnectionStatus::Gathering;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void GuiRuntime::HandleConnectionStatusChange() {
|
||||
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
|
||||
if (!recent_connection_ids_.empty()) {
|
||||
nlohmann::json j;
|
||||
j["type"] = "recent_connections_presence";
|
||||
j["user_id"] = client_id_;
|
||||
j["devices"] = nlohmann::json::array();
|
||||
for (const auto &id : recent_connection_ids_) {
|
||||
std::string pure_id = id;
|
||||
size_t pos_y = pure_id.find('Y');
|
||||
size_t pos_n = pure_id.find('N');
|
||||
size_t pos = std::string::npos;
|
||||
if (pos_y != std::string::npos &&
|
||||
(pos_n == std::string::npos || pos_y < pos_n)) {
|
||||
pos = pos_y;
|
||||
} else if (pos_n != std::string::npos) {
|
||||
pos = pos_n;
|
||||
}
|
||||
if (pos != std::string::npos) {
|
||||
pure_id = pure_id.substr(0, pos);
|
||||
}
|
||||
j["devices"].push_back(pure_id);
|
||||
}
|
||||
auto s = j.dump();
|
||||
SendSignalMessage(peer_, s.data(), s.size());
|
||||
}
|
||||
}
|
||||
need_to_send_recent_connections_ = false;
|
||||
}
|
||||
|
||||
void GuiRuntime::HandlePendingPresenceProbe() {
|
||||
bool has_action = false;
|
||||
bool should_connect = false;
|
||||
bool remember_password = false;
|
||||
std::string remote_id;
|
||||
std::string password;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
|
||||
if (!pending_presence_probe_ || !pending_presence_result_ready_) {
|
||||
return;
|
||||
}
|
||||
|
||||
has_action = true;
|
||||
should_connect = pending_presence_online_;
|
||||
remote_id = pending_presence_remote_id_;
|
||||
password = pending_presence_password_;
|
||||
remember_password = pending_presence_remember_password_;
|
||||
|
||||
pending_presence_probe_ = false;
|
||||
pending_presence_result_ready_ = false;
|
||||
pending_presence_online_ = false;
|
||||
pending_presence_remote_id_.clear();
|
||||
pending_presence_password_.clear();
|
||||
pending_presence_remember_password_ = false;
|
||||
}
|
||||
|
||||
if (!has_action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (should_connect) {
|
||||
ConnectTo(remote_id, password.c_str(), remember_password, true);
|
||||
return;
|
||||
}
|
||||
|
||||
offline_warning_text_ =
|
||||
localization::device_offline[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleConnectionTimeouts() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
bool presence_probe_timed_out = false;
|
||||
std::string presence_remote_id;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
|
||||
if (pending_presence_probe_ && !pending_presence_result_ready_ &&
|
||||
now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) {
|
||||
presence_probe_timed_out = true;
|
||||
presence_remote_id = pending_presence_remote_id_;
|
||||
pending_presence_probe_ = false;
|
||||
pending_presence_result_ready_ = false;
|
||||
pending_presence_online_ = false;
|
||||
pending_presence_remote_id_.clear();
|
||||
pending_presence_password_.clear();
|
||||
pending_presence_remember_password_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (presence_probe_timed_out) {
|
||||
offline_warning_text_ =
|
||||
localization::device_offline[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
|
||||
}
|
||||
|
||||
bool rejoin_state_changed = false;
|
||||
for (auto &[_, props] : remote_sessions_) {
|
||||
if (!props || !props->connection_attempt_active_.load()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ConnectionStatus status = props->connection_status_.load();
|
||||
if (!IsConnectionAttemptPending(status)) {
|
||||
props->connection_attempt_active_.store(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now - props->connection_attempt_started_at_ <
|
||||
kConnectionAttemptTimeout) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
|
||||
static_cast<int>(status));
|
||||
props->connection_attempt_active_.store(false);
|
||||
props->connection_established_ = false;
|
||||
props->rejoin_ = false;
|
||||
props->connection_status_.store(ConnectionStatus::Failed);
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
show_connection_status_window_ = true;
|
||||
rejoin_state_changed = true;
|
||||
}
|
||||
|
||||
if (rejoin_state_changed) {
|
||||
need_to_rejoin_ = false;
|
||||
for (const auto &[_, props] : remote_sessions_) {
|
||||
if (props && props->rejoin_) {
|
||||
need_to_rejoin_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id,
|
||||
const char *password,
|
||||
bool remember_password) {
|
||||
if (!signal_connected_ || !peer_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
|
||||
pending_presence_probe_ = true;
|
||||
pending_presence_result_ready_ = false;
|
||||
pending_presence_online_ = false;
|
||||
pending_presence_probe_started_at_ = std::chrono::steady_clock::now();
|
||||
pending_presence_remote_id_ = remote_id;
|
||||
pending_presence_password_ = password ? password : "";
|
||||
pending_presence_remember_password_ = remember_password;
|
||||
}
|
||||
|
||||
nlohmann::json j;
|
||||
j["type"] = "recent_connections_presence";
|
||||
j["user_id"] = client_id_;
|
||||
j["devices"] = nlohmann::json::array({remote_id});
|
||||
auto s = j.dump();
|
||||
|
||||
int ret = SendSignalMessage(peer_, s.data(), s.size());
|
||||
if (ret != 0) {
|
||||
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
|
||||
pending_presence_probe_ = false;
|
||||
pending_presence_result_ready_ = false;
|
||||
pending_presence_online_ = false;
|
||||
pending_presence_remote_id_.clear();
|
||||
pending_presence_password_.clear();
|
||||
pending_presence_remember_password_ = false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
SDL_FlushEvent(STREAM_REFRESH_EVENT);
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
std::vector<unsigned char> buffer_copy(*frame_snapshot);
|
||||
std::string remote_id = props->remote_id_;
|
||||
std::string remote_host_name = props->remote_host_name_;
|
||||
std::string password =
|
||||
props->remember_password_ ? props->remote_password_ : "";
|
||||
|
||||
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
||||
remote_host_name, password,
|
||||
thumbnail = thumbnail_]() {
|
||||
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width,
|
||||
video_height, remote_id, remote_host_name,
|
||||
password);
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
|
||||
thumbnail_save_threads_.emplace_back(std::move(save_thread));
|
||||
}
|
||||
}
|
||||
|
||||
if (props->peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
LOG_INFO("Destroy peer [{}]", props->local_id_);
|
||||
DestroyPeer(&props->peer_);
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseAllRemoteSessions() {
|
||||
if (peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
|
||||
LeaveConnection(peer_, client_id_);
|
||||
is_client_mode_ = false;
|
||||
devices_.StopMouseController();
|
||||
devices_.StopScreenCapturer();
|
||||
devices_.StopSpeakerCapturer();
|
||||
devices_.StopKeyboardCapturer();
|
||||
LOG_INFO("Destroy peer [{}]", client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
|
||||
{
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
CloseRemoteSession(props);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
remote_sessions_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::WaitForThumbnailSaveTasks() {
|
||||
std::vector<std::thread> threads_to_join;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
|
||||
threads_to_join.swap(thumbnail_save_threads_);
|
||||
}
|
||||
|
||||
if (threads_to_join.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto &thread : threads_to_join) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
if (props->stream_texture_) {
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<GuiRuntime::RemoteSession>
|
||||
GuiRuntime::FindRemoteSession(
|
||||
const std::string &remote_id) {
|
||||
if (remote_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
auto it = remote_sessions_.find(remote_id);
|
||||
if (it == remote_sessions_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
|
||||
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
if (ret != 0) {
|
||||
offline_warning_text_ =
|
||||
localization::device_offline[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
|
||||
} else {
|
||||
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
||||
bool exists =
|
||||
(remote_sessions_.find(remote_id) != remote_sessions_.end());
|
||||
// shared_lock.unlock();
|
||||
|
||||
if (!exists) {
|
||||
PeerPtr *peer_to_init = nullptr;
|
||||
std::string local_id;
|
||||
|
||||
{
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
||||
remote_sessions_[remote_id] =
|
||||
std::make_shared<RemoteSession>();
|
||||
auto props = remote_sessions_[remote_id];
|
||||
props->local_id_ = "C-" + std::string(client_id_);
|
||||
props->remote_id_ = remote_id;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 7.0f;
|
||||
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
if (!props->peer_) {
|
||||
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (const auto &display_info : devices_.display_info_list()) {
|
||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
|
||||
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
|
||||
peer_to_init = props->peer_;
|
||||
local_id = props->local_id_;
|
||||
}
|
||||
}
|
||||
|
||||
if (peer_to_init) {
|
||||
LOG_INFO("[{}] Create peer instance successful", local_id);
|
||||
Init(peer_to_init);
|
||||
LOG_INFO("[{}] Peer init finish", local_id);
|
||||
}
|
||||
}
|
||||
|
||||
int ret = -1;
|
||||
// std::shared_lock read_lock(remote_sessions_mutex_);
|
||||
auto props = remote_sessions_[remote_id];
|
||||
if (!props->connection_established_) {
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
props->connection_attempt_active_.store(true);
|
||||
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
props->remember_password_ = remember_password;
|
||||
if (strcmp(password, "") != 0 &&
|
||||
strcmp(password, props->remote_password_) != 0) {
|
||||
strncpy(props->remote_password_, password,
|
||||
sizeof(props->remote_password_) - 1);
|
||||
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
|
||||
}
|
||||
|
||||
std::string remote_id_with_pwd = remote_id + "@" + password;
|
||||
if (props->peer_) {
|
||||
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
|
||||
if (0 == ret) {
|
||||
props->rejoin_ = false;
|
||||
} else {
|
||||
props->rejoin_ = true;
|
||||
need_to_rejoin_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// read_lock.unlock();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -4,14 +4,16 @@
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_PRESENCE_H_
|
||||
#define _DEVICE_PRESENCE_H_
|
||||
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class DevicePresence {
|
||||
namespace crossdesk {
|
||||
|
||||
class DevicePresenceCache {
|
||||
public:
|
||||
void SetOnline(const std::string &device_id, bool online) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
@@ -20,7 +22,8 @@ class DevicePresence {
|
||||
|
||||
bool IsOnline(const std::string &device_id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return cache_.count(device_id) > 0 && cache_.at(device_id);
|
||||
const auto it = cache_.find(device_id);
|
||||
return it != cache_.end() && it->second;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
@@ -33,4 +36,6 @@ class DevicePresence {
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
#endif
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
@@ -0,0 +1,246 @@
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
GuiRuntime::GuiRuntime()
|
||||
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
|
||||
keyboard_(*this), peer_events_(*this) {}
|
||||
|
||||
GuiRuntime::~GuiRuntime() = default;
|
||||
|
||||
int GuiRuntime::CreateConnectionPeer() {
|
||||
params_.use_cfg_file = false;
|
||||
|
||||
std::string signal_server_ip;
|
||||
int signal_server_port;
|
||||
int coturn_server_port;
|
||||
|
||||
if (config_center_->IsSelfHosted()) {
|
||||
signal_server_ip = config_center_->GetSignalServerHost();
|
||||
signal_server_port = config_center_->GetSignalServerPort();
|
||||
coturn_server_port = config_center_->GetCoturnServerPort();
|
||||
|
||||
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
|
||||
const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity();
|
||||
if (!use_cached_id) {
|
||||
LOG_INFO(
|
||||
"secure_cache_v2.enc not found, will use empty id to get new id from "
|
||||
"server");
|
||||
}
|
||||
|
||||
if (use_cached_id && strlen(self_hosted_id_) > 0) {
|
||||
memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_));
|
||||
strncpy(self_hosted_user_id_, self_hosted_id_,
|
||||
sizeof(self_hosted_user_id_) - 1);
|
||||
self_hosted_user_id_[sizeof(self_hosted_user_id_) - 1] = '\0';
|
||||
params_.user_id = self_hosted_user_id_;
|
||||
} else {
|
||||
memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_));
|
||||
params_.user_id = self_hosted_user_id_;
|
||||
LOG_INFO(
|
||||
"Using empty id for self-hosted server, server will assign new id");
|
||||
}
|
||||
} else {
|
||||
signal_server_ip = config_center_->GetDefaultServerHost();
|
||||
signal_server_port = config_center_->GetDefaultSignalServerPort();
|
||||
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
|
||||
params_.user_id = client_id_with_password_;
|
||||
}
|
||||
|
||||
// self hosted server config
|
||||
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
||||
sizeof(signal_server_ip_self_) - 1);
|
||||
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
|
||||
int signal_port = config_center_->GetSignalServerPort();
|
||||
if (signal_port > 0) {
|
||||
strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(),
|
||||
sizeof(signal_server_port_self_) - 1);
|
||||
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
|
||||
} else {
|
||||
signal_server_port_self_[0] = '\0';
|
||||
}
|
||||
int coturn_port = config_center_->GetCoturnServerPort();
|
||||
if (coturn_port > 0) {
|
||||
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
|
||||
sizeof(coturn_server_port_self_) - 1);
|
||||
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
|
||||
} else {
|
||||
coturn_server_port_self_[0] = '\0';
|
||||
}
|
||||
|
||||
// peer config
|
||||
strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(),
|
||||
sizeof(params_.signal_server_ip) - 1);
|
||||
params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0';
|
||||
params_.signal_server_port = signal_server_port;
|
||||
strncpy((char *)params_.stun_server_ip, signal_server_ip.c_str(),
|
||||
sizeof(params_.stun_server_ip) - 1);
|
||||
params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0';
|
||||
params_.stun_server_port = coturn_server_port;
|
||||
strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(),
|
||||
sizeof(params_.turn_server_ip) - 1);
|
||||
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
|
||||
params_.turn_server_port = coturn_server_port;
|
||||
strncpy((char *)params_.turn_server_username, "crossdesk",
|
||||
sizeof(params_.turn_server_username) - 1);
|
||||
params_.turn_server_username[sizeof(params_.turn_server_username) - 1] = '\0';
|
||||
strncpy((char *)params_.turn_server_password, "crossdeskpw",
|
||||
sizeof(params_.turn_server_password) - 1);
|
||||
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
|
||||
|
||||
strncpy(params_.log_path, dll_log_path_.c_str(),
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
||||
? true
|
||||
: false;
|
||||
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
|
||||
params_.enable_srtp = config_center_->IsEnableSrtp();
|
||||
params_.video_quality =
|
||||
static_cast<VideoQuality>(config_center_->GetVideoQuality());
|
||||
params_.on_receive_video_buffer = nullptr;
|
||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||
|
||||
params_.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer;
|
||||
|
||||
params_.on_signal_status = PeerEventHandler::OnSignalStatus;
|
||||
params_.on_signal_message = PeerEventHandler::OnSignalMessage;
|
||||
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
|
||||
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
|
||||
|
||||
params_.user_data = &peer_events_;
|
||||
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
if (peer_) {
|
||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||
Init(peer_);
|
||||
LOG_INFO("Peer [{}] init finish", client_id_);
|
||||
} else {
|
||||
LOG_INFO("Create peer [{}] instance failed", client_id_);
|
||||
}
|
||||
|
||||
if (0 == devices_.InitializeScreenCapturer()) {
|
||||
for (const auto &display_info : devices_.display_info_list()) {
|
||||
AddVideoStream(peer_, display_info.name.c_str());
|
||||
}
|
||||
|
||||
AddAudioStream(peer_, audio_label_.c_str());
|
||||
AddDataStream(peer_, data_label_.c_str(), false);
|
||||
AddDataStream(peer_, mouse_label_.c_str(), false);
|
||||
AddDataStream(peer_, keyboard_label_.c_str(), true);
|
||||
AddDataStream(peer_, control_data_label_.c_str(), true);
|
||||
AddDataStream(peer_, file_label_.c_str(), true);
|
||||
AddDataStream(peer_, file_feedback_label_.c_str(), true);
|
||||
AddDataStream(peer_, clipboard_label_.c_str(), true);
|
||||
return 0;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::UpdateLabels() {
|
||||
if (!label_inited_ ||
|
||||
localization_language_index_last_ != localization_language_index_) {
|
||||
connect_button_label_ =
|
||||
connect_button_pressed_
|
||||
? localization::disconnect[localization_language_index_]
|
||||
: localization::connect[localization_language_index_];
|
||||
label_inited_ = true;
|
||||
localization_language_index_last_ = localization_language_index_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void GuiRuntime::HandleRecentConnections() {
|
||||
if (reload_recent_connections_ && main_renderer_) {
|
||||
uint32_t now_time = SDL_GetTicks();
|
||||
if (now_time - recent_connection_image_save_time_ >= 50) {
|
||||
int ret = thumbnail_->LoadThumbnail(main_renderer_, recent_connections_,
|
||||
&recent_connection_image_width_,
|
||||
&recent_connection_image_height_);
|
||||
if (!ret) {
|
||||
LOG_INFO("Load recent connection thumbnails");
|
||||
}
|
||||
reload_recent_connections_ = false;
|
||||
|
||||
recent_connection_ids_.clear();
|
||||
for (const auto &conn : recent_connections_) {
|
||||
recent_connection_ids_.push_back(conn.first);
|
||||
}
|
||||
need_to_send_recent_connections_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
GuiRuntime *runtime = static_cast<GuiRuntime *>(userdata);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (1) {
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
for (const auto &it : runtime->remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
if (props->peer_) {
|
||||
SendAudioFrame(props->peer_, (const char *)stream, len,
|
||||
runtime->audio_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
memcpy(runtime->audio_buffer_, stream, len);
|
||||
runtime->audio_len_ = len;
|
||||
SDL_Delay(10);
|
||||
runtime->audio_buffer_fresh_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
|
||||
[[maybe_unused]] Uint8 *stream,
|
||||
[[maybe_unused]] int len) {
|
||||
// GuiApplication *runtime = (GuiApplication *)userdata;
|
||||
// for (auto it : runtime->remote_sessions_) {
|
||||
// auto props = it.second;
|
||||
// if (props->connection_status_ == SignalStatus::SignalConnected) {
|
||||
// SendAudioFrame(props->peer_, (const char *)stream, len);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!runtime->audio_buffer_fresh_) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// SDL_memset(stream, 0, len);
|
||||
|
||||
// if (runtime->audio_len_ == 0) {
|
||||
// return;
|
||||
// } else {
|
||||
// }
|
||||
|
||||
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
|
||||
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
|
||||
// SDL_MIX_MAXVOLUME);
|
||||
// runtime->audio_buffer_fresh_ = false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,96 @@
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "features/clipboard/clipboard_controller.h"
|
||||
#include "features/devices/session_device_manager.h"
|
||||
#include "features/file_transfer/file_transfer_manager.h"
|
||||
#include "features/input/keyboard_controller.h"
|
||||
#include "runtime/gui_state.h"
|
||||
#include "runtime/peer_event_handler.h"
|
||||
#include "features/settings/settings_manager.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||
class GuiRuntime : protected gui_detail::GuiState {
|
||||
protected:
|
||||
using FileTransferState = gui_detail::FileTransferState;
|
||||
using RemoteSession = gui_detail::RemoteSession;
|
||||
|
||||
enum class RemoteUnlockState {
|
||||
none,
|
||||
service_unavailable,
|
||||
lock_screen,
|
||||
credential_ui,
|
||||
secure_desktop,
|
||||
};
|
||||
|
||||
GuiRuntime();
|
||||
~GuiRuntime();
|
||||
|
||||
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len);
|
||||
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len);
|
||||
|
||||
int CreateConnectionPeer();
|
||||
int ConnectTo(const std::string &remote_id, const char *password,
|
||||
bool remember_password, bool bypass_presence_check = false);
|
||||
int RequestSingleDevicePresence(const std::string &remote_id,
|
||||
const char *password, bool remember_password);
|
||||
|
||||
void UpdateLabels();
|
||||
void HandleRecentConnections();
|
||||
void HandleConnectionStatusChange();
|
||||
void HandlePendingPresenceProbe();
|
||||
void HandleConnectionTimeouts();
|
||||
void HandleWindowsServiceIntegration();
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||
void CloseAllRemoteSessions();
|
||||
void ResetRemoteSessionResources(
|
||||
std::shared_ptr<RemoteSession> props);
|
||||
void WaitForThumbnailSaveTasks();
|
||||
std::shared_ptr<RemoteSession>
|
||||
FindRemoteSession(const std::string &remote_id);
|
||||
|
||||
void ResetRemoteServiceStatus(RemoteSession &props);
|
||||
void ApplyRemoteServiceStatus(RemoteSession &props,
|
||||
const ServiceStatus &status);
|
||||
RemoteUnlockState
|
||||
GetRemoteUnlockState(const RemoteSession &props) const;
|
||||
#if _WIN32
|
||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
bool CheckScreenRecordingPermission();
|
||||
bool CheckAccessibilityPermission();
|
||||
void OpenScreenRecordingPreferences();
|
||||
void OpenAccessibilityPreferences();
|
||||
void RefreshMacPermissionStatus(bool force);
|
||||
bool EnsureMacScreenRecordingPermission();
|
||||
bool EnsureMacAccessibilityPermission();
|
||||
#endif
|
||||
|
||||
ClipboardController clipboard_;
|
||||
SessionDeviceManager devices_;
|
||||
FileTransferManager transfers_;
|
||||
SettingsManager settings_;
|
||||
KeyboardController keyboard_;
|
||||
PeerEventHandler peer_events_;
|
||||
|
||||
private:
|
||||
friend class ClipboardController;
|
||||
friend class SessionDeviceManager;
|
||||
friend class FileTransferManager;
|
||||
friend class SettingsManager;
|
||||
friend class KeyboardController;
|
||||
friend class PeerEventHandler;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Aggregate state visible to GuiRuntime and GuiApplication.
|
||||
*
|
||||
* Definitions live beside their owners: SDL/window state in application and
|
||||
* connection/session state in runtime. This header is intentionally only the
|
||||
* composition point.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_GUI_STATE_H_
|
||||
#define CROSSDESK_GUI_STATE_H_
|
||||
|
||||
#include "application/application_state.h"
|
||||
#include "runtime/runtime_state.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct GuiState : ApplicationState, RuntimeState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_STATE_H_
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500;
|
||||
|
||||
void OpenPrivacyPreferences(const char *pane) {
|
||||
if (pane == nullptr || pane[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string command =
|
||||
"open \"x-apple.systempreferences:com.apple.preference.security?";
|
||||
command += pane;
|
||||
command += "\"";
|
||||
system(command.c_str());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool GuiRuntime::CheckScreenRecordingPermission() {
|
||||
// CGPreflightScreenCaptureAccess is available on macOS 10.15+
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool granted = CGPreflightScreenCaptureAccess();
|
||||
return granted;
|
||||
}
|
||||
// for older macOS versions, assume permission is granted
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GuiRuntime::CheckAccessibilityPermission() {
|
||||
NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO};
|
||||
bool trusted =
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
return trusted;
|
||||
}
|
||||
|
||||
void GuiRuntime::OpenAccessibilityPreferences() {
|
||||
if (!mac_accessibility_permission_requested_) {
|
||||
NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_Accessibility");
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::OpenScreenRecordingPreferences() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
if (!mac_screen_recording_permission_requested_) {
|
||||
CGRequestScreenCaptureAccess();
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||
}
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::RefreshMacPermissionStatus(bool force) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (!force && mac_permission_status_initialized_ &&
|
||||
now - mac_permission_last_check_tick_ <
|
||||
kPermissionRefreshIntervalVisibleMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool old_screen_recording_granted =
|
||||
mac_screen_recording_permission_granted_;
|
||||
const bool old_accessibility_granted = mac_accessibility_permission_granted_;
|
||||
|
||||
mac_screen_recording_permission_granted_ = CheckScreenRecordingPermission();
|
||||
mac_accessibility_permission_granted_ = CheckAccessibilityPermission();
|
||||
mac_permission_last_check_tick_ = now;
|
||||
mac_permission_status_initialized_ = true;
|
||||
|
||||
if (old_screen_recording_granted !=
|
||||
mac_screen_recording_permission_granted_ ||
|
||||
old_accessibility_granted != mac_accessibility_permission_granted_) {
|
||||
LOG_INFO("macOS permission status: screen_recording={}, accessibility={}",
|
||||
mac_screen_recording_permission_granted_,
|
||||
mac_accessibility_permission_granted_);
|
||||
}
|
||||
}
|
||||
|
||||
bool GuiRuntime::EnsureMacScreenRecordingPermission() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
if (mac_screen_recording_permission_granted_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_request_permission_window_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GuiRuntime::EnsureMacAccessibilityPermission() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
if (mac_accessibility_permission_granted_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_request_permission_window_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,267 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
bool BuildAbsoluteMousePosition(const std::vector<DisplayInfo> &displays,
|
||||
int display_index, float normalized_x,
|
||||
float normalized_y, int *absolute_x_out,
|
||||
int *absolute_y_out) {
|
||||
if (absolute_x_out == nullptr || absolute_y_out == nullptr ||
|
||||
display_index < 0 || display_index >= static_cast<int>(displays.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const DisplayInfo &display = displays[display_index];
|
||||
if (display.width <= 0 || display.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f);
|
||||
const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f);
|
||||
*absolute_x_out = static_cast<int>(clamped_x * display.width) + display.left;
|
||||
*absolute_y_out = static_cast<int>(clamped_y * display.height) + display.top;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
|
||||
const char *stage) {
|
||||
if (last_tick == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
*last_tick = now;
|
||||
LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path "
|
||||
"cannot drive the Windows password UI",
|
||||
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void PeerEventHandler::OnReceiveDataBuffer(
|
||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string source_id = std::string(src_id, src_id_size);
|
||||
if (source_id == runtime->file_label_) {
|
||||
std::string remote_user_id = std::string(user_id, user_id_size);
|
||||
|
||||
static FileReceiver receiver;
|
||||
// Update output directory from config
|
||||
std::string configured_path =
|
||||
runtime->config_center_->GetFileTransferSavePath();
|
||||
if (!configured_path.empty()) {
|
||||
receiver.SetOutputDir(std::filesystem::u8path(configured_path));
|
||||
} else if (receiver.OutputDir().empty()) {
|
||||
receiver = FileReceiver(); // re-init with default desktop path
|
||||
}
|
||||
receiver.SetOnSendAck([runtime,
|
||||
remote_user_id](const FileTransferAck &ack) -> int {
|
||||
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
||||
if (is_server_sending) {
|
||||
auto props =
|
||||
runtime->FindRemoteSession(remote_user_id);
|
||||
if (props) {
|
||||
PeerPtr *peer = props->peer_;
|
||||
return SendReliableDataFrame(
|
||||
peer, reinterpret_cast<const char *>(&ack),
|
||||
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return SendReliableDataFrame(
|
||||
runtime->peer_, reinterpret_cast<const char *>(&ack),
|
||||
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
|
||||
});
|
||||
|
||||
receiver.OnData(data, size);
|
||||
return;
|
||||
} else if (source_id == runtime->clipboard_label_) {
|
||||
if (size > 0) {
|
||||
std::string remote_user_id(user_id, user_id_size);
|
||||
auto props =
|
||||
runtime->FindRemoteSession(remote_user_id);
|
||||
if (props && !props->enable_mouse_control_) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime->clipboard_.QueueRemoteText(data, size);
|
||||
}
|
||||
return;
|
||||
} else if (source_id == runtime->file_feedback_label_) {
|
||||
runtime->transfers_.HandleAck(data, size);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string json_str(data, size);
|
||||
RemoteAction remote_action{};
|
||||
if (!remote_action.from_json(json_str)) {
|
||||
LOG_ERROR("Failed to parse RemoteAction JSON payload");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::service_status) {
|
||||
auto props_it = runtime->remote_sessions_.find(remote_id);
|
||||
if (props_it != runtime->remote_sessions_.end()) {
|
||||
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::service_command) {
|
||||
#if _WIN32
|
||||
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
||||
runtime->pending_windows_service_sas_.store(true,
|
||||
std::memory_order_relaxed);
|
||||
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
|
||||
if (!LockWorkStation()) {
|
||||
LOG_WARN("Remote lock workstation request failed, error={}",
|
||||
GetLastError());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::host_infomation) {
|
||||
bool is_client_mode = false;
|
||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||
{
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
auto props_it = runtime->remote_sessions_.find(remote_id);
|
||||
if (props_it != runtime->remote_sessions_.end()) {
|
||||
is_client_mode = true;
|
||||
props = props_it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_client_mode) {
|
||||
// client mode
|
||||
if (props && props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->display_info_list_.push_back(
|
||||
DisplayInfo(remote_action.i.display_list[i],
|
||||
remote_action.i.left[i], remote_action.i.top[i],
|
||||
remote_action.i.right[i], remote_action.i.bottom[i]));
|
||||
}
|
||||
}
|
||||
remote_action_codec::Free(remote_action);
|
||||
} else {
|
||||
// server mode
|
||||
std::string host_name(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
{
|
||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||
runtime->connection_host_names_[remote_id] = host_name;
|
||||
}
|
||||
LOG_INFO("Remote hostname: [{}]", host_name);
|
||||
remote_action_codec::Free(remote_action);
|
||||
}
|
||||
} else {
|
||||
// remote
|
||||
#if _WIN32
|
||||
if (runtime->local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
|
||||
remote_action.type != ControlType::keyboard &&
|
||||
remote_action.type != ControlType::keyboard_state) {
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
int absolute_x = 0;
|
||||
int absolute_y = 0;
|
||||
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
|
||||
runtime->selected_display_,
|
||||
remote_action.m.x, remote_action.m.y,
|
||||
&absolute_x, &absolute_y)) {
|
||||
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
|
||||
"mapping: display_index={}, x={}, y={}",
|
||||
runtime->selected_display_, remote_action.m.x,
|
||||
remote_action.m.y);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string response = SendCrossDeskSecureDesktopMouseInput(
|
||||
absolute_x, absolute_y, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
LogSecureDesktopInputBlocked(
|
||||
&runtime->last_local_secure_input_block_log_tick_, "local",
|
||||
runtime->local_interactive_stage_.c_str());
|
||||
LOG_WARN(
|
||||
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
|
||||
"flag={}, response={}",
|
||||
absolute_x, absolute_y, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
runtime->devices_.SendMouseCommand(remote_action,
|
||||
runtime->selected_display_);
|
||||
} else if (remote_action.type == ControlType::audio_capture) {
|
||||
if (remote_action.a && !runtime->start_speaker_capturer_)
|
||||
runtime->devices_.StartSpeakerCapturer();
|
||||
else if (!remote_action.a && runtime->start_speaker_capturer_)
|
||||
runtime->devices_.StopSpeakerCapturer();
|
||||
} else if (remote_action.type == ControlType::keyboard) {
|
||||
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
|
||||
} else if (remote_action.type == ControlType::keyboard_state) {
|
||||
runtime->keyboard_.ApplyRemoteState(remote_id, remote_action);
|
||||
} else if (remote_action.type == ControlType::display_id) {
|
||||
const int ret = runtime->devices_.SwitchDisplay(remote_action.d);
|
||||
if (ret == 0) {
|
||||
runtime->selected_display_ = remote_action.d;
|
||||
} else {
|
||||
LOG_WARN("Display switch skipped, invalid display_id={}",
|
||||
remote_action.d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,524 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
||||
void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime || !message || size == 0) {
|
||||
return;
|
||||
}
|
||||
std::string s(message, size);
|
||||
auto j = nlohmann::json::parse(s, nullptr, false);
|
||||
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
|
||||
return;
|
||||
}
|
||||
std::string type = j["type"].get<std::string>();
|
||||
if (type == "presence") {
|
||||
if (j.contains("devices") && j["devices"].is_array()) {
|
||||
for (auto &dev : j["devices"]) {
|
||||
if (!dev.is_object()) {
|
||||
continue;
|
||||
}
|
||||
if (!dev.contains("id") || !dev["id"].is_string()) {
|
||||
continue;
|
||||
}
|
||||
if (!dev.contains("online") || !dev["online"].is_boolean()) {
|
||||
continue;
|
||||
}
|
||||
std::string id = dev["id"].get<std::string>();
|
||||
bool online = dev["online"].get<bool>();
|
||||
runtime->device_presence_cache_.SetOnline(id, online);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
runtime->pending_presence_probe_mutex_);
|
||||
if (runtime->pending_presence_probe_ &&
|
||||
runtime->pending_presence_remote_id_ == id) {
|
||||
runtime->pending_presence_result_ready_ = true;
|
||||
runtime->pending_presence_online_ = online;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == "presence_update") {
|
||||
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
|
||||
j["online"].is_boolean()) {
|
||||
std::string id = j["id"].get<std::string>();
|
||||
bool online = j["online"].get<bool>();
|
||||
if (!id.empty()) {
|
||||
runtime->device_presence_cache_.SetOnline(id, online);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
runtime->pending_presence_probe_mutex_);
|
||||
if (runtime->pending_presence_probe_ &&
|
||||
runtime->pending_presence_remote_id_ == id) {
|
||||
runtime->pending_presence_result_ready_ = true;
|
||||
runtime->pending_presence_online_ = online;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
|
||||
size_t user_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string client_id(user_id, user_id_size);
|
||||
if (client_id == runtime->client_id_) {
|
||||
runtime->signal_status_ = status;
|
||||
if (SignalStatus::SignalConnecting == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalConnected == status) {
|
||||
runtime->signal_connected_ = true;
|
||||
runtime->need_to_send_recent_connections_ = true;
|
||||
LOG_INFO("[{}] connected to signal server", client_id);
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalClosed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalReconnecting == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalTlsCertError == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
}
|
||||
} else {
|
||||
if (client_id.rfind("C-", 0) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(client_id.begin() + 2, client_id.end());
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
props->signal_status_ = status;
|
||||
if (SignalStatus::SignalConnecting == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalConnected == status) {
|
||||
props->signal_connected_ = true;
|
||||
LOG_INFO("[{}] connected to signal server", remote_id);
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalClosed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalReconnecting == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalTlsCertError == status) {
|
||||
props->signal_connected_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
const char *user_id,
|
||||
const size_t user_id_size,
|
||||
void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime)
|
||||
return;
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||
{
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
auto it = runtime->remote_sessions_.find(remote_id);
|
||||
if (it != runtime->remote_sessions_.end()) {
|
||||
props = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (props) {
|
||||
runtime->is_client_mode_ = true;
|
||||
runtime->show_connection_status_window_ = true;
|
||||
props->connection_status_.store(status);
|
||||
if (status != ConnectionStatus::Connecting &&
|
||||
status != ConnectionStatus::Gathering) {
|
||||
props->connection_attempt_active_.store(false);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num =
|
||||
runtime->devices_.display_info_list().size();
|
||||
remote_action.i.display_list =
|
||||
(char **)malloc(remote_action.i.display_num * sizeof(char *));
|
||||
remote_action.i.left =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
runtime->devices_.display_info_list()[i].name);
|
||||
remote_action.i.display_list[i] = (char *)malloc(
|
||||
runtime->devices_.display_info_list()[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
runtime->devices_.display_info_list()[i].name.c_str(),
|
||||
runtime->devices_.display_info_list()[i].name.length());
|
||||
remote_action.i.display_list
|
||||
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
|
||||
remote_action.i.left[i] =
|
||||
runtime->devices_.display_info_list()[i].left;
|
||||
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
|
||||
remote_action.i.right[i] =
|
||||
runtime->devices_.display_info_list()[i].right;
|
||||
remote_action.i.bottom[i] =
|
||||
runtime->devices_.display_info_list()[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||
runtime->control_data_label_.c_str());
|
||||
remote_action_codec::Free(remote_action);
|
||||
}
|
||||
|
||||
if (!runtime->need_to_create_stream_window_ &&
|
||||
!runtime->remote_sessions_.empty()) {
|
||||
runtime->need_to_create_stream_window_ = true;
|
||||
}
|
||||
props->connection_established_ = true;
|
||||
props->stream_render_rect_ = {
|
||||
0, (int)runtime->title_bar_height_, (int)runtime->stream_window_width_,
|
||||
(int)(runtime->stream_window_height_ - runtime->title_bar_height_)};
|
||||
props->stream_render_rect_f_ = {
|
||||
0.0f, runtime->title_bar_height_, runtime->stream_window_width_,
|
||||
runtime->stream_window_height_ - runtime->title_bar_height_};
|
||||
runtime->start_keyboard_capturer_ = true;
|
||||
break;
|
||||
}
|
||||
case ConnectionStatus::Disconnected:
|
||||
case ConnectionStatus::Failed:
|
||||
case ConnectionStatus::Closed: {
|
||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
||||
"connection_closed");
|
||||
props->connection_established_ = false;
|
||||
props->enable_mouse_control_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
event.type = runtime->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props.get();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
runtime->focus_on_stream_window_ = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case ConnectionStatus::IncorrectPassword: {
|
||||
runtime->password_validating_ = false;
|
||||
runtime->password_validating_time_++;
|
||||
if (runtime->connect_button_pressed_) {
|
||||
runtime->connect_button_pressed_ = false;
|
||||
props->connection_established_ = false;
|
||||
runtime->connect_button_label_ =
|
||||
localization::connect[runtime->localization_language_index_];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConnectionStatus::NoSuchTransmissionId:
|
||||
case ConnectionStatus::RemoteUnavailable: {
|
||||
if (runtime->connect_button_pressed_) {
|
||||
props->connection_established_ = false;
|
||||
runtime->connect_button_label_ =
|
||||
localization::connect[runtime->localization_language_index_];
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
runtime->is_client_mode_ = false;
|
||||
runtime->show_connection_status_window_ = true;
|
||||
{
|
||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||
runtime->connection_status_[remote_id] = status;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
#if _WIN32
|
||||
runtime->last_windows_service_status_tick_ = 0;
|
||||
#endif
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num =
|
||||
runtime->devices_.display_info_list().size();
|
||||
remote_action.i.display_list =
|
||||
(char **)malloc(remote_action.i.display_num * sizeof(char *));
|
||||
remote_action.i.left =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
runtime->devices_.display_info_list()[i].name);
|
||||
remote_action.i.display_list[i] = (char *)malloc(
|
||||
runtime->devices_.display_info_list()[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
runtime->devices_.display_info_list()[i].name.c_str(),
|
||||
runtime->devices_.display_info_list()[i].name.length());
|
||||
remote_action.i.display_list
|
||||
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
|
||||
remote_action.i.left[i] =
|
||||
runtime->devices_.display_info_list()[i].left;
|
||||
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
|
||||
remote_action.i.right[i] =
|
||||
runtime->devices_.display_info_list()[i].right;
|
||||
remote_action.i.bottom[i] =
|
||||
runtime->devices_.display_info_list()[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
|
||||
runtime->control_data_label_.c_str());
|
||||
remote_action_codec::Free(remote_action);
|
||||
}
|
||||
|
||||
runtime->need_to_create_server_window_ = true;
|
||||
runtime->is_server_mode_ = true;
|
||||
runtime->start_screen_capturer_ = true;
|
||||
runtime->start_speaker_capturer_ = true;
|
||||
runtime->remote_client_id_ = remote_id;
|
||||
runtime->start_mouse_controller_ = true;
|
||||
{
|
||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
||||
if (std::all_of(runtime->connection_status_.begin(),
|
||||
runtime->connection_status_.end(), [](const auto &kv) {
|
||||
return kv.first.find("web") != std::string::npos;
|
||||
})) {
|
||||
runtime->show_cursor_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ConnectionStatus::Disconnected:
|
||||
case ConnectionStatus::Failed:
|
||||
case ConnectionStatus::Closed: {
|
||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
||||
"connection_closed");
|
||||
bool all_disconnected = false;
|
||||
{
|
||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
||||
all_disconnected =
|
||||
std::all_of(runtime->connection_status_.begin(),
|
||||
runtime->connection_status_.end(), [](const auto &kv) {
|
||||
return kv.second == ConnectionStatus::Closed ||
|
||||
kv.second == ConnectionStatus::Failed ||
|
||||
kv.second == ConnectionStatus::Disconnected;
|
||||
});
|
||||
}
|
||||
if (all_disconnected) {
|
||||
runtime->need_to_destroy_server_window_ = true;
|
||||
runtime->is_server_mode_ = false;
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
// Keep Wayland capture session warm to avoid black screen on
|
||||
// subsequent reconnects.
|
||||
runtime->start_screen_capturer_ = true;
|
||||
LOG_INFO("Keeping Wayland screen capturer running after "
|
||||
"disconnect to preserve reconnect stability");
|
||||
} else {
|
||||
runtime->start_screen_capturer_ = false;
|
||||
}
|
||||
#else
|
||||
runtime->start_screen_capturer_ = false;
|
||||
#endif
|
||||
runtime->start_speaker_capturer_ = false;
|
||||
runtime->start_mouse_controller_ = false;
|
||||
runtime->start_keyboard_capturer_ = false;
|
||||
runtime->remote_client_id_ = "";
|
||||
if (props)
|
||||
props->connection_established_ = false;
|
||||
if (runtime->audio_capture_) {
|
||||
runtime->devices_.StopSpeakerCapturer();
|
||||
runtime->audio_capture_ = false;
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||
runtime->connection_status_.erase(remote_id);
|
||||
runtime->connection_host_names_.erase(remote_id);
|
||||
}
|
||||
runtime->devices_.ResetToInitialDisplay();
|
||||
}
|
||||
|
||||
{
|
||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
||||
if (std::all_of(runtime->connection_status_.begin(),
|
||||
runtime->connection_status_.end(), [](const auto &kv) {
|
||||
return kv.first.find("web") == std::string::npos;
|
||||
})) {
|
||||
runtime->show_cursor_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnNetStatusReport(
|
||||
const char *client_id, size_t client_id_size, TraversalMode mode,
|
||||
const XNetTrafficStats *net_traffic_stats, const char *user_id,
|
||||
const size_t user_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
||||
std::string id, password;
|
||||
const char *at_pos = strchr(client_id, '@');
|
||||
if (at_pos == nullptr) {
|
||||
id = client_id;
|
||||
password.clear();
|
||||
} else {
|
||||
id.assign(client_id, at_pos - client_id);
|
||||
password = at_pos + 1;
|
||||
}
|
||||
|
||||
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
|
||||
|
||||
if (is_self_hosted) {
|
||||
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
|
||||
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
|
||||
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
|
||||
strncpy(runtime->password_saved_, password.c_str(),
|
||||
sizeof(runtime->password_saved_) - 1);
|
||||
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->self_hosted_id_, 0, sizeof(runtime->self_hosted_id_));
|
||||
strncpy(runtime->self_hosted_id_, client_id,
|
||||
sizeof(runtime->self_hosted_id_) - 1);
|
||||
runtime->self_hosted_id_[sizeof(runtime->self_hosted_id_) - 1] = '\0';
|
||||
|
||||
LOG_INFO("Use self-hosted client id [{}] and save to cache file", id);
|
||||
|
||||
runtime->settings_.PersistSelfHostedIdentity(client_id);
|
||||
} else {
|
||||
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
|
||||
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
|
||||
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
|
||||
strncpy(runtime->password_saved_, password.c_str(),
|
||||
sizeof(runtime->password_saved_) - 1);
|
||||
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->client_id_with_password_, 0,
|
||||
sizeof(runtime->client_id_with_password_));
|
||||
strncpy(runtime->client_id_with_password_, client_id,
|
||||
sizeof(runtime->client_id_with_password_) - 1);
|
||||
runtime
|
||||
->client_id_with_password_[sizeof(runtime->client_id_with_password_) -
|
||||
1] = '\0';
|
||||
|
||||
LOG_INFO("Use client id [{}] and save id into cache file", id);
|
||||
runtime->settings_.Save();
|
||||
}
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
if (props->traversal_mode_ != mode) {
|
||||
props->traversal_mode_ = mode;
|
||||
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
|
||||
}
|
||||
|
||||
if (!net_traffic_stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only display client side net status if connected to itself
|
||||
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
|
||||
props->net_traffic_stats_ = *net_traffic_stats;
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiRuntime;
|
||||
|
||||
// Adapts MiniRTC C callbacks to the GUI runtime.
|
||||
class PeerEventHandler {
|
||||
public:
|
||||
explicit PeerEventHandler(GuiRuntime &owner);
|
||||
|
||||
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
|
||||
const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size,
|
||||
void *user_data);
|
||||
static void OnReceiveAudioBuffer(const char *data, size_t size,
|
||||
const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size,
|
||||
void *user_data);
|
||||
static void OnReceiveDataBuffer(const char *data, size_t size,
|
||||
const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size,
|
||||
void *user_data);
|
||||
static void OnSignalStatus(SignalStatus status, const char *user_id,
|
||||
size_t user_id_size, void *user_data);
|
||||
static void OnSignalMessage(const char *message, size_t size,
|
||||
void *user_data);
|
||||
static void OnConnectionStatus(ConnectionStatus status, const char *user_id,
|
||||
size_t user_id_size, void *user_data);
|
||||
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats *net_traffic_stats,
|
||||
const char *user_id, size_t user_id_size,
|
||||
void *user_data);
|
||||
|
||||
private:
|
||||
GuiRuntime &owner_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
const XVideoFrame *video_frame, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
GuiRuntime::RemoteSession *props =
|
||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
|
||||
if (!props->back_frame_) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
}
|
||||
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
event.type = runtime->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props;
|
||||
SDL_PushEvent(&event);
|
||||
props->streaming_ = true;
|
||||
|
||||
if (props->net_traffic_stats_button_pressed_) {
|
||||
props->frame_count_++;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - props->last_time_)
|
||||
.count();
|
||||
|
||||
if (elapsed >= 1000) {
|
||||
props->fps_ = props->frame_count_ * 1000 / elapsed;
|
||||
props->frame_count_ = 0;
|
||||
props->last_time_ = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnReceiveAudioBuffer(
|
||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime->audio_buffer_fresh_ = true;
|
||||
|
||||
runtime->devices_.PushAudio(data, size);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "runtime/remote_action_codec.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace crossdesk::remote_action_codec {
|
||||
|
||||
std::vector<char> Serialize(const RemoteAction &action) {
|
||||
std::vector<char> buffer;
|
||||
buffer.push_back(static_cast<char>(action.type));
|
||||
|
||||
auto insert_bytes = [&](const void *ptr, size_t len) {
|
||||
buffer.insert(buffer.end(), static_cast<const char *>(ptr),
|
||||
static_cast<const char *>(ptr) + len);
|
||||
};
|
||||
|
||||
if (action.type == ControlType::host_infomation) {
|
||||
insert_bytes(&action.i.host_name_size, sizeof(size_t));
|
||||
insert_bytes(action.i.host_name, action.i.host_name_size);
|
||||
|
||||
size_t num = action.i.display_num;
|
||||
insert_bytes(&num, sizeof(size_t));
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
const size_t len = std::strlen(action.i.display_list[i]);
|
||||
insert_bytes(&len, sizeof(size_t));
|
||||
insert_bytes(action.i.display_list[i], len);
|
||||
}
|
||||
|
||||
insert_bytes(action.i.left, sizeof(int) * num);
|
||||
insert_bytes(action.i.top, sizeof(int) * num);
|
||||
insert_bytes(action.i.right, sizeof(int) * num);
|
||||
insert_bytes(action.i.bottom, sizeof(int) * num);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool Deserialize(const char *data, size_t size, RemoteAction &out) {
|
||||
size_t offset = 0;
|
||||
auto read = [&](void *dst, size_t len) -> bool {
|
||||
if (offset + len > size) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(dst, data + offset, len);
|
||||
offset += len;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (size < 1) {
|
||||
return false;
|
||||
}
|
||||
out.type = static_cast<ControlType>(data[offset++]);
|
||||
|
||||
if (out.type == ControlType::host_infomation) {
|
||||
size_t name_len;
|
||||
if (!read(&name_len, sizeof(size_t)) ||
|
||||
name_len >= sizeof(out.i.host_name)) {
|
||||
return false;
|
||||
}
|
||||
if (!read(out.i.host_name, name_len)) {
|
||||
return false;
|
||||
}
|
||||
out.i.host_name[name_len] = '\0';
|
||||
out.i.host_name_size = name_len;
|
||||
|
||||
size_t num;
|
||||
if (!read(&num, sizeof(size_t))) {
|
||||
return false;
|
||||
}
|
||||
out.i.display_num = num;
|
||||
|
||||
out.i.display_list =
|
||||
static_cast<char **>(std::malloc(num * sizeof(char *)));
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
size_t len;
|
||||
if (!read(&len, sizeof(size_t)) || offset + len > size) {
|
||||
return false;
|
||||
}
|
||||
out.i.display_list[i] = static_cast<char *>(std::malloc(len + 1));
|
||||
std::memcpy(out.i.display_list[i], data + offset, len);
|
||||
out.i.display_list[i][len] = '\0';
|
||||
offset += len;
|
||||
}
|
||||
|
||||
auto alloc_int_array = [&](int *&array) {
|
||||
array = static_cast<int *>(std::malloc(num * sizeof(int)));
|
||||
return read(array, num * sizeof(int));
|
||||
};
|
||||
|
||||
return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) &&
|
||||
alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Free(RemoteAction &action) {
|
||||
if (action.type != ControlType::host_infomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < action.i.display_num; ++i) {
|
||||
std::free(action.i.display_list[i]);
|
||||
}
|
||||
std::free(action.i.display_list);
|
||||
std::free(action.i.left);
|
||||
std::free(action.i.top);
|
||||
std::free(action.i.right);
|
||||
std::free(action.i.bottom);
|
||||
|
||||
action.i.display_list = nullptr;
|
||||
action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr;
|
||||
action.i.display_num = 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk::remote_action_codec
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk::remote_action_codec {
|
||||
|
||||
std::vector<char> Serialize(const RemoteAction &action);
|
||||
bool Deserialize(const char *data, size_t size, RemoteAction &out);
|
||||
void Free(RemoteAction &action);
|
||||
|
||||
} // namespace crossdesk::remote_action_codec
|
||||
|
||||
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
@@ -0,0 +1,175 @@
|
||||
#ifndef CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
#define CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "display_info.h"
|
||||
#include "imgui.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct FileTransferState {
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
bool file_transfer_window_hovered_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
std::string remote_id;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
};
|
||||
|
||||
// Runtime state for one connected or connecting remote endpoint. It groups the
|
||||
// connection, media, input, window and transfer data that share one lifetime.
|
||||
struct RemoteSession {
|
||||
Params params_;
|
||||
PeerPtr *peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
std::string keyboard_label_ = "keyboard";
|
||||
std::string file_label_ = "file";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
std::string local_id_;
|
||||
std::string remote_id_;
|
||||
bool exit_ = false;
|
||||
bool signal_connected_ = false;
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
bool connection_established_ = false;
|
||||
bool rejoin_ = false;
|
||||
std::atomic<bool> connection_attempt_active_ = false;
|
||||
std::chrono::steady_clock::time_point connection_attempt_started_at_;
|
||||
bool net_traffic_stats_button_pressed_ = false;
|
||||
bool enable_mouse_control_ = true;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool audio_capture_button_pressed_ = true;
|
||||
bool control_mouse_ = true;
|
||||
bool streaming_ = false;
|
||||
bool is_control_bar_in_left_ = true;
|
||||
bool control_bar_hovered_ = false;
|
||||
bool display_selectable_hovered_ = false;
|
||||
bool shortcut_selectable_hovered_ = false;
|
||||
bool control_bar_expand_ = true;
|
||||
bool reset_control_bar_pos_ = false;
|
||||
bool control_window_width_is_changing_ = false;
|
||||
bool control_window_height_is_changing_ = false;
|
||||
bool p2p_mode_ = true;
|
||||
bool remember_password_ = false;
|
||||
char remote_password_[7] = "";
|
||||
float sub_stream_window_width_ = 1280;
|
||||
float sub_stream_window_height_ = 720;
|
||||
float control_window_min_width_ = 20;
|
||||
float control_window_max_width_ = 300;
|
||||
float control_window_min_height_ = 38;
|
||||
float control_window_max_height_ = 180;
|
||||
float control_window_width_ = 300;
|
||||
float control_window_height_ = 38;
|
||||
float control_bar_pos_x_ = 0;
|
||||
float control_bar_pos_y_ = 30;
|
||||
float mouse_diff_control_bar_pos_x_ = 0;
|
||||
float mouse_diff_control_bar_pos_y_ = 0;
|
||||
double control_bar_button_pressed_time_ = 0;
|
||||
double net_traffic_stats_button_pressed_time_ = 0;
|
||||
|
||||
// Written by the decode callback thread and consumed by the SDL thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
float mouse_pos_y_ = 0;
|
||||
float mouse_pos_x_last_ = 0;
|
||||
float mouse_pos_y_last_ = 0;
|
||||
int texture_width_ = 1280;
|
||||
int texture_height_ = 720;
|
||||
int video_width_ = 0;
|
||||
int video_height_ = 0;
|
||||
int video_width_last_ = 0;
|
||||
int video_height_last_ = 0;
|
||||
int selected_display_ = 0;
|
||||
size_t video_size_ = 0;
|
||||
bool tab_selected_ = false;
|
||||
bool tab_opened_ = true;
|
||||
std::optional<float> pos_x_before_docked_;
|
||||
std::optional<float> pos_y_before_docked_;
|
||||
float render_window_x_ = 0;
|
||||
float render_window_y_ = 0;
|
||||
float render_window_width_ = 0;
|
||||
float render_window_height_ = 0;
|
||||
std::string fullscreen_button_label_ = "Fullscreen";
|
||||
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
|
||||
std::string mouse_control_button_label_ = "Mouse Control";
|
||||
std::string audio_capture_button_label_ = "Audio Capture";
|
||||
std::string remote_host_name_;
|
||||
bool remote_service_status_received_ = false;
|
||||
bool remote_service_available_ = false;
|
||||
std::string remote_interactive_stage_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
SDL_Texture *stream_texture_ = nullptr;
|
||||
uint8_t *argb_buffer_ = nullptr;
|
||||
int argb_buffer_size_ = 0;
|
||||
SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
SDL_Rect stream_render_rect_{};
|
||||
SDL_Rect stream_render_rect_last_{};
|
||||
ImVec2 control_window_pos_{};
|
||||
|
||||
// Shared by minirtc callbacks, SDL rendering and SDL audio callbacks.
|
||||
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
||||
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||
int fps_ = 0;
|
||||
int frame_count_ = 0;
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_{};
|
||||
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
using FileTransferInfo = FileTransferState::FileTransferInfo;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
|
||||
using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
@@ -0,0 +1,186 @@
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_STATE_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_STATE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "config_center.h"
|
||||
#include "path_manager.h"
|
||||
#include "runtime/device_presence_cache.h"
|
||||
#include "runtime/remote_session.h"
|
||||
#include "thumbnail.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct InfrastructureState {
|
||||
std::unique_ptr<ConfigCenter> config_center_;
|
||||
ConfigCenter::LANGUAGE localization_language_ =
|
||||
ConfigCenter::LANGUAGE::CHINESE;
|
||||
std::unique_ptr<PathManager> path_manager_;
|
||||
std::string exec_log_path_;
|
||||
std::string dll_log_path_;
|
||||
std::string cache_path_;
|
||||
int localization_language_index_ = -1;
|
||||
int localization_language_index_last_ = -1;
|
||||
bool modules_inited_ = false;
|
||||
unsigned char aes128_key_[16]{};
|
||||
unsigned char aes128_iv_[16]{};
|
||||
};
|
||||
|
||||
struct RecentConnectionsState {
|
||||
std::shared_ptr<Thumbnail> thumbnail_;
|
||||
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
|
||||
recent_connections_;
|
||||
std::vector<std::string> recent_connection_ids_;
|
||||
int recent_connection_image_width_ = 160;
|
||||
int recent_connection_image_height_ = 90;
|
||||
uint32_t recent_connection_image_save_time_ = 0;
|
||||
DevicePresenceCache device_presence_cache_;
|
||||
bool need_to_send_recent_connections_ = true;
|
||||
};
|
||||
|
||||
struct PeerState {
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
std::string signal_status_str_;
|
||||
bool signal_connected_ = false;
|
||||
PeerPtr *peer_ = nullptr;
|
||||
PeerPtr *peer_reserved_ = nullptr;
|
||||
std::string video_primary_label_ = "primary_display";
|
||||
std::string video_secondary_label_ = "secondary_display";
|
||||
std::string audio_label_ = "audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
std::string keyboard_label_ = "keyboard";
|
||||
std::string info_label_ = "info";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_label_ = "file";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
Params params_;
|
||||
};
|
||||
|
||||
// OS-specific service and permission state is kept separate from transport
|
||||
// state so platform code does not appear to be part of the peer protocol.
|
||||
struct PlatformIntegrationState {
|
||||
#if _WIN32
|
||||
std::atomic<bool> pending_windows_service_sas_{false};
|
||||
bool local_service_status_received_ = false;
|
||||
bool local_service_available_ = false;
|
||||
std::string local_interactive_stage_;
|
||||
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
||||
uint32_t last_windows_service_status_tick_ = 0;
|
||||
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
#if CROSSDESK_PORTABLE
|
||||
enum class PortableServiceInstallState {
|
||||
idle,
|
||||
installing,
|
||||
succeeded,
|
||||
failed,
|
||||
};
|
||||
bool portable_service_prompt_checked_ = false;
|
||||
bool show_portable_service_install_window_ = false;
|
||||
bool show_portable_service_prompt_suppressed_window_ = false;
|
||||
bool portable_service_do_not_remind_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
bool show_request_permission_window_ = true;
|
||||
bool mac_permission_status_initialized_ = false;
|
||||
uint32_t mac_permission_last_check_tick_ = 0;
|
||||
bool mac_screen_recording_permission_granted_ = false;
|
||||
bool mac_accessibility_permission_granted_ = false;
|
||||
bool mac_screen_recording_permission_requested_ = false;
|
||||
bool mac_accessibility_permission_requested_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct UserSettingsState {
|
||||
char client_id_[10] = "";
|
||||
char client_id_display_[12] = "";
|
||||
char client_id_with_password_[17] = "";
|
||||
char password_saved_[7] = "";
|
||||
char self_hosted_id_[17] = "";
|
||||
char self_hosted_user_id_[17] = "";
|
||||
int language_button_value_ = 0;
|
||||
int video_quality_button_value_ = 2;
|
||||
int video_frame_rate_button_value_ = 1;
|
||||
int video_encode_format_button_value_ = 0;
|
||||
bool enable_hardware_video_codec_ = true;
|
||||
bool enable_turn_ = true;
|
||||
bool enable_srtp_ = false;
|
||||
char signal_server_ip_[256] = "api.crossdesk.cn";
|
||||
char signal_server_port_[6] = "9099";
|
||||
char coturn_server_port_[6] = "3478";
|
||||
bool enable_self_hosted_ = false;
|
||||
int language_button_value_last_ = 0;
|
||||
int video_quality_button_value_last_ = 0;
|
||||
int video_frame_rate_button_value_last_ = 0;
|
||||
int video_encode_format_button_value_last_ = 0;
|
||||
bool enable_hardware_video_codec_last_ = false;
|
||||
bool enable_turn_last_ = true;
|
||||
bool enable_srtp_last_ = false;
|
||||
bool enable_self_hosted_last_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_autostart_last_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool enable_daemon_last_ = false;
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_minimize_to_tray_last_ = false;
|
||||
char file_transfer_save_path_buf_[512] = "";
|
||||
std::string file_transfer_save_path_last_;
|
||||
char signal_server_ip_self_[256] = "";
|
||||
char signal_server_port_self_[6] = "";
|
||||
char coturn_server_port_self_[6] = "";
|
||||
bool settings_window_pos_reset_ = true;
|
||||
bool self_hosted_server_config_window_pos_reset_ = true;
|
||||
std::string selected_current_file_path_;
|
||||
bool show_file_browser_ = true;
|
||||
};
|
||||
|
||||
struct ConnectionState {
|
||||
using RemoteSessionMap =
|
||||
std::unordered_map<std::string, RemoteSessionPtr>;
|
||||
RemoteSessionMap remote_sessions_;
|
||||
std::shared_mutex remote_sessions_mutex_;
|
||||
std::vector<std::thread> thumbnail_save_threads_;
|
||||
std::mutex thumbnail_save_threads_mutex_;
|
||||
|
||||
std::shared_mutex connection_status_mutex_;
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::string selected_server_remote_id_;
|
||||
std::string selected_server_remote_hostname_;
|
||||
std::mutex pending_presence_probe_mutex_;
|
||||
bool pending_presence_probe_ = false;
|
||||
bool pending_presence_result_ready_ = false;
|
||||
bool pending_presence_online_ = false;
|
||||
std::chrono::steady_clock::time_point pending_presence_probe_started_at_;
|
||||
std::string pending_presence_remote_id_;
|
||||
std::string pending_presence_password_;
|
||||
bool pending_presence_remember_password_ = false;
|
||||
};
|
||||
|
||||
struct RuntimeState : InfrastructureState,
|
||||
RecentConnectionsState,
|
||||
PeerState,
|
||||
PlatformIntegrationState,
|
||||
UserSettingsState,
|
||||
ConnectionState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_RUNTIME_STATE_H_
|
||||
@@ -0,0 +1,262 @@
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
#if _WIN32
|
||||
struct WindowsServiceInteractiveStatus {
|
||||
bool available = false;
|
||||
bool sas_secure_desktop_grace_active = false;
|
||||
unsigned int error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
|
||||
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
|
||||
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
|
||||
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
|
||||
|
||||
bool IsTransientWindowsServiceStatusError(const std::string &error) {
|
||||
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
|
||||
error == "pipe_read_failed";
|
||||
}
|
||||
|
||||
RemoteAction
|
||||
BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) {
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::service_status;
|
||||
action.ss.available = status.available;
|
||||
std::strncpy(action.ss.interactive_stage, status.interactive_stage.c_str(),
|
||||
sizeof(action.ss.interactive_stage) - 1);
|
||||
action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = '\0';
|
||||
return action;
|
||||
}
|
||||
|
||||
bool QueryWindowsServiceInteractiveStatus(
|
||||
WindowsServiceInteractiveStatus *status) {
|
||||
if (status == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*status = WindowsServiceInteractiveStatus{};
|
||||
const std::string response =
|
||||
QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object()) {
|
||||
status->error = "invalid_service_status_json";
|
||||
return false;
|
||||
}
|
||||
|
||||
status->available = json.value("ok", false);
|
||||
if (!status->available) {
|
||||
status->error = json.value("error", std::string("service_unavailable"));
|
||||
status->error_code = json.value("code", 0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||
status->sas_secure_desktop_grace_active =
|
||||
json.value("sas_secure_desktop_grace_active", false);
|
||||
|
||||
if (ShouldNormalizeUnlockToUserDesktop(
|
||||
json.value("interactive_lock_screen_visible", false),
|
||||
status->interactive_stage, json.value("session_locked", false),
|
||||
json.value("interactive_logon_ui_visible", false),
|
||||
json.value("interactive_secure_desktop_active",
|
||||
json.value("secure_desktop_active", false)),
|
||||
json.value("credential_ui_visible", false),
|
||||
json.value("password_box_visible", false),
|
||||
json.value("unlock_ui_visible", false),
|
||||
json.value("last_session_event", std::string()))) {
|
||||
status->interactive_stage = "user-desktop";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
void GuiRuntime::ResetRemoteServiceStatus(RemoteSession &props) {
|
||||
props.remote_service_status_received_ = false;
|
||||
props.remote_service_available_ = false;
|
||||
props.remote_interactive_stage_.clear();
|
||||
}
|
||||
|
||||
void GuiRuntime::ApplyRemoteServiceStatus(RemoteSession &props,
|
||||
const ServiceStatus &status) {
|
||||
props.remote_service_status_received_ = true;
|
||||
props.remote_service_available_ = status.available;
|
||||
props.remote_interactive_stage_ = status.interactive_stage;
|
||||
}
|
||||
|
||||
GuiRuntime::RemoteUnlockState
|
||||
GuiRuntime::GetRemoteUnlockState(const RemoteSession &props) const {
|
||||
if (!props.remote_service_status_received_) {
|
||||
return RemoteUnlockState::none;
|
||||
}
|
||||
if (!props.remote_service_available_) {
|
||||
return RemoteUnlockState::service_unavailable;
|
||||
}
|
||||
if (props.remote_interactive_stage_ == "credential-ui") {
|
||||
return RemoteUnlockState::credential_ui;
|
||||
}
|
||||
if (props.remote_interactive_stage_ == "lock-screen") {
|
||||
return RemoteUnlockState::lock_screen;
|
||||
}
|
||||
if (props.remote_interactive_stage_ == "secure-desktop") {
|
||||
return RemoteUnlockState::secure_desktop;
|
||||
}
|
||||
return RemoteUnlockState::none;
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleWindowsServiceIntegration() {
|
||||
#if _WIN32
|
||||
static bool last_logged_service_available = true;
|
||||
static unsigned int last_logged_service_error_code = 0;
|
||||
static std::string last_logged_service_error;
|
||||
|
||||
if (!is_server_mode_ || peer_ == nullptr) {
|
||||
ResetLocalWindowsServiceState(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool has_connected_remote = [&] {
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
return std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||
[](const auto &entry) {
|
||||
return entry.second == ConnectionStatus::Connected;
|
||||
});
|
||||
}();
|
||||
if (!has_connected_remote) {
|
||||
ResetLocalWindowsServiceState(false);
|
||||
return;
|
||||
}
|
||||
|
||||
bool force_broadcast = false;
|
||||
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
|
||||
const std::string response =
|
||||
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
LOG_WARN("Remote SAS request failed: {}", response);
|
||||
} else {
|
||||
LOG_INFO("Remote SAS request forwarded to local Windows service");
|
||||
optimistic_windows_secure_desktop_until_tick_ =
|
||||
static_cast<uint32_t>(SDL_GetTicks()) +
|
||||
kWindowsServiceSasSecureDesktopGraceMs;
|
||||
local_service_status_received_ = true;
|
||||
local_service_available_ = true;
|
||||
local_interactive_stage_ = "secure-desktop";
|
||||
}
|
||||
last_windows_service_status_tick_ = 0;
|
||||
force_broadcast = true;
|
||||
}
|
||||
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (!force_broadcast && last_windows_service_status_tick_ != 0 &&
|
||||
now - last_windows_service_status_tick_ <
|
||||
kWindowsServiceStatusIntervalMs) {
|
||||
return;
|
||||
}
|
||||
last_windows_service_status_tick_ = now;
|
||||
|
||||
WindowsServiceInteractiveStatus status;
|
||||
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
|
||||
WindowsServiceInteractiveStatus broadcast_status = status;
|
||||
const bool previous_secure_desktop_interaction =
|
||||
IsSecureDesktopInteractionRequired(local_interactive_stage_);
|
||||
const bool optimistic_secure_desktop_active =
|
||||
optimistic_windows_secure_desktop_until_tick_ != 0 &&
|
||||
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
|
||||
now) > 0;
|
||||
const bool keep_optimistic_secure_desktop =
|
||||
status_ok && status.available && optimistic_secure_desktop_active &&
|
||||
status.sas_secure_desktop_grace_active &&
|
||||
status.interactive_stage == "user-desktop";
|
||||
local_service_status_received_ =
|
||||
status_ok || previous_secure_desktop_interaction;
|
||||
local_service_available_ = status.available;
|
||||
if (status.available) {
|
||||
if (keep_optimistic_secure_desktop) {
|
||||
local_interactive_stage_ = "secure-desktop";
|
||||
broadcast_status.interactive_stage = local_interactive_stage_;
|
||||
} else {
|
||||
local_interactive_stage_ = status.interactive_stage;
|
||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
}
|
||||
} else if (!previous_secure_desktop_interaction) {
|
||||
local_interactive_stage_.clear();
|
||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
}
|
||||
|
||||
if (status_ok) {
|
||||
const bool availability_changed =
|
||||
status.available != last_logged_service_available;
|
||||
const bool error_changed =
|
||||
!status.available &&
|
||||
(status.error != last_logged_service_error ||
|
||||
status.error_code != last_logged_service_error_code);
|
||||
if (availability_changed || error_changed) {
|
||||
if (status.available) {
|
||||
LOG_INFO(
|
||||
"Local Windows service available for secure desktop integration");
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO("Local Windows service temporarily unavailable, keeping last "
|
||||
"secure desktop state: error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Local Windows service unavailable, secure desktop integration "
|
||||
"disabled: error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
}
|
||||
last_logged_service_available = status.available;
|
||||
last_logged_service_error = status.error;
|
||||
last_logged_service_error_code = status.error_code;
|
||||
}
|
||||
} else if (last_logged_service_available ||
|
||||
last_logged_service_error != "invalid_service_status_json") {
|
||||
LOG_WARN(
|
||||
"Local Windows service status query failed, secure desktop integration "
|
||||
"disabled");
|
||||
last_logged_service_available = false;
|
||||
last_logged_service_error = "invalid_service_status_json";
|
||||
last_logged_service_error_code = 0;
|
||||
}
|
||||
|
||||
RemoteAction remote_action =
|
||||
BuildWindowsServiceStatusAction(broadcast_status);
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
||||
control_data_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Broadcast Windows service status failed, ret={}", ret);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
|
||||
last_windows_service_status_tick_ = 0;
|
||||
if (clear_pending_sas) {
|
||||
pending_windows_service_sas_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
local_service_status_received_ = false;
|
||||
local_service_available_ = false;
|
||||
local_interactive_stage_.clear();
|
||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,48 +0,0 @@
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::StatusBar() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float status_bar_width = io.DisplaySize.x;
|
||||
float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT;
|
||||
|
||||
static bool a, b, c, d, e;
|
||||
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
|
||||
ImGui::BeginChild("StatusBar", ImVec2(status_bar_width, status_bar_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
|
||||
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
|
||||
ImColor(1.0f, 1.0f, 1.0f), 100);
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
|
||||
ImColor(signal_connected_ ? 0.0f : 1.0f,
|
||||
signal_connected_ ? 1.0f : 0.0f, 0.0f),
|
||||
100);
|
||||
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
draw_list->AddText(
|
||||
ImVec2(status_bar_width * 0.045f,
|
||||
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)),
|
||||
ImColor(0.0f, 0.0f, 0.0f),
|
||||
signal_connected_
|
||||
? localization::signal_connected[localization_language_index_].c_str()
|
||||
: localization::signal_disconnected[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -1,13 +1,13 @@
|
||||
#include <random>
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::LocalWindow() {
|
||||
int GuiApplication::LocalWindow() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float local_window_width = io.DisplaySize.x * 0.5f;
|
||||
float local_window_height =
|
||||
@@ -266,7 +266,7 @@ int Render::LocalWindow() {
|
||||
'\0';
|
||||
}
|
||||
|
||||
SaveSettingsIntoCacheFile();
|
||||
settings_.Save();
|
||||
|
||||
memset(new_password_, 0, sizeof(new_password_));
|
||||
|
||||
@@ -0,0 +1,656 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::string TrimConnectionAlias(const char *value) {
|
||||
std::string alias = value ? value : "";
|
||||
|
||||
auto not_space = [](unsigned char ch) { return !std::isspace(ch); };
|
||||
alias.erase(alias.begin(),
|
||||
std::find_if(alias.begin(), alias.end(), not_space));
|
||||
alias.erase(std::find_if(alias.rbegin(), alias.rend(), not_space).base(),
|
||||
alias.end());
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
void SetDarkTextTooltip(const char *text) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.05f, 0.05f, 0.05f, 1.0f));
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::Text("%s", text);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int GuiApplication::RecentConnectionsWindow() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float recent_connection_window_width = io.DisplaySize.x;
|
||||
float recent_connection_window_height =
|
||||
io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f),
|
||||
ImGuiCond_Always);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsWindow",
|
||||
ImVec2(recent_connection_window_width, recent_connection_window_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::recent_connections[localization_language_index_].c_str());
|
||||
|
||||
ShowRecentConnections();
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::ShowRecentConnections() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float recent_connection_panel_width = io.DisplaySize.x * 0.912f;
|
||||
float recent_connection_panel_height = io.DisplaySize.y * 0.29f;
|
||||
float recent_connection_image_height = recent_connection_panel_height * 0.6f;
|
||||
float recent_connection_image_width = recent_connection_image_height * 16 / 9;
|
||||
float recent_connection_sub_container_width =
|
||||
recent_connection_image_width * 1.2f;
|
||||
float recent_connection_sub_container_height =
|
||||
recent_connection_image_height * 1.4f;
|
||||
float recent_connection_button_width = recent_connection_image_width * 0.15f;
|
||||
float recent_connection_button_height =
|
||||
recent_connection_image_height * 0.25f;
|
||||
float recent_connection_footer_height =
|
||||
recent_connection_button_height * 1.18f;
|
||||
float recent_connection_name_width = recent_connection_image_width;
|
||||
const float recent_connection_spacing = recent_connection_image_width * 0.16f;
|
||||
const float recent_connections_content_width =
|
||||
recent_connections_.empty()
|
||||
? 0.0f
|
||||
: recent_connections_.size() * recent_connection_sub_container_width +
|
||||
(recent_connections_.size() - 1) * recent_connection_spacing;
|
||||
const float recent_connections_available_width =
|
||||
recent_connection_panel_width - 2.0f * ImGui::GetStyle().WindowPadding.x;
|
||||
const bool has_horizontal_overflow =
|
||||
recent_connections_content_width > recent_connections_available_width;
|
||||
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
|
||||
|
||||
std::map<std::string, ImVec2> sub_containers_pos;
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg,
|
||||
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
|
||||
const ImGuiWindowFlags container_flags =
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoScrollWithMouse |
|
||||
(has_horizontal_overflow ? ImGuiWindowFlags_AlwaysHorizontalScrollbar
|
||||
: ImGuiWindowFlags_None);
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsContainer",
|
||||
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
|
||||
ImGuiChildFlags_Borders, container_flags);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
size_t recent_connections_count = recent_connections_.size();
|
||||
int count = 0;
|
||||
for (auto &it : recent_connections_) {
|
||||
sub_containers_pos[it.first] = ImGui::GetCursorPos();
|
||||
std::string recent_connection_sub_window_name =
|
||||
"RecentConnectionsSubContainer" + it.first;
|
||||
// recent connections sub container
|
||||
ImGui::BeginChild(recent_connection_sub_window_name.c_str(),
|
||||
ImVec2(recent_connection_sub_container_width,
|
||||
recent_connection_sub_container_height),
|
||||
ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
std::string connection_info = it.first;
|
||||
|
||||
// remote id length is 9
|
||||
// password length is 6
|
||||
// connection_info -> remote_id + 'Y' + host_name + '@' + password
|
||||
// -> remote_id + 'N' + host_name
|
||||
bool invalid_connection_info = false;
|
||||
if (connection_info.size() > 9 && 'Y' == connection_info[9] &&
|
||||
connection_info.size() >= 16) {
|
||||
size_t pos_y = connection_info.find('Y');
|
||||
size_t pos_at = connection_info.find('@');
|
||||
|
||||
if (pos_y == std::string::npos || pos_at == std::string::npos ||
|
||||
pos_y >= pos_at) {
|
||||
LOG_ERROR("Invalid filename");
|
||||
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] &&
|
||||
connection_info.size() >= 10) {
|
||||
size_t pos_n = connection_info.find('N');
|
||||
|
||||
if (pos_n == std::string::npos) {
|
||||
LOG_ERROR("Invalid filename");
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
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 = settings_.RecentConnectionDisplayName(it.second);
|
||||
bool online = device_presence_cache_.IsOnline(it.second.remote_id);
|
||||
|
||||
ImVec2 image_pos =
|
||||
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
|
||||
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
|
||||
|
||||
ImGui::SetCursorPos(image_pos);
|
||||
ImVec2 image_screen_pos = ImGui::GetCursorScreenPos();
|
||||
ImGui::Image(
|
||||
(ImTextureID)(intptr_t)it.second.texture,
|
||||
ImVec2(recent_connection_image_width, recent_connection_image_height));
|
||||
|
||||
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_image_height +
|
||||
(recent_connection_footer_height -
|
||||
recent_connection_button_height) *
|
||||
0.5f);
|
||||
|
||||
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_image_height +
|
||||
(recent_connection_footer_height -
|
||||
recent_connection_button_height) *
|
||||
0.5f);
|
||||
|
||||
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) {
|
||||
const ImVec2 mouse_pos = ImGui::GetMousePos();
|
||||
const bool place_tooltip_on_left = mouse_pos.x > io.DisplaySize.x * 0.7f;
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(mouse_pos.x + (place_tooltip_on_left ? -12.0f : 12.0f),
|
||||
mouse_pos.y - 8.0f),
|
||||
ImGuiCond_Always, ImVec2(place_tooltip_on_left ? 1.0f : 0.0f, 1.0f));
|
||||
ImGui::BeginTooltip();
|
||||
|
||||
ImGui::SetWindowFontScale(0.4f);
|
||||
|
||||
ImGui::Text(
|
||||
"%s: %s",
|
||||
localization::device_name[localization_language_index_].c_str(),
|
||||
display_name.c_str());
|
||||
|
||||
if (!it.second.remote_host_name.empty() &&
|
||||
it.second.remote_host_name != display_name) {
|
||||
ImGui::Text("%s", it.second.remote_host_name.c_str());
|
||||
}
|
||||
|
||||
ImGui::Text("%s: %s",
|
||||
localization::remote_id[localization_language_index_].c_str(),
|
||||
it.second.remote_id.c_str());
|
||||
|
||||
ImGui::Text("%s",
|
||||
(online ? localization::online[localization_language_index_]
|
||||
: localization::offline[localization_language_index_])
|
||||
.c_str());
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImDrawList *draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
// connection name footer
|
||||
{
|
||||
ImVec2 footer_pos =
|
||||
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
|
||||
|
||||
ImVec2 footer_screen_pos =
|
||||
ImVec2(image_screen_pos.x,
|
||||
image_screen_pos.y + recent_connection_image_height);
|
||||
|
||||
ImVec2 footer_screen_end =
|
||||
ImVec2(footer_screen_pos.x + recent_connection_name_width,
|
||||
footer_screen_pos.y + recent_connection_footer_height);
|
||||
|
||||
float footer_rounding = recent_connection_footer_height * 0.16f;
|
||||
|
||||
draw_list->AddRectFilled(footer_screen_pos, footer_screen_end,
|
||||
IM_COL32(0, 0, 0, 40), footer_rounding,
|
||||
ImDrawFlags_RoundCornersBottom);
|
||||
|
||||
const float status_left_margin = recent_connection_footer_height * 0.22f;
|
||||
const float status_gap = recent_connection_footer_height * 0.20f;
|
||||
const float dot_radius = recent_connection_footer_height * 0.18f;
|
||||
|
||||
const ImVec2 dot_center(
|
||||
footer_screen_pos.x + status_left_margin + dot_radius,
|
||||
footer_screen_pos.y + recent_connection_footer_height * 0.5f);
|
||||
const ImU32 dot_color =
|
||||
online ? IM_COL32(34, 197, 94, 255) : IM_COL32(156, 163, 175, 255);
|
||||
|
||||
// Layered halo simulates a radial gradient glow for the online state.
|
||||
if (online) {
|
||||
const float halo_radius = dot_radius * 2.2f;
|
||||
const int halo_layers = 8;
|
||||
for (int i = halo_layers; i > 0; --i) {
|
||||
const float t = static_cast<float>(i) / halo_layers;
|
||||
const float r = dot_radius + (halo_radius - dot_radius) * t;
|
||||
const int alpha = static_cast<int>(14.0f * (1.0f - t) + 4.0f);
|
||||
draw_list->AddCircleFilled(dot_center, r,
|
||||
IM_COL32(74, 222, 128, alpha));
|
||||
}
|
||||
}
|
||||
draw_list->AddCircleFilled(dot_center, dot_radius, dot_color);
|
||||
|
||||
const float status_block_end_x = dot_center.x + dot_radius;
|
||||
|
||||
ImVec2 text_min =
|
||||
ImVec2(status_block_end_x + status_gap, footer_screen_pos.y);
|
||||
|
||||
ImVec2 text_max =
|
||||
ImVec2(card_hovered ? toolbar_screen_pos.x - status_gap
|
||||
: 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);
|
||||
}
|
||||
|
||||
// Toolbar / three buttons, right-aligned in the connection name footer.
|
||||
if (card_hovered) {
|
||||
float toolbar_rounding = recent_connection_button_height * 0.22f;
|
||||
|
||||
draw_list->AddRectFilled(
|
||||
ImVec2(toolbar_screen_pos.x, toolbar_screen_pos.y + 1.0f),
|
||||
ImVec2(toolbar_screen_end.x, toolbar_screen_end.y + 1.0f),
|
||||
IM_COL32(0, 0, 0, 70), toolbar_rounding);
|
||||
|
||||
draw_list->AddRectFilled(toolbar_screen_pos, toolbar_screen_end,
|
||||
IM_COL32(96, 100, 106, 140), toolbar_rounding);
|
||||
|
||||
draw_list->AddRect(toolbar_screen_pos, toolbar_screen_end,
|
||||
IM_COL32(255, 255, 255, 48), toolbar_rounding);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toolbar_rounding);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 0.18f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(0.35f, 0.55f, 0.95f, 0.45f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.95f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
|
||||
// edit alias button
|
||||
{
|
||||
ImGui::SetCursorPos(toolbar_pos);
|
||||
|
||||
std::string edit = ICON_FA_PEN;
|
||||
std::string recent_connection_edit_button_name =
|
||||
edit + "##RecentConnectionAlias" + it.first;
|
||||
|
||||
if (ImGui::Button(recent_connection_edit_button_name.c_str(),
|
||||
ImVec2(recent_connection_button_width,
|
||||
recent_connection_button_height))) {
|
||||
settings_.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 (delete_connection_ && delete_connection_name_ == it.first) {
|
||||
if (!thumbnail_->DeleteThumbnail(it.first)) {
|
||||
settings_.EraseRecentConnectionAlias(it.second.remote_id);
|
||||
settings_.SaveRecentConnectionAliases();
|
||||
reload_recent_connections_ = true;
|
||||
delete_connection_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (count != recent_connections_count - 1) {
|
||||
ImVec2 line_start =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.18f,
|
||||
image_screen_pos.y);
|
||||
ImVec2 line_end =
|
||||
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.18f,
|
||||
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);
|
||||
}
|
||||
|
||||
count++;
|
||||
ImGui::SameLine(0, count != recent_connections_count
|
||||
? recent_connection_spacing
|
||||
: 0.0f);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
if (show_confirm_delete_connection_) {
|
||||
ConfirmDeleteConnection();
|
||||
}
|
||||
if (show_edit_connection_alias_window_) {
|
||||
EditRecentConnectionAliasWindow();
|
||||
}
|
||||
if (show_offline_warning_window_) {
|
||||
OfflineWarningWindow();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::ConfirmDeleteConnection() {
|
||||
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("ConfirmDeleteConnectionWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto connection_status_window_width = ImGui::GetWindowSize().x;
|
||||
auto connection_status_window_height = ImGui::GetWindowSize().y;
|
||||
|
||||
std::string text =
|
||||
localization::confirm_delete_connection[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.33f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
|
||||
// ok
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
|
||||
delete_connection_ = true;
|
||||
show_confirm_delete_connection_ = false;
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
// cancel
|
||||
if (ImGui::Button(
|
||||
localization::cancel[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
delete_connection_ = false;
|
||||
show_confirm_delete_connection_ = false;
|
||||
}
|
||||
|
||||
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
|
||||
ImGui::SetCursorPosX((connection_status_window_width - text_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.2f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::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()) {
|
||||
settings_.EraseRecentConnectionAlias(edit_connection_alias_remote_id_);
|
||||
} else {
|
||||
settings_.SetRecentConnectionAlias(edit_connection_alias_remote_id_,
|
||||
alias);
|
||||
}
|
||||
|
||||
settings_.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 GuiApplication::OfflineWarningWindow() {
|
||||
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("OfflineWarningWindow", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto window_width = ImGui::GetWindowSize().x;
|
||||
auto window_height = ImGui::GetWindowSize().y;
|
||||
|
||||
ImGui::SetCursorPosX(window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(window_height * 0.67f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_offline_warning_window_ = false;
|
||||
}
|
||||
|
||||
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
|
||||
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(window_height * 0.2f);
|
||||
ImGui::Text("%s", offline_warning_text_.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static int InputTextCallback(ImGuiInputTextCallbackData *data);
|
||||
|
||||
int Render::RemoteWindow() {
|
||||
int GuiApplication::RemoteWindow() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float remote_window_width = io.DisplaySize.x * 0.5f;
|
||||
float remote_window_height =
|
||||
@@ -98,10 +98,10 @@ int Render::RemoteWindow() {
|
||||
target_remote_id = props.remote_id;
|
||||
target_password = props.password;
|
||||
{
|
||||
// std::shared_lock lock(client_properties_mutex_);
|
||||
if (client_properties_.find(remote_id) !=
|
||||
client_properties_.end()) {
|
||||
if (!client_properties_[remote_id]->connection_established_) {
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
if (remote_sessions_.find(remote_id) !=
|
||||
remote_sessions_.end()) {
|
||||
if (!remote_sessions_[remote_id]->connection_established_) {
|
||||
should_connect = true;
|
||||
} else {
|
||||
already_connected = true;
|
||||
@@ -134,8 +134,8 @@ int Render::RemoteWindow() {
|
||||
if (elapsed >= 1000) {
|
||||
last_rejoin_check_time_ = now;
|
||||
need_to_rejoin_ = false;
|
||||
// std::shared_lock lock(client_properties_mutex_);
|
||||
for (const auto& [_, props] : client_properties_) {
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (const auto &[_, props] : remote_sessions_) {
|
||||
if (props->rejoin_) {
|
||||
ConnectTo(props->remote_id_, props->remote_password_,
|
||||
props->remember_password_);
|
||||
@@ -164,112 +164,4 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (!bypass_presence_check && !device_presence_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
if (ret != 0) {
|
||||
offline_warning_text_ =
|
||||
localization::device_offline[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
|
||||
} else {
|
||||
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
// std::shared_lock shared_lock(client_properties_mutex_);
|
||||
bool exists =
|
||||
(client_properties_.find(remote_id) != client_properties_.end());
|
||||
// shared_lock.unlock();
|
||||
|
||||
if (!exists) {
|
||||
PeerPtr* peer_to_init = nullptr;
|
||||
std::string local_id;
|
||||
|
||||
{
|
||||
// std::unique_lock unique_lock(client_properties_mutex_);
|
||||
if (client_properties_.find(remote_id) == client_properties_.end()) {
|
||||
client_properties_[remote_id] =
|
||||
std::make_shared<SubStreamWindowProperties>();
|
||||
auto props = client_properties_[remote_id];
|
||||
props->local_id_ = "C-" + std::string(client_id_);
|
||||
props->remote_id_ = remote_id;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 7.0f;
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
if (!props->peer_) {
|
||||
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (auto& display_info : display_info_list_) {
|
||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
|
||||
peer_to_init = props->peer_;
|
||||
local_id = props->local_id_;
|
||||
}
|
||||
}
|
||||
|
||||
if (peer_to_init) {
|
||||
LOG_INFO("[{}] Create peer instance successful", local_id);
|
||||
Init(peer_to_init);
|
||||
LOG_INFO("[{}] Peer init finish", local_id);
|
||||
}
|
||||
}
|
||||
|
||||
int ret = -1;
|
||||
// std::shared_lock read_lock(client_properties_mutex_);
|
||||
auto props = client_properties_[remote_id];
|
||||
if (!props->connection_established_) {
|
||||
props->remember_password_ = remember_password;
|
||||
if (strcmp(password, "") != 0 &&
|
||||
strcmp(password, props->remote_password_) != 0) {
|
||||
strncpy(props->remote_password_, password,
|
||||
sizeof(props->remote_password_) - 1);
|
||||
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
|
||||
}
|
||||
|
||||
std::string remote_id_with_pwd = remote_id + "@" + password;
|
||||
if (props->peer_) {
|
||||
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
|
||||
if (0 == ret) {
|
||||
props->rejoin_ = false;
|
||||
} else {
|
||||
props->rejoin_ = true;
|
||||
need_to_rejoin_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// read_lock.unlock();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -6,11 +6,11 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "file_transfer.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -32,7 +32,8 @@ void ShowControlBarTooltip(const std::string& text) {
|
||||
} // namespace
|
||||
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
if (number == 0)
|
||||
return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ int LossRateDisplay(float loss_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Render::OpenFileDialog(std::string title) {
|
||||
std::string GuiApplication::OpenFileDialog(std::string title) {
|
||||
const char *path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
@@ -69,86 +70,8 @@ std::string Render::OpenFileDialog(std::string title) {
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
void Render::ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState* file_transfer_state =
|
||||
props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
std::filesystem::path file_path = std::filesystem::u8path(path);
|
||||
|
||||
// Get file size
|
||||
std::error_code ec;
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
// Add file to transfer list
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().u8string();
|
||||
info.file_path = file_path; // Store full path for precise matching
|
||||
info.file_size = file_size;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
info.sent_bytes = 0;
|
||||
info.file_id = 0;
|
||||
info.rate_bps = 0;
|
||||
file_transfer_state->file_transfer_list_.push_back(info);
|
||||
}
|
||||
file_transfer_state->file_transfer_window_visible_ = true;
|
||||
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
// Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
} else {
|
||||
StartFileTransfer(props, file_path, file_label, remote_id);
|
||||
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
} else {
|
||||
// Failed to start (race condition: another file started between
|
||||
// check and call) Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO(
|
||||
"File added to queue after race condition: {} ({} files in "
|
||||
"queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
int GuiApplication::ControlBar(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
float button_width = title_bar_height_ * 0.8f;
|
||||
float button_height = title_bar_height_ * 0.8f;
|
||||
float line_padding = title_bar_height_ * 0.12f;
|
||||
@@ -191,7 +114,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::display_id;
|
||||
remote_action.d = i;
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
@@ -215,7 +138,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
auto send_service_command = [&](ServiceCommandFlag flag,
|
||||
const char *log_action) {
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::service_command;
|
||||
@@ -290,9 +213,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
draw_list->AddLine(
|
||||
ImVec2(disable_mouse_x - line_thickness * 0.7f,
|
||||
disable_mouse_y + line_thickness * 0.7f),
|
||||
ImVec2(
|
||||
mouse_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
mouse_y + button_height - line_padding + line_thickness * 0.7f),
|
||||
ImVec2(mouse_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
mouse_y + button_height - line_padding +
|
||||
line_thickness * 0.7f),
|
||||
mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
line_thickness);
|
||||
@@ -341,9 +264,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
draw_list->AddLine(
|
||||
ImVec2(disable_audio_x - line_thickness * 0.7f,
|
||||
disable_audio_y + line_thickness * 0.7f),
|
||||
ImVec2(
|
||||
audio_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
audio_y + button_height - line_padding + line_thickness * 0.7f),
|
||||
ImVec2(audio_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
audio_y + button_height - line_padding +
|
||||
line_thickness * 0.7f),
|
||||
audio_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
line_thickness);
|
||||
@@ -356,7 +279,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
std::string title =
|
||||
localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
ProcessSelectedFile(path, props, file_label_);
|
||||
transfers_.ProcessSelectedFile(path, props, file_label_);
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::select_file[localization_language_index_]);
|
||||
@@ -428,7 +351,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(close_button.c_str(),
|
||||
ImVec2(button_width, button_height))) {
|
||||
CleanupPeer(props);
|
||||
CloseRemoteSession(props);
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::disconnect[localization_language_index_]);
|
||||
@@ -487,7 +410,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
int GuiApplication::NetTrafficStats(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
|
||||
props->control_window_min_height_));
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int GuiApplication::StatusBar() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float status_bar_width = io.DisplaySize.x;
|
||||
float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT;
|
||||
|
||||
static bool a, b, c, d, e;
|
||||
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
|
||||
ImGui::BeginChild("StatusBar", ImVec2(status_bar_width, status_bar_height),
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleColor(2);
|
||||
|
||||
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
|
||||
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
|
||||
ImDrawList *draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
|
||||
ImColor(1.0f, 1.0f, 1.0f), 100);
|
||||
bool tls_cert_error = signal_status_ == SignalStatus::SignalTlsCertError;
|
||||
draw_list->AddCircleFilled(
|
||||
dot_pos, status_bar_height * 0.2f,
|
||||
tls_cert_error ? ImColor(1.0f, 0.65f, 0.0f)
|
||||
: ImColor(signal_connected_ ? 0.0f : 1.0f,
|
||||
signal_connected_ ? 1.0f : 0.0f, 0.0f),
|
||||
100);
|
||||
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
const char *signal_status_text =
|
||||
tls_cert_error
|
||||
? localization::signal_tls_cert_error[localization_language_index_]
|
||||
.c_str()
|
||||
: (signal_connected_
|
||||
? localization::signal_connected[localization_language_index_]
|
||||
.c_str()
|
||||
: localization::signal_disconnected
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
draw_list->AddText(ImVec2(status_bar_width * 0.045f,
|
||||
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)),
|
||||
ImColor(0.0f, 0.0f, 0.0f), signal_status_text);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "rounded_corner_button.h"
|
||||
|
||||
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
|
||||
@@ -9,7 +9,7 @@ constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::TitleBar(bool main_window) {
|
||||
int GuiApplication::TitleBar(bool main_window) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float title_bar_width = title_bar_width_;
|
||||
float title_bar_height = title_bar_height_;
|
||||
@@ -259,8 +259,7 @@ int Render::TitleBar(bool main_window) {
|
||||
float maximize_pos_x = title_bar_width - title_bar_button_width * 1.5f -
|
||||
title_bar_button_height * 0.165f;
|
||||
float maximize_pos_y = title_bar_button_height * 0.33f;
|
||||
std::string window_maximize_button =
|
||||
"##maximize"; // ICON_FA_SQUARE_FULL;
|
||||
std::string window_maximize_button = "##maximize"; // ICON_FA_SQUARE_FULL;
|
||||
if (ImGui::Button(
|
||||
window_maximize_button.c_str(),
|
||||
ImVec2(title_bar_button_width, title_bar_button_height))) {
|
||||
@@ -300,17 +299,12 @@ int Render::TitleBar(bool main_window) {
|
||||
}
|
||||
|
||||
if (close_button_clicked) {
|
||||
#if _WIN32
|
||||
if (enable_minimize_to_tray_) {
|
||||
tray_->MinimizeToTray();
|
||||
} else {
|
||||
#endif
|
||||
const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
|
||||
if (!minimized_to_tray) {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
#if _WIN32
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool Render::OpenUrl(const std::string& url) {
|
||||
bool GuiApplication::OpenUrl(const std::string &url) {
|
||||
#if defined(_WIN32)
|
||||
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
|
||||
if (wide_len <= 0) {
|
||||
@@ -46,7 +46,7 @@ bool Render::OpenUrl(const std::string& url) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void Render::Hyperlink(const std::string& label, const std::string& url,
|
||||
void GuiApplication::Hyperlink(const std::string &label, const std::string &url,
|
||||
const float window_width) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
@@ -65,7 +65,7 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
|
||||
}
|
||||
}
|
||||
|
||||
int Render::AboutWindow() {
|
||||
int GuiApplication::AboutWindow() {
|
||||
if (show_about_window_) {
|
||||
float about_window_width = title_bar_button_width_ * 7.5f;
|
||||
float about_window_height = latest_version_.empty()
|
||||
+40
-11
@@ -1,12 +1,12 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool Render::ConnectionStatusWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
bool GuiApplication::ConnectionStatusWindow(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
bool ret_flag = false;
|
||||
|
||||
@@ -31,8 +31,9 @@ bool Render::ConnectionStatusWindow(
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string text;
|
||||
const ConnectionStatus status = props->connection_status_.load();
|
||||
|
||||
if (ConnectionStatus::Connecting == props->connection_status_) {
|
||||
if (ConnectionStatus::Connecting == status) {
|
||||
text = localization::p2p_connecting[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -48,7 +49,23 @@ bool Render::ConnectionStatusWindow(
|
||||
}
|
||||
ret_flag = true;
|
||||
}
|
||||
} else if (ConnectionStatus::Connected == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Gathering == status) {
|
||||
text = localization::p2p_gathering[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
// cancel
|
||||
if (ImGui::Button(
|
||||
localization::cancel[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
re_enter_remote_id_ = true;
|
||||
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
|
||||
if (props->peer_) {
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
}
|
||||
ret_flag = true;
|
||||
}
|
||||
} else if (ConnectionStatus::Connected == status) {
|
||||
text = localization::p2p_connected[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -58,7 +75,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::Disconnected == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Disconnected == status) {
|
||||
text = localization::p2p_disconnected[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -68,7 +85,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::Failed == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Failed == status) {
|
||||
text = localization::p2p_failed[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -78,7 +95,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::Closed == props->connection_status_) {
|
||||
} else if (ConnectionStatus::Closed == status) {
|
||||
text = localization::p2p_closed[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -88,7 +105,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
}
|
||||
} else if (ConnectionStatus::IncorrectPassword == props->connection_status_) {
|
||||
} else if (ConnectionStatus::IncorrectPassword == status) {
|
||||
if (!password_validating_) {
|
||||
if (password_validating_time_ == 1) {
|
||||
text = localization::input_password[localization_language_index_];
|
||||
@@ -151,8 +168,7 @@ bool Render::ConnectionStatusWindow(
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
}
|
||||
} else if (ConnectionStatus::NoSuchTransmissionId ==
|
||||
props->connection_status_) {
|
||||
} else if (ConnectionStatus::NoSuchTransmissionId == status) {
|
||||
text = localization::no_such_id[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
@@ -164,6 +180,19 @@ bool Render::ConnectionStatusWindow(
|
||||
DestroyPeer(&props->peer_);
|
||||
ret_flag = true;
|
||||
}
|
||||
} else if (ConnectionStatus::RemoteUnavailable == status) {
|
||||
text = localization::device_offline[localization_language_index_];
|
||||
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
|
||||
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
|
||||
// ok
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
|
||||
show_connection_status_window_ = false;
|
||||
re_enter_remote_id_ = true;
|
||||
DestroyPeer(&props->peer_);
|
||||
ret_flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
int GuiApplication::ControlWindow(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
double time_duration =
|
||||
ImGui::GetTime() - props->control_bar_button_pressed_time_;
|
||||
if (props->control_window_width_is_changing_) {
|
||||
@@ -77,8 +78,10 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
pos_x = current_x_rel + delta.x;
|
||||
pos_y = current_y_rel + delta.y;
|
||||
if (pos_x < 0.0f) pos_x = 0.0f;
|
||||
if (pos_y < 0.0f) pos_y = 0.0f;
|
||||
if (pos_x < 0.0f)
|
||||
pos_x = 0.0f;
|
||||
if (pos_y < 0.0f)
|
||||
pos_y = 0.0f;
|
||||
if (pos_x + props->control_window_width_ > container_w)
|
||||
pos_x = container_w - props->control_window_width_;
|
||||
if (pos_y + props->control_window_height_ > container_h)
|
||||
@@ -185,8 +188,10 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
float current_y_rel = props->control_window_pos_.y - container_pos.y;
|
||||
pos_x = current_x_rel;
|
||||
pos_y = current_y_rel;
|
||||
if (pos_x < 0.0f) pos_x = 0.0f;
|
||||
if (pos_y < 0.0f) pos_y = 0.0f;
|
||||
if (pos_x < 0.0f)
|
||||
pos_x = 0.0f;
|
||||
if (pos_y < 0.0f)
|
||||
pos_y = 0.0f;
|
||||
if (pos_x + props->control_window_width_ > container_w)
|
||||
pos_x = container_w - props->control_window_width_;
|
||||
if (pos_y + props->control_window_height_ > container_h)
|
||||
+22
-21
@@ -2,16 +2,17 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
if (number == 0)
|
||||
return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
@@ -28,14 +29,14 @@ int BitrateDisplay(int bitrate) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int Render::FileTransferWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
int GuiApplication::FileTransferWindow(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
FileTransferState *state = &transfers_.state_for(props);
|
||||
state->file_transfer_window_hovered_ = false;
|
||||
|
||||
// Only show window if there are files in transfer list or currently
|
||||
// transferring
|
||||
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
|
||||
std::vector<RemoteSession::FileTransferInfo> file_list;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
file_list = state->file_transfer_list_;
|
||||
@@ -44,19 +45,19 @@ int Render::FileTransferWindow(
|
||||
// Sort file list: Sending first, then Completed, then Queued, then Failed
|
||||
std::sort(
|
||||
file_list.begin(), file_list.end(),
|
||||
[](const SubStreamWindowProperties::FileTransferInfo& a,
|
||||
const SubStreamWindowProperties::FileTransferInfo& b) {
|
||||
[](const RemoteSession::FileTransferInfo &a,
|
||||
const RemoteSession::FileTransferInfo &b) {
|
||||
// Priority: Sending > Completed > Queued > Failed
|
||||
auto get_priority =
|
||||
[](SubStreamWindowProperties::FileTransferStatus status) {
|
||||
[](RemoteSession::FileTransferStatus status) {
|
||||
switch (status) {
|
||||
case SubStreamWindowProperties::FileTransferStatus::Sending:
|
||||
case RemoteSession::FileTransferStatus::Sending:
|
||||
return 0;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Completed:
|
||||
case RemoteSession::FileTransferStatus::Completed:
|
||||
return 1;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Queued:
|
||||
case RemoteSession::FileTransferStatus::Queued:
|
||||
return 2;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Failed:
|
||||
case RemoteSession::FileTransferStatus::Failed:
|
||||
return 3;
|
||||
}
|
||||
return 3;
|
||||
@@ -67,8 +68,8 @@ int Render::FileTransferWindow(
|
||||
// Only show window if file_transfer_window_visible_ is true
|
||||
// Window can be closed by user even during transfer
|
||||
// It will be reopened automatically when:
|
||||
// 1. A file transfer completes (in render_callback.cpp)
|
||||
// 2. A new file starts sending from queue (in render.cpp)
|
||||
// 1. A file transfer completes (via FileTransferManager)
|
||||
// 2. FileTransferManager starts the next queued file.
|
||||
if (!state->file_transfer_window_visible_) {
|
||||
return 0;
|
||||
}
|
||||
@@ -156,26 +157,26 @@ int Render::FileTransferWindow(
|
||||
const char *status_text = "";
|
||||
|
||||
switch (info.status) {
|
||||
case SubStreamWindowProperties::FileTransferStatus::Queued:
|
||||
case RemoteSession::FileTransferStatus::Queued:
|
||||
status_icon = ICON_FA_CLOCK;
|
||||
status_color =
|
||||
ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued
|
||||
status_text =
|
||||
localization::queued[localization_language_index_].c_str();
|
||||
break;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Sending:
|
||||
case RemoteSession::FileTransferStatus::Sending:
|
||||
status_icon = ICON_FA_ARROW_UP;
|
||||
status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f);
|
||||
status_text =
|
||||
localization::sending[localization_language_index_].c_str();
|
||||
break;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Completed:
|
||||
case RemoteSession::FileTransferStatus::Completed:
|
||||
status_icon = ICON_FA_CHECK;
|
||||
status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f);
|
||||
status_text =
|
||||
localization::completed[localization_language_index_].c_str();
|
||||
break;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Failed:
|
||||
case RemoteSession::FileTransferStatus::Failed:
|
||||
status_icon = ICON_FA_XMARK;
|
||||
status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
||||
status_text =
|
||||
@@ -191,7 +192,7 @@ int Render::FileTransferWindow(
|
||||
|
||||
// Progress bar for sending files
|
||||
if (info.status ==
|
||||
SubStreamWindowProperties::FileTransferStatus::Sending &&
|
||||
RemoteSession::FileTransferStatus::Sending &&
|
||||
info.file_size > 0) {
|
||||
float progress = static_cast<float>(info.sent_bytes) /
|
||||
static_cast<float>(info.file_size);
|
||||
@@ -210,7 +211,7 @@ int Render::FileTransferWindow(
|
||||
ImGui::SetCursorPosX(speed_x_pos);
|
||||
BitrateDisplay(static_cast<int>(info.rate_bps));
|
||||
} else if (info.status ==
|
||||
SubStreamWindowProperties::FileTransferStatus::Completed) {
|
||||
RemoteSession::FileTransferStatus::Completed) {
|
||||
// Show completed size
|
||||
char size_str[64];
|
||||
if (info.file_size < 1024) {
|
||||
+4
-14
@@ -1,7 +1,7 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::SettingWindow() {
|
||||
int GuiApplication::SettingWindow() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float portable_y_padding = 0.0f;
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
@@ -356,10 +356,6 @@ int Render::SettingWindow() {
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
#ifndef _WIN32
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
#endif
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
@@ -375,10 +371,6 @@ int Render::SettingWindow() {
|
||||
|
||||
ImGui::Checkbox("##enable_minimize_to_tray_",
|
||||
&enable_minimize_to_tray_);
|
||||
#ifndef _WIN32
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndDisabled();
|
||||
#endif
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
@@ -626,14 +618,12 @@ int Render::SettingWindow() {
|
||||
}
|
||||
enable_daemon_last_ = enable_daemon_;
|
||||
|
||||
#if _WIN32
|
||||
if (enable_minimize_to_tray_) {
|
||||
config_center_->SetMinimizeToTray(true);
|
||||
} else {
|
||||
config_center_->SetMinimizeToTray(false);
|
||||
}
|
||||
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
||||
#endif
|
||||
|
||||
// File transfer save path
|
||||
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
|
||||
@@ -642,12 +632,12 @@ int Render::SettingWindow() {
|
||||
settings_window_pos_reset_ = true;
|
||||
|
||||
// Recreate peer instance
|
||||
LoadSettingsFromCacheFile();
|
||||
settings_.Load();
|
||||
|
||||
// Recreate peer instance
|
||||
if (!stream_window_inited_) {
|
||||
LOG_INFO("Recreate peer instance");
|
||||
CleanupPeers();
|
||||
CloseAllRemoteSessions();
|
||||
CreateConnectionPeer();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::MainWindow() {
|
||||
int GuiApplication::MainWindow() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float local_remote_window_width = io.DisplaySize.x;
|
||||
float local_remote_window_height =
|
||||
@@ -39,13 +39,13 @@ int Render::MainWindow() {
|
||||
StatusBar();
|
||||
|
||||
if (show_connection_status_window_) {
|
||||
// std::unique_lock lock(client_properties_mutex_);
|
||||
for (auto it = client_properties_.begin();
|
||||
it != client_properties_.end();) {
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
for (auto it = remote_sessions_.begin();
|
||||
it != remote_sessions_.end();) {
|
||||
auto &props = it->second;
|
||||
if (focused_remote_id_ == props->remote_id_) {
|
||||
if (ConnectionStatusWindow(props)) {
|
||||
it = client_properties_.erase(it);
|
||||
it = remote_sessions_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
+11
-12
@@ -1,4 +1,4 @@
|
||||
#include "render.h"
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
|
||||
@@ -45,8 +45,7 @@ bool InstallServiceWithElevation() {
|
||||
executable_path.parent_path() / L"crossdesk_session_helper.exe";
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR(
|
||||
"Portable service install failed: service binaries missing, "
|
||||
LOG_ERROR("Portable service install failed: service binaries missing, "
|
||||
"service={}, "
|
||||
"helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
@@ -97,7 +96,7 @@ bool InstallServiceWithElevation() {
|
||||
|
||||
} // namespace
|
||||
|
||||
void Render::CheckPortableWindowsService() {
|
||||
void GuiApplication::CheckPortableWindowsService() {
|
||||
if (portable_service_prompt_checked_) {
|
||||
return;
|
||||
}
|
||||
@@ -116,7 +115,7 @@ void Render::CheckPortableWindowsService() {
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void Render::StartPortableWindowsServiceInstall() {
|
||||
void GuiApplication::StartPortableWindowsServiceInstall() {
|
||||
portable_service_do_not_remind_ = false;
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
@@ -139,13 +138,13 @@ void Render::StartPortableWindowsServiceInstall() {
|
||||
});
|
||||
}
|
||||
|
||||
void Render::JoinPortableWindowsServiceInstallThread() {
|
||||
void GuiApplication::JoinPortableWindowsServiceInstallThread() {
|
||||
if (portable_service_install_thread_.joinable()) {
|
||||
portable_service_install_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
int Render::PortableServiceInstallWindow() {
|
||||
int GuiApplication::PortableServiceInstallWindow() {
|
||||
if (!show_portable_service_install_window_ &&
|
||||
!show_portable_service_prompt_suppressed_window_) {
|
||||
return 0;
|
||||
@@ -162,8 +161,8 @@ int Render::PortableServiceInstallWindow() {
|
||||
const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f,
|
||||
title_bar_button_width_ * 4.6f);
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f,
|
||||
ImVec2(viewport->WorkPos.x +
|
||||
(viewport->WorkSize.x - notice_width) / 2.0f,
|
||||
viewport->WorkPos.y +
|
||||
(viewport->WorkSize.y - notice_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
@@ -219,9 +218,9 @@ int Render::PortableServiceInstallWindow() {
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
|
||||
viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f),
|
||||
ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
|
||||
viewport->WorkPos.y +
|
||||
(viewport->WorkSize.y - window_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
@@ -0,0 +1,187 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool GuiApplication::DrawToggleSwitch(const char *id, bool active,
|
||||
bool enabled) {
|
||||
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
||||
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
||||
const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f;
|
||||
const float KNOB_PADDING = 2.0f;
|
||||
const float KNOB_HEIGHT = TRACK_HEIGHT - 4.0f;
|
||||
const float KNOB_WIDTH = KNOB_HEIGHT * 1.2f;
|
||||
const float KNOB_RADIUS = KNOB_HEIGHT * 0.5f;
|
||||
const float DISABLED_ALPHA = 0.6f;
|
||||
const float KNOB_ALPHA_DISABLED = 0.9f;
|
||||
|
||||
const ImVec4 COLOR_ACTIVE = ImVec4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
const ImVec4 COLOR_ACTIVE_HOVER = ImVec4(0.26f, 0.59f, 0.98f, 1.0f);
|
||||
const ImVec4 COLOR_INACTIVE = ImVec4(0.60f, 0.60f, 0.60f, 1.0f);
|
||||
const ImVec4 COLOR_INACTIVE_HOVER = ImVec4(0.70f, 0.70f, 0.70f, 1.0f);
|
||||
const ImVec4 COLOR_KNOB = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
ImDrawList *draw_list = ImGui::GetWindowDrawList();
|
||||
ImVec2 track_pos = ImGui::GetCursorScreenPos();
|
||||
|
||||
ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT));
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool clicked = ImGui::IsItemClicked() && enabled;
|
||||
|
||||
ImVec4 track_color =
|
||||
active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
||||
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
||||
|
||||
if (!enabled) {
|
||||
track_color.w *= DISABLED_ALPHA;
|
||||
}
|
||||
|
||||
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
||||
ImVec2 track_max =
|
||||
ImVec2(track_pos.x + TRACK_WIDTH, track_pos.y + TRACK_HEIGHT - 0.5f);
|
||||
draw_list->AddRectFilled(track_min, track_max,
|
||||
ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
||||
|
||||
float knob_position = active ? 1.0f : 0.0f;
|
||||
float knob_min_x = track_pos.x + KNOB_PADDING;
|
||||
float knob_max_x = track_pos.x + TRACK_WIDTH - KNOB_WIDTH - KNOB_PADDING;
|
||||
float knob_x = knob_min_x + knob_position * (knob_max_x - knob_min_x);
|
||||
float knob_y = track_pos.y + (TRACK_HEIGHT - KNOB_HEIGHT) * 0.5f;
|
||||
|
||||
ImVec4 knob_color = COLOR_KNOB;
|
||||
if (!enabled) {
|
||||
knob_color.w = KNOB_ALPHA_DISABLED;
|
||||
}
|
||||
|
||||
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
||||
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
||||
draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color),
|
||||
KNOB_RADIUS);
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
int GuiApplication::RequestPermissionWindow() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
|
||||
const bool screen_recording_granted =
|
||||
mac_screen_recording_permission_granted_;
|
||||
const bool accessibility_granted = mac_accessibility_permission_granted_;
|
||||
|
||||
show_request_permission_window_ =
|
||||
!screen_recording_granted || !accessibility_granted;
|
||||
|
||||
if (!show_request_permission_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
float window_width = localization_language_index_ == 0
|
||||
? REQUEST_PERMISSION_WINDOW_WIDTH_CN
|
||||
: REQUEST_PERMISSION_WINDOW_WIDTH_EN;
|
||||
float window_height = localization_language_index_ == 0
|
||||
? REQUEST_PERMISSION_WINDOW_HEIGHT_CN
|
||||
: REQUEST_PERMISSION_WINDOW_HEIGHT_EN;
|
||||
|
||||
float checkbox_padding = localization_language_index_ == 0
|
||||
? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN
|
||||
: REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN;
|
||||
|
||||
ImVec2 center_pos = ImVec2(
|
||||
(viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x,
|
||||
(viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y);
|
||||
ImGui::SetNextWindowPos(center_pos, ImGuiCond_Once);
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
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::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::Begin(
|
||||
localization::request_permissions[localization_language_index_].c_str(),
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoSavedSettings);
|
||||
|
||||
ImGui::SetWindowFontScale(0.3f);
|
||||
|
||||
// use system font
|
||||
if (main_windows_system_chinese_font_ != nullptr) {
|
||||
ImGui::PushFont(main_windows_system_chinese_font_);
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() +
|
||||
5.0f);
|
||||
ImGui::SetCursorPosX(10.0f);
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
localization::permission_required_message[localization_language_index_]
|
||||
.c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// accessibility permission
|
||||
ImGui::SetCursorPosX(10.0f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"1. %s:",
|
||||
localization::accessibility_permission[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::SetCursorPosX(checkbox_padding);
|
||||
if (accessibility_granted) {
|
||||
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
||||
} else {
|
||||
if (DrawToggleSwitch("accessibility_toggle", false, true)) {
|
||||
OpenAccessibilityPreferences();
|
||||
mac_accessibility_permission_requested_ = true;
|
||||
RefreshMacPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// screen recording permission
|
||||
ImGui::SetCursorPosX(10.0f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"2. %s:",
|
||||
localization::screen_recording_permission[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::SetCursorPosX(checkbox_padding);
|
||||
if (screen_recording_granted) {
|
||||
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
||||
} else {
|
||||
if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
|
||||
OpenScreenRecordingPreferences();
|
||||
mac_screen_recording_permission_requested_ = true;
|
||||
RefreshMacPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.45f);
|
||||
|
||||
// pop system font
|
||||
if (main_windows_system_chinese_font_ != nullptr) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleVar(4);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
+2
-2
@@ -5,10 +5,10 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -28,7 +28,7 @@ std::vector<std::string> GetRootEntries() {
|
||||
return roots;
|
||||
}
|
||||
|
||||
int Render::SelfHostedServerWindow() {
|
||||
int GuiApplication::SelfHostedServerWindow() {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
if (show_self_hosted_server_config_window_) {
|
||||
if (self_hosted_server_config_window_pos_reset_) {
|
||||
@@ -1,20 +1,23 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "rounded_corner_button.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
if (number == 0)
|
||||
return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ std::string FormatBytes(uint64_t bytes) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int Render::ServerWindow() {
|
||||
int GuiApplication::ServerWindow() {
|
||||
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
|
||||
ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
@@ -130,7 +133,7 @@ int Render::ServerWindow() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::RemoteClientInfoWindow() {
|
||||
int GuiApplication::RemoteClientInfoWindow() {
|
||||
float remote_client_info_window_width = server_window_width_ * 0.8f;
|
||||
float remote_client_info_window_height =
|
||||
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
|
||||
@@ -149,6 +152,8 @@ int Render::RemoteClientInfoWindow() {
|
||||
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> remote_entries;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
remote_entries.reserve(connection_status_.size());
|
||||
for (const auto &kv : connection_status_) {
|
||||
const auto host_it = connection_host_names_.find(kv.first);
|
||||
@@ -158,6 +163,7 @@ int Render::RemoteClientInfoWindow() {
|
||||
: kv.first;
|
||||
remote_entries.emplace_back(kv.first, display_name);
|
||||
}
|
||||
}
|
||||
|
||||
auto find_display_name_by_remote_id =
|
||||
[&remote_entries](const std::string &remote_id) -> std::string {
|
||||
@@ -220,10 +226,14 @@ int Render::RemoteClientInfoWindow() {
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
|
||||
if (!selected_server_remote_id_.empty()) {
|
||||
ConnectionStatus status = ConnectionStatus::Closed;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
auto it = connection_status_.find(selected_server_remote_id_);
|
||||
const ConnectionStatus status = (it == connection_status_.end())
|
||||
? ConnectionStatus::Closed
|
||||
: it->second;
|
||||
if (it != connection_status_.end()) {
|
||||
status = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
@@ -242,24 +252,21 @@ int Render::RemoteClientInfoWindow() {
|
||||
localization::p2p_connecting[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Disconnected:
|
||||
ImGui::Text("%s",
|
||||
localization::p2p_disconnected[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_disconnected[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Failed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
"%s", localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Closed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_closed[localization_language_index_].c_str());
|
||||
"%s", localization::p2p_closed[localization_language_index_].c_str());
|
||||
break;
|
||||
default:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
"%s", localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -278,12 +285,14 @@ int Render::RemoteClientInfoWindow() {
|
||||
std::string path = OpenFileDialog(title);
|
||||
LOG_INFO("Selected file path: {}", path.c_str());
|
||||
|
||||
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
|
||||
transfers_.ProcessSelectedFile(path, nullptr, file_label_,
|
||||
selected_server_remote_id_);
|
||||
}
|
||||
|
||||
if (file_transfer_.file_transfer_window_visible_) {
|
||||
auto &file_transfer = transfers_.global_state();
|
||||
if (file_transfer.file_transfer_window_visible_) {
|
||||
ImGui::SameLine();
|
||||
const bool is_sending = file_transfer_.file_sending_.load();
|
||||
const bool is_sending = file_transfer.file_sending_.load();
|
||||
|
||||
if (is_sending) {
|
||||
// Simple animation: cycle icon every 0.5s while sending.
|
||||
@@ -299,9 +308,9 @@ int Render::RemoteClientInfoWindow() {
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
|
||||
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
|
||||
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
|
||||
const uint64_t sent_bytes = file_transfer.file_sent_bytes_.load();
|
||||
const uint64_t total_bytes = file_transfer.file_total_bytes_.load();
|
||||
const uint32_t rate_bps = file_transfer.file_send_rate_bps_.load();
|
||||
|
||||
float progress = 0.0f;
|
||||
if (total_bytes > 0) {
|
||||
@@ -311,11 +320,11 @@ int Render::RemoteClientInfoWindow() {
|
||||
}
|
||||
|
||||
std::string current_file_name;
|
||||
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
|
||||
const uint32_t current_file_id = file_transfer.current_file_id_.load();
|
||||
if (current_file_id != 0) {
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_.file_transfer_list_mutex_);
|
||||
for (const auto& info : file_transfer_.file_transfer_list_) {
|
||||
file_transfer.file_transfer_list_mutex_);
|
||||
for (const auto &info : file_transfer.file_transfer_list_) {
|
||||
if (info.file_id == current_file_id) {
|
||||
current_file_name = info.file_name;
|
||||
break;
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "application/gui_application.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void Render::DrawConnectionStatusText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
void GuiApplication::DrawConnectionStatusText(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
std::string text;
|
||||
switch (props->connection_status_) {
|
||||
switch (props->connection_status_.load()) {
|
||||
case ConnectionStatus::Disconnected:
|
||||
text = localization::p2p_disconnected[localization_language_index_];
|
||||
break;
|
||||
@@ -31,10 +31,10 @@ void Render::DrawConnectionStatusText(
|
||||
}
|
||||
}
|
||||
|
||||
void Render::DrawReceivingScreenText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
void GuiApplication::DrawReceivingScreenText(
|
||||
std::shared_ptr<RemoteSession> &props) {
|
||||
if (!props->connection_established_ ||
|
||||
props->connection_status_ != ConnectionStatus::Connected) {
|
||||
props->connection_status_.load() != ConnectionStatus::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,12 +59,12 @@ void Render::DrawReceivingScreenText(
|
||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str());
|
||||
}
|
||||
|
||||
void Render::CloseTab(decltype(client_properties_)::iterator& it) {
|
||||
// std::unique_lock lock(client_properties_mutex_);
|
||||
if (it != client_properties_.end()) {
|
||||
CleanupPeer(it->second);
|
||||
it = client_properties_.erase(it);
|
||||
if (client_properties_.empty()) {
|
||||
void GuiApplication::CloseTab(decltype(remote_sessions_)::iterator &it) {
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
if (it != remote_sessions_.end()) {
|
||||
CloseRemoteSession(it->second);
|
||||
it = remote_sessions_.erase(it);
|
||||
if (remote_sessions_.empty()) {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
@@ -72,7 +72,7 @@ void Render::CloseTab(decltype(client_properties_)::iterator& it) {
|
||||
}
|
||||
}
|
||||
|
||||
int Render::StreamWindow() {
|
||||
int GuiApplication::StreamWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_),
|
||||
ImGuiCond_Always);
|
||||
@@ -117,22 +117,22 @@ int Render::StreamWindow() {
|
||||
ImGuiTabBarFlags_AutoSelectNewTabs)) {
|
||||
is_tab_bar_hovered_ = ImGui::IsWindowHovered();
|
||||
|
||||
// std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto it = client_properties_.begin();
|
||||
it != client_properties_.end();) {
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto it = remote_sessions_.begin();
|
||||
it != remote_sessions_.end();) {
|
||||
auto &props = it->second;
|
||||
if (!props->tab_opened_) {
|
||||
std::string remote_id_to_close = props->remote_id_;
|
||||
// lock.unlock();
|
||||
{
|
||||
// std::unique_lock unique_lock(client_properties_mutex_);
|
||||
auto close_it = client_properties_.find(remote_id_to_close);
|
||||
if (close_it != client_properties_.end()) {
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
auto close_it = remote_sessions_.find(remote_id_to_close);
|
||||
if (close_it != remote_sessions_.end()) {
|
||||
CloseTab(close_it);
|
||||
}
|
||||
}
|
||||
// lock.lock();
|
||||
it = client_properties_.begin();
|
||||
it = remote_sessions_.begin();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,13 +180,13 @@ int Render::StreamWindow() {
|
||||
std::string remote_id_to_erase = props->remote_id_;
|
||||
// lock.unlock();
|
||||
{
|
||||
// std::unique_lock unique_lock(client_properties_mutex_);
|
||||
auto erase_it = client_properties_.find(remote_id_to_erase);
|
||||
if (erase_it != client_properties_.end()) {
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
auto erase_it = remote_sessions_.find(remote_id_to_erase);
|
||||
if (erase_it != remote_sessions_.end()) {
|
||||
// Ensure we flush pending STREAM_REFRESH_EVENT events and
|
||||
// clean up peer resources before erasing the entry, otherwise
|
||||
// SDL events may still hold raw pointers to freed
|
||||
// SubStreamWindowProperties (including video_frame_mutex_),
|
||||
// RemoteSession (including video_frame_mutex_),
|
||||
// leading to std::system_error when locking.
|
||||
CloseTab(erase_it);
|
||||
}
|
||||
@@ -194,7 +194,7 @@ int Render::StreamWindow() {
|
||||
// lock.lock();
|
||||
ImGui::End();
|
||||
ImGui::EndTabItem();
|
||||
it = client_properties_.begin();
|
||||
it = remote_sessions_.begin();
|
||||
continue;
|
||||
} else {
|
||||
DrawConnectionStatusText(props);
|
||||
@@ -209,14 +209,14 @@ int Render::StreamWindow() {
|
||||
std::string remote_id_to_close = props->remote_id_;
|
||||
// lock.unlock();
|
||||
{
|
||||
// std::unique_lock unique_lock(client_properties_mutex_);
|
||||
auto close_it = client_properties_.find(remote_id_to_close);
|
||||
if (close_it != client_properties_.end()) {
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
auto close_it = remote_sessions_.find(remote_id_to_close);
|
||||
if (close_it != remote_sessions_.end()) {
|
||||
CloseTab(close_it);
|
||||
}
|
||||
}
|
||||
// lock.lock();
|
||||
it = client_properties_.begin();
|
||||
it = remote_sessions_.begin();
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
@@ -228,22 +228,22 @@ int Render::StreamWindow() {
|
||||
|
||||
ImGui::End(); // End TabBar
|
||||
} else {
|
||||
// std::shared_lock lock(client_properties_mutex_);
|
||||
for (auto it = client_properties_.begin();
|
||||
it != client_properties_.end();) {
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto it = remote_sessions_.begin();
|
||||
it != remote_sessions_.end();) {
|
||||
auto &props = it->second;
|
||||
if (!props->tab_opened_) {
|
||||
std::string remote_id_to_close = props->remote_id_;
|
||||
// lock.unlock();
|
||||
{
|
||||
// std::unique_lock unique_lock(client_properties_mutex_);
|
||||
auto close_it = client_properties_.find(remote_id_to_close);
|
||||
if (close_it != client_properties_.end()) {
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
auto close_it = remote_sessions_.find(remote_id_to_close);
|
||||
if (close_it != remote_sessions_.end()) {
|
||||
CloseTab(close_it);
|
||||
}
|
||||
}
|
||||
// lock.lock();
|
||||
it = client_properties_.begin();
|
||||
it = remote_sessions_.begin();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -284,14 +284,14 @@ int Render::StreamWindow() {
|
||||
std::string remote_id_to_erase = props->remote_id_;
|
||||
// lock.unlock();
|
||||
{
|
||||
// std::unique_lock unique_lock(client_properties_mutex_);
|
||||
auto erase_it = client_properties_.find(remote_id_to_erase);
|
||||
if (erase_it != client_properties_.end()) {
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
auto erase_it = remote_sessions_.find(remote_id_to_erase);
|
||||
if (erase_it != remote_sessions_.end()) {
|
||||
CloseTab(erase_it);
|
||||
}
|
||||
}
|
||||
// lock.lock();
|
||||
it = client_properties_.begin();
|
||||
it = remote_sessions_.begin();
|
||||
continue;
|
||||
} else {
|
||||
DrawConnectionStatusText(props);
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "application/gui_application.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -50,7 +50,7 @@ std::string CleanMarkdown(const std::string& markdown) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int Render::UpdateNotificationWindow() {
|
||||
int GuiApplication::UpdateNotificationWindow() {
|
||||
if (show_update_notification_window_ && update_available_) {
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500;
|
||||
|
||||
void OpenPrivacyPreferences(const char* pane) {
|
||||
if (pane == nullptr || pane[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string command =
|
||||
"open \"x-apple.systempreferences:com.apple.preference.security?";
|
||||
command += pane;
|
||||
command += "\"";
|
||||
system(command.c_str());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
||||
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
||||
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
||||
const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f;
|
||||
const float KNOB_PADDING = 2.0f;
|
||||
const float KNOB_HEIGHT = TRACK_HEIGHT - 4.0f;
|
||||
const float KNOB_WIDTH = KNOB_HEIGHT * 1.2f;
|
||||
const float KNOB_RADIUS = KNOB_HEIGHT * 0.5f;
|
||||
const float DISABLED_ALPHA = 0.6f;
|
||||
const float KNOB_ALPHA_DISABLED = 0.9f;
|
||||
|
||||
const ImVec4 COLOR_ACTIVE = ImVec4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
const ImVec4 COLOR_ACTIVE_HOVER = ImVec4(0.26f, 0.59f, 0.98f, 1.0f);
|
||||
const ImVec4 COLOR_INACTIVE = ImVec4(0.60f, 0.60f, 0.60f, 1.0f);
|
||||
const ImVec4 COLOR_INACTIVE_HOVER = ImVec4(0.70f, 0.70f, 0.70f, 1.0f);
|
||||
const ImVec4 COLOR_KNOB = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
ImVec2 track_pos = ImGui::GetCursorScreenPos();
|
||||
|
||||
ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT));
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool clicked = ImGui::IsItemClicked() && enabled;
|
||||
|
||||
ImVec4 track_color =
|
||||
active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
||||
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
||||
|
||||
if (!enabled) {
|
||||
track_color.w *= DISABLED_ALPHA;
|
||||
}
|
||||
|
||||
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
||||
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH,
|
||||
track_pos.y + TRACK_HEIGHT - 0.5f);
|
||||
draw_list->AddRectFilled(track_min, track_max,
|
||||
ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
||||
|
||||
float knob_position = active ? 1.0f : 0.0f;
|
||||
float knob_min_x = track_pos.x + KNOB_PADDING;
|
||||
float knob_max_x = track_pos.x + TRACK_WIDTH - KNOB_WIDTH - KNOB_PADDING;
|
||||
float knob_x = knob_min_x + knob_position * (knob_max_x - knob_min_x);
|
||||
float knob_y = track_pos.y + (TRACK_HEIGHT - KNOB_HEIGHT) * 0.5f;
|
||||
|
||||
ImVec4 knob_color = COLOR_KNOB;
|
||||
if (!enabled) {
|
||||
knob_color.w = KNOB_ALPHA_DISABLED;
|
||||
}
|
||||
|
||||
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
||||
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
||||
draw_list->AddRectFilled(knob_min, knob_max,
|
||||
ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
||||
|
||||
return clicked;
|
||||
}
|
||||
|
||||
bool Render::CheckScreenRecordingPermission() {
|
||||
// CGPreflightScreenCaptureAccess is available on macOS 10.15+
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool granted = CGPreflightScreenCaptureAccess();
|
||||
return granted;
|
||||
}
|
||||
// for older macOS versions, assume permission is granted
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Render::CheckAccessibilityPermission() {
|
||||
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO};
|
||||
bool trusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
return trusted;
|
||||
}
|
||||
|
||||
void Render::OpenAccessibilityPreferences() {
|
||||
if (!mac_accessibility_permission_requested_) {
|
||||
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_Accessibility");
|
||||
}
|
||||
}
|
||||
|
||||
void Render::OpenScreenRecordingPreferences() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
if (!mac_screen_recording_permission_requested_) {
|
||||
CGRequestScreenCaptureAccess();
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||
}
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||
}
|
||||
}
|
||||
|
||||
void Render::RefreshMacPermissionStatus(bool force) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (!force && mac_permission_status_initialized_ &&
|
||||
now - mac_permission_last_check_tick_ <
|
||||
kPermissionRefreshIntervalVisibleMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool old_screen_recording_granted =
|
||||
mac_screen_recording_permission_granted_;
|
||||
const bool old_accessibility_granted = mac_accessibility_permission_granted_;
|
||||
|
||||
mac_screen_recording_permission_granted_ =
|
||||
CheckScreenRecordingPermission();
|
||||
mac_accessibility_permission_granted_ = CheckAccessibilityPermission();
|
||||
mac_permission_last_check_tick_ = now;
|
||||
mac_permission_status_initialized_ = true;
|
||||
|
||||
if (old_screen_recording_granted !=
|
||||
mac_screen_recording_permission_granted_ ||
|
||||
old_accessibility_granted != mac_accessibility_permission_granted_) {
|
||||
LOG_INFO("macOS permission status: screen_recording={}, accessibility={}",
|
||||
mac_screen_recording_permission_granted_,
|
||||
mac_accessibility_permission_granted_);
|
||||
}
|
||||
}
|
||||
|
||||
bool Render::EnsureMacScreenRecordingPermission() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
if (mac_screen_recording_permission_granted_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_request_permission_window_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Render::EnsureMacAccessibilityPermission() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
if (mac_accessibility_permission_granted_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_request_permission_window_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int Render::RequestPermissionWindow() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
|
||||
const bool screen_recording_granted =
|
||||
mac_screen_recording_permission_granted_;
|
||||
const bool accessibility_granted = mac_accessibility_permission_granted_;
|
||||
|
||||
show_request_permission_window_ =
|
||||
!screen_recording_granted || !accessibility_granted;
|
||||
|
||||
if (!show_request_permission_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
float window_width = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_WIDTH_CN
|
||||
: REQUEST_PERMISSION_WINDOW_WIDTH_EN;
|
||||
float window_height = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_HEIGHT_CN
|
||||
: REQUEST_PERMISSION_WINDOW_HEIGHT_EN;
|
||||
|
||||
float checkbox_padding = localization_language_index_ == 0
|
||||
? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN
|
||||
: REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN;
|
||||
|
||||
ImVec2 center_pos = ImVec2((viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x,
|
||||
(viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y);
|
||||
ImGui::SetNextWindowPos(center_pos, ImGuiCond_Once);
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always);
|
||||
|
||||
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::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::Begin(
|
||||
localization::request_permissions[localization_language_index_].c_str(), nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings);
|
||||
|
||||
ImGui::SetWindowFontScale(0.3f);
|
||||
|
||||
// use system font
|
||||
if (main_windows_system_chinese_font_ != nullptr) {
|
||||
ImGui::PushFont(main_windows_system_chinese_font_);
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() + 5.0f);
|
||||
ImGui::SetCursorPosX(10.0f);
|
||||
ImGui::TextWrapped(
|
||||
"%s", localization::permission_required_message[localization_language_index_].c_str());
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// accessibility permission
|
||||
ImGui::SetCursorPosX(10.0f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("1. %s:",
|
||||
localization::accessibility_permission[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::SetCursorPosX(checkbox_padding);
|
||||
if (accessibility_granted) {
|
||||
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
||||
} else {
|
||||
if (DrawToggleSwitch("accessibility_toggle", false, true)) {
|
||||
OpenAccessibilityPreferences();
|
||||
mac_accessibility_permission_requested_ = true;
|
||||
RefreshMacPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// screen recording permission
|
||||
ImGui::SetCursorPosX(10.0f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("2. %s:",
|
||||
localization::screen_recording_permission[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::SetCursorPosX(checkbox_padding);
|
||||
if (screen_recording_granted) {
|
||||
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
||||
} else {
|
||||
if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
|
||||
OpenScreenRecordingPreferences();
|
||||
mac_screen_recording_permission_requested_ = true;
|
||||
RefreshMacPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.45f);
|
||||
|
||||
// pop system font
|
||||
if (main_windows_system_chinese_font_ != nullptr) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleVar(4);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -111,6 +111,7 @@ int ScreenCapturerDxgi::Resume(int monitor_index) {
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
|
||||
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
|
||||
return -1;
|
||||
@@ -121,6 +122,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
if (!CreateDuplicationForMonitor(monitor_index_)) {
|
||||
LOG_ERROR("DXGI: create duplication failed for monitor {}",
|
||||
monitor_index_.load());
|
||||
paused_ = false; // Reset paused_ on failure
|
||||
return -2;
|
||||
}
|
||||
paused_ = false;
|
||||
@@ -130,6 +132,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
|
||||
}
|
||||
|
||||
int ScreenCapturerDxgi::ResetToInitialMonitor() {
|
||||
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||
if (display_info_list_.empty()) return -1;
|
||||
int target = initial_monitor_index_;
|
||||
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
|
||||
@@ -245,6 +248,7 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||
std::lock_guard<std::mutex> lock(switch_mutex_);
|
||||
ReleaseDuplication();
|
||||
int current_monitor = monitor_index_.load();
|
||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||
@@ -374,8 +378,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
even_width, even_width, even_height);
|
||||
|
||||
if (callback_) {
|
||||
int idx = monitor_index_.load();
|
||||
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
display_info_list_[monitor_index_].name.c_str());
|
||||
display_info_list_[idx].name.c_str());
|
||||
} else {
|
||||
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
}
|
||||
}
|
||||
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
|
||||
@@ -72,6 +72,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
std::thread thread_;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_ = nullptr;
|
||||
std::mutex switch_mutex_;
|
||||
|
||||
unsigned char* nv12_frame_ = nullptr;
|
||||
int nv12_width_ = 0;
|
||||
|
||||
@@ -148,7 +148,14 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& di = display_info_list_[monitor_index_];
|
||||
int idx = monitor_index_.load();
|
||||
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
continue;
|
||||
}
|
||||
const auto& di = display_info_list_[idx];
|
||||
int left = di.left;
|
||||
int top = di.top;
|
||||
int width = di.width & ~1;
|
||||
|
||||
@@ -306,7 +306,7 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (monitor_index >= display_info_list_.size()) {
|
||||
if (monitor_index < 0 || monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_ERROR("Invalid monitor index: {}", monitor_index);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ struct SecureDesktopServiceStatus {
|
||||
DWORD active_session_id = 0xFFFFFFFF;
|
||||
DWORD error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
@@ -134,12 +135,16 @@ class WgcPluginCapturer final : public ScreenCapturer {
|
||||
|
||||
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
||||
bool show_cursor,
|
||||
const std::string& stage) {
|
||||
const std::string& stage,
|
||||
const std::string& desktop) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top
|
||||
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0);
|
||||
if (!stage.empty()) {
|
||||
stream << ":" << stage;
|
||||
if (!desktop.empty()) {
|
||||
stream << ":" << desktop;
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
@@ -147,13 +152,17 @@ std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
||||
std::string BuildSecureCaptureStartCommand(int left, int top, int width,
|
||||
int height, bool show_cursor,
|
||||
int fps,
|
||||
const std::string& stage) {
|
||||
const std::string& stage,
|
||||
const std::string& desktop) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputCaptureStartCommandPrefix << left << ":" << top
|
||||
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0)
|
||||
<< ":" << fps;
|
||||
if (!stage.empty()) {
|
||||
stream << ":" << stage;
|
||||
if (!desktop.empty()) {
|
||||
stream << ":" << desktop;
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
@@ -284,6 +293,7 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
||||
json.value("last_session_event", std::string()))) {
|
||||
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status->interactive_stage = "user-desktop";
|
||||
status->interactive_desktop.clear();
|
||||
status->capture_active = false;
|
||||
return true;
|
||||
}
|
||||
@@ -291,6 +301,8 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
||||
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status->helper_running = json.value("secure_input_helper_running", false);
|
||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||
status->interactive_desktop =
|
||||
json.value("interactive_input_desktop", std::string());
|
||||
const bool secure_desktop_active =
|
||||
json.value("interactive_secure_desktop_active",
|
||||
json.value("secure_desktop_active", false));
|
||||
@@ -358,6 +370,7 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
int width, int height, bool show_cursor,
|
||||
const std::string& stage,
|
||||
const std::string& desktop,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* captured_width_out,
|
||||
int* captured_height_out,
|
||||
@@ -368,7 +381,8 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
}
|
||||
|
||||
const std::string command =
|
||||
BuildSecureCaptureCommand(left, top, width, height, show_cursor, stage);
|
||||
BuildSecureCaptureCommand(left, top, width, height, show_cursor, stage,
|
||||
desktop);
|
||||
std::vector<uint8_t> response;
|
||||
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||
error_out)) {
|
||||
@@ -823,6 +837,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
||||
secure_shared_fps_ = 0;
|
||||
secure_shared_show_cursor_ = true;
|
||||
secure_shared_stage_.clear();
|
||||
secure_shared_desktop_.clear();
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
||||
@@ -952,7 +967,8 @@ bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
|
||||
|
||||
bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
DWORD session_id, int left, int top, int width, int height,
|
||||
const std::string& stage, bool show_cursor, int fps,
|
||||
const std::string& stage, const std::string& desktop, bool show_cursor,
|
||||
int fps,
|
||||
std::string* error_out) {
|
||||
const size_t payload_size = static_cast<size_t>(width) * height * 3 / 2;
|
||||
const size_t mapping_size =
|
||||
@@ -968,7 +984,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
secure_shared_session_id_ == session_id &&
|
||||
secure_shared_left_ == left && secure_shared_top_ == top &&
|
||||
secure_shared_width_ == width && secure_shared_height_ == height &&
|
||||
secure_shared_stage_ == stage &&
|
||||
secure_shared_stage_ == stage && secure_shared_desktop_ == desktop &&
|
||||
secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps &&
|
||||
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
return true;
|
||||
@@ -978,7 +994,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
|
||||
const std::string command =
|
||||
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
|
||||
stage);
|
||||
stage, desktop);
|
||||
std::vector<uint8_t> response;
|
||||
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||
error_out)) {
|
||||
@@ -1002,6 +1018,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
secure_shared_show_cursor_ = show_cursor;
|
||||
secure_shared_fps_ = fps;
|
||||
secure_shared_stage_ = stage;
|
||||
secure_shared_desktop_ = desktop;
|
||||
|
||||
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
StopSecureDesktopSharedCapture(session_id);
|
||||
@@ -1161,8 +1178,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
|
||||
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
|
||||
width, height,
|
||||
status.interactive_stage, show_cursor,
|
||||
shared_fps, &error_message) &&
|
||||
status.interactive_stage,
|
||||
status.interactive_desktop,
|
||||
show_cursor, shared_fps,
|
||||
&error_message) &&
|
||||
ReadSecureDesktopSharedFrame(
|
||||
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
|
||||
&captured_width, &captured_height, &error_message)) {
|
||||
@@ -1177,6 +1196,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
|
||||
width, height, show_cursor,
|
||||
status.interactive_stage,
|
||||
status.interactive_desktop,
|
||||
&secure_frame, &captured_width,
|
||||
&captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
|
||||
@@ -75,6 +75,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
int secure_shared_fps_ = 0;
|
||||
bool secure_shared_show_cursor_ = true;
|
||||
std::string secure_shared_stage_;
|
||||
std::string secure_shared_desktop_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
@@ -87,6 +88,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top,
|
||||
int width, int height,
|
||||
const std::string& stage,
|
||||
const std::string& desktop,
|
||||
bool show_cursor, int fps,
|
||||
std::string* error_out);
|
||||
void StopSecureDesktopSharedCapture(DWORD session_id);
|
||||
|
||||
@@ -64,6 +64,7 @@ struct ScopedEnvironmentBlock {
|
||||
LPVOID environment = nullptr;
|
||||
};
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& value);
|
||||
std::string WideToUtf8(const std::wstring& value);
|
||||
|
||||
std::wstring GetCurrentExecutablePathW() {
|
||||
@@ -358,24 +359,32 @@ std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel,
|
||||
|
||||
std::string BuildSecureInputHelperKeyboardCommand(
|
||||
int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
const std::string& interactive_stage) {
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":"
|
||||
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
|
||||
if (!interactive_stage.empty()) {
|
||||
stream << ":" << interactive_stage;
|
||||
if (!interactive_desktop.empty()) {
|
||||
stream << ":" << interactive_desktop;
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string BuildSecureInputHelperMouseCommand(
|
||||
int x, int y, int wheel, int flag,
|
||||
const std::string& interactive_stage) {
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
|
||||
<< wheel << ":" << flag;
|
||||
if (!interactive_stage.empty()) {
|
||||
stream << ":" << interactive_stage;
|
||||
if (!interactive_desktop.empty()) {
|
||||
stream << ":" << interactive_desktop;
|
||||
}
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
@@ -592,15 +601,23 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
|
||||
}
|
||||
|
||||
bool IsCredentialUiVisible(bool prelogin, bool session_locked,
|
||||
bool logon_ui_running,
|
||||
bool logon_ui_running, bool consent_ui_visible,
|
||||
bool input_desktop_available,
|
||||
bool secure_desktop_active) {
|
||||
return (prelogin || session_locked || secure_desktop_active) &&
|
||||
(logon_ui_running || !input_desktop_available);
|
||||
return consent_ui_visible ||
|
||||
((prelogin || session_locked || secure_desktop_active) &&
|
||||
(logon_ui_running || !input_desktop_available));
|
||||
}
|
||||
|
||||
std::wstring SecureInputHelperDesktopForStage(
|
||||
const std::string& interactive_stage) {
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
if (!interactive_desktop.empty()) {
|
||||
std::wstring interactive_desktop_w = Utf8ToWide(interactive_desktop);
|
||||
if (!interactive_desktop_w.empty()) {
|
||||
return L"winsta0\\" + interactive_desktop_w;
|
||||
}
|
||||
}
|
||||
if (interactive_stage == "credential-ui" ||
|
||||
interactive_stage == "secure-desktop") {
|
||||
return L"winsta0\\Winlogon";
|
||||
@@ -663,7 +680,12 @@ bool QuerySessionLockState(DWORD session_id, bool* session_locked_out) {
|
||||
return success;
|
||||
}
|
||||
|
||||
bool IsLogonUiRunningInSession(DWORD session_id) {
|
||||
bool IsProcessRunningInSession(const wchar_t* executable_name,
|
||||
DWORD session_id) {
|
||||
if (executable_name == nullptr || executable_name[0] == L'\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
@@ -674,7 +696,7 @@ bool IsLogonUiRunningInSession(DWORD session_id) {
|
||||
bool found = false;
|
||||
if (Process32FirstW(snapshot, &entry)) {
|
||||
do {
|
||||
if (_wcsicmp(entry.szExeFile, L"LogonUI.exe") != 0) {
|
||||
if (_wcsicmp(entry.szExeFile, executable_name) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -691,6 +713,14 @@ bool IsLogonUiRunningInSession(DWORD session_id) {
|
||||
return found;
|
||||
}
|
||||
|
||||
bool IsLogonUiRunningInSession(DWORD session_id) {
|
||||
return IsProcessRunningInSession(L"LogonUI.exe", session_id);
|
||||
}
|
||||
|
||||
bool IsConsentUiRunningInSession(DWORD session_id) {
|
||||
return IsProcessRunningInSession(L"Consent.exe", session_id);
|
||||
}
|
||||
|
||||
InputDesktopInfo GetInputDesktopInfo() {
|
||||
InputDesktopInfo info;
|
||||
HDESK desktop = OpenInputDesktop(0, FALSE, GENERIC_READ);
|
||||
@@ -724,6 +754,24 @@ InputDesktopInfo GetInputDesktopInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int size_needed =
|
||||
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
|
||||
if (size_needed <= 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(size_needed), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, result.data(),
|
||||
size_needed);
|
||||
result.pop_back();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string WideToUtf8(const std::wstring& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
@@ -1024,6 +1072,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
secure_input_helper_last_error_code_ = 0;
|
||||
session_locked_ = false;
|
||||
logon_ui_visible_ = false;
|
||||
consent_ui_visible_ = false;
|
||||
prelogin_ = false;
|
||||
secure_desktop_active_ = false;
|
||||
input_desktop_available_ = false;
|
||||
@@ -1032,6 +1081,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
session_helper_report_input_desktop_available_ = false;
|
||||
session_helper_report_lock_app_visible_ = false;
|
||||
session_helper_report_logon_ui_visible_ = false;
|
||||
session_helper_report_consent_ui_visible_ = false;
|
||||
session_helper_report_secure_desktop_active_ = false;
|
||||
session_helper_report_credential_ui_visible_ = false;
|
||||
session_helper_report_unlock_ui_visible_ = false;
|
||||
@@ -1056,6 +1106,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
session_helper_report_interactive_stage_.clear();
|
||||
secure_input_helper_last_error_.clear();
|
||||
secure_input_helper_interactive_stage_.clear();
|
||||
secure_input_helper_interactive_desktop_.clear();
|
||||
last_session_event_type_ = 0;
|
||||
last_session_event_session_id_ = active_session_id_;
|
||||
RefreshSessionState();
|
||||
@@ -1283,6 +1334,7 @@ void CrossDeskServiceHost::RefreshSessionState() {
|
||||
process_session_id_ = process_session_id;
|
||||
}
|
||||
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
|
||||
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
|
||||
InputDesktopInfo desktop_info = GetInputDesktopInfo();
|
||||
input_desktop_available_ = desktop_info.available;
|
||||
input_desktop_error_code_ = desktop_info.error_code;
|
||||
@@ -1312,6 +1364,7 @@ void CrossDeskServiceHost::ResetSessionHelperReportedStateLocked(
|
||||
session_helper_report_input_desktop_.clear();
|
||||
session_helper_report_lock_app_visible_ = false;
|
||||
session_helper_report_logon_ui_visible_ = false;
|
||||
session_helper_report_consent_ui_visible_ = false;
|
||||
session_helper_report_secure_desktop_active_ = false;
|
||||
session_helper_report_credential_ui_visible_ = false;
|
||||
session_helper_report_unlock_ui_visible_ = false;
|
||||
@@ -1334,10 +1387,12 @@ bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
|
||||
const bool service_host_credential_ui_visible =
|
||||
!session_helper_status_ok_ &&
|
||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||
consent_ui_visible_,
|
||||
input_desktop_available_,
|
||||
secure_desktop_active_);
|
||||
return IsSasSecureDesktopGraceActiveLocked() || prelogin_ ||
|
||||
secure_desktop_active_ || service_host_credential_ui_visible ||
|
||||
session_helper_report_consent_ui_visible_ ||
|
||||
session_helper_report_credential_ui_visible_ ||
|
||||
session_helper_report_secure_desktop_active_ ||
|
||||
session_helper_report_unlock_ui_visible_ ||
|
||||
@@ -1392,6 +1447,7 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
|
||||
const bool service_host_credential_ui_visible =
|
||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||
consent_ui_visible_,
|
||||
input_desktop_available_,
|
||||
secure_desktop_active_);
|
||||
return DetermineInteractiveStage(
|
||||
@@ -1401,6 +1457,33 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
session_helper_report_secure_desktop_active_ || secure_desktop_active_);
|
||||
}
|
||||
|
||||
std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked(
|
||||
const std::string& interactive_stage) const {
|
||||
if (interactive_stage == "lock-screen") {
|
||||
return "Default";
|
||||
}
|
||||
|
||||
if (session_helper_status_ok_ &&
|
||||
session_helper_report_input_desktop_available_ &&
|
||||
!session_helper_report_input_desktop_.empty() &&
|
||||
(interactive_stage == "credential-ui" ||
|
||||
session_helper_report_consent_ui_visible_)) {
|
||||
return session_helper_report_input_desktop_;
|
||||
}
|
||||
|
||||
if (input_desktop_available_ && !input_desktop_name_.empty() &&
|
||||
(interactive_stage == "credential-ui" || consent_ui_visible_)) {
|
||||
return input_desktop_name_;
|
||||
}
|
||||
|
||||
if (interactive_stage == "credential-ui" ||
|
||||
interactive_stage == "secure-desktop") {
|
||||
return "Winlogon";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::wstring CrossDeskServiceHost::GetSessionHelperPath() const {
|
||||
std::wstring current_executable = GetCurrentExecutablePathW();
|
||||
if (current_executable.empty()) {
|
||||
@@ -1491,6 +1574,7 @@ void CrossDeskServiceHost::ReapSecureInputHelper() {
|
||||
secure_input_helper_exit_code_ = exit_code;
|
||||
secure_input_helper_started_at_tick_ = 0;
|
||||
secure_input_helper_interactive_stage_.clear();
|
||||
secure_input_helper_interactive_desktop_.clear();
|
||||
}
|
||||
|
||||
if (process_handle != nullptr) {
|
||||
@@ -1550,6 +1634,7 @@ void CrossDeskServiceHost::StopSecureInputHelper() {
|
||||
secure_input_helper_process_id_ = 0;
|
||||
secure_input_helper_started_at_tick_ = 0;
|
||||
secure_input_helper_interactive_stage_.clear();
|
||||
secure_input_helper_interactive_desktop_.clear();
|
||||
}
|
||||
|
||||
if (stop_event_handle != nullptr) {
|
||||
@@ -1678,7 +1763,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
|
||||
}
|
||||
|
||||
bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||
DWORD session_id, const std::string& interactive_stage) {
|
||||
DWORD session_id, const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
std::wstring helper_path = GetSecureInputHelperPath();
|
||||
if (helper_path.empty() || !std::filesystem::exists(helper_path)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
@@ -1713,7 +1799,7 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||
STARTUPINFOW startup_info{};
|
||||
startup_info.cb = sizeof(startup_info);
|
||||
std::wstring secure_input_helper_desktop =
|
||||
SecureInputHelperDesktopForStage(interactive_stage);
|
||||
SecureInputHelperDesktopForStage(interactive_stage, interactive_desktop);
|
||||
startup_info.lpDesktop =
|
||||
const_cast<LPWSTR>(secure_input_helper_desktop.c_str());
|
||||
PROCESS_INFORMATION process_info{};
|
||||
@@ -1765,13 +1851,14 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||
secure_input_helper_running_ = true;
|
||||
secure_input_helper_started_at_tick_ = GetTickCount64();
|
||||
secure_input_helper_interactive_stage_ = interactive_stage;
|
||||
secure_input_helper_interactive_desktop_ = interactive_desktop;
|
||||
}
|
||||
|
||||
LOG_INFO(
|
||||
"Secure input helper started: session_id={}, pid={}, stage='{}', "
|
||||
"desktop='{}'",
|
||||
"interactive_desktop='{}', desktop='{}'",
|
||||
session_id, process_info.dwProcessId, interactive_stage,
|
||||
WideToUtf8(secure_input_helper_desktop));
|
||||
interactive_desktop, WideToUtf8(secure_input_helper_desktop));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1860,6 +1947,8 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
||||
json.value("lock_app_visible", false);
|
||||
session_helper_report_logon_ui_visible_ =
|
||||
json.value("logon_ui_visible", false);
|
||||
session_helper_report_consent_ui_visible_ =
|
||||
json.value("consent_ui_visible", false);
|
||||
session_helper_report_secure_desktop_active_ =
|
||||
json.value("secure_desktop_active", false);
|
||||
session_helper_report_credential_ui_visible_ =
|
||||
@@ -1885,6 +1974,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
||||
process_session_id_ = process_session_id;
|
||||
}
|
||||
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
|
||||
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
|
||||
InputDesktopInfo desktop_info = GetInputDesktopInfo();
|
||||
input_desktop_available_ = desktop_info.available;
|
||||
input_desktop_error_code_ = desktop_info.error_code;
|
||||
@@ -1911,7 +2001,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
||||
LOG_INFO("Session event: type={}, session_id={}, active_session_id={}",
|
||||
SessionEventToString(event_type), session_id, active_session_id_);
|
||||
EnsureSessionHelper();
|
||||
if (!secure_desktop_active_ && !logon_ui_visible_) {
|
||||
if (!secure_desktop_active_ && !logon_ui_visible_ && !consent_ui_visible_) {
|
||||
StopSecureInputHelper();
|
||||
}
|
||||
}
|
||||
@@ -1955,10 +2045,13 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
bool launch_secure_input_helper = false;
|
||||
DWORD secure_input_target_session_id = 0xFFFFFFFF;
|
||||
std::string secure_input_interactive_stage;
|
||||
std::string secure_input_interactive_desktop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
secure_input_target_session_id = active_session_id_;
|
||||
secure_input_interactive_stage = ResolveInteractiveStageLocked();
|
||||
secure_input_interactive_desktop =
|
||||
ResolveInteractiveDesktopLocked(secure_input_interactive_stage);
|
||||
keep_secure_input_helper =
|
||||
ShouldKeepSecureInputHelperLocked(secure_input_target_session_id);
|
||||
launch_secure_input_helper =
|
||||
@@ -1966,14 +2059,17 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
(!secure_input_helper_running_ ||
|
||||
secure_input_helper_session_id_ != secure_input_target_session_id ||
|
||||
secure_input_helper_interactive_stage_ !=
|
||||
secure_input_interactive_stage);
|
||||
secure_input_interactive_stage ||
|
||||
secure_input_helper_interactive_desktop_ !=
|
||||
secure_input_interactive_desktop);
|
||||
}
|
||||
|
||||
if (keep_secure_input_helper) {
|
||||
if (launch_secure_input_helper) {
|
||||
StopSecureInputHelper();
|
||||
LaunchSecureInputHelper(secure_input_target_session_id,
|
||||
secure_input_interactive_stage);
|
||||
secure_input_interactive_stage,
|
||||
secure_input_interactive_desktop);
|
||||
}
|
||||
} else {
|
||||
StopSecureInputHelper();
|
||||
@@ -1999,6 +2095,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
EscapeJsonString(secure_input_helper_last_error_);
|
||||
std::string secure_input_helper_interactive_stage =
|
||||
EscapeJsonString(secure_input_helper_interactive_stage_);
|
||||
std::string secure_input_helper_interactive_desktop =
|
||||
EscapeJsonString(secure_input_helper_interactive_desktop_);
|
||||
bool interactive_state_ready = session_helper_status_ok_;
|
||||
const bool sas_secure_desktop_grace_active =
|
||||
IsSasSecureDesktopGraceActiveLocked();
|
||||
@@ -2011,6 +2109,7 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
: false;
|
||||
const bool service_host_credential_ui_visible =
|
||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||
consent_ui_visible_,
|
||||
input_desktop_available_,
|
||||
secure_desktop_active_);
|
||||
bool credential_ui_visible =
|
||||
@@ -2062,6 +2161,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
<< ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
|
||||
<< ",\"credential_ui_visible\":"
|
||||
<< (credential_ui_visible ? "true" : "false")
|
||||
<< ",\"consent_ui_visible\":"
|
||||
<< (consent_ui_visible_ ? "true" : "false")
|
||||
<< ",\"password_box_visible\":"
|
||||
<< (credential_ui_visible ? "true" : "false")
|
||||
<< ",\"logon_ui_visible\":" << (logon_ui_visible_ ? "true" : "false")
|
||||
@@ -2104,6 +2205,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
<< (session_helper_report_lock_app_visible_ ? "true" : "false")
|
||||
<< ",\"session_helper_report_logon_ui_visible\":"
|
||||
<< (session_helper_report_logon_ui_visible_ ? "true" : "false")
|
||||
<< ",\"session_helper_report_consent_ui_visible\":"
|
||||
<< (session_helper_report_consent_ui_visible_ ? "true" : "false")
|
||||
<< ",\"session_helper_report_secure_desktop_active\":"
|
||||
<< (session_helper_report_secure_desktop_active_ ? "true" : "false")
|
||||
<< ",\"session_helper_report_credential_ui_visible\":"
|
||||
@@ -2134,6 +2237,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
<< secure_input_helper_last_error_code_
|
||||
<< ",\"secure_input_helper_stage\":\""
|
||||
<< secure_input_helper_interactive_stage << "\""
|
||||
<< ",\"secure_input_helper_desktop\":\""
|
||||
<< secure_input_helper_interactive_desktop << "\""
|
||||
<< ",\"secure_input_helper_uptime_ms\":"
|
||||
<< (secure_input_helper_started_at_tick_ >= started_at_tick_
|
||||
? (GetTickCount64() - secure_input_helper_started_at_tick_)
|
||||
@@ -2190,12 +2295,15 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
||||
bool helper_running = false;
|
||||
bool can_inject = false;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
target_session_id = active_session_id_;
|
||||
interactive_stage = ResolveInteractiveStageLocked();
|
||||
interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage);
|
||||
const bool helper_stage_matches =
|
||||
secure_input_helper_interactive_stage_ == interactive_stage;
|
||||
secure_input_helper_interactive_stage_ == interactive_stage &&
|
||||
secure_input_helper_interactive_desktop_ == interactive_desktop;
|
||||
helper_running = secure_input_helper_running_ &&
|
||||
secure_input_helper_session_id_ == target_session_id &&
|
||||
helper_stage_matches;
|
||||
@@ -2211,7 +2319,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
||||
|
||||
if (!helper_running) {
|
||||
StopSecureInputHelper();
|
||||
if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
|
||||
if (!LaunchSecureInputHelper(target_session_id, interactive_stage,
|
||||
interactive_desktop)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
||||
secure_input_helper_last_error_code_);
|
||||
@@ -2221,7 +2330,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
||||
return QueryNamedPipeMessage(
|
||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
||||
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
|
||||
extended, interactive_stage),
|
||||
extended, interactive_stage,
|
||||
interactive_desktop),
|
||||
1000);
|
||||
}
|
||||
|
||||
@@ -2237,12 +2347,15 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
||||
bool helper_running = false;
|
||||
bool can_inject = false;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
target_session_id = active_session_id_;
|
||||
interactive_stage = ResolveInteractiveStageLocked();
|
||||
interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage);
|
||||
const bool helper_stage_matches =
|
||||
secure_input_helper_interactive_stage_ == interactive_stage;
|
||||
secure_input_helper_interactive_stage_ == interactive_stage &&
|
||||
secure_input_helper_interactive_desktop_ == interactive_desktop;
|
||||
helper_running = secure_input_helper_running_ &&
|
||||
secure_input_helper_session_id_ == target_session_id &&
|
||||
helper_stage_matches;
|
||||
@@ -2258,7 +2371,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
||||
|
||||
if (!helper_running) {
|
||||
StopSecureInputHelper();
|
||||
if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
|
||||
if (!LaunchSecureInputHelper(target_session_id, interactive_stage,
|
||||
interactive_desktop)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
||||
secure_input_helper_last_error_code_);
|
||||
@@ -2267,7 +2381,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
||||
|
||||
return QueryNamedPipeMessage(
|
||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
||||
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage),
|
||||
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage,
|
||||
interactive_desktop),
|
||||
1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ class CrossDeskServiceHost {
|
||||
void ReapSecureInputHelper();
|
||||
void StopSecureInputHelper();
|
||||
bool LaunchSecureInputHelper(DWORD session_id,
|
||||
const std::string& interactive_stage);
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop);
|
||||
std::wstring GetSessionHelperPath() const;
|
||||
std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
|
||||
std::wstring GetSecureInputHelperPath() const;
|
||||
@@ -60,6 +61,8 @@ class CrossDeskServiceHost {
|
||||
bool IsSasSecureDesktopGraceActiveLocked() const;
|
||||
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
|
||||
std::string ResolveInteractiveStageLocked() const;
|
||||
std::string ResolveInteractiveDesktopLocked(
|
||||
const std::string& interactive_stage) const;
|
||||
void RefreshSessionHelperReportedState();
|
||||
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
||||
std::string HandleIpcCommand(const std::string& command);
|
||||
@@ -108,6 +111,7 @@ class CrossDeskServiceHost {
|
||||
ULONGLONG sas_secure_desktop_until_tick_ = 0;
|
||||
bool session_locked_ = false;
|
||||
bool logon_ui_visible_ = false;
|
||||
bool consent_ui_visible_ = false;
|
||||
bool prelogin_ = false;
|
||||
bool secure_desktop_active_ = false;
|
||||
bool input_desktop_available_ = false;
|
||||
@@ -117,6 +121,7 @@ class CrossDeskServiceHost {
|
||||
bool session_helper_report_input_desktop_available_ = false;
|
||||
bool session_helper_report_lock_app_visible_ = false;
|
||||
bool session_helper_report_logon_ui_visible_ = false;
|
||||
bool session_helper_report_consent_ui_visible_ = false;
|
||||
bool session_helper_report_secure_desktop_active_ = false;
|
||||
bool session_helper_report_credential_ui_visible_ = false;
|
||||
bool session_helper_report_unlock_ui_visible_ = false;
|
||||
@@ -137,6 +142,7 @@ class CrossDeskServiceHost {
|
||||
std::string session_helper_report_interactive_stage_;
|
||||
std::string secure_input_helper_last_error_;
|
||||
std::string secure_input_helper_interactive_stage_;
|
||||
std::string secure_input_helper_interactive_desktop_;
|
||||
|
||||
static CrossDeskServiceHost* instance_;
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ struct HelperState {
|
||||
std::string input_desktop_name;
|
||||
bool lock_app_visible = false;
|
||||
bool logon_ui_visible = false;
|
||||
bool consent_ui_visible = false;
|
||||
bool secure_desktop_active = false;
|
||||
ULONGLONG started_at_tick = 0;
|
||||
ULONGLONG last_update_tick = 0;
|
||||
@@ -64,6 +65,7 @@ struct SecureCaptureRequest {
|
||||
bool show_cursor = true;
|
||||
int fps = 30;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
};
|
||||
|
||||
struct SecureMouseRequest {
|
||||
@@ -72,6 +74,7 @@ struct SecureMouseRequest {
|
||||
int wheel = 0;
|
||||
int flag = 0;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
};
|
||||
|
||||
struct SecureCaptureBuffers {
|
||||
@@ -288,6 +291,10 @@ bool IsLogonUiRunningInCurrentSession(DWORD session_id) {
|
||||
return IsProcessRunningInCurrentSession(L"LogonUI.exe", session_id);
|
||||
}
|
||||
|
||||
bool IsConsentUiRunningInCurrentSession(DWORD session_id) {
|
||||
return IsProcessRunningInCurrentSession(L"Consent.exe", session_id);
|
||||
}
|
||||
|
||||
bool IsLockAppRunningInCurrentSession(DWORD session_id) {
|
||||
return IsProcessRunningInCurrentSession(L"LockApp.exe", session_id);
|
||||
}
|
||||
@@ -341,10 +348,12 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
|
||||
}
|
||||
|
||||
bool IsCredentialUiVisible(bool session_locked, bool logon_ui_running,
|
||||
bool consent_ui_visible,
|
||||
bool input_desktop_available,
|
||||
bool secure_desktop_active) {
|
||||
return (session_locked || secure_desktop_active) &&
|
||||
(logon_ui_running || !input_desktop_available);
|
||||
return consent_ui_visible ||
|
||||
((session_locked || secure_desktop_active) &&
|
||||
(logon_ui_running || !input_desktop_available));
|
||||
}
|
||||
|
||||
std::string BuildErrorJson(const char* error, DWORD error_code = 0) {
|
||||
@@ -367,6 +376,10 @@ void UpdateHelperState(HelperState* helper_state) {
|
||||
IsLockAppRunningInCurrentSession(helper_state->session_id);
|
||||
bool logon_ui_visible =
|
||||
IsLogonUiRunningInCurrentSession(helper_state->session_id);
|
||||
bool consent_ui_visible =
|
||||
IsConsentUiRunningInCurrentSession(helper_state->session_id);
|
||||
const bool consent_on_input_desktop =
|
||||
desktop_info.available && consent_ui_visible;
|
||||
const bool input_desktop_is_winlogon =
|
||||
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
|
||||
const bool inaccessible_secure_input_desktop =
|
||||
@@ -388,6 +401,8 @@ void UpdateHelperState(HelperState* helper_state) {
|
||||
helper_state->input_desktop_name = desktop_info.name;
|
||||
helper_state->lock_app_visible = lock_app_visible;
|
||||
helper_state->logon_ui_visible = logon_ui_visible;
|
||||
helper_state->consent_ui_visible =
|
||||
consent_on_input_desktop || consent_ui_visible;
|
||||
helper_state->secure_desktop_active = secure_desktop_active;
|
||||
helper_state->last_update_tick = GetTickCount64();
|
||||
}
|
||||
@@ -401,6 +416,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
|
||||
std::lock_guard<std::mutex> lock(helper_state->mutex);
|
||||
const bool credential_ui_visible = IsCredentialUiVisible(
|
||||
helper_state->session_locked, helper_state->logon_ui_visible,
|
||||
helper_state->consent_ui_visible,
|
||||
helper_state->input_desktop_available,
|
||||
helper_state->secure_desktop_active);
|
||||
const bool unlock_ui_visible =
|
||||
@@ -414,6 +430,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
|
||||
json["input_desktop"] = helper_state->input_desktop_name;
|
||||
json["lock_app_visible"] = helper_state->lock_app_visible;
|
||||
json["logon_ui_visible"] = helper_state->logon_ui_visible;
|
||||
json["consent_ui_visible"] = helper_state->consent_ui_visible;
|
||||
json["secure_desktop_active"] = helper_state->secure_desktop_active;
|
||||
json["credential_ui_visible"] = credential_ui_visible;
|
||||
json["unlock_ui_visible"] = unlock_ui_visible;
|
||||
@@ -639,8 +656,12 @@ bool EnsureThreadInteractiveDesktop(HDESK* opened_desktop_out = nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const wchar_t* DesktopNameForInteractiveStage(
|
||||
const std::string& interactive_stage) {
|
||||
std::wstring DesktopNameForInteractiveStage(
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
if (!interactive_desktop.empty()) {
|
||||
return Utf8ToWide(interactive_desktop);
|
||||
}
|
||||
if (interactive_stage == "credential-ui" ||
|
||||
interactive_stage == "secure-desktop") {
|
||||
return L"Winlogon";
|
||||
@@ -648,7 +669,7 @@ const wchar_t* DesktopNameForInteractiveStage(
|
||||
if (interactive_stage == "lock-screen") {
|
||||
return L"Default";
|
||||
}
|
||||
return nullptr;
|
||||
return {};
|
||||
}
|
||||
|
||||
struct DesktopSwitchDetails {
|
||||
@@ -666,14 +687,14 @@ struct InputInjectionResult {
|
||||
};
|
||||
|
||||
DesktopSwitchDetails BuildDesktopSwitchDetails(
|
||||
const std::string& interactive_stage) {
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
DesktopSwitchDetails details;
|
||||
details.stage = interactive_stage;
|
||||
const wchar_t* desktop_name =
|
||||
DesktopNameForInteractiveStage(interactive_stage);
|
||||
const std::wstring desktop_name =
|
||||
DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
|
||||
details.target_desktop =
|
||||
desktop_name != nullptr ? WideToUtf8(std::wstring(desktop_name))
|
||||
: "input-or-Winlogon";
|
||||
!desktop_name.empty() ? WideToUtf8(desktop_name) : "input-or-Winlogon";
|
||||
details.current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
return details;
|
||||
}
|
||||
@@ -709,6 +730,7 @@ Json BuildInputFailureJson(const InputInjectionResult& result) {
|
||||
|
||||
bool EnsureThreadInteractiveDesktopForStage(
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop,
|
||||
HDESK* opened_desktop_out = nullptr,
|
||||
DesktopSwitchDetails* switch_details = nullptr) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
@@ -716,15 +738,15 @@ bool EnsureThreadInteractiveDesktopForStage(
|
||||
}
|
||||
|
||||
DesktopSwitchDetails local_details =
|
||||
BuildDesktopSwitchDetails(interactive_stage);
|
||||
BuildDesktopSwitchDetails(interactive_stage, interactive_desktop);
|
||||
if (switch_details != nullptr) {
|
||||
*switch_details = local_details;
|
||||
}
|
||||
|
||||
const wchar_t* desktop_name =
|
||||
DesktopNameForInteractiveStage(interactive_stage);
|
||||
if (desktop_name != nullptr) {
|
||||
if (EnsureThreadDesktop(desktop_name, opened_desktop_out)) {
|
||||
const std::wstring desktop_name =
|
||||
DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
|
||||
if (!desktop_name.empty()) {
|
||||
if (EnsureThreadDesktop(desktop_name.c_str(), opened_desktop_out)) {
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->current_desktop =
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
@@ -741,7 +763,7 @@ bool EnsureThreadInteractiveDesktopForStage(
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper to stage desktop, stage='{}', "
|
||||
"desktop='{}', error={}, current='{}'",
|
||||
interactive_stage, WideToUtf8(std::wstring(desktop_name)),
|
||||
interactive_stage, WideToUtf8(desktop_name),
|
||||
error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(error);
|
||||
@@ -790,10 +812,12 @@ bool PreferSideSpecificVkInjection(int key_code) {
|
||||
|
||||
InputInjectionResult InjectKeyboardInput(
|
||||
int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
const std::string& interactive_stage) {
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
ScopedDesktopHandle desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(interactive_stage,
|
||||
interactive_desktop,
|
||||
&desktop.handle,
|
||||
&desktop_switch)) {
|
||||
const DWORD error = GetLastError();
|
||||
@@ -851,11 +875,37 @@ InputInjectionResult InjectKeyboardInput(
|
||||
return BuildInputSuccess();
|
||||
}
|
||||
|
||||
void ParseInteractionTail(const std::string& tail,
|
||||
std::string* interactive_stage_out,
|
||||
std::string* interactive_desktop_out) {
|
||||
if (interactive_stage_out != nullptr) {
|
||||
interactive_stage_out->clear();
|
||||
}
|
||||
if (interactive_desktop_out != nullptr) {
|
||||
interactive_desktop_out->clear();
|
||||
}
|
||||
if (tail.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t separator = tail.find(':');
|
||||
if (interactive_stage_out != nullptr) {
|
||||
*interactive_stage_out = separator == std::string::npos
|
||||
? tail
|
||||
: tail.substr(0, separator);
|
||||
}
|
||||
if (separator != std::string::npos && interactive_desktop_out != nullptr) {
|
||||
*interactive_desktop_out = tail.substr(separator + 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseSecureInputKeyboardCommand(const std::string& command,
|
||||
int* key_code_out, bool* is_down_out,
|
||||
uint32_t* scan_code_out,
|
||||
bool* extended_out,
|
||||
std::string* interactive_stage_out =
|
||||
nullptr,
|
||||
std::string* interactive_desktop_out =
|
||||
nullptr) {
|
||||
if (key_code_out == nullptr || is_down_out == nullptr ||
|
||||
scan_code_out == nullptr || extended_out == nullptr) {
|
||||
@@ -867,6 +917,9 @@ bool ParseSecureInputKeyboardCommand(const std::string& command,
|
||||
if (interactive_stage_out != nullptr) {
|
||||
interactive_stage_out->clear();
|
||||
}
|
||||
if (interactive_desktop_out != nullptr) {
|
||||
interactive_desktop_out->clear();
|
||||
}
|
||||
|
||||
if (command.rfind(crossdesk::kCrossDeskSecureInputKeyboardCommandPrefix, 0) !=
|
||||
0) {
|
||||
@@ -926,8 +979,9 @@ bool ParseSecureInputKeyboardCommand(const std::string& command,
|
||||
: command.substr(extended_separator + 1,
|
||||
stage_separator - extended_separator - 1);
|
||||
if (stage_separator != std::string::npos &&
|
||||
interactive_stage_out != nullptr) {
|
||||
*interactive_stage_out = command.substr(stage_separator + 1);
|
||||
(interactive_stage_out != nullptr || interactive_desktop_out != nullptr)) {
|
||||
ParseInteractionTail(command.substr(stage_separator + 1),
|
||||
interactive_stage_out, interactive_desktop_out);
|
||||
}
|
||||
if (extended_str == "1" || extended_str == "true") {
|
||||
*extended_out = true;
|
||||
@@ -951,6 +1005,7 @@ bool ParseSecureInputMouseCommand(const std::string& command,
|
||||
return false;
|
||||
}
|
||||
request_out->interactive_stage.clear();
|
||||
request_out->interactive_desktop.clear();
|
||||
|
||||
const size_t x_begin =
|
||||
std::strlen(crossdesk::kCrossDeskSecureInputMouseCommandPrefix);
|
||||
@@ -993,7 +1048,9 @@ bool ParseSecureInputMouseCommand(const std::string& command,
|
||||
? command.substr(flag_begin)
|
||||
: command.substr(flag_begin, stage_separator - flag_begin));
|
||||
if (stage_separator != std::string::npos) {
|
||||
request_out->interactive_stage = command.substr(stage_separator + 1);
|
||||
ParseInteractionTail(command.substr(stage_separator + 1),
|
||||
&request_out->interactive_stage,
|
||||
&request_out->interactive_desktop);
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
@@ -1013,6 +1070,7 @@ bool ParseSecureInputCaptureCommand(const std::string& command,
|
||||
return false;
|
||||
}
|
||||
request_out->interactive_stage.clear();
|
||||
request_out->interactive_desktop.clear();
|
||||
|
||||
const size_t values_begin =
|
||||
std::strlen(crossdesk::kCrossDeskSecureInputCaptureCommandPrefix);
|
||||
@@ -1042,9 +1100,11 @@ bool ParseSecureInputCaptureCommand(const std::string& command,
|
||||
request_out->width = parsed_values[2] & ~1;
|
||||
request_out->height = parsed_values[3] & ~1;
|
||||
request_out->show_cursor = parsed_values[4] != 0;
|
||||
request_out->interactive_stage =
|
||||
separator == std::string::npos ? std::string()
|
||||
: command.substr(token_begin);
|
||||
if (separator != std::string::npos) {
|
||||
ParseInteractionTail(command.substr(token_begin),
|
||||
&request_out->interactive_stage,
|
||||
&request_out->interactive_desktop);
|
||||
}
|
||||
return request_out->width > 0 && request_out->height > 0;
|
||||
}
|
||||
|
||||
@@ -1059,6 +1119,7 @@ bool ParseSecureInputCaptureStartCommand(const std::string& command,
|
||||
return false;
|
||||
}
|
||||
request_out->interactive_stage.clear();
|
||||
request_out->interactive_desktop.clear();
|
||||
|
||||
const size_t values_begin = std::strlen(
|
||||
crossdesk::kCrossDeskSecureInputCaptureStartCommandPrefix);
|
||||
@@ -1089,9 +1150,11 @@ bool ParseSecureInputCaptureStartCommand(const std::string& command,
|
||||
request_out->height = parsed_values[3] & ~1;
|
||||
request_out->show_cursor = parsed_values[4] != 0;
|
||||
request_out->fps = parsed_values[5] > 0 ? parsed_values[5] : 30;
|
||||
request_out->interactive_stage =
|
||||
separator == std::string::npos ? std::string()
|
||||
: command.substr(token_begin);
|
||||
if (separator != std::string::npos) {
|
||||
ParseInteractionTail(command.substr(token_begin),
|
||||
&request_out->interactive_stage,
|
||||
&request_out->interactive_desktop);
|
||||
}
|
||||
return request_out->width > 0 && request_out->height > 0;
|
||||
}
|
||||
|
||||
@@ -1129,6 +1192,7 @@ InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
|
||||
ScopedDesktopHandle desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle,
|
||||
&desktop_switch)) {
|
||||
const DWORD error = GetLastError();
|
||||
@@ -1205,6 +1269,7 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
|
||||
ScopedDesktopHandle desktop;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildTextResponseBytes(BuildErrorJson(
|
||||
@@ -1355,6 +1420,7 @@ void SecureDesktopSharedCaptureThread(
|
||||
|
||||
ScopedDesktopHandle desktop;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle)) {
|
||||
LOG_ERROR("Secure shared capture desktop switch failed, error={}",
|
||||
GetLastError());
|
||||
@@ -1607,11 +1673,13 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
if (ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,
|
||||
&extended, &interactive_stage)) {
|
||||
&extended, &interactive_stage,
|
||||
&interactive_desktop)) {
|
||||
const InputInjectionResult inject_result =
|
||||
InjectKeyboardInput(key_code, is_down, scan_code, extended,
|
||||
interactive_stage);
|
||||
interactive_stage, interactive_desktop);
|
||||
if (!inject_result.ok) {
|
||||
LOG_WARN(
|
||||
"Secure input helper input failed for key_code={}, is_down={}, "
|
||||
@@ -1631,6 +1699,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
json["scan_code"] = scan_code;
|
||||
json["extended"] = extended;
|
||||
json["stage"] = interactive_stage;
|
||||
json["interactive_desktop"] = interactive_desktop;
|
||||
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
return BuildTextResponseBytes(json.dump());
|
||||
}
|
||||
@@ -1658,6 +1727,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
json["wheel"] = mouse_request.wheel;
|
||||
json["flag"] = mouse_request.flag;
|
||||
json["stage"] = mouse_request.interactive_stage;
|
||||
json["interactive_desktop"] = mouse_request.interactive_desktop;
|
||||
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
return BuildTextResponseBytes(json.dump());
|
||||
}
|
||||
@@ -1887,6 +1957,7 @@ int main(int argc, char* argv[]) {
|
||||
std::string last_desktop_name;
|
||||
bool last_lock_app = false;
|
||||
bool last_logon_ui = false;
|
||||
bool last_consent_ui = false;
|
||||
bool last_secure_desktop = false;
|
||||
bool last_session_locked = false;
|
||||
std::string last_stage;
|
||||
@@ -1898,6 +1969,7 @@ int main(int argc, char* argv[]) {
|
||||
bool input_desktop_available = false;
|
||||
bool lock_app_visible = false;
|
||||
bool logon_ui_running = false;
|
||||
bool consent_ui_visible = false;
|
||||
bool secure_desktop_active = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(helper_state.mutex);
|
||||
@@ -1906,10 +1978,12 @@ int main(int argc, char* argv[]) {
|
||||
input_desktop_available = helper_state.input_desktop_available;
|
||||
lock_app_visible = helper_state.lock_app_visible;
|
||||
logon_ui_running = helper_state.logon_ui_visible;
|
||||
consent_ui_visible = helper_state.consent_ui_visible;
|
||||
secure_desktop_active = helper_state.secure_desktop_active;
|
||||
}
|
||||
const bool credential_ui_visible =
|
||||
IsCredentialUiVisible(session_locked, logon_ui_running,
|
||||
consent_ui_visible,
|
||||
input_desktop_available,
|
||||
secure_desktop_active);
|
||||
std::string stage = DetermineInteractiveStage(
|
||||
@@ -1919,17 +1993,19 @@ int main(int argc, char* argv[]) {
|
||||
session_locked != last_session_locked ||
|
||||
lock_app_visible != last_lock_app ||
|
||||
logon_ui_running != last_logon_ui ||
|
||||
consent_ui_visible != last_consent_ui ||
|
||||
secure_desktop_active != last_secure_desktop || stage != last_stage) {
|
||||
LOG_INFO(
|
||||
"Session helper state: session_id={}, input_desktop='{}', "
|
||||
"session_locked={}, lock_app_visible={}, logon_ui_running={}, "
|
||||
"secure_desktop_active={}, stage={}",
|
||||
"consent_ui_visible={}, secure_desktop_active={}, stage={}",
|
||||
current_session_id, desktop_name, session_locked, lock_app_visible,
|
||||
logon_ui_running, secure_desktop_active, stage);
|
||||
logon_ui_running, consent_ui_visible, secure_desktop_active, stage);
|
||||
last_desktop_name = desktop_name;
|
||||
last_session_locked = session_locked;
|
||||
last_lock_app = lock_app_visible;
|
||||
last_logon_ui = logon_ui_running;
|
||||
last_consent_ui = consent_ui_visible;
|
||||
last_secure_desktop = secure_desktop_active;
|
||||
last_stage = stage;
|
||||
}
|
||||
|
||||
@@ -225,9 +225,12 @@ int Thumbnail::LoadThumbnail(
|
||||
std::string remote_id;
|
||||
std::string cipher_password;
|
||||
std::string remote_host_name;
|
||||
std::string password;
|
||||
std::string original_image_name;
|
||||
bool remember_password = false;
|
||||
|
||||
if ('Y' == cipher_image_name[9] && cipher_image_name.size() >= 16) {
|
||||
if (cipher_image_name.size() > 9 && 'Y' == cipher_image_name[9] &&
|
||||
cipher_image_name.size() >= 16) {
|
||||
size_t pos_y = cipher_image_name.find('Y');
|
||||
size_t pos_at = cipher_image_name.find('@');
|
||||
|
||||
@@ -241,10 +244,11 @@ int Thumbnail::LoadThumbnail(
|
||||
remote_host_name =
|
||||
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
|
||||
cipher_password = cipher_image_name.substr(pos_at + 1);
|
||||
password = AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
||||
remember_password = true;
|
||||
|
||||
original_image_name =
|
||||
remote_id + 'Y' + remote_host_name + "@" +
|
||||
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
||||
original_image_name = remote_id + 'Y' + remote_host_name + "@" +
|
||||
password;
|
||||
} else {
|
||||
size_t pos_n = cipher_image_name.find('N');
|
||||
// size_t pos_at = cipher_image_name.find('@');
|
||||
@@ -257,16 +261,19 @@ int Thumbnail::LoadThumbnail(
|
||||
remote_id = cipher_image_name.substr(0, pos_n);
|
||||
remote_host_name = cipher_image_name.substr(pos_n + 1);
|
||||
|
||||
original_image_name =
|
||||
remote_id + 'N' + remote_host_name + "@" +
|
||||
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
|
||||
original_image_name = remote_id + 'N' + remote_host_name;
|
||||
}
|
||||
|
||||
std::string image_path = save_path_ + cipher_image_name;
|
||||
Thumbnail::RecentConnection recent_connection;
|
||||
recent_connection.remote_id = remote_id;
|
||||
recent_connection.remote_host_name = remote_host_name;
|
||||
recent_connection.password = password;
|
||||
recent_connection.remember_password = remember_password;
|
||||
recent_connections.emplace_back(
|
||||
std::make_pair(original_image_name, Thumbnail::RecentConnection()));
|
||||
std::make_pair(original_image_name, recent_connection));
|
||||
LoadTextureFromFile(image_path.c_str(), renderer,
|
||||
&(recent_connections[i].second.texture), width,
|
||||
&(recent_connections.back().second.texture), width,
|
||||
height);
|
||||
}
|
||||
return 0;
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: bb0fae0617...658768547f
@@ -0,0 +1,102 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current /
|
||||
"submodules/minirtc/src/api/minirtc.h")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path &path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char *name, const std::string &value,
|
||||
const std::string &expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char *name, const std::string &value,
|
||||
const std::string &unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string minirtc_api =
|
||||
ReadFile(repo_root / "submodules/minirtc/src/api/minirtc.h");
|
||||
const std::string peer_connection =
|
||||
ReadFile(repo_root / "submodules/minirtc/src/pc/peer_connection.cpp");
|
||||
const std::string connection_status_window = ReadFile(
|
||||
repo_root / "src/gui/views/windows/connection_status_window.cpp");
|
||||
const std::string runtime_state_h =
|
||||
ReadFile(repo_root / "src/gui/runtime/runtime_state.h");
|
||||
const std::string remote_session_h =
|
||||
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
|
||||
const std::string connection_runtime_cpp =
|
||||
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
||||
ok &= ExpectNotContains("minirtc.h", minirtc_api, "DeviceOffline");
|
||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||
"\"Remote unavailable\"");
|
||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||
"ConnectionStatus::RemoteUnavailable");
|
||||
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
||||
"\"Device offline\"");
|
||||
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
||||
"ConnectionStatus::DeviceOffline");
|
||||
ok &= ExpectContains("connection_status_window.cpp", connection_status_window,
|
||||
"localization::device_offline");
|
||||
ok &= ExpectContains("runtime_state.h", runtime_state_h,
|
||||
"pending_presence_probe_started_at_");
|
||||
ok &= ExpectContains("remote_session.h", remote_session_h,
|
||||
"connection_attempt_started_at_");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"HandleConnectionTimeouts");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"kPresenceProbeTimeout");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"kConnectionAttemptTimeout");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"connection_attempt_started_at_");
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -10,7 +10,8 @@ std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/gui/toolbars/control_bar.cpp")) {
|
||||
std::filesystem::exists(current /
|
||||
"src/gui/views/toolbars/control_bar.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
@@ -63,8 +64,7 @@ bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||
}
|
||||
|
||||
std::cerr << name << " expected at least " << min_count
|
||||
<< " occurrences of: " << expected << ", found " << count
|
||||
<< "\n";
|
||||
<< " occurrences of: " << expected << ", found " << count << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ int main() {
|
||||
}
|
||||
|
||||
const std::string control_bar =
|
||||
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||
ReadFile(repo_root / "src/gui/views/toolbars/control_bar.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -25,18 +25,17 @@ int main() {
|
||||
ok &= ExpectEqual("initial flags", state.flags(), 0);
|
||||
ok &= ExpectEqual("left shift down", state.Update(0xA0, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("shifted semicolon keeps shift",
|
||||
state.Update(0xBA, true),
|
||||
ok &= ExpectEqual("shifted semicolon keeps shift", state.Update(0xBA, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("semicolon up keeps shift", state.Update(0xBA, false),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("right shift down while left held",
|
||||
state.Update(0xA1, true),
|
||||
ok &=
|
||||
ExpectEqual("right shift down while left held", state.Update(0xA1, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("left shift up while right held", state.Update(0xA0, false),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("right shift up clears shift", state.Update(0xA1, false),
|
||||
0);
|
||||
ok &=
|
||||
ExpectEqual("right shift up clears shift", state.Update(0xA1, false), 0);
|
||||
|
||||
ok &= ExpectEqual("left control down", state.Update(0xA2, true),
|
||||
crossdesk::kMacInjectedModifierControl);
|
||||
@@ -53,8 +52,7 @@ int main() {
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("right alt up leaves command", state.Update(0xA5, false),
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("left command up clears all", state.Update(0x5B, false),
|
||||
0);
|
||||
ok &= ExpectEqual("left command up clears all", state.Update(0x5B, false), 0);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#include "path_manager.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "path_manager.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <limits.h>
|
||||
#include <mach-o/dyld.h>
|
||||
#else
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
@@ -43,8 +43,7 @@ std::filesystem::path GetExecutableDirectory() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ExpectEqual(const char* name,
|
||||
const std::filesystem::path& actual,
|
||||
bool ExpectEqual(const char* name, const std::filesystem::path& actual,
|
||||
const std::filesystem::path& expected) {
|
||||
if (actual.lexically_normal() == expected.lexically_normal()) {
|
||||
return true;
|
||||
|
||||
@@ -130,11 +130,12 @@ int main() {
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
#ifdef _WIN32
|
||||
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
ok &=
|
||||
ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
ok &= ExpectActivationContext(repo_root /
|
||||
"scripts/windows/crossdesk_debug.manifest");
|
||||
ok &= ExpectActivationContext(repo_root /
|
||||
"scripts/windows/crossdesk_portable.manifest");
|
||||
#endif
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ std::filesystem::path FindRepoRoot() {
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "src/device_controller/mouse/windows/mouse_controller.cpp")) {
|
||||
current /
|
||||
"src/device_controller/mouse/windows/mouse_controller.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
|
||||
@@ -12,8 +12,8 @@ std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "src/service/windows/service_host.cpp")) {
|
||||
std::filesystem::exists(current /
|
||||
"src/service/windows/service_host.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
@@ -71,9 +71,11 @@ int main() {
|
||||
}
|
||||
|
||||
const std::string control_bar =
|
||||
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||
const std::string render_h = ReadFile(repo_root / "src/gui/render.h");
|
||||
ReadFile(repo_root / "src/gui/views/toolbars/control_bar.cpp");
|
||||
const std::string windows_service_runtime =
|
||||
ReadFile(repo_root / "src/gui/runtime/windows_service_runtime.cpp");
|
||||
const std::string runtime_state_h =
|
||||
ReadFile(repo_root / "src/gui/runtime/runtime_state.h");
|
||||
const std::string service_host =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||
const std::string service_host_h =
|
||||
@@ -82,9 +84,9 @@ int main() {
|
||||
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectTrue("secure desktop input routing",
|
||||
crossdesk::IsSecureDesktopInteractionRequired(
|
||||
"secure-desktop"));
|
||||
ok &= ExpectTrue(
|
||||
"secure desktop input routing",
|
||||
crossdesk::IsSecureDesktopInteractionRequired("secure-desktop"));
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"CanSendSecureAttentionSequence("
|
||||
"props->remote_interactive_stage_)");
|
||||
@@ -92,18 +94,20 @@ int main() {
|
||||
"ImGui::BeginDisabled();\n"
|
||||
" }\n"
|
||||
" if (ImGui::Selectable(sas_label.c_str()))");
|
||||
ok &= ExpectNotContains("render.cpp", render, "sas_requires_lock_screen");
|
||||
ok &= ExpectContains("render.h", render_h,
|
||||
ok &= ExpectNotContains("windows_service_runtime.cpp",
|
||||
windows_service_runtime, "sas_requires_lock_screen");
|
||||
ok &= ExpectContains("runtime_state.h", runtime_state_h,
|
||||
"optimistic_windows_secure_desktop_until_tick_");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"kWindowsServiceSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"status->sas_secure_desktop_grace_active");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &=
|
||||
ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"json.value(\"sas_secure_desktop_grace_active\", false)");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"status.sas_secure_desktop_grace_active");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"local_interactive_stage_ = \"secure-desktop\"");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"sas_secure_desktop_until_tick_");
|
||||
@@ -126,7 +130,8 @@ int main() {
|
||||
"now + kSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"\\\"sas_secure_desktop_grace_active\\\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
ok &=
|
||||
ExpectContains("service_host.cpp", service_host,
|
||||
"raw_interactive_stage = ResolveInteractiveStageLocked()");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"kSessionHelperStatePollMs = 1000");
|
||||
|
||||
@@ -10,7 +10,8 @@ std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/service/windows/service_host.cpp")) {
|
||||
std::filesystem::exists(current /
|
||||
"src/service/windows/service_host.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
@@ -64,17 +65,18 @@ int main() {
|
||||
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||
const std::string session_helper =
|
||||
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||
const std::string targets =
|
||||
ReadFile(repo_root / "xmake/targets.lua");
|
||||
const std::string targets = ReadFile(repo_root / "xmake/targets.lua");
|
||||
const std::string interactive_state =
|
||||
ReadFile(repo_root / "src/service/windows/interactive_state.h");
|
||||
const std::string render_callback =
|
||||
ReadFile(repo_root / "src/gui/render_callback.cpp");
|
||||
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||
const std::string gui_input_sources =
|
||||
ReadFile(repo_root / "src/gui/runtime/peer_data_callbacks.cpp") + "\n" +
|
||||
ReadFile(repo_root / "src/gui/features/input/keyboard_controller.cpp");
|
||||
const std::string windows_service_runtime =
|
||||
ReadFile(repo_root / "src/gui/runtime/windows_service_runtime.cpp");
|
||||
const std::string screen_capturer_h =
|
||||
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.h");
|
||||
const std::string screen_capturer_cpp =
|
||||
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp");
|
||||
const std::string screen_capturer_cpp = ReadFile(
|
||||
repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
@@ -84,7 +86,7 @@ int main() {
|
||||
ok &= ExpectContains("targets.lua", targets,
|
||||
"target(\"crossdesk_session_helper\")");
|
||||
ok &= ExpectContains("targets.lua", targets,
|
||||
"add_files(\"scripts/windows/crossdesk.rc\")");
|
||||
"add_files(crossdesk_windows_resource)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnablePerMonitorDpiAwareness");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
@@ -93,7 +95,8 @@ int main() {
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnablePerMonitorDpiAwareness();\n\n"
|
||||
" InitializeHelperLogger();");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
ok &= ExpectContains(
|
||||
"service_host.cpp", service_host,
|
||||
"const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"while (GetTickCount64() <= deadline_tick)");
|
||||
@@ -103,51 +106,80 @@ int main() {
|
||||
"BuildSecureInputHelperKeyboardCommand(");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"const std::string& interactive_stage");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
ok &= ExpectContains(
|
||||
"service_host.h", service_host_h,
|
||||
"bool LaunchSecureInputHelper(DWORD session_id,\n"
|
||||
" const std::string& interactive_stage)");
|
||||
" const std::string& interactive_stage,\n"
|
||||
" const std::string& interactive_desktop)");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"std::string secure_input_helper_interactive_stage_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"SecureInputHelperDesktopForStage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"IsConsentUiRunningInSession");
|
||||
ok &= ExpectContains("service_host.cpp", service_host, "L\"Consent.exe\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"IsConsentUiRunningInCurrentSession");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"L\"Consent.exe\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"desktop_info.available && consent_ui_visible");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"session_helper_report_input_desktop_available_ &&");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"session_helper_report_consent_ui_visible_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_desktop_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"SecureInputHelperDesktopForStage("
|
||||
"interactive_stage, interactive_desktop)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\\" + interactive_desktop_w");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\Winlogon\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\default\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
ok &= ExpectContains(
|
||||
"service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_ == interactive_stage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
ok &= ExpectContains(
|
||||
"service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_ = interactive_stage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_.clear()");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"LaunchSecureInputHelper(target_session_id, interactive_stage)");
|
||||
ok &= ExpectContains(
|
||||
"service_host.cpp", service_host,
|
||||
"LaunchSecureInputHelper(target_session_id, interactive_stage,\n"
|
||||
" interactive_desktop)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"\\\"secure_input_helper_stage\\\":\\\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"session_helper_report_interactive_stage_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return SendSecureDesktopMouseInput");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"constexpr DWORD kWindowsServiceQueryTimeoutMs = 500");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
ok &=
|
||||
ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"IsTransientWindowsServiceStatusError(status.error)");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"IsTransientWindowsServiceStatusError(status.error)");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"Local Windows service temporarily unavailable");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
ok &= ExpectContains(
|
||||
"screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"Windows capturer secure desktop service temporarily unavailable");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
ok &= ExpectContains(
|
||||
"screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"Windows capturer secure desktop transient frame query failed");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"if (transient_error) {\n"
|
||||
" LOG_INFO(");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
ok &= ExpectContains("gui_input_sources.cpp", gui_input_sources,
|
||||
"IsTransientSecureDesktopInputFailure");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
ok &= ExpectContains("gui_input_sources.cpp", gui_input_sources,
|
||||
"Secure desktop keyboard injection transient failure");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE");
|
||||
@@ -187,30 +219,36 @@ int main() {
|
||||
"json[\"current_desktop\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"stage\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,\n"
|
||||
" &extended, &interactive_stage)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
ok &= ExpectContains(
|
||||
"session_helper_main.cpp", session_helper,
|
||||
"ParseSecureInputKeyboardCommand(command, &key_code, &is_down, "
|
||||
"&scan_code,\n"
|
||||
" &extended, &interactive_stage,\n"
|
||||
" &interactive_desktop)");
|
||||
ok &= ExpectContains(
|
||||
"session_helper_main.cpp", session_helper,
|
||||
"InjectKeyboardInput(key_code, is_down, scan_code, extended,\n"
|
||||
" interactive_stage)");
|
||||
" interactive_stage, interactive_desktop)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"InjectMouseInput(mouse_request)");
|
||||
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||
ok &=
|
||||
ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadDesktop(L\"Winlogon\", &secure_desktop)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"winsta0\\\\default");
|
||||
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||
ok &= ExpectContains("service_host.cpp", service_host, "winsta0\\\\default");
|
||||
ok &= ExpectNotContains(
|
||||
"service_host.cpp", service_host,
|
||||
"startup_info.lpDesktop = const_cast<LPWSTR>(L\"winsta0\\\\Winlogon\")");
|
||||
ok &= ExpectContains("interactive_state.h", interactive_state,
|
||||
"interactive_stage == \"lock-screen\"");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
ok &= ExpectContains("gui_input_sources.cpp", gui_input_sources,
|
||||
"RemoteAction remote_action{};");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
ok &= ExpectContains("windows_service_runtime.cpp", windows_service_runtime,
|
||||
"previous_secure_desktop_interaction");
|
||||
ok &= ExpectNotContains(
|
||||
"render_callback.cpp", render_callback,
|
||||
"render->local_service_available_ &&\n"
|
||||
" IsSecureDesktopInteractionRequired(render->local_interactive_stage_)");
|
||||
"gui_input_sources.cpp", gui_input_sources,
|
||||
"runtime->local_service_available_ &&\n"
|
||||
" "
|
||||
"IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_)");
|
||||
ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h,
|
||||
"std::string secure_shared_stage_;");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
|
||||
+9
-3
@@ -35,7 +35,12 @@ function setup_platform_settings()
|
||||
add_links("pulse-simple", "pulse")
|
||||
add_requires("libyuv")
|
||||
add_syslinks("pthread", "dl")
|
||||
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes")
|
||||
add_links("SDL3", "asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
|
||||
"Xrandr", "Xfixes")
|
||||
add_existing_include_dirs({
|
||||
"/usr/include/freetype2",
|
||||
"/usr/local/include/freetype2"
|
||||
}, {system = true})
|
||||
|
||||
if is_config("USE_DRM", true) then
|
||||
add_links("drm")
|
||||
@@ -75,7 +80,8 @@ function setup_platform_settings()
|
||||
add_links("SDL3")
|
||||
add_ldflags("-Wl,-ld_classic")
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation",
|
||||
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
|
||||
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
|
||||
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
|
||||
"AudioToolbox")
|
||||
end
|
||||
end
|
||||
+24
-8
@@ -44,6 +44,17 @@ function setup_targets()
|
||||
add_includedirs("src/device_controller")
|
||||
add_files("tests/macos_keyboard_modifier_state_test.cpp")
|
||||
|
||||
target("keyboard_state_protocol_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_includedirs("src/device_controller", "src/common")
|
||||
add_files("tests/keyboard_state_protocol_test.cpp")
|
||||
|
||||
target("connection_status_protocol_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/connection_status_protocol_test.cpp")
|
||||
|
||||
target("windows_manifest_resource_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
@@ -203,16 +214,21 @@ function setup_targets()
|
||||
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
|
||||
"path_manager", "screen_capturer", "speaker_capturer",
|
||||
"device_controller", "thumbnail", "version_checker", "tools")
|
||||
add_files("src/gui/*.cpp", "src/gui/panels/*.cpp", "src/gui/toolbars/*.cpp",
|
||||
"src/gui/windows/*.cpp")
|
||||
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
||||
"src/gui/windows", {public = true})
|
||||
add_files("src/gui/render.cpp", "src/gui/application/*.cpp",
|
||||
"src/gui/runtime/*.cpp",
|
||||
"src/gui/features/devices/*.cpp", "src/gui/features/input/*.cpp",
|
||||
"src/gui/features/clipboard/*.cpp", "src/gui/features/file_transfer/*.cpp",
|
||||
"src/gui/features/settings/*.cpp", "src/gui/views/panels/*.cpp",
|
||||
"src/gui/views/toolbars/*.cpp", "src/gui/views/windows/*.cpp")
|
||||
add_includedirs("src/gui", {public = true})
|
||||
if is_os("windows") then
|
||||
add_files("src/gui/tray/*.cpp")
|
||||
add_includedirs("src/gui/tray", "src/service/windows",
|
||||
{public = true})
|
||||
add_files("src/gui/platform/tray/win_tray.cpp")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_files("src/gui/windows/*.mm")
|
||||
add_files("src/gui/runtime/*.mm", "src/gui/views/windows/*.mm",
|
||||
"src/gui/platform/tray/*.mm")
|
||||
elseif is_os("linux") then
|
||||
add_files("src/gui/platform/tray/linux_tray.cpp")
|
||||
end
|
||||
|
||||
if is_os("windows") then
|
||||
|
||||
Reference in New Issue
Block a user